파이썬 메타클래스(metaclass)// 인프런 파이썬 강의 Level 3 (3-1)

2022. 1. 16. 23:31
  • 인프런의 '프로그래밍 시작하기:파이썬(Level3)'의 내용을 따라가면서 정리
  • (3-1)에서는 다음에 관하여 다룸
    • 파이썬 메타클래스

1.메타클래스 

  • 인스턴스는 클래스를 기반으로 한다. 즉 인스턴스의 원형은 클래스이다
  • 그런데 비슷한 원리로, 클래스 역시도 원형이되는 클래스가 있으며 이를 '메타클래스'라고 한다.
  • 메타클래스를 잘 조작하면, 새로이 생성되는 클래스들이 어떠한 특성을 가질 수 있도록 의도를 담을 수 있음
  • 프레임워크에 필수. ex)Django는 메타클래스가 사용되는 대표적인 예시

 

클래스 == 객체(object)

  • 우리는 인스턴스가 객체이지만, 해당 객체가 어떠한 클래스에 의하여 탄생했다는 것을 전제하기 위하여 '인스턴스'라고 표현을 한다.
  • 그런데, 인스턴스뿐 아니라 클래스들도 다른 클래스로부터 탄생된 거라면 결국 인스턴스와 크게 다를바가 없다
  • 그러한 관점에서 보면 클래스역시도 다른 클래스로부터 탄생되는, 인스턴스와 비슷한 특징을 지닌 '객체(object)'이다
  • 모든것은 '객체(object)'다 라고 주장하는 파이썬의 맥과 일맥상통한다.

 

__class__  // type

  • 우선 다음의 예제를 보자.
  • __class__ 메소드를 활용하면 해당 객체가(클래스를 포함하여) 어떤 클래스에 속하는지(>>원형이 되는 클래스가 무엇인지)가 출력이된다. 
  • 또 type을 활용하면 인자로 받는 것의 자료형을 알 수 있다.
#클래스 생성
class SampleA():
    pass

#인스턴스 선언
a = SampleA()

###출력###
print("a.__class__:",a.__class__)
print("type(a) :",type(a))

  • 둘다 동일하게 나온다. 
  • a.__class__는 'a는 SampleA에 의해 탄생한 객체'
  • type(a)는 'a는 SampleA의 클래스에 속해있는 자료형'이다! 정도로 생각하면 될듯하다

 

  • 위의 예제가 어색해서 그렇지, 사실은 아주 많이 봐온 것들이다.
  • int나 list, 그외에 tuple이나 dict 모두 클래스이다.
    • 이건 실제로 vscode에서 ctrl을 누르고 int , list등을 클릭하면 builtin 파일로 이동하는데 해당 파일에서 int나 list 등의 클래스가 선언되어 있는 것을 확인할 수 있음
  • 따라서 밑의 예제처럼 ' ex) i = int(3) ' 변수를 선언하는 것 자체가 클래스에 의해 생성되는 객체인 인스턴스를 생성하는 것이다.
  • 따라서 인스턴스인 선언된 변수를(여기선 i와 l) ,type에 넣거나 __class__ 메소드를 활용하면 해당 클래스가 출력되는 것이다.
#int
i = int(3)
print(i)
print(i.__class__)
print(type(i))

print("-----구분선-----")

#list
l = list([1,2,3,4,5])
print(l)
print(l.__class__)
print(type(l))

 

메타클래스 

  • 앞서 언급했듯, 인스턴스와 클래스의 관계처럼, 클래스에게도 원형이 되는 클래스인 메타클래스가 존재한다. 
    • 주의 : 원형이 되는 메타클래스와 상속은 다른 것이다!
  • 메타클래스를 확인하는 방법은 간단함.
    • 인스턴스.__class__ 를 하면 인스턴스의 원형이 되는 클래스가 나옴
    • 그러면 인스턴스.__class__.__class__를 하면 클래스의 원형이 되는 클래스인 메타클래스가 나온다.
    • 인스턴스.__class__.__class__ == type(인스턴스.__class__) 
#클래스 생성
class SampleA():#object를 받는 것
    pass

#인스턴스
a = SampleA()

