Python

What are metaclasses in Python? 해결법

티멀 2023. 1. 16. 08:43
728x90
반응형

객체로서의 클래스

메타 클래스를 이해하기 전에 파이썬에서 클래스를 마스터해야 합니다. 파이썬은 스몰토크 언어에서 차용한 클래스가 무엇인지에 대한 매우 독특한 생각을 가지고 있습니다.

대부분의 언어에서 클래스는 객체를 생성하는 방법을 설명하는 코드 조각일 뿐입니다. 그것은 파이썬에서도 약간 사실이다:

>>> class ObjectCreator(object): ...       pass ...  >>> my_object = ObjectCreator() >>> print(my_object) <__main__.ObjectCreator object at 0x8974f2c> 

하지만 파이썬에서는 클래스가 그 이상이다. 클래스도 객체입니다.

네, 물건들이요.

키워드 class를 사용하는 즉시 Python은 이를 실행하고 생성합니다 강한[강력한[강력한] 물건. 설명서

>>> class ObjectCreator(object): ...       pass ... 

메모리에 이름이 인 개체를 만듭니다개체 생성자.

<강함>이 객체(클래스) 자체는 객체(인스턴스)를 생성할 수 있습니다, 그리고 이것이 그것이 수업인 이유이다[강력하다.</강함>

하지만 여전히, 그것은 물체이고, 따라서:

  • 변수에 할당할 수 있습니다.
  • 복사할 수 있습니다.
  • 속성을 추가할 수 있습니다.
  • 함수 매개 변수로 전달할 수 있습니다.

e.g.:

>>> print(ObjectCreator) # you can print a class because it's an object <class '__main__.ObjectCreator'> >>> def echo(o): ...       print(o) ... >>> echo(ObjectCreator) # you can pass a class as a parameter <class '__main__.ObjectCreator'> >>> print(hasattr(ObjectCreator, 'new_attribute')) False >>> ObjectCreator.new_attribute = 'foo' # you can add attributes to a class >>> print(hasattr(ObjectCreator, 'new_attribute')) True >>> print(ObjectCreator.new_attribute) foo >>> ObjectCreatorMirror = ObjectCreator # you can assign a class to a variable >>> print(ObjectCreatorMirror.new_attribute) foo >>> print(ObjectCreatorMirror()) <__main__.ObjectCreator object at 0x8997b4c> 

동적으로 클래스 만들기

클래스는 개체이므로 다른 개체와 마찬가지로 즉시 만들 수 있습니다.

먼저 class:

를 사용하여 함수에 클래스를 만들 수 있습니다

>>> def choose_class(name): ...     if name == 'foo': ...         class Foo(object): ...             pass ...         return Foo # return the class, not an instance ...     else: ...         class Bar(object): ...             pass ...         return Bar ... >>> MyClass = choose_class('foo') >>> print(MyClass) # the function returns a class, not an instance <class '__main__.Foo'> >>> print(MyClass()) # you can create an object from this class <__main__.Foo object at 0x89c6d4c> 

하지만 수업 전체를 직접 써야 하기 때문에 그렇게 역동적이지는 않아요.

클래스는 객체이기 때문에 무언가에 의해 생성되어야 합니다.

class 키워드를 사용하면 Python이 자동으로 이 개체를 만듭니다. 하지만 로서 파이썬에서 대부분의 것을 사용하면 수동으로 수행할 수 있습니다.

기능 유형을 기억하십니까? 당신이 무엇을 알 수 있게 해주는 좋은 오래된 기능 개체 유형:

>>> print(type(1)) <type 'int'> >>> print(type("1")) <type 'str'> >>> print(type(ObjectCreator)) <type 'type'> >>> print(type(ObjectCreator())) <class '__main__.ObjectCreator'> 

자, 유형도 완전히 다른 능력을 가지고 있습니다. 즉, 즉시 클래스를 만들 수 있습니다. type는 클래스의 설명을 매개 변수로 사용할 수 있습니다, 그리고 수업을 반납한다.

(알고 있어, 같은 기능이 전달하는 매개 변수에 따라 완전히 다른 두 가지 용도를 가질 수 있다는 것은 어리석은 일이야. 후진성으로 인한 문제입니다 Python 호환성)/p>

유형/코드는 다음과 같이 작동합니다.

type(name, bases, attrs) 

위치:

  • 이름:클래스명
  • 삭제 : 상위 클래스의 튜플(상속의 경우 비워 둘 수 있음)
  • 특성: 속성 이름 및 값이 포함된 사전

e.g.:

>>> class MyShinyClass(object): ...       pass 

다음과 같은 방법으로 수동으로 생성할 수 있습니다.

>>> MyShinyClass = type('MyShinyClass', (), {}) # returns a class object >>> print(MyShinyClass) <class '__main__.MyShinyClass'> >>> print(MyShinyClass()) # create an instance with the class <__main__.MyShinyClass object at 0x8997cec> 

클래스 이름으로 MyShinyClass가 사용됩니다 클래스 참조를 저장할 변수로 사용합니다. 다를 수도 있지만, 하지만 일을 복잡하게 만들 이유는 없어요.

type는 사전을 사용하여 클래스의 속성을 정의할 수 있습니다. 그래서:

>>> class Foo(object): ...       bar = True 

번역 가능:

>>> Foo = type('Foo', (), {'bar':True}) 

그리고 일반 클래스로 사용:

>>> print(Foo) <class '__main__.Foo'> >>> print(Foo.bar) True >>> f = Foo() >>> print(f) <__main__.Foo object at 0x8a9b84c> >>> print(f.bar) True 

그리고 물론, 당신은 그것으로부터 물려받을 수 있습니다, 그래서:

>>>   class FooChild(Foo): ...         pass 

다음과 같습니다.

>>> FooChild = type('FooChild', (Foo,), {}) >>> print(FooChild) <class '__main__.FooChild'> >>> print(FooChild.bar) # bar is inherited from Foo True 

마지막으로 메소드를 클래스에 추가해야 합니다. 함수를 정의하기만 하면 됩니다 적절한 서명을 사용하여 속성으로 할당합니다.

>>> def echo_bar(self): ...       print(self.bar) ... >>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar}) >>> hasattr(Foo, 'echo_bar') False >>> hasattr(FooChild, 'echo_bar') True >>> my_foo = FooChild() >>> my_foo.echo_bar() True 

