티스토리 뷰

Python

[파이썬] 클래스 메타프로그래밍

안톨리니 2022. 7. 30. 16:53

Reference

  • Fluent Python
  • Stackoverflow
  • docs.Python.org

 

type()

필자가 이때까지 알았던 type함수는 인수를 하나만 받고

해당 객체가 어떤 타입인지(object.__class__) 알려주는 함수로써만 동작하는지 알았다.

x = 1
print(type(x))


>>> <class 'int'>

하지만 type함수가 세 개의 인수(name, bases, dict)를 받으면

클래스를 동적으로 생성하는 클래스 팩토리처럼 동작하게 된다.


※ 인수가 세개인 경우 type의 매개변수

 

name: 생성할 객체(클래스)의 이름 --> class.__name__

bases(튜플): 생성할 객체(클래스)가 상속받을 클래스 --> class.__bases__

dict(딕셔너리): 생성할 객체(클래스)의 __dict__설정(속성을 설정) --> class.__dict__

 

참고로 생성한 클래스의 속성을 설정하는 마지막 매개변수인 dict의 의미는 꼭 기억하자! 


class X:
    a = 1

Y = type('X', (), dict(a=1))

x = X()
y = Y()

print(x)
>>> <__main__.X object at 0x102c12fa0>
print(y)
>>> <__main__.X object at 0x102c12fa0>

print(dir(x))
>>> ['__class__', '__delattr__', '__dict__', '__dir__', ...]
print(dir(y))
>>> ['__class__', '__delattr__', '__dict__', '__dir__', ...]

처음에는 이게 무슨말인지 싶지만

생각보다 간단하다.

 

우리가 보통 특정 부류의 객체들을 어떻게 생성할지에 대해

class 키워드를 통해 정의한다.

 

하지만 type함수를 사용하면 class키워드 없이도

클래스를 동적으로 생성할 수 있다는 뜻이다.

docs.python.org bulit-in-function(type)

이렇게 클래스를 만들 수 있는 클래스를 '메타클래스'라고 한다.

위에서 봤듯이 type은 메타클래스인것이다.

 

파이썬은 순수 객체지향 언어로써 모든 것이 다 객체이다.

그렇기 때문에 우리가 정의하는 class 또한

어떤 특정 타입 class의 객체라고 할 수 있지 않을까?

x = int
y = bool
z = str
Y = type('X', (), dict(a=1))

print(type(x))
>>> <class 'type'>
print(type(y))
>>> <class 'type'>
print(type(z))
>>> <class 'type'>
print(type(Y))
>>> <class 'type'>

실제로 우리가 정의한 클래스와 파이썬에 내장된 클래스들을 type함수의 인수로 전달해보면

type의 객체인걸 알 수 있다.

 

한 마디로 우리가 정의한 클래스나 내장된 클래스는 우리 눈엔 보이지 않지만

type(메타클래스)을 통해서 생성된 클래스들인 것이다.

 

우리가 클래스에서 특정 객체를 생성할 때

__new____init__ 메서드를 거친다는 사실을 안다면

메타클래스를 통해 메타클래스의 객체인 클래스를 커스터마이즈 할 수 있다는 건

어쩌면 당연한 사실처럼 느껴질 수 있을 것이다.

 

 

 


메타 클래스

 

파이썬에는 여러가지 메타클래스가 존재한다.

type 외에도 추상 클래스를 만들 수 있는 abc라던지 Enum 등의 메타클래스가 존재하는데

결국은 어떤 메타클래스든 type의 서브클래스다.

 

이번에는 필자가 따로 메타클래스를 만들어보겠다.

메타클래스를 만드는 법은 굉장히 간단하다.

필자가 위에서 언급했듯이

그냥 일반적인 클래스를 type의 서브클래스(상속)로 등록하면 된다.

class Meta(type):

    def __new__(cls, name, bases, dic):
		pass

 

( 메타클래스는 클래스(객체)를 만드는 클래스라는 걸 한 번 더 생각하고 이 코드를 보면 좋다. )

 

일단 우리가 일반적인 클래스를 정의하고 객체를 생성하려고 하면