##출력##
print("a.__class__.__class__:",a.__class__.__class__)
print("type(a.__class__) :",type(a.__class__))

  • 놀랍게도 class 'type'이 출력된다. 즉 클래스들의 원형인 메타클래스는 바로 type이다
  • list같은, 파이썬 내부적으로 이미 구현되어 있는 클래스에 적용해봐도 동일하게 결과가 나온다
#list
l = list([1,2,3,4,5])
print("l.__class__.__class__ :",l.__class__.__class__)
print("type(l.__class__) :",type(l.__class__))

 

  • 그러면 type의 메타클래스는 뭘까? type의 원형이 있을까? 없다. type의 메타클래스는 type이다.
print("type.__class__ :",type.__class__)
print("type(type) :",type(type))

 

2.메타클래스 구현 방법1) Type을 활용한 동적 클래스 생성

  • 결국 모든 클래스의 원형은 type이다. 즉 type은 단순히 자료형을 알아보는 것 외에도, 메타클래스를 구현하는데 사용할 수 있음
  • type을 활용해 메타클래스를 두가지 방법으로 구현할 수 있음 두가지 용도로 사용할 수 있음
    • 방법 1) : type을 활용하여 동적으로 클래스를 생성하는 방식 
    • 방법 2) : type을 상속시켜, 클래스의 원형이 되는 메타클래스를 구현하는 방식
    • 선생님은 두가지다 메타클래스를 구현하는 방법이라고 했는데, 개인적으로 2)이야말로 진짜 메타클래스를 구현하는 것이 아닌가 싶다.
    • 1)도 클래스를 구현하는 방식에 영향을 끼치므로 '메타클래스'라는 개념과 어느정도 접점이 있긴한데.. 개인적으로 좀 체크를 해봐야할듯

 

Type을 활용한 동적 클래스 생성(방법1) - 예제 1

  • 'type('클래스이름', 상속받을 클래스(튜플형태), 네임스페이스(딕셔너리형태)) '와 같은 방식으로 클래스를 생성
  • 네임스페이스를 딕셔너리형태로 받는데, 네임스페이스는 실제로 딕셔너리 형태
  • 상속받을 클래스를 넣어주지 않으면 object를 default값으로 받음. 이건 type을 활용한 클래스 생성방식뿐 아니라 정적인(일반적인) 클래스 생성방식에서도 동일함. object를 상속받기 때문에, 따로 설정해주지 않아도 다양한 메소드들이(object의 메소드들이) 자동으로 해당 클래스의 속성값으로 들어가는 것임

 

  • 아래의 예제는 상속할 부모클래스인 SampleA를 만들고, 일반적(정적)방법으로 생성한 SampleB와 type을 활용해 생성한 클래스인 SampleC를 비교하였음
  • SampleB와 SampleC의 인스턴스를 만들고, 메소드와 변수값(상속받은 것들 포함)을 출력해봄
  • type을 활용해서 클래스를 생성시 lambda를 사용하였음. 효율적인 방법
#상속할 부모 클래스
class SampleA():#이렇게 하면 SampleA(object)임. 즉 알아서 object클래스를 상속받게 되어 있음. 그래서 따로 설정해주지 않아도 엄청나게 많은 메소드들이 자동으로 생기는 것임
    attrA = 30
    def add(self,m,n):
        return m + n
    
#일반적인(정적인) 클래스 생성
class SampleB(SampleA):
    attrB = 100
    def mul(self,m,n):
        return m*n

#type을 활용한 동적인 클래스 생성
SampleC = type("SampleC",#클래스 이름
               (SampleA,),#상속받을 클래스를 튜플혀앹로받음
               dict(attrC = 100,mul = lambda self,x,y : x*y)#네임스페이스 > 딕셔너리형태로 받음
               )


#####출력######

print("-----B-----")        
b = SampleB()
print(b.attrA)
print(b.attrB)
print(b.add(5,5))
print(b.mul(5,5))   

print("-----C-----")    

c = SampleC()
print(c.attrA)
print(c.attrC)
print(c.add(5,5))
print(c.mul(5,5))

  • 동일한 결과가 출력된다!

 

Type을 활용한 동적 클래스 생성(방법1) - 예제2

  • 네임스페이스에 추가해줄 함수를 구현해준 후, 클래스 생성시에 함께 넣어주었음
###네임스페이스에 넣을 함수들###
def cus_mul(self,d):
    for i in range(len(self)):
        self[i] = self[i]*d
        
