파이썬 코딩의 기술 책 5장 내용인 클래스와 인터페이스에 대해 리뷰를 하겠다. collections.abc와 믹스인에 대해서는 다루지 않겠다. 

  1. 훅 , __call__ 
  2. 애트리뷰트
  3. @classmethod
  4. super
  5. 번외 - collections.abc

 


 

1. 훅

훅은 파이썬 내장 API를 호출할 때, 동작을 원하는 대로 바꿀 수 있는 함수이다. 예시를 보면 이해하기 쉬운데, 다음과 같이 sort에서 들어가는 len 함수가 훅이라고 할 수 있다.

tmpl = ['a','bb','ccc','abcd']
tmpl.sort(key = len) # len 이 훅!

 

또 다른 예시는 defaultdict에서 사용할 때다. defaultdict에서 딕셔너리가 변경될 때마다 print 하는 함수를 만들고, 이를 훅으로 사용할 수 있다. 

from collections import defaultdict
def log_print():
    print('revised')
    return

dic = defaultdict(log_print, {}) # log_print가 훅!
dic['a'] = 1 # -> revised
dic['b'] = 2 # -> revised

 

위 defaultdict에서의 훅으로 작은 클래스를 만들어 사용할 수도 있다. defaultdict에 값이 추가될 때 마다 count를 증가시키는 훅을 만든다고 가정하자.

class Count:
    def __init__(self):
        self.added = 0 #이 added를 애트리뷰트라고 함
        
    def missing(self):
        self.added += 1
        return
        
my_count = Count()
dic = defaultdict(my_count.missing, {})

 

이때 Count 클래스 내 __call__메서드를 사용하는 방법으로 바꿔줄 수 있다.  이 __call__은 객체를 함수처럼 호출할 수 있게 되기 때문에, 아래와 같이 코드를 생성할 수 있다. 

class Count2:
    def __init__(self):
        self.added = 0
        
    def __call__(self): #위 코드에서는 missing이었던 부분을 __call__로 치환
        self.added += 1
        return
        
my_count = Count2()
dic = defaultdict(my_count, {}) #callable 객체이기 때문에!
dic['a'] = 1
dic['b'] = 2
print(my_count.added) # -> 2 !

 

 

 

 

2. 애트리뷰트

훅 예제에서 만든 위 클래스에서는 self. 를 이용해 애트리뷰트를 만들었는데 이름의 유형을 간단 정리하면 아래와 같다.

self.added self.__added
공개 애트리뷰트 비공개 애트리뷰트
밑줄 두개, 비공개 필드
클래스 외부에서 접근 X

비공개 애트리뷰트는 자식 클래스에서도 (원래는) 접근을 못한다.

class Myclass:
    def __init__(self):
        self.__added = 1


class MyChildclass(Myclass):
    def get_added(self):
        return self.__added

 

그 이유는 파이썬 컴파일러가 비공개 애트리뷰트에 접근할 때 다음과 같은 접근 코드를 쓰기 때문이다. 자식 클래스 이름이 코드로 들어가기 때문에, 부모 클래스의 __added를 접근하지 못하는 것이다. 즉, __added의 접근 이름은 Myclass__added이란 것!

#하위 클래스에서 비공개 어트리뷰트 접근할 때

c = MyChildclasss()
c.get_added() #-> error! 접근 코드 : _MyChildclass__added

그렇기 때문에 접근 코드 이름을 하위 클래스인 _MyChildclass가 아닌 부모 클래스로 바꿔주면 된다.

assert c._Myclass__added == 1

 

 

 

3. @classmethod

cf.) 다형성 : 클래스가 자신에게 맞는 유일한 메서드 버전을 구현할 수 있다. 이를 활용하면 하위 클래스 객체를 만들거나 연결할 수 있는 제너릭 방법을 사용할 수 있다. 

 

class Myclass:
    @classmethod       # 클래스매서드 정의
    def generate_inputs(cls, config): 
    	# 파라미터인 cls, config는 하위클래스의 파라미터
        # 새로운 Myclass 인스턴스를 생성
        raise NotImplementedError
        
        
class Subclass(Myclass): #Myclass의 하위클래스
    #...
    def generate_inputs(cls, config):
        #...

위 예시코드에서 cls()는 제너릭 생성자로 작용한다. 간단하게 말하자면 self로 메서드를 호출하는 대신 cls을 사용하여 접근하는 것이다. 