자동적으로 __new__가 호출되고 __init__초기화 메서드가 구현되어 있다면

__init__메서드까지 진행하여 여러 속성들을 초기화한 상태로 객체를 생성한다.

 

클래스 또한 객체이고 그 방식과 똑같이 메타클래스는 클래스 객체를 커스텀하기 위해

__new__(객체 생성) 구현부를 우리가 원하는 형태로 오버라이드 하는 것이다.

class Meta(type):

    def __new__(cls, name, bases, dic):

        def __init__(self, **kwargs):
            for key, value in kwargs.items():
                setattr(self, key, value)

        def __repr__(self):
            obj_attr = []
            for key, value in self.__dict__.items():
                obj_attr.append(f'{key}={value}')
            all_attr = ', '.join(obj_attr)
            return f'{self.__class__.__name__}({all_attr})'

        def __iter__(self):
            for name in self.__dict__:
                yield getattr(self, name)

        new_dict = dict(__init__=__init__, __repr__=__repr__, __iter__=__iter__)

        return super(Meta, cls).__new__(cls, name, bases, new_dict)

필자는 이 메타클래스의 객체(클래스)들이 __init__, __repr__, __iter__매직 메서드들을 가질 수 있도록 했다.

 

이렇게 메타클래스를 만들어 놓고 해당 메타클래스를 통해서 만들 클래스에

적용해줘야 한다.

(적용하지 않으면 기본적으로 type메타클래스를 받는다. )

과거에는 __metaclass__라는 속성에 적용받을 메타클래스를 입력하면 됐지만

현재는 적용받을 클래스 정의 부분에 metaclass키워드 인수에 입력해주면 된다.

 

필자는 위에서 만든 Meta를 Student클래스에 메타클래스로 지정했다.

class Student(metaclass=Meta):
    pass

사실 이 방법보다는 위에서 했던 type을 통해 클래스를 만드는 방식으로

접근하는 게 이해하기 더 쉽다.

Student = Meta('Student', (), {})
print(Student.__dict__)

아까 우리는 type을 통해 클래스를 생성했지만

이번에는 필자가 따로 정의한 메타클래스인 Meta를 통해 Student클래스를 만들어보았다.

print(Student.__dict__)


>>>{'__init__': <function Meta.__new__.<locals>.__init__ at 0x1053aef70>,
'__repr__': <function Meta.__new__.<locals>.__repr__ at 0x1054638b0>,
'__iter__': <function Meta.__new__.<locals>.__iter__ at 0x105463940>,
'__module__': '__main__',
'__dict__': <attribute '__dict__' of 'Student' objects>,
'__weakref__': <attribute '__weakref__' of 'Student' objects>,
'__doc__': None}

Student클래스의 __dict__속성을 보면

__init__, __repr__, __iter__가 있는 걸 확인할 수 있다.

x = Student(name='Antoliny', age=23, major='ComputerScience')
print(x)
>>> Student(name=Antoliny, age=23, major=ComputerScience)

Student클래스의 객체를 생성해보면

Student클래스가 가진 __init__, __repr__이 잘 동작되는 걸 확인할 수 있다.

 

이번에는 일반적인 형태인 type을 통해 Fruit클래스를 만들어보고

똑같이 __dict__을 출력한 뒤에 각각 다른 메타클래스로 만든 객체(클래스)의 차이점을 살펴보겠다.

Fruit = type('Fruit', (), {})
print(Fruit.__dict__)

>>> {'__module__': '__main__',
'__dict__': <attribute '__dict__' of 'Fruit' objects>,
'__weakref__': <attribute '__weakref__' of 'Fruit' objects>,
'__doc__': None}

type메타클래스로 만든 객체인 Fruit클래스는 위 Student클래스와 달리

__init__, __repr__, __iter__가 없다.

 

이런 차이가 발생한 이유는 필자의 Meta(메타클래스)코드를 보면 알 수 있듯이

필자가 정의한 Meta의 __new__메서드를 보면

__init__, __repr__, __iter__매직메서드가 구현되어 있다.

 

그리고 가장 핵심은 리턴문이라고 볼 수 있는데

return super(Meta, cls).__new__(cls, name, bases, new_dict)