def cus_replace(self,old,new):
    while old in self:#old가 self안에 있다면
        self[self.index(old)] = new


###type을 활용한 동적 클래스 생성###
CustomList1 = type(
                'CustomList1', #이름
                (list,), #상속(base)
                {'description' : '커스럼 리스트1', #네임스페이스
                'cus_mul' : cus_mul,
                'cus_replace' : cus_replace}
            )

#인스턴스화
c1 = CustomList1([1,2,3,4,5,6,7,8,9])

###출력###
print("c1 description :",c1.description)
print("c1 :",c1)
c1.cus_mul(100)
print("cus_mul 적용 후 :",c1)
c1.cus_replace(500,777)
print("cus_replace 적용 후 :",c1)

  • 코드에서 한가지 헷갈리는 부분은 c1 = CustomList1([1,2,3,4,5]) 이부분임. __init__을 따로 설정해둔것도 아니고, [1,2,3,4,5]를 어디다가 받는거지?
    • 우선은 list 클래스를 상속받았기 때문에 가능하다.. 정도로 생각해두면 됨
    • 앞서 말했듯 list역시 클래스임. 또 그 클래스인 list를 활용해서 우리는 일상적으로 A = list([1,2,3,4,5])와 같은 코드를 많이 구현함. 
    • 그렇기 때문에 list클래스를 상속받은 customlist도 'c1 = CustomList1([1,2,3,4,5])' 처럼, list와 동일한 방식으로 인자를 받을 수 있음.
    • 즉, 어떤 메커니즘을 통해서 [1,2,3,4,5]를 인자로 받는지는 몰라도, list클래스도 그렇게 사용할 수 있었으니 list를 상속받은 customlist도 동일한 방식으로 인자를 받을 수 있따!로 생각해두고 넘어가면 됨.
    • 이부분이 정확히 어떤식으로 이루어지는지.. 즉 Customlist가 어떻게 인자로 [1,2,3,4...9]를 받을 수 있고 이게 어떻게 전달되는지는 좀 찾아봐야 할듯함..

 

3.메타클래스 구현 방법 2) : type상속 > 메타클래스 구현

  • 메타클래스를 구현하는 방법 중 두번째 방법 : type을 상속시켜, 클래스의 원형이 되는 메타클래스를 구현하는 방식
  • 바로위의 예제2를 두번째 방법(type 상속 > 메타클래스 구현)으로 구현해보고자 함
  • 한번 더 방법1)과 방법2)의 차이에 대해 설명하면
    • 방법 1) : type을 활용해 입맛대로 클래스를 생성
    • 방법 2) : type을 상속시켜 클래스를 입맛대로 만들어낼 수 있는 메타클래스를 구현 > 만든 메타클래스로 클래스를 생성
    • 따라서 네임스페이스에 추가할 내용도 클래스를 생성할 때 넣어주는게 아니라, 메타클래스한테 넣어줌. 메타클래스는 받은 내용에 따라 클래스를 생성함. 또한 그렇기 때문에 클래스가 생성될 때에는 네임스페이스가 비어있음.(메타클래스에서 만들어서 네임스페이스에 넣어주니까 비어있어도 메타클래스의 내용이 채워짐)

 

type 클래스의 __new__

  • type(type class)의 매직메소드 __new__를 사용하면 새 인스턴스를 생성할수 있도록 파이썬 내부적으로 정의되어 있음.
  • 따라서, 클래스를 만드는 모든 메타클래스는 반드시 type class의 매직메소드 __new__를 사용해야 한다.(이걸 써야 인스턴스가 생성될 수 있음)
    • __new__ 메소드는, 해당 메소드가 포함된 클래스가 인스턴스 객체(클래스 포함)를 생성할 때 자동으로 호출되는 메소드이다.
    • 앞의 예제에서 type을 활용하여 클래스를 동적으로 생성할 수 있는 것도, 클래스를 생성할 때 type클래스의 __new__메소드가 작동하기 때문에 가능한 것이다.
    • type을 상속받아 만들어지는 메타클래스는 자동적으로 type의 __new__메소드를 상속받는다.(그래서 type를 상속받으면 type처럼 클래스를 동적으로 만들 수 있음)
    • 그러나 만약 __new__메소드를 오버라이딩 해주었을 경우 반드시 type의 __new__를 활용해야 한다.
