클래스와 인스턴스

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

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

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

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

>>> 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

 

+ Recent posts