필자가 Meta.__new__에 정의한 __init__, __repr__, __iter__를 묶어서 new_dict에 저장한 다음

Meta의 상위 클래스를 나타내는 super --> type의 마지막 매개변수의 인수로 보내줬다.

return type.__new__(cls, name, bases, new_dict)

 

아무리 필자가 메타클래스를 정의했다 할지라도 결국은

type.__new__를 통해 객체가 생성된다.

( 모든 클래스는 직간접적으로 type의 객체라는 걸 알 수 있다. )

 

결론적으로 보면

필자가 만든 메타클래스(Meta)는 결국 type.__new__의 마지막 매개변수를 위한 과정이다.

 

객체(클래스)의 속성을 만드는 type.__new__의 마지막 매개변수를

사용자가 만든 메타클래스를 통해 커스터마이징 해준걸 type.__new__의 인수로 보내서

커스터마이징 객체(클래스)를 만들 수 있도록 하는 방식이다.

 

 

 


 

메타클래스는 어디에 쓰일까??

 

 

일단 가장 대표적인 예를 하나 들자면

웹 프레임워크 Django의 ORM이 메타클래스를 통해 만들어졌다.

(ORM --> 객체와 관계 데이터베이스를 연결)

(이 기능을 통해 장고에서는 데이터베이스 언어를 잘 몰라도 사용이 가능하다.)

 

이 외에도 Fluent Python에 의하면

실제 메타클래스는 프레임워크와 라이브러리에서 다음과 같은 작업을 수행할 수 있도록 사용된다고 한다.

 

  • 속성 검증
  • 많은 메서드에 데커레이터를 일괄 적용
  • 객체 관계 매핑
  • 객체 직렬화 및 데이터 변환
  • 객체 기반 영속성
  • 다른 언어에서 만든 클래스 구조체를 파이썬 클래스로 동적 변환

-Fluent Python-

 

하지만 일반적인 사용자가 메타클래스를 따로 구현할 필요는 없다고 한다.

메타클래스는 복잡하고 충돌할 가능성도 높으며

딱히 클래스 변형이 필요한 순간이 너무나도 적기 때문이라고 한다.

 

만약 변형이 필요한 순간이 온다면 메타클래스보단

클래스 데코레이터, __set_name__, __init_subclass__을 활용하여

해결하는 게 더 낫다고 한다.

알렉스 마르텔리 소프트웨어 엔지니어

파이썬 메타프로그래밍의 대가일 뿐만 아니라 이 세상에서 가장 큰 핵심적인

파이썬 프로젝트에 참여하고 있는 성공한 소프트웨어 엔지니어인

'알렉스 마르텔리'가 메타프로그래밍 관련하여 남긴 덕담을 마지막으로 메타프로그래밍 포스팅을 마무리하겠다. :)

 

배포용 코드에서 절대로 ABC나 메타클래스를 직접 구현하지 '말라'.
ABC를 구현하고 싶은 생각이 든다면,
'멋진 망치를 새로 장만한 사람에게 모든 문제가 못으로 보이는' 증세라고 확신할 수 있다.
깊이를 억제하고, 직관적이고 단순한 코드를 고수한 덕분에
나중에 여러분 코드를 유지보수할 사람이 훨씬 더 행복해질 것이다.

-알렉스 마르텔리-

 

 

후기

 

Fluent Python의 마지막챕터인 메타클래스 부분의 포스팅을 끝냈다.

 

이제는 파이썬 공식문서의 데이터 모델 부분을 읽으면서

필자에게 흥미로운 부분들을 포스팅하고자 한다.

 

Fluent Python이라는 책을 통해 많이 배운 것 같다.

비록 오래된 책이라 트렌드와는 거리가 멀지라도

근본적인 부분은 변하지 않는다.

 

'Python' 카테고리의 다른 글

[파이썬] 디스크립터(descriptor) - [2]  (0) 2022.07.25
[파이썬]디스크립터(descriptor) - [1]  (0) 2022.07.21
[파이썬] yield from  (0) 2022.07.19
[파이썬] 코루틴  (0) 2022.07.11
[파이썬] 데코레이터  (0) 2022.07.04
댓글
공지사항