class CustomListMeta(type):
    def __new__(metacls,name,bases,namespace):
        namespace['description'] = "커스텀 리스트2"
        namespace['cus_mul'] = cus_mul
        namespace['cus_replace'] = cus_replace
        
        return super().__new__(metacls,name,bases,namespace)
        #return type.__new__(metacls,name,bases,namespace)
  • type을 상속받는 메타클래스에서 __new__메소드를 오버라이딩
  • 그러면 type의 __new__메소드는 상속되지 않아, 해당 메타클래스에서 type의 __new__는 작동하지 않음
  • 따라서 type class의 __new__메소드를 리턴해줘서, type class의 __new__를 사용할 수 있게 하였음

 

  • 암튼 상속을 받은 return으로 type의 __new__를 참조를 하든간에, 인스턴스(클래스 포함)를 생성하려면 type의 __new__를 써야함
  • type 클래스의 __new__ 메소드는 다음과 같이 코드가 구성된다 :  '__new__(metacls, name, bases, namespace)' 
    • metacls의 인자로 클래스를 생성하는 메타클래스가 자동으로 들어감
    • 그외는 이름(name), 상속(base), 네임스페이스인데, 이는 앞서 type을 활용한 동적클래스 생성에서 클래스 생성시에 받는 인자와 동일함
    • 즉, type을 활용해서 동적클래스를 생성할때도 type의 __new__함수가 사용되고, type이 메타클래스이므로 자동으로 metacls의 인자로 들어가고, 나머지 정보를 받아서 클래스를 생성하는 것이다.

 

메타클래스 구현 방법2) - 예제

  • 아래의 예제에서는 메타클래스에서 __new__메소드를 오버라이딩 하였으므로, type의 __new__를 사용하기 위해 super().__new__를 리턴해줌
  • 또한 type의 __new__에 필요한 정보를 그대로 전달해주기 위해, 오버라이딩한 __new__메소드에서도 똑같이 인자를 받음 :  '__new__(metacls, name, bases, namespace)' 
  • 메타클래스(CustomListMeta)의 __new__메소드에서  네임스페이스에 값을 넣어 주었음 > 클래스를 생성할때 네임스페이스를 지정해주지 않아도 메타클래스의 __new__에서 저장해둔 속성값들이, 클래스의 네임스페이스에 속성(함수나 변수)들이 저장됨
###네임스페이스에 넣을 함수들###
def cus_mul(self,d):
    for i in range(len(self)):
        self[i] = self[i]*d
        
def cus_replace(self,old,new):
    while old in self:#old가 self안에 있다면
        self[self.index(old)] = new


###type상속을 활용한 메타클래스 구현###
class CustomListMeta(type):
    def __new__(metacls,name,bases,namespace):
        namespace['description'] = "커스텀 리스트2"
        namespace['cus_mul'] = cus_mul
        namespace['cus_replace'] = cus_replace
        
        return super().__new__(metacls,name,bases,namespace) 
        #super를 활용해 부모클래스의 __new__를 가져오는 것. 따라서
        #'return type.__new__(metacls,name,bases,namespace)' 이렇게 해도 동일함

###상속된 메타클래스를 활용한 클래스 생성###
CustomList2 = CustomListMeta("CustomList2",#이름
                            (list,),#상속(base)
                            {}#네임스페이스는 비어있음. but 메타클래스의 __new__메소드에 의해 채워질것임
                            )

#인스턴스
c2 = CustomList2([1,2,3,4,5,6,7,7,8,9])

#####출력#####
print("c2 description :",c2.description)
print("c2 :",c2)
c2.cus_mul(100)
print("cus_mul 적용 후 :",c2)
c2.cus_replace(500,777)
print("cus_replace 적용 후 :",c2)

  • 바로위의 예제와 동일한 결과가 출력된다.

 