그렇다면 어떤 경우에 굳이 왜 인스턴스 매서드(self로 접근할 수 있었던)가 아닌 클래스 메서드를 사용하는 걸까? 바로 다형성과 제너릭이다.

 

클래스 상속으로 다형성을 구현할 수는 있지만, 이 방법은 제너릭(타입에 상관없이 작동하는)하지 않다. 이를 위해 클래스 메서드 다형성을 사용해 클래스 전체에 적용한다.

 

 

 

 

4. super

cf.) __init__  : 이 메서드는 부모 클래스를 초기화하기 위해 자식클래스에서 쓰는 매서드이다. 참고로 파이썬에 있는 유일한 생성자 매서드다. 부모 클래스가 여러 개면 이 init의 작동 순서가 정해져 있지 않기 때문에 문제가 생길 수 있다. 

class Myclass:
    def __init__(self, value):
        self.value = value
        
        
class Myclass1:
    def __init__(self):
        self.value += 5
 

class Myclass2:
    def __init__(self):
        self.value *= 5


class Subclass(Myclass, Myclass1, Myclass2): #부모클래스 3개를 상속
    def __init__(self, value):
        Myclass.__init__(self, value)
        Myclass1.__init__(self)
        Myclass2.__init__(self)
        
        
sub = Subclass(value = 3) # 과연 self.value의 값이 어떻게 나올까?

 

 

super는 pytorch를 사용하는 사람들은 많이 봤을 법한 내장 함수로, 생성자인 __init__ 함수 아래에 사용하는 방법이다. super는 여러개 클래스를 상속받는 상황일 때 상위 클래스는 한 번만 호출하게끔 보장한다. 이에 따라 위 코드는 아래와 같이 다시 작성할 수 있다.

class Myclass:
    def __init__(self, value):
        self.value = value
        
        
class Myclass1(Myclass):
    def __init__(self, value):
        super().__init__(value)
        self.value += 5
        

class Myclass2(Myclass):
    def __init__(self, value):
        super().__init__(value)
        self.value *= 5
        

class SuperSubclass(Myclass1, Myclass2): #부모클래스 2개 + 최상위클래스 1개를 상속
    def __init__(self, value):
        super().__init__(value)
        
        
sub2 = SuperSubclass(value = 3) # 3 * 5 + 5 == 20

mro_str = '-> '.join(repr(cls) for cls in SuperSubclass.mro())
print(mro_str) # <class '__main__.SuperSubclass'>-> <class '__main__.Myclass1'>-> <class '__main__.Myclass2'>-> <class '__main__.Myclass'>-> <class 'object'>

참고로 super init 호출 순서는 MRO 정의를 따르는데, 이 MRO는 최상위 클래스부터 object 까지의 순서로 호출하지만, 작업은 호출된 순서의 역순으로 가게 된다. 즉, Myclass, Myclass2, Myclass1 순서로 실제 value가 계산되는 것이다. 

 

super 안 파라미터는 2개를 받을 수 있는데, 첫번째는 접근하고 싶은 MRO 뷰를 제공할 부모 타입(주로 내가 부를 클래스의 이름을 적는다), 다른 하나는 방금 지정한 MRO 뷰에 접근할 때 사용할 인스턴스(self)이다. 그러면 아래와 같이 SuperSubclass를 다시 정의할 수 있다. 

 

class SuperSubclass(Myclass1, Myclass2): #부모클래스 3개를 상속
    def __init__(self, value):
        super(SuperSubclass, self).__init__(value) 
        # SuperSubclass : MRO 뷰를 제공할 부모 타입
        # self : 위에 선언한 MRO 뷰 제공 타입에 접근할 인스턴스
sub2 = SuperSubclass(value = 3)

 

 

5. 번외

cf.) collection.abc : 커스텀 컨테이너 타입에 잘 메서드가 구현되어 있는지 확인하고, 실수한 부분을 알려주는 모듈이다. 아래와 같이 불러올 수 있다.

from collections.abc import Sequence
class Myclass(Sequence):
    ...

 


 

이번에는 클래스를 활용할 때 사용할 수 있는 여러 기능에 대해 알아보았다.  훅 함수의 제대로 된 정의, 공개 애트리뷰트와 비공개 애트리뷰트의 차이, 다형성을 위한 @classmethod로 제너릭을 부여하는 올바른 사용법과 ML 엔지니어라면 친숙할 super 에 대해 기술했다. 코드를 잘 짜는 ML 엔지이너가 되기, 힘내자! 공부한 모든 것을 실전에서 잘 활용할 수 있기를 바라며.

728x90

+ Recent posts