Why does the class definition's metaclass keyword argument accept a callable?

前端 未结 3 1006
一整个雨季
一整个雨季 2021-02-08 22:27

Background

The Python 3 documentation clearly describes how the metaclass of a class is determined:

  • if no bases and no explicit metaclas
3条回答
  •  旧巷少年郎
    2021-02-08 22:37

    Concerning question 1, I think the "metaclass" of a class cls should be understood as type(cls). That way of understanding is compatible with Python's error message in the following example:

    >>> class Meta1(type): pass
    ... 
    >>> class Meta2(type): pass
    ... 
    >>> def metafunc(name, bases, methods):
    ...     if methods.get('version') == 1:
    ...         return Meta1(name, bases, methods)
    ...     return Meta2(name, bases, methods)
    ... 
    >>> class C1:
    ...     __metaclass__ = metafunc
    ...     version = 1
    ... 
    >>> class C2:
    ...     __metaclass__ = metafunc
    ...     version = 2
    ... 
    >>> type(C1)
    
    >>> type(C2)
    
    >>> class C3(C1,C2): pass
    ... 
    Traceback (most recent call last):
      File "", line 1, in 
    TypeError: Error when calling the metaclass bases
        metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
    

    I.e., according to the error message, the metaclass of a class is a class, even though the callable used to construct the class can be just anything.

    Concerning the second question, indeed with a subclass of type used as a metaclass, you can do the same as with any other callable. In particular, it is possible that it yields something that is not its instance:

    >>> class Mockup(type):
    ...     def __new__(cls, name, bases, methods):
    ...         return Meta1(name, bases, methods)
    ... 
    >>> class Foo:
    ...     __metaclass__ = Mockup
    ... 
    >>> type(Foo)
    
    >>> isinstance(Foo, Mockup)
    False
    >>> Foo.__metaclass__
    
    

    As to why Python gives the freedom of using any callable: The previous example shows that it is actually irrelevant whether the callable is a type or not.

    BTW, here is a fun example: It is possible to code metaclasses that, themselves, have a metaclass different from type---let's call it a metametaclass. The metametaclass implements what happens when a metaclass is called. In that way, it is possible to create a class with two bases whose metaclasses are not subclass of each other (compare with Python's error message in the example above!). Indeed, only the metaclass of the resulting class is subclass of the metaclass of the bases, and this metaclass is created on the fly:

    >>> class MetaMeta(type):
    ...     def __call__(mcls, name, bases, methods):
    ...         metabases = set(type(X) for X in bases)
    ...         metabases.add(mcls)
    ...         if len(metabases) > 1:
    ...             mcls = type(''.join([X.__name__ for X in metabases]), tuple(metabases), {})
    ...         return mcls.__new__(mcls, name, bases, methods)
    ... 
    >>> class Meta1(type):
    ...     __metaclass__ = MetaMeta
    ... 
    >>> class Meta2(type):
    ...     __metaclass__ = MetaMeta
    ... 
    >>> class C1:
    ...     __metaclass__ = Meta1
    ... 
    >>> class C2:
    ...     __metaclass__ = Meta2
    ... 
    >>> type(C1)
    
    >>> type(C2)
    
    >>> class C3(C1,C2): pass
    ... 
    >>> type(C3)
    
    

    What is less fun: The preceding example won't work in Python 3. If I understand correctly, Python 2 creates the class and checks whether its metaclass is a subclass of all its bases, whereas Python 3 first checks whether there is one base whose metaclass is superclass of the metaclasses of all other bases, and only then creates the new class. That's a regression, from my point of view. But that shall be the topic of a new question that I am about to post...

    Edit: The new question is here

提交回复
热议问题