4. 인스턴스 생성 내부동작 : __new__ , __init__,__call__

  • 클래스로부터 인스턴스가 생성되는 과정을 알아보고자 한다.
    • 참고로 몇 번 언급했듯이 , 클래스역시 메타클래스로부터 탄생하는 인스턴스 객체이다.
    • 즉, '클래스의 인스턴스가 생성되는 동작을 이해한다'는 '메타클래스로부터 클래스가 생성되는과정을 알아본다 '와 거의 일맥상통한다.
  • 어떤 클래스로부터 인스턴스가 생성될 때 다음의 메소드가 자동 & 순서대로 클래스로부터 호출이 된다.
    • 메타클래스의 __call__ : 클래스의 __new__와 __init__이 차례로 실행되도록 함
    • 클래스의 __new__ : 클래스 인스턴스를 생성(메모리 할당). 즉 인스턴스를 생성
    • 클래스의 __init__ : 생성된 인스턴스의 초기화값 설정. 즉 생성된 인스턴스에 다양한 변수들의 초기값 설정

 

 __new__ , __init__

  • __new__와 __init__을 먼저 생각해보자
  • 인스턴스가 처음 생성될때 __init__을 자주 사용하다보니, __init__이 생성자라고 생각할 수 있다. 그러나, 생성 그자체는 __new__가 한다. 
    • 즉 일반적으로 우리가 모를뿐 __new__ 메소드에 의해 인스턴스가 생성된 이후,  우리가 보통 설정한 __init__이 실행되어 초기값이 설정되는 것이다.
    • 그리고 이 __new__는 모든 클래스의 시초인 type의 __new__임(소름 ㄷㄷ) . 왜냐면 type은 모든클래스의 메타클래스이고, 결국 모든 클래스의 생성에 관여하는 것이 type의 __new__이기 때문임. type을 상속받아 메타클래스를 새로이 만든다하더라도, 그 메타클래스에서도 결국 type의 __new__를 어떤식으로든(super등) 활용하기 때문에, 결국 모든 클래스의 인스턴스는 type의 __new__에 의해 탄생하는 것임.
    • 참고 : A가 B라는 클래스의 메타클래스라는 것과, A가 B라는 클래스의 부모클래스라는 것은 다른 것임

 

__call__

  • 이제 __new__와 __init__은 대충 알겠음
  • 그럼 __call__은 왜필요하고 언제 사용되는 것일까?
  • 우선 __call__에 대해 집고 넘어가자(내가 모르니까..ㅋㅋ) 
    • 일반적으로 __call__은 해당 객체가 호출될 때 자동으로 수행되는 메소드이다.
    • 객체의 호출에 의해 사용될 경우, 자동으로 객체를 self(첫번째 인자로) 받음
class Ex():
    def __init__(self,x,y):
        self.x = x
        self.y = y
        print(f'__init__ called.\nself.x = {self.x}\nself.y = {self.y}')
        
    def __call__(self,x,y):
        self.x = x
        self.y = y
        print(f'__init__ called.\nself.x = {self.x}\nself.y = {self.y}')       
        
#__init__실행      
ex = Ex(1,2)
print("ex.x : ",ex.x)
#__call__실행
ex(10,20)
print("ex.x : ",ex.x)

  • 인스턴스 생성시 __init__에 의해 self.x 와 self.y의 값이 각각 1,2로 설정됨
  • 그후 인스턴스가 호출될 때, __call__에 의해 self.x와 self.y의 값이 10과 20으로 설정됨
  • __call__메소드가 호출될 때, 자동으로 인스턴스를 self로 받았음

 

메타클래스의 __call__

  • __call__메소드가, 객체가 호출될때 자동으로 사용되는 메소드라는 건 알겠음
  • 그럼 아까 앞에서 말했던, 인스턴스를 생성할 때 메타클래스의 __call__이 먼저 호출되서, 클래스의 __new__와 __init__이 실행되도록 한다고 한게 뭔지 한번 확인해보자.
class ExMetaClass(type):
    def __new__(metacls,name,base,namespace):
        print('metaclass.__new__ called')
        return super().__new__(metacls,name,base,namespace)
    
    def __init__(cls, *args, **kwargs):
        print('metaclass.__init__ called')
        super().__init__(*args,**kwargs)
        
        
    def __call__(cls, *args, **kwds):
        print('metaclass.__call__ called')
        return super().__call__(*args,**kwds)
    
class ExClass(metaclass = ExMetaClass):
    def __init__(self):
        print("class.__init__ called")
        
    def __new__(self):
        print('class.__new__ called')
        return super().__new__(self)
    
    def __call__(self):
        print("class.__call__ called")
        