또한 일반적으로 생성된 클래스 개체에 메서드를 추가하는 것과 마찬가지로 클래스를 동적으로 생성한 후에도 더 많은 메서드를 추가할 수 있습니다.

>>> def echo_bar_more(self): ...       print('yet another method') ... >>> FooChild.echo_bar_more = echo_bar_more >>> hasattr(FooChild, 'echo_bar_more') True 

파이썬에서 클래스는 객체이며 동적으로 클래스를 만들 수 있습니다.

이것은 키워드 class를 사용할 때 Python이 하는 일이며 메타 클래스를 사용함으로써 그렇게 한다.

메타클래스란? (최종적으로)

메타클래스는 클래스를 만드는 '물건'이다.

객체를 만들기 위해 클래스를 정의합니다?

하지만 우리는 파이썬 클래스가 객체라는 것을 배웠다.

음, 메타클래스가 이러한 물체들을 만들어내는 것입니다. 그것들은 수업의 수업이다, 당신은 그들을 이렇게 상상할 수 있다:

MyClass = MetaClass() my_object = MyClass() 

<코드> 유형을 통해 다음과 같은 작업을 수행할 수 있습니다. </코드>

MyClass = type('MyClass', (), {}) 

그것은 기능 <코드> 유형이 사실상 메타 클래스이기 때문이다. 유형은 Python이 모든 클래스를 백그라운드에서 만드는 데 사용하는 메타 클래스입니다.</코드>

이제 당신은 "도대체 왜 그것이 <코드>가 아니라 소문자로 쓰여 있는지 궁금하다유형은?"</코드>

음, 나는 그것이 만드는 클래스인 str와의 일관성의 문제라고 생각한다 strings 객체와 정수 객체를 만드는 클래스를 포함합니다. 유형은 다음과 같습니다 클래스 개체를 만드는 클래스만 해당됩니다.

_class__ 속성을 확인하면 알 수 있습니다.

파이썬에서는 모든 것이 객체입니다. 여기에는 정수가 포함됩니다, 문자열, 함수 및 클래스. 그것들은 모두 물체입니다. 그리고 그들 모두는 클래스에서 생성됨:

>>> age = 35 >>> age.__class__ <type 'int'> >>> name = 'bob' >>> name.__class__ <type 'str'> >>> def foo(): pass >>> foo.__class__ <type 'function'> >>> class Bar(object): pass >>> b = Bar() >>> b.__class__ <class '__main__.Bar'> 

Now, what is the __class__ of any __class__ ?

>>> age.__class__.__class__ <type 'type'> >>> name.__class__.__class__ <type 'type'> >>> foo.__class__.__class__ <type 'type'> >>> b.__class__.__class__ <type 'type'> 

