클래스와 인스턴스

  • 내장형 데이터 외에 새로운 타입을 만들 때 클래스를 선언한다.

    • 파이썬에서는 클래스가 정의되면 하나의 독립적인 네임스페이스를 생성한다.

    • 네임스페이스란 변수가 객체를 바인딩(대입 연산자를 사용해서 저장하는 것)할 때 그 둘 사이의 관계를 저장하고 있는 공간을 의미한다.

    • 클래스 내에 정의된 변수(=클래스 변수)는 생성된 네임스페이스 안에 딕셔너리 타입으로 저장된다.

>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']

>>> class Obj:
...     number = 1
...

>>> dir() # 클래스가 생성되었다.
['Obj', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']

>>> Obj.__dict__ # 네임스페이스에 추가된 변수를 확인할 수 있다.
mappingproxy({'__module__': '__main__',
'number': 1,
'__dict__': <attribute '__dict__' of 'Obj' objects>,
'__weakref__': <attribute '__weakref__' of 'Obj' objects>,
'__doc__': None})

>>> Obj.number # 클래스 변수에 접근하기
1
  • 클래스는 객체의 설계도이며, 인스턴스는 이 설계도를 기반으로 메모리에서 생성되어 여러 작업을 수행한다. 이는 하나의 클래스로 여러 객체가 생성될 수 있음을 의미한다.

  • 객체를 생성하는 과정을 인스턴스화라고 하며, 생성된 객체를 인스턴스라고 부른다.

    • 파이썬은 인스턴스마다 별도의 네임스페이스를 가진다. 즉, 클래스의 네임스페이스와 인스턴스의 네임스페이스는 메모리 상에 다른 위치에 있다.

    • 어떤 인스턴스의 멤버에 접근할 때는 인스턴스의 네임스페이스를 먼저 찾아보고 없으면 클래스의 네임스페이스를 찾는다. 만약 여기에도 없다면 AttributeError 예외를 던진다.

    • 참고: 인스턴스 외부에서 멤버를 추가하는 행위를 몽키 패치(monkey patch)라고 한다.

>>> o1 = Obj()
>>> o2 = Obj()
>>> o1.__dict__
{}
>>> o2.__dict__
{}
>>> o1.number = 4 # 인스턴스 변수 생성
>>> o1.__dict__
{'number': 4}
>>> o2.__dict__
{}
>>> o2.number # 클래스 변수에 접근
1
>>> Obj.number # 클래스 변수에 접근
1
>>> o2.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Obj' object has no attribute 'x'
  • 인스턴스는 런타임(run-time) 환경에서 메모리 상의 서로 다른 주소를 가진다. 즉, 클래스는 같더라도 인스턴스는 다를 수 있으며, isinstance(변수명, 클래스명) 함수로 해당 변수에 바인딩된 인스턴스의 클래스 타입을 확인할 수 있다.

    • 동일 객체라는 말은 메모리 주소가 같다는 의미이다.

>>> o1 = Obj()
>>> o2 = Obj()
>>> o1 is o2
False
>>> id(o1), id(o2) # 다른 인스턴스이므로 다른 주소값이 나온다.
(4518943184, 4519522896)
>>> isinstance(o1, Obj)
True
>>> isinstance(o2, Obj)
True

 

클래스 변수와 클래스 메소드

  • 클래스 네임스페이스에 저장된 변수와 메소드로 모든 인스턴스에 대해 독립적이다.

  • 클래스 메소드는 다음과 같이 @classmethod 를 써서 선언하며, 첫번째 인자로 cls 를 받는다.

    • 참고로 첫번째 인자는 파이썬에서 자동으로 넘겨주기 때문에 별도로 매개변수로 넣지 않아도 된다.

class Obj:
    number = 3
    
    @classmethod
    def print_number(cls):
        print('{}: number = {}'.format(cls.__name__, cls.number))
  • 클래스 메소드 내부에서는 cls.변수명 또는 cls.메소드명(...) 으로 클래스 멤버에 접근할 수 있다.

  • 클래스 외부에서는 클래스 이름으로 접근이 가능하다. 

  • 인스턴스에 영향을 받지 않고, 모든 인스턴스가 공유할 필요가 있다면, 클래스 변수 및 메소드로 선언하는 것이 좋다.

>>> class Obj:
...     number = 3
...
...     @classmethod
...     def print_number(cls):
...         print('{}: number = {}'.format(cls.__name__, cls.number))
...
>>> o1 = Obj()
>>> o1.print_number()
Obj: number = 3
>>> o2 = Obj()
>>> o2.print_number()
Obj: number = 3
>>> Obj.number = 10
>>> o1.print_number()
Obj: number = 10
>>> o2.print_number()
Obj: number = 10
>>> o1.number = 1 # 인스턴스 변수에 접근 (클래스 변수가 아니다)
>>> o1.print_number()
Obj: number = 10
>>> o1.number
1
  • 클래스에 종속적이란 의미는 상속 시 상위 클래스와 하위 클래스를 구분한다는 의미이다. 상위 클래스에 선언된 클래스 메소드라 할지라도 하위 클래스의 클래스 변수에 접근한다.

 

인스턴스 변수와 인스턴스 메소드

  • 인스턴스 네임스페이스에 있는 변수와 메소드로, 인스턴스에 종속적이며, 보통 인스턴스 메소드를 일반 메소드라고 한다.

  • 인스턴스에 종속적이기 때문에 인스턴스마다 변수가 가지고 있는 값도 다르며 멤버의 메모리 주소도 다르다.

    • 따라서 인스턴스끼리 구분해서 사용하는 변수나 메소드는 인스턴스 멤버로 선언해야 한다.

  • 클래스 메소드는 클래스 멤버에만 접근 가능하며, 인스턴스 메소드는 인스턴스 멤버와 type(self) 로 클래스 멤버에도 접근할 수 있다. 즉, 클래스 정의 시 변수의 범위(scope)를 먼저 정하고, 메소드가 인스턴스 멤버에 접근해야 한다면 인스턴스 메소드로, 클래스 멤버에만 접근한다면 클래스 메소드로 선언하는 것이 좋다.

    • 예를 들어, 사람마다 이름과 나이가 다르니 이름과 나이는 인스턴스 변수로, 이름을 말하는 행위는 인스턴스 메소드로 선언해야 한다. 사람은 모두 지구에 살고 있기 때문에 거주중인 행성 이름은 클래스 변수로, 다른 행성으로 이사가는 행위는 클래스 메소드로 선언해야 할 것이다.

  • 인스턴스 메소드에서는 첫번째 인자로 self 를 받으며, 메소드 내부에서는 self.변수명 또는 self.메소드명(...)으로 접근한다.

    • 메소드 외부에서는 객체를 바인딩한 변수 이름으로 인스턴스 멤버에 접근한다.

    • self 변수를 통해 현재 메소드를 실행할 인스턴스를 구분한다.

>>> class Obj:
...     def method1(self, name):
...             print(id(self))
...             print('Hi!', name)
...
>>> o1 = Obj()
>>> o1.method1('best')
4519961600
Hi! best
>>> o2 = Obj()
>>> o2.method1('Lisa') # 인스턴스의 주소가 다르게 나온다!
4519522896
Hi! Lisa
  • 접근 제한자: 파이썬에서 공식적인 접근 제한자(Access Modifier)는 없지만, 멤버의 이름에 언더스코어를 추가하여 이를 구분한다. 일반적으로 언더스코어 1개는 private 이며, 2개는 protected, 없으면 public 이다.

    • private 은 현재 인스턴스에서만 사용 가능한 것

    • protected 는 현재 인스턴스가 상속한 부모 인스턴스에도 접근할 수 있는 것

    • public 은 외부로부터 인스턴스 멤버에 접근을 허용하는 것

 

Magic Method

  • 종류가 많기 때문에 전부 설명하기 보다는 자주 사용되는 것들 위주로 나열해보겠다. 자세한 설명은 문서를 참고.

  • __init__(self), __del__(self) 은 생성자와 소멸자, 선언하지 않으면 self만 인자로 받는 메소드가 생성된다. 따라서, 생성할 때 추가로 인자가 필요하다면 두 메소드를 다시 선언해야 된다.

>>> class Obj:
...     def __init__(self, name):
...             self.name = name
...             print('hi', name)
...     def __del__(self):
...             print('bye', self.name)
...
>>> o = Obj('Lisa')
hi Lisa
>>> del o
bye Lisa
  • __iter__(self)이터레이터(Iterator) 객체(=자기자신)를 반환하는 메소드이다. 이 메소드를 선언하면 해당 인스턴스는 이터레이션을 제공하는 이터러블(itarable) 객체가 된다.

  • __next__(self) 은 이터레이터를 제공하는 경우 다음 원소를 반환하는 메소드이다. __iter__(self) 와 함께 선언되어 자주 쓰인다. 이 메소드를 선언하는 해당 인스턴스는 이터레이터(iterator) 객체가 된다.

>>> class IterableObj:
...    def __init__(self, end):
...        self.number = 0
...        self.end = end
...    
...    def __iter__(self):
...        return self
...    
...    def __next__(self):
...        if self.number < self.end:
...            num = self.number
...            self.number += 1
...            return num
...        else:
...            raise StopIteration
...
>>> o = IterableObj(3)
>>> next(o)
0
>>> next(o)
1
>>> next(o)
2
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in __next__
StopIteration

>>> for i in IterableObj(3):
    print(i, end=' ')
...
0 1 2
  • __call__(self) 은 C언어에서 operator() 함수와 동일한 역할을 한다. 파이썬의 함수는 모두 객체이며 function 클래스 타입에서 __call__ 메소드가 실행된 것이다.

>>> class Obj:
...     def __call__(self):
...             print('__call__({})'.format(id(self)))
...
>>> o = Obj()
>>> o()
__call__(4519170592)
  • __enter__(self), __exit__(self, exc_type, exc_val, exc_tb)Context Manager 로 쓰이는 with 구문을 위한 메소드이다. with 문은 프로그램에서 리소스를 사용 후 자동으로 닫아주기 때문에 파일 읽기/쓰기 작업 시 사용이 권장된다. enter은 with 문으로 열기(open) 상태일 때 exit은 닫기(close) 상태일 때 각각 호출된다.

    • 대용량 데이터를 처리할 때, 중간에 끊기는 상황이 자주 생긴다. 이때 처음부터 다시 처리하지 않고 중간부터 시작하길 원한다면 __enter__ 메소드에서 시작할 위치를 찾는 코드 및 처리결과를 기록하는 파일을 여는 코드를 작성하고 __exit__ 메소드에서 처리결과 파일을 close 하도록 처리하면 될 것이다.

>>> class Obj:
...     def __enter__(self):
...         print('__enter__({})'.format(id(self)))
...
...     def __exit__(self, exc_type, exc_val, exc_tb):
...         print('__exit__({})'.format(id(self)))
...
>>> with Obj() as o:
...     print('This is inside with statement!')
...
__enter__(4557148704)
This is inside with statement!
__exit__(4557148704)

 

정적 메소드

  • 클래스 메소드는 cls를 인자로 받아서 호출한 클래스의 네임 스페이스에 접근할 수 있다. 메소드 주소를 확인해보면 동일한 주소값이 나온다.

>>> class Obj:
...     name = 'Lisa'
...
...     @classmethod
...     def get_name(cls):
...         print(id(cls))
...         print('hi', cls.name)
...
>>> o1 = Obj()
>>> o1.get_name()
140513752745968
hi Lisa
>>> o2 = Obj()
>>> o2.get_name()
140513752745968
hi Lisa
>>> Obj.get_name()
140513752745968
hi Lisa
  • 정적 메소드는 클래스와 인스턴스 모두에 독립적인 연산을 수행할 때 사용되는 메소드이며, 첫번째 인자로 아무것도 받지 않는다. 따라서 위와 같이 클래스를 구분하는 행위를 할 수 없다. 마찬가지로 인스턴스 멤버에도 접근할 수 없다.

  • 정적 메소드가 쓰일 때는 클래스 멤버나 인스턴스 멤버에 영향을 주지 않는 연산을 수행하는 경우이다. 단, 클래스 멤버의 경우 외부에서 클래스 이름으로 접근할 수 있듯이 정적 메소드에서 클래스 이름으로 접근할 수는 있다.

    • 정적 메소드도 마찬가지로 외부에서 클래스 이름으로 접근할 수 있다.

  • 메소드 선언 시 위에 @staticmethod 를 쓴다.

>>> class Parent:
...     number = 42 # 42는 Parent 클래스의 네임스페이스에 저장된다.
...
...     @staticmethod
...     def print_number():
...         print(Parent.number) # 항상 Parent 클래스의 네임스페이스에 접근한다.
...
>>> class Child(Parent): # 자식은 부모 클래스의 네임 스페이스에 접근 가능하므로
...     number = 24 # 24는 Child 클래스의 네임스페이스에 저장된다.
...
>>> Child.print_number() # 자식으로 부모 클래스의 정적 메소드를 호출 할 수 있다.
42

 

추상 메소드

  • 추상 클래스는 메소드를 정의만하고 실제 구현은 상속받은 하위 클래스들에게 위임하는 클래스이다.

  • 다음과 같이 abc 모듈을 import 하여 추상 베이스 클래스를 상속받아 구현할 수 있다.

  • 위임할 메소드 위에는 @abstractmethod를 쓴다.

  • 상속받은 하위 클래스들은 반드시 추상 메소드를 구현해야 한다.

>>> from abc import ABC, abstractmethod
>>> class BaseClass(ABC):
...     @abstractmethod
...     def fn(self):
...         raise NotImplementedError
...

>>> class MyObject(BaseClass):
...     def fn(self):
...         pass
...
>>> o = MyObject()
>>> o.fn()

>>> class MyObject(BaseClass):
...     pass
...
>>> o = MyObject()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyObject with abstract methods fn
  • 주로 여러 인스턴스가 공통된 행위를 해야할 때 추상 클래스에 해당 행위(=메소드)들을 모아놓는다.

  • 여러 디자인 패턴을 구현하는데 많이 활용되며 자바의 인터페이스와 같은 역할을 한다.

  • 흔히 OOP에서는 인스턴스에서 자주 변하는 부분에 대해 느슨한 결합을 강조하는데 이는 메소드의 구현을 강제하면서 동시에 실제 구현을 하위 클래스에 위임하는 성질과 찰떡궁합이다.

>>> import abc
>>> class Person(abc.ABC):
...     @abc.abstractmethod
...     def greet(self):
...         pass
...
>>> class Doctor(Person):
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         print('Hi, I am', self.name, 'and I am a doctor!')
...
>>> d = Doctor('Lisa')
>>> d.greet()
Hi, I am Lisa and I am a doctor!

>>> class Student(Person):
...     def __init__(self, age):
...             self.age = age
...
>>> s = Student(17) # 추상 메소드를 정의하지 않을 경우 TypeError 발생
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Student with abstract methods greet

 

LEGB 규칙

  • 파이썬에서는 변수의 영역(scope)을 다음과 같이 나누며, 함수에서의 우선순위는 지역변수 > 외부 함수의 지역변수 > 전역변수 > 내장형 이다.

    • Local: 함수 안의 범위

    • Enclosed function locals: 내부 함수 밖에, 외부 함수 안의 범위

    • Global: 모듈 범위로, .py 파일 내부의 모든 범위

    • Built-in: 인터프리터 전역적으로 사용가능한 내장형 함수 및 타입을 의미

 

클로저(Closure)

  • 함수 안에 함수를 선언하는 것으로, 바깥 함수를 외부 함수(outer), 안쪽 함수를 내부 함수(inner) 또는 클로저(closure)라고 한다.

  • 내부 함수에서는 외부 함수의 지역변수(매개변수 포함)에 접근할 수 있다.

    • 단, 내부 함수에서 외부 변수의 쓰기 작업을 수행하려면 nonlocal 키워드를 사용해야 한다.

    • 읽기는 자유롭게 가능하다.

>>> def outer(num):
...     num += 3
...     def inner():
...             nonlocal num
...             num *= 3
...             return num
...     return inner
...
>>> f = outer(1)
>>> f()
12
>>> def outer(num): # nonlocal 키워드를 쓰지 않을 경우 다음과 같은 예외가 발생한다.
...     num += 3
...     def inner():
...             num *= 3
...             return num
...     return inner
...
>>> f = outer(1)
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in inner
UnboundLocalError: local variable 'num' referenced before assignment
  • 메모리 관점: 외부 함수가 호출되면 외부 함수 자체는 메모리에 남아있지만(함수 주소) 변수들은 모두 제거된다. 그러나 내부 함수는 외부 함수 범위의 변수들을 사용하면 자신의 영역에 별도로 저장하기 때문에 f = outer(1) 이후에도 nums 변수는 메모리(내부 함수 영역)에 남아 있는 것이다.

    • 파이썬에서 함수는 일급 객체로 실제로 type(outer) 를 해보면 <class 'function'>이 출력된다. 따라서, 위와 같은 정보를 확인하려면 다음과 같이 객체의 멤버 변수나 함수를 이용하면 된다.

>>> type(outer)
<class 'function'>
>>> f.__closure__ # cell 이라는 객체로 튜플 타입으로 저장되어 있다.
(<cell at 0x1061e49d0: int object at 0x10607bbe0>,)
>>> type(f.__closure__[0])
<class 'cell'>
>>> f.__closure__[0].cell_contents # 변수에 저장된 값이 메모리에 남아있다!
12
  • 외부 함수에는 여러 개의 내부 함수(closure)를 선언할 수 있고 내부 함수끼리 서로 호출이 가능하다.

    • 그러나, 외부 함수의 closure 변수는 모든 내부 함수의 정보를 가지고 있지 않다.

    • 다음과 같이 내부 함수가 여러 개일 경우 호출되는 순서에 따라 내부 함수들이 저장되며, 내부 함수 A가 내부 함수 B를 호출하면 내부 함수 A의 __closure__ 에 내부 함수 B가 저장된다.

    • 사용되지 않는 내부 함수는 저장되지 않는다.

>>> def fn1():
...     def inner():
...             return 1
...     def inner2():
...             n = inner()
...             return n * 3
...     def inner3():
...             n = inner2()
...             return n + 5
...     return inner3
...

>>> x = fn1()
>>> x()
8
>>> x
<function fn1.<locals>.inner3 at 0x106303820>
>>> x.__closure__
(<cell at 0x1062dd400: function object at 0x106303940>,)
>>> x.__closure__[0].cell_contents # inner3 함수 객체 안에 inner2 함수 객체가 저장되어 있다.
<function fn1.<locals>.inner2 at 0x106303940>
>>> x.__closure__[0].cell_contents.__closure__
(<cell at 0x1062dd430: function object at 0x106303af0>,)
>>> x.__closure__[0].cell_contents.__closure__[0].cell_contents # 마찬가지.
<function fn1.<locals>.inner at 0x106303af0>

 

데코레이터(Decorator)

  • 클로저가 가장 많이 사용되는 코딩 패턴으로는 데코레이터(Decorator)가 있다. 이름에서 알 수 있듯이 코드에서 무언가를 꾸며주는 역할을 한다. 실제로 객체 지향 패턴에도 데코레이터 패턴이라는게 있는데 파이썬에서는 클래스로 선언하는 것보다 함수로 선언하는 것이 많이 사용된다.

  • 특징은 함수를 인자로 받아서 내부 함수로 한번 감싸서(wrapping) 반환을 한다는 점이다.

  • 보통 둘 이상의 함수에 대해 추가적인 작업이 선행되어야 하는 경우 자주 쓰인다.

    • 예를 들어, 데이터를 넘기기 전 공통된 전처리가 필요한 경우, 로깅, 리다이렉트, 성능 테스트

def decorator_fn(original_fn):
    def wrapped_fn(*args, **kwargs):
        if len(args) == 2:
        	args = (args[0] + args[1], args[0] * args[1])
        return original_fn(*args, **kwargs)
    return wrapped_fn

@decorator_fn
def fn1():
	print("HI")

@decorator_fn
def fn2(a, b):
	print(a, b)

fn1() # HI
fn2(2, 3) # 5, 6
  • 위에서 @을 썼는데 이는 fn = decorator_fn(fn1) 과 동일하게 동작하나 후자는 가독성이 낮아 잘 쓰이지 않는다.

  • 클래스에서는 __call__() 이라는 메소드를 이용해서 다음과 같이 선언할 수 있다. 위와 동일하게 동작한다.

class DecoratorClass:
	def __init__(self, original_fn):
    	self.fn = original_fn
        
    def __call__(self, *args, **kwargs):
    	if len(args) == 2:
        	args = (args[0] + args[1], args[0] * args[1])
        return self.fn(*args, **kwargs)
        
@DecoratorClass
def fn1():
	print("HI")

@DecoratorClass
def fn2(a, b):
	print(a, b)

fn1() # "HI"
fn2(2, 3) # 5, 6

 

+ Recent posts