V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
saximi
V2EX  ›  Python

请教一个关于元类的问题

  •  
  •   saximi · 2017-08-26 00:22:31 +08:00 · 1803 次点击
    这是一个创建于 2443 天前的主题,其中的信息可能已经有所发展或是发生改变。
    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 ?  
    
    
    11 条回复    2017-08-27 21:42:42 +08:00
    keakon
        1
    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
        2
    saximi  
    OP
       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
        3
    keakon  
       2017-08-27 01:19:39 +08:00   ❤️ 1
    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
        4
    saximi  
    OP
       2017-08-27 01:38:37 +08:00
    @keakon 感谢指点,我先去学习一下在你个人网站上的文章。另外,调用元类的 type.__call__方法时等同于调用元类的子类的 new 和 init 方法。关于这一点我目前是硬记的,不知要如何去理解。
    keakon
        5
    keakon  
       2017-08-27 11:11:06 +08:00
    其实不需要理解,只是 Python 是这样实现的。
    你去看它的 C 实现,调用时会先用 __new__ 方法去生成一个对象,如果这个对象是目标类的实例,就再调用 __init__ 方法,否则直接返回。其他面向对象的语言也需要某种机制去调用父类的构造函数,至于是 runtime 或编译器自动做的,还是手写,就看语言的设计者了。
    实际使用中你根本不会有 override type.__call__ 的需求,元类的 __new__ 已经能干任何事了,它连签名都和 __call__ 一样,只是不会自动调用 __init__ 而已。
    另外,type.__call__() 等效于 type()。
    saximi
        6
    saximi  
    OP
       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
        7
    keakon  
       2017-08-27 11:29:44 +08:00
    keakon
        8
    keakon  
       2017-08-27 11:37:20 +08:00
    类在声明时定义了 __call__ 方法后,它的实例才是 callable 的。
    元类都继承自 type,type 定义了 __call__ 方法。类是元类的实例,所以类都是 callable 的。元类也是 type 的实例,也是类,所以也是 callable。
    saximi
        9
    saximi  
    OP
       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
        10
    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
        11
    rust  
       2017-08-27 21:42:42 +08:00
    我觉得这个帖子可以成为"如何提问以及使用何种态度来沟通"的典范.论坛上的戾气太重了,要是都如楼上两位礼貌就好啦~~
    人不知,而不愠,不亦君子乎?

    与看到的诸君共勉~ ^_^
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2321 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 03:09 · PVG 11:09 · LAX 20:09 · JFK 23:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.