请教一个关于元类的问题

2017-08-26 00:22:31 +08:00
 saximi
class Meta1(type): 
        def __new__(meta, classname, supers, classdict): 
                print('In Meta1 new: ', classname, supers, classdict, sep='\n...') 
                return type.__new__(meta, classname, supers, classdict) 
        def __init__(Class, classname, supers, classdict): 
                print('In Meta1 init:', classname, supers, classdict, sep='\n...') 
        def __call__(meta, classname, supers, classdict): 
                print('In Meta1 call: ', classname, supers, classdict, sep='\n...') 
                return type.__call__(meta, classname, supers, classdict) 

class Meta2(type, metaclass=Meta1): 
        def __new__(meta, classname, supers, classdict): 
                print('In Meta2 new: ', classname, supers, classdict, sep='\n...') 
                return type.__new__(meta, classname, supers, classdict) 
        def __init__(Class, classname, supers, classdict): 
                print('In Meta2 init:', classname, supers, classdict, sep='\n...') 
                print('...init class object:', list(Class.__dict__.keys())) 
        def __call__(meta): 
                print('In Meta2 call')  
                return type.__call__(meta) 
class Eggs: 
        pass 

print('making class') 
class Spam(Eggs, metaclass=Meta2): 
        data = 1 
        def meth(self, arg): 
                pass 
print('making instance') 
X = Spam() 
print('data:', X.data) 


上面代码输出如下: 
In Meta1 new:  
...Meta2 
...(,) 
...{'__module__': '__main__', '__qualname__': 'Meta2', '__new__': , '__init__': , '__call__': } 
In Meta1 init: 
...Meta2 
...(,) 
...{'__module__': '__main__', '__qualname__': 'Meta2', '__new__': , '__init__': , '__call__': } 
making class 
In Meta1 call:  
...Spam 
...(,) 
...{'__module__': '__main__', '__qualname__': 'Spam', 'data': 1, 'meth': } 
In Meta2 new:  
...Spam 
...(,) 
...{'__module__': '__main__', '__qualname__': 'Spam', 'data': 1, 'meth': } 
In Meta2 init: 
...Spam 
...(,) 
...{'__module__': '__main__', '__qualname__': 'Spam', 'data': 1, 'meth': } 
...init class object: ['__module__', 'data', 'meth', '__doc__'] 
making instance 
In Meta2 call 
data: 1 


我的问题如下: 


1、Spam 的元类是 Meta2,Meta2 的元类是 Meta1。为何执行“ class Spam(Eggs, metaclass=Meta2)”定义 Spam 的时候,在对 Meta2 执行 new 和 init 做初始化之前,要先去执行 Meta1 的 call ? 


2、执行“ X = Spam()”时,Meta1 和 Meta2 都定义了 call 方法的前提下,为何不是执行 Meta1 的 call,而是去执行 Meta2 的 call ?  

1899 次点击
所在节点    Python
11 条回复
keakon
2017-08-26 14:21:51 +08:00
你先理解元类是类的类,元类的实例是类。

所以在定义 Meta2 时( class Meta2(type, metaclass=Meta1)),实际上是生成了一个 Meta1 的实例,就需要调用 Meta1 的 __new__ 和 __init__。
而在定义 Spam 时( class Spam(Eggs, metaclass=Meta2)),需要生成一个 Meta2 的实例作为 Spam 类,也就是执行
Meta2()。Meta2 是个 callable 的对象,调用它实际上是调用 Meta2.__call__(...),这个调用又会被转变成 Meta1.__call__(Meta2, ...)。而你在这个方法里调用了 type.__call__(),这个方法会负责去调用 Meta2.__new__() 和 Meta2.__init__()。

最后,Meta1.__call__() 是负责创建 Meta2 的对象的,Meta2.__call__() 是负责创建 Spam 的对象的。在执行 Spam() 时,Meta2 已经在定义 Spam 类时就被创建过了,创建 Spam 的对象时不需要再调用 Meta1.__call__() 创建新的 Meta2,而是直接使用已有的 Meta2 创建 Spam 的对象。
saximi
2017-08-26 23:30:15 +08:00
@keakon 非常感谢您详细耐心的解答,关于您的说明,我还有两个疑问:
1、Meta2 因为声明了 call 方法,所以是个 callable 对象,定义 Spam 时会调用 Meta2.__call__。同样的,Meta1 也是个 callable 对象,那为何声明 Meta2 时不是去调用 Meta1.__call__而却是去调用 Meta1 的 new 和 init 方法呢?
2、声明 Spam 时调用了 Meta2.__call__,为何这个调用会被转变成 Meta1.__call__,这是基于什么原理呢?
万分感谢!
keakon
2017-08-27 01:19:39 +08:00
1. Meta2 是 callable,不是因为它定义了 __call__ 方法,而是它的父类( Meta1 )定义了 __call__ 方法。
另外,__new__ 和 __init__ 是由 type.__call__() 负责调用的。
2. 假设 obj 是 Cls 类的实例,obj() 等效于 obj.__call__(),等效于 Cls.__call__(obj)。
或者更通用点,obj.f(1) 等效于 Cls.f(obj, 1)。
这个语法实现在 Python 文档里有描述(搜「 Instance methods 」): https://docs.python.org/3/reference/datamodel.html
实现原理是用 descriptor,详细实现可以参考: https://www.keakon.net/2009/12/08/%E7%94%A8Descriptor%E5%AE%9E%E7%8E%B0%E5%8F%A6%E4%B8%80%E7%A7%8D%E9%A3%8E%E6%A0%BC%E7%9A%84Decorator
saximi
2017-08-27 01:38:37 +08:00
@keakon 感谢指点,我先去学习一下在你个人网站上的文章。另外,调用元类的 type.__call__方法时等同于调用元类的子类的 new 和 init 方法。关于这一点我目前是硬记的,不知要如何去理解。
keakon
2017-08-27 11:11:06 +08:00
其实不需要理解,只是 Python 是这样实现的。
你去看它的 C 实现,调用时会先用 __new__ 方法去生成一个对象,如果这个对象是目标类的实例,就再调用 __init__ 方法,否则直接返回。其他面向对象的语言也需要某种机制去调用父类的构造函数,至于是 runtime 或编译器自动做的,还是手写,就看语言的设计者了。
实际使用中你根本不会有 override type.__call__ 的需求,元类的 __new__ 已经能干任何事了,它连签名都和 __call__ 一样,只是不会自动调用 __init__ 而已。
另外,type.__call__() 等效于 type()。
saximi
2017-08-27 11:26:46 +08:00
@keakon 关于您说的这句话“ Meta2 是 callable,不是因为它定义了 __call__ 方法,而是它的父类( Meta1 )定义了 __call__ 方法”,我查了一些资料,builtin.py 中对 callable()的定义如下:

```
def callable(p_object): # real signature unknown; restored from __doc__
"""
callable(object) -> bool

Return whether the object is callable (i.e., some kind of function).
Note that classes are callable, as are instances with a __call__() method.
"""
return False

```

这个定义是否可以理解为类都是 callable 的,而实例必须实现了__call__方法后才是 callable 的?
单从这个说明来看,并没有特意提到只有当元类定义了__call__方法后,元类的子类才是 callable 的吧?毕竟元类的子类本身就是类,所以本身就是 callable,而不需要通过元类来判断?
keakon
2017-08-27 11:29:44 +08:00
keakon
2017-08-27 11:37:20 +08:00
类在声明时定义了 __call__ 方法后,它的实例才是 callable 的。
元类都继承自 type,type 定义了 __call__ 方法。类是元类的实例,所以类都是 callable 的。元类也是 type 的实例,也是类,所以也是 callable。
saximi
2017-08-27 14:35:30 +08:00
@keakon 谢谢,您看我以下的总结是否正确
1、type 类是 callable 的,这是一个基本定义,没有理由(并不是因为 type 类实现了__call__方法所以才 callable )。

2、因为所有其它类都是 type 类的实例,并且 type 类实现了__call__方法,所以其它类也默认实现了__call__方法,因此其它类都是 callable 的。所以类名()=类名.__call__()。

3、因为 Meta2 元类是 Meta1,声明 Meta2 的时候,执行到 class Meta2...这句的末尾时会调用 Meta1()生成一个 Meta1 实例。而执行 Meta1()等于分别执行 Meta1 的__new__和__init__,这就是声明 Meta2 时会执行 Meta1 的__new__和__init__的原因。

4、因为 Spam 元类是 Meta2,声明 Spam 的时候,执行到 class Spam...这句的末尾时会调用 Meta2()生成一个 Meta2 实例。
因为 Meta2 的元类是 Meta1,所以 Meta2 是 Meta1 的实例,即 Meta2=Meta1() => Meta2()=Meta1()()。而执行 Meta1()()等于依序执行 Meta1 的__new__、__init__和__call__。
因为之前声明 Meta2 的时候已经执行了 Meta1 的__new__和__init__,所以现在不会因为调用 Meta2()而重复执行 Meta1 的__new__和__init__,而是会直接执行 Meta1 的__call__。在执行 Meta1 的__call__时,因为 PYTHON 自身的机制,使得 type.__call__语句会转而去执行 Meta1 实例(即 Meta2)的__new__和__init__。

5、执行“ X=Spam()”生成 Spam 实例时,因为 Spam 的元类是 Meta2,所以 Spam=Meta2() => Spam()=Meta2()()。而执行 Meta2()()等于依序执行 Meta2 的__new__、__init__和__call__。
因为之前声明 Spam 的时候已经执行了 Meta2 的__new__和__init__,所以现在不会因为 Spam()而重复执行 Meta2 的__new__和__init__,而是会直接执行 Meta2 的__call__。在执行 Meta2 的__call__时,调用了 type.__call__(meta),值得注意的是,因为之前 Meta2()已经执行完毕,所以 Spam 对象已经生成,所以 type.__call__的参数 meta 其实就是 Spam 对象,而不是 Meta2 对象。
keakon
2017-08-27 16:24:06 +08:00
1. callable 的检查是查看它的类型是否有 tp_call: x->ob_type->tp_call != NULL。
而 type 在它的 C 实现里,将 tp_call 设为了之前我提到的 type_call 函数。
而且 type 的类型也是 type,所以 type 的类型实现了 tp_call,因此它是 callable 的。

3. 生成的那个 Meta1 实例就是 Meta2。接着 type.__call__ 调用了 Meta1 定义里的 __new__ 和 __init__ 对 Meta2 进行初始化。

4. 定义 Spam 类时,不会再调用 Meta1(),因为你传入的是 metaclass=Meta2,而不是 metaclass=Meta1(...)。
不是因为 Meta2 已经生成了,所以不需要重新生成,而是你传入的就是 Meta2 这个对象,而不是再传入一个新的 Meta1 类的实例。

5. 同上。
rust
2017-08-27 21:42:42 +08:00
我觉得这个帖子可以成为"如何提问以及使用何种态度来沟通"的典范.论坛上的戾气太重了,要是都如楼上两位礼貌就好啦~~
人不知,而不愠,不亦君子乎?

与看到的诸君共勉~ ^_^

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/385872

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX