반응형

원본 링크



Python Metaclasses



Table of Contents

  • Old-Style vs. New-Style Classes
    • Old-Style Classes
    • New-Style Classes
  • Type and Class
  • Defining a Class Dynamically
    • Example 1
    • Example 2
    • Example 3
    • Example 4
  • Custom Metaclasses
  • Is This Really Necessary?
  • Conclusion

메타프로그래밍(metaprogramming) 용어는 프로그램이 자신에 대한 지식 또는 조작할 수 있는 잠재력을 나타낸다. 파이썬은 메타클래스(metaclass)로 불리우는 클래스를 위한 메타프로그래밍의 형태를 지원한다.

메타클래스는 가상으로 모든 파이썬 코드 뒤에 숨어있는 소수만 아는 OOP 개념이다. 이를 인지하던 안하던 이미 사용하고 있다. 대부분 이를 인지할 필요가 없다. 대부분의 파이썬 프로그래머는 메타클래스에 대해 생각할 필요가 거의 없다.

필요성이 생길때 파이썬은 모든 OOP 언어가 지원하지 않는 능력을 제공한다: 내부로 들어가서 커스텀 메타클래스를 정의할 수 있다. 커스텀 메타클래스의 용도는 Zen of Python의 저자인 Python 전문가인 Tim Peters가 다음의 인용으로 제시한 것 처럼 다소 논란이 많다.

메타클래스는 99%의 사용자가 항상 걱정해야 하는 것보다 더 심오한 마술이다. 만약 이것이 필요한지가 궁금하다면 필요없는 것이다.(실제로 필요한 사람은 확실하게 알고 왜 필요한지에 대한 설명이 필요없다.)

커스텀 메타클래스를 결코 사용할 수 없다고 믿는 파이썬 프로그래머(파이썬 매니아로 알려진)가 있다. 좀 멀리갈 수도 있지만, 커스텀 메타클래스가 가장 대부분 필요없다는 것은 아마도 사실이다. 만약 문제가 메타클래스를 명확하게 요구하지 않는 경우 더 간한 방법으로 해결하면 더 명확하고 가독성이 좋아 질 수 있다.

여전히 파이썬 메타클래스를 이해하는 것은 가치가 있다. 이것이 보통 더 나은 파이썬 클래스의 내부에 대한 이해로 이어지기 때문이다.



Old-Style vs. New-Style Classes

파이썬 영역에서 클래스는 두 종류중 하나일 수 있다.결정된 공식 용어는 없다. 따라서 비공식적으로 예전 형태(old-style)과 새로운 형태(new-style) 클래스로 나타낸다.


Old-Style Classes

Old-style 클래스는 클래스(class)와 타입(type)이 완전히 동일하지 않다. Old-style 클래스의 인스턴스는 instance로 불리우는 하나의 빌트인 타입(built-in type)으로 항상 구현된다. 만약 obj가 old-style 클래스의 인스턴스라면, obj.__class__는 클래스를 나타내지만 type(obj)은 항상 instance이다. 아래 예제는 파이썬 2.7에서 가지고 온 것이다.


>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>


New-Style Classes

New-style 클래스는 클래시와 타입의 개념을 합친다. 만약 obj가 new-style 클래스의 인스턴스라면 type(obj)는 동일하게 obj.__class__이다.


>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> obj.__class__ is type(obj)
True


>>> n = 5
>>> d = { 'x' : 1, 'y' : 2 }

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> for obj in (n, d, x):
...     print(type(obj) is obj.__class__)
...
True
True
True



Type and Class

파이썬 3에서는 모든 클래스는 new-style 클래스이다 따라서 파이썬 3에서는 객체의 타입과 클래스를 서로 참조하는 것이 타당하다.

Note : 파이썬 2에서 클래스는 기본적으로 old-style이다. 파이선 2.2이전에는 new-style 클래스는 전혀 지원되지 않았다. 파이선 2.2 이후부터 new-style을 생성할 수 있었지만, new-style로써 명시적으로 선언해야 했다.

파이썬에서 모든 것은 객체라는 것을 기억하자. 따라서 클래스 역시 객체이다. 결과적으로 클래스는 타입을 가져야 한다. 클래스의 타입은 무엇일까?

아래 코드를 보자


>>> class Foo:
...     pass
...
>>> x = Foo()

>>> type(x)
<class '__main__.Foo'>

>>> type(Foo)
<class 'type'>

x의 타입은 Foo 클래스지만 Foo 클래스 자신의 타입은 type이다. 보통 new-style 클래스의 타입은 type이다.

익숙한 빌트인 클래스의 타입 역시 type이다.


>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>

type의 타입도 type이다.


>>> type(type)
<class 'type'>

type은 클래스가 인스턴스인 메타클래스이다. 파이썬에서 객체가 보통new-style 클래스인 클래스의 인스턴스인것 처럼 파이썬 3에서 클래서는 type 메타클래스의 인스턴스이다.

위의 경우에

  • x는 Foo 클래스의 인스턴스이다.
  • Foo는 type 메타클래스의 인스턴스이다.
  • type은 또한 type 메타클래스의 인스턴스 이다. 따라서 type은 자기자신의 인스턴스이다.
    
    


Defining a Class Dynamically

하나의 인자를 갖는 빌트인 type() 함수는 객체의 타입을 반환한다. New-style 클래스에 대해 이것은 보통 object의 __class_attribute와 동일하다.


>>> type(3)
<class 'int'>

>>> type(['foo', 'bar', 'baz'])
<class 'list'>

>>> t = (1, 2, 3, 4, 5)
>>> type(t)
<class 'tuple'>

>>> class Foo:
...     pass
...
>>> type(Foo())
<class '__main__.Foo'>

3개의 인자를 가진 type()또한 호출할 수 있다. - type(<name>, <bases>, <dct>)

  • <name>은 클래스 이름을 나타낸다. 이것은 클래스의 __name__ 속성이 된다.
  • <bases>은 클래스가 상속하는 기본 클래스 튜플(tuple)을 나타낸다. 이것은 클래스의 __bases__속성이 된다.
  • <name>은 클래스 내용(body)에 대한 정의를 포함하는 namespace dictionary를 나타난다. 이것은 클래스의 __dict__속성이 된다.

이 방법으로 type()을 호출하면 type 메타클래스의 새로운 인스턴스가 생성된다. 다른 말로 새로운 클래스를 동적으로 생성한다.

이후 예제에서 맨위 코드는 type()으로 동적 클래스를 정의하는 반면 아래 코드는 class 명령으로 일반적인 방법으로 클래스를 정의한다. 두 코드는 기능적으로는 동일하다.


Example 1

첫번째 예제에서는 type()의 <bases>와 <dct> 인자를 비운다. 다른 클래스로부터의 상속도 없다. 그리고 namespace dictionary에 아무것도 초기화 시키지 않는다. 이것이 가장 간단한 클래스 정의이다.


>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>


>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>


Example 2

여기서는 <bases>는 Bar가 상속한 부모 클래스를 나타내는 하나의 Foo 요소를 가진 튜플이다. attr 속성은 초기 namespace dictionary로 설정된다.


>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)


>>> class Bar(Foo):
...     attr = 100
...

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)


Example 3

이번에는 다시 <bases>를 비운다. 두 객체가 <dct> 인자를 통해 namespace dictionary로 설정한다. 첫전째는 attr로 이름지어진 속성이고 두번째는 attr_val로 이름지어진 함수이다. 이것은 정의된 클래스의 메소드가 된다.


>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100


>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100


Example 4

오직 간단한 함수만이 파이썬 lambda로 정의될 수 있다. 다음 예제에서 좀더 복잡함 함수가 외부에서 정의되고 이름 f를 사용하여 namespace dictionary에 attr_val로 할당한다.


>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': f
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100


>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100



Custom Metaclasses

다시 다음 예제를 보자.


>>> class Foo:
...     pass
...
>>> f = Foo()

Foo() 표현은 새로운 Foo 클래스의 인스턴스를 만든다. 인터프리터가 Foo()를 만나면 아래와 같은 일이 발생한다.

  • Foo의 부모 클래스의 __call__() 메소드가 호출된다. Foo가 표준 new-style 클래스이기 때문에 Foo의 부모 클래스는 type 메타클래스이다 따라서 type의 __call__() 메소드가 호출된다.
  • __call__() 메소드는 차례로 다음을 호출한다.
    • __new__()
    • __init__()

만약 Foo에 __new__()와 __init__메소드가 없다면 기본 메소드가 Foo의 조상으로부터 상속된다. 하지만 있다면, 오버라이드(override)한다. 이는 Foo를 인스턴스화할때 커스텀 동작을 할 수 있게 한다.

아래 코드는 new()라는 커스텀 메소드가 정의되고 Foo의 __new__()메소드로 할당된다.


>>> def new(cls):
...     x = object.__new__(cls)
...     x.attr = 100
...     return x
...
>>> Foo.__new__ = new

>>> f = Foo()
>>> f.attr
100

>>> g = Foo()
>>> g.attr
100

이것은 Foo 클래스의 인스턴스화 동작을 수정한다. Foo의 인스턴스가 생성될때마다 기본적으로 100을 저장하고 있는 attr 속성으로 초기화 된다. (이같은 코드는 보통 __init__() 메소드에서 나타나고 __new__()에서는 보통 나타나지 않는다. 이 예제는 데모 목적으로 작성되었다.)

클래스 역시 객체이다. Foo같은 클래스를 생성할 때 인스턴스 생성 동작을 커스텀한다고 가정해 보자. 만약 위의 패턴을 따른다면 다시 커스텀 메소드를 정의하고 이를 Foo가 인스턴스인 클래스에 __new()__() 메소드에 할당해야한다. Foo는 type 메타클래스의 인스턴스이다. 따라서 다음과 같은 코드가 작성될 것이다.


# Spoiler alert:  This doesn't work!
>>> def new(cls):
...     x = type.__new__(cls)
...     x.attr = 100
...     return x
...
>>> type.__new__ = new
Traceback (most recent call last):
  File "<pyshell#77>", line 1, in <module>
    type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'

위에 보이는 것과 같이 type 메타클래스에 __new__() 메소드를 재할당 할 수 없다. 이는 파이썬에서 허용되지 않는다.

type은 모든 new-style 클래스가 파생되는 메타클래스이다.

클래스의 인스턴스화를 커스텀하기 위한 하나의 방법은 커스텀 메타 클래스이다. 근본적으로 type 메타클래스로 이리저리 해보는 것 대신 type에서 파생된 메타클래스를 정의할 수 있다. 그려면 이 파생 클래스로 이리저리 해 볼수 있게 된다.

type에서 파생된 메타클래스를 정의하는 첫번째 단계는 다음과 같다.


>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
...

'class Meta(type):'은 type으로부터 Meta가 파생된 것을 나타낸다. type이 메타클래스이기 때문에 Meta를 메타클래스로 만든다.

커스텀 __new__() 메소드가 Meta에 정의되어 있는 것에 주목하자. 이 방버은 type 메타클래스에 직접 적용할 수 없다. __new__() 메소드는 다음과 같은 동작을 한다.

  • 실제 새로운 클래스를 생성하는 부모 메타클래스(type)의 __new__() 메소드를 super()를 통해 대신한다.(delegate)
  • 100을 저장한 커스텀 속성 attr을 클래스에 할당한다.
  • 새롭게 생성된 클래스를 반환한다.

새로운 클래스 Foo를 정의하고 이 클래스의 메타클래스가 표준 메타클래스 type이 아닌 커스텀 메타클래스 Meta로 한다. 이것은 아래와 같이 클래스 정의에서 metaclass 키워드를 사용하여 수행할 수 있다.


>>> class Foo(metaclass=Meta):
...     pass
...
>>> Foo.attr
100

Foo가 Meta 메타클래스의 attr값에 자옫으로 접근했다. 물론 비슷하게 정의한 어떠한 클래라도 아래와 같이 동작할 것이다.


>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

객체 생성을 위한 템플릿으로 클래스 함수가 있는 것과 같은 방식으로 클래스의 생성을 위한 템플릿으로 메타클래스 함수가 있다. 메타클래스는 때때로 class factory로 표현한다.

아래 두 예제를 비교해보자.

Object Factory:


>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

>>> x = Foo()
>>> x.attr
100

>>> y = Foo()
>>> y.attr
100

>>> z = Foo()
>>> z.attr
100

Class Factory:


>>> class Meta(type):
...     def __init__(
...         cls, name, bases, dct
...     ):
...         cls.attr = 100
...
>>> class X(metaclass=Meta):
...     pass
...
>>> X.attr
100

>>> class Y(metaclass=Meta):
...     pass
...
>>> Y.attr
100

>>> class Z(metaclass=Meta):
...     pass
...
>>> Z.attr
100



Is This Really Necessary?

위 클래스 팩토리예제 만큼이나 간단하고 메타클래스가 동작하는 근본이다. 그것은 클래스 인스턴스화를 커스텀할 수 있게 한다.

새롭게 생성된 클래스 각각에 커스텀 속성 attr을 부여하는 것은 여전히 번잡하다. 이것을 위해 정말 메타 클래스가 필요할까?

파이썬에서는 효과적으로 동일한 작업을 수행할 수 있는 적어도 몇가지 방법이 존재한다.

Simple Inheritance:


>>> class Base:
...     attr = 100
...

>>> class X(Base):
...     pass
...

>>> class Y(Base):
...     pass
...

>>> class Z(Base):
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

Class Decorator:


>>> def decorator(cls):
...     class NewClass(cls):
...         attr = 100
...     return NewClass
...
>>> @decorator
... class X:
...     pass
...
>>> @decorator
... class Y:
...     pass
...
>>> @decorator
... class Z:
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100



Conclusion

Tim Peters가 제안했던 것처럼 메타클래스는 "문제를 찾는 해결책"이 있는 영역으로 쉽게 방향을 바꿀 수 있다. 이것은 보통 커스텀 메타클래스를 생성할 필요가 없다. 만약 직면한 문제가 더 간단한 방법으로 해결될 수 있다면, 아마도 그렇게 할 것이다. 일반적인 파이썬 클래스를 이해하고 실제로 메타 클래스가 사용하기 위해 적합한 도구일 때를 알수 있게 하기 위해 메타클래스를 이해하는 것은 여전히 좋다.

반응형

+ Recent posts