print("=======구분선=========")	
Exinstance = ExClass()

  • 우선 먼저 집고 넘어갈 것이 있다.
  • 출력된 것을 보면, ExClass의 인스턴스인 Exinstance가 생성되기도 전에,  ExMetaclass의 __new__와 __init__이 호출된 것을 확인할 수 있다.
  • 즉, 메타클래스에 의한 클래스 생성은, 그냥 클래스(여기선 ExClass)에 대한 코드가 끝나는 순간 바로 생성된다는 것이다.
  • 메타클래스(ExMetaClass)의 객체인 클래스(ExClass)가 생성되었다는 것은 > __new__메소드가 호출되었다는 것. 그리고  그 후에 Metaclass의 __init__클래스가 호출되어 ExClass의 변수초기값들을 설정하는 것이다.
  • 그렇기 때문에, 구분을 위해 출력한 ======== 이전에 메타클래스의 __new__와 __init__에 관한 내용이 출력되는 것임
  • 여담이긴 한데 이러한 관점에서 보면, __init__이 첫 인자로 인스턴스를 받는게 좀 납득이 됨(그럴듯함). 클래스와 인스턴스가 같은 정체성?의 것이 아니라 별개의 객체로 보면, 클래스가 생성된 인스턴스를 대상으로 __init__ 메소드를 시행하는 것이므로, __init__메소드를 사용할 때 메소드를 적용할 대상이 무엇인지 가르쳐 주는 것임. 다만 편의를 위해, 인스턴스로 호출되었을 경우 그 인스턴스를 첫인자로 받아줄 뿐 ㅇㅇ

 

  • 그럼 이제 메타클래스의 __call__에 대해 집중해보자
  • Exinstance = ExClass() 라고, 클래스를 사용하여 인스턴스를 생성했을 때, 메타클래스의 __call__이 호출되고, 그 후 class의 __new__와 __init__이 호출된 것을 확인할 수 있다.
  • 이게 앞서 얘기했던, 클래스의 메타클래스의 __call__메소드가 수행되고, 그 메타클래스의 __call__ 메소드가 클래스의 __new__와 __init__을 시행시켜 클래스를 완성시킨다는 것임
  • 즉 어떤 클래스의 인스턴스(클래스를 포함하여)를 만들때 다음과 같은 작업이 파이썬 내부적으로 발생한다. 
    1. 인스턴스 객체 생성 시도
    2. 클래스의 메타클래스 호출
    3. 메타클래스의 호출에 의해 메타클래스의 __call__이 호출됨
    4. 메타클래스의 __call__에 의해 클래스의 __new__ 호출
    5. 클래스의 __new__에 의해 인스턴스 객체 생성
    6. 그후, 메타클래스의 __call__은 클래스의 __init__을 호출
    7. 클래스의 __init__에 의해 인스턴스 변수들의 초기값설정
    8. 인스턴스 객체 반환(완성)
  • 위의 예제에서 출력되진 않았지만, 구분선 (=======)위에 메타클래스에 의해 클래스가 탄생할 때, 메타클래스의 메타클래스가 되는 type클래스의 __call__이 호출되었을 것임..

 

  • (여담. 안봐도 됨)개인적으로 약간 의문이 들었던 것은, 어떻게 모든 __new__나 __call__이 어떻게 type처럼 작동하지? 라는 것이였음.  type 혹은 object 클래스의 __call__이나 __new__가 조작할 수 있는 범위를 넘어선(cpython의 영역으로 들어가야 한다함..) 것에 있는 것은 알겠는데, 어떻게 모든 클래스의 메소드가 이런식으로 작동을하지?
    • 근데 생각해보면 너무 당연한건데,  모든 __new__나 __call__, __init__등은 결국 파이썬에서 기본적으로 설정한 것을 사용하도록 되어있음.
    • 모든 클래스는 기본적으로 type클래스를 메타클래스로 삼아 태어나고, object클래스를 기본적으로 상속받음
    • 즉 클래스의 가장 본질(?)에 있는 type클래스와 object클래스의 __new__나 __call__을 보통 그대로 상속받거나,쉽게 활용할 수 있게 되어 있는 듯함(super를 사용한다던지 등)

 

 

참고한 사이트

 

BELATED ARTICLES

more