So, a metaclass is just the stuff that creates class objects.

You can call it a 'class factory' if you wish.

type is the built-in metaclass Python uses, but of course, you can create your own metaclass.

The __metaclass__ attribute

In Python 2, you can add a __metaclass__ attribute when you write a class (see next section for the Python 3 syntax):

class Foo(object):     __metaclass__ = something...     [...] 

If you do so, Python will use the metaclass to create the class Foo.

Careful, it's tricky.

You write class Foo(object) first, but the class object Foo is not created in memory yet.

Python will look for __metaclass__ in the class definition. If it finds it, it will use it to create the object class Foo. If it doesn't, it will use type to create the class.

Read that several times.

When you do:

class Foo(Bar):     pass 

Python does the following:

Is there a __metaclass__ attribute in Foo?

If yes, create in-memory a class object (I said a class object, stay with me here), with the name Foo by using what is in __metaclass__.

If Python can't find __metaclass__, it will look for a __metaclass__ at the MODULE level, and try to do the same (but only for classes that don't inherit anything, basically old-style classes).

Then if it can't find any __metaclass__ at all, it will use the Bar's (the first parent) own metaclass (which might be the default type) to create the class object.

Be careful here that the __metaclass__ attribute will not be inherited, the metaclass of the parent (Bar.__class__) will be. If Bar used a __metaclass__ attribute that created Bar with type() (and not type.__new__()), the subclasses will not inherit that behavior.

Now the big question is, what can you put in __metaclass__?

The answer is something that can create a class.

And what can create a class? type, or anything that subclasses or uses it.

Metaclasses in Python 3

The syntax to set the metaclass has been changed in Python 3:

class Foo(object, metaclass=something):     ... 

i.e. the __metaclass__ attribute is no longer used, in favor of a keyword argument in the list of base classes.

The behavior of metaclasses however stays largely the same.

One thing added to metaclasses in Python 3 is that you can also pass attributes as keyword-arguments into a metaclass, like so:

class Foo(object, metaclass=something, kwarg1=value1, kwarg2=value2):     ... 

Read the section below for how Python handles this.

Custom metaclasses

The main purpose of a metaclass is to change the class automatically, when it's created.

You usually do this for APIs, where you want to create classes matching the current context.

Imagine a stupid example, where you decide that all classes in your module should have their attributes written in uppercase. There are several ways to do this, but one way is to set __metaclass__ at the module level.

This way, all classes of this module will be created using this metaclass, and we just have to tell the metaclass to turn all attributes to uppercase.

Luckily, __metaclass__ can actually be any callable, it doesn't need to be a formal class (I know, something with 'class' in its name doesn't need to be a class, go figure... but it's helpful).

So we will start with a simple example, by using a function.

# the metaclass will automatically get passed the same argument # that you usually pass to `type` def upper_attr(future_class_name, future_class_parents, future_class_attrs):     """       Return a class object, with the list of its attribute turned       into uppercase.     """     # pick up any attribute that doesn't start with '__' and uppercase it     uppercase_attrs = {         attr if attr.startswith("__") else attr.upper(): v         for attr, v in future_class_attrs.items()     }      # let `type` do the class creation     return type(future_class_name, future_class_parents, uppercase_attrs)  __metaclass__ = upper_attr # this will affect all classes in the module  class Foo(): # global __metaclass__ won't work with "object" though     # but we can define __metaclass__ here instead to affect only this class     # and this will work with "object" children     bar = 'bip' 

Let's check:

>>> hasattr(Foo, 'bar') False >>> hasattr(Foo, 'BAR') True >>> Foo.BAR 'bip' 

Now, let's do exactly the same, but using a real class for a metaclass:

# remember that `type` is actually a class like `str` and `int` # so you can inherit from it class UpperAttrMetaclass(type):     # __new__ is the method called before __init__     # it's the method that creates the object and returns it     # while __init__ just initializes the object passed as parameter     # you rarely use __new__, except when you want to control how the object     # is created.     # here the created object is the class, and we want to customize it     # so we override __new__     # you can do some stuff in __init__ too if you wish     # some advanced use involves overriding __call__ as well, but we won't     # see this     def __new__(upperattr_metaclass, future_class_name,                 future_class_parents, future_class_attrs):         uppercase_attrs = {             attr if attr.startswith("__") else attr.upper(): v             for attr, v in future_class_attrs.items()         }         return type(future_class_name, future_class_parents, uppercase_attrs) 

Let's rewrite the above, but with shorter and more realistic variable names now that we know what they mean:

class UpperAttrMetaclass(type):     def __new__(cls, clsname, bases, attrs):         uppercase_attrs = {             attr if attr.startswith("__") else attr.upper(): v             for attr, v in attrs.items()         }         return type(clsname, bases, uppercase_attrs) 

You may have noticed the extra argument cls. There is nothing special about it: __new__ always receives the class it's defined in, as the first parameter. Just like you have self for ordinary methods which receive the instance as the first parameter, or the defining class for class methods.

But this is not proper OOP. We are calling type directly and we aren't overriding or calling the parent's __new__. Let's do that instead:

class UpperAttrMetaclass(type):     def __new__(cls, clsname, bases, attrs):         uppercase_attrs = {             attr if attr.startswith("__") else attr.upper(): v             for attr, v in attrs.items()         }         return type.__new__(cls, clsname, bases, uppercase_attrs) 

We can make it even cleaner by using super, which will ease inheritance (because yes, you can have metaclasses, inheriting from metaclasses, inheriting from type):

class UpperAttrMetaclass(type):     def __new__(cls, clsname, bases, attrs):         uppercase_attrs = {             attr if attr.startswith("__") else attr.upper(): v             for attr, v in attrs.items()         }          # Python 2 requires passing arguments to super:         return super(UpperAttrMetaclass, cls).__new__(             cls, clsname, bases, uppercase_attrs)          # Python 3 can use no-arg super() which infers them:         return super().__new__(cls, clsname, bases, uppercase_attrs) 

Oh, and in Python 3 if you do this call with keyword arguments, like this:

class Foo(object, metaclass=MyMetaclass, kwarg1=value1):     ... 

It translates to this in the metaclass to use it:

class MyMetaclass(type):     def __new__(cls, clsname, bases, dct, kwargs1=default):         ... 

That's it. There is really nothing more about metaclasses.

The reason behind the complexity of the code using metaclasses is not because of metaclasses, it's because you usually use metaclasses to do twisted stuff relying on introspection, manipulating inheritance, vars such as __dict__, etc.

Indeed, metaclasses are especially useful to do black magic, and therefore complicated stuff. But by themselves, they are simple:

  • intercept a class creation
  • modify the class
  • return the modified class

Why would you use metaclasses classes instead of functions?

Since __metaclass__ can accept any callable, why would you use a class since it's obviously more complicated?

There are several reasons to do so:

  • The intention is clear. When you read UpperAttrMetaclass(type), you know what's going to follow
  • You can use OOP. Metaclass can inherit from metaclass, override parent methods. Metaclasses can even use metaclasses.
  • Subclasses of a class will be instances of its metaclass if you specified a metaclass-class, but not with a metaclass-function.
  • You can structure your code better. You never use metaclasses for something as trivial as the above example. It's usually for something complicated. Having the ability to make several methods and group them in one class is very useful to make the code easier to read.
  • You can hook on __new__, __init__ and __call__. Which will allow you to do different stuff, Even if usually you can do it all in __new__, some people are just more comfortable using __init__.
  • These are called metaclasses, damn it! It must mean something!

Why would you use metaclasses?

Now the big question. Why would you use some obscure error-prone feature?

Well, usually you don't:

Metaclasses are deeper magic that 99% of users should never worry about it. If you wonder whether you need them, you don't (the people who actually need them know with certainty that they need them, and don't need an explanation about why).

Python Guru Tim Peters

The main use case for a metaclass is creating an API. A typical example of this is the Django ORM. It allows you to define something like this:

class Person(models.Model):     name = models.CharField(max_length=30)     age = models.IntegerField() 

But if you do this:

person = Person(name='bob', age='35') print(person.age) 

It won't return an IntegerField object. It will return an int, and can even take it directly from the database.

This is possible because models.Model defines __metaclass__ and it uses some magic that will turn the Person you just defined with simple statements into a complex hook to a database field.

Django makes something complex look simple by exposing a simple API and using metaclasses, recreating code from this API to do the real job behind the scenes.

The last word

First, you know that classes are objects that can create instances.

Well, in fact, classes are themselves instances. Of metaclasses.

>>> class Foo(object): pass >>> id(Foo) 142630324 

Everything is an object in Python, and they are all either instance of classes or instances of metaclasses.

Except for type.

type is actually its own metaclass. This is not something you could reproduce in pure Python, and is done by cheating a little bit at the implementation level.

Secondly, metaclasses are complicated. You may not want to use them for very simple class alterations. You can change classes by using two different techniques:

99% of the time you need class alteration, you are better off using these.

But 98% of the time, you don't need class alteration at all.

728x90
반응형