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
waibunleung
V2EX  ›  Python

python3 的元类问题 心地善良的给些指点吧

  •  
  •   waibunleung · 2019-07-12 21:03:00 +08:00 · 2366 次点击
    这是一个创建于 1987 天前的主题,其中的信息可能已经有所发展或是发生改变。
    # -*- coding:utf-8 -*-
    
    class MyMeta(type):
        def __new__(cls, name, bases, attr):
            # attr['add'] = lambda self, x, y : x+y
            # attr['age'] = 0
            attr['addr'] = 'gz'
            return type.__new__(cls, name, bases, attr)
    
        def __init__(cls, name, bases, attr):
            super(MyMeta, cls).__init__(name, bases, attr)
            attr['age'] = 0
            cls.gender = 'male'
            print(cls)
            print(name)
            print(bases)
            print(attr)
    
            
    
    class MyClass(object, metaclass=MyMeta):
    
        def __init__(self):
            # self.age = 1
            self.name = 'hh'
    
    
    m = MyClass()
    
    print(m.name)
    print(m.gender)
    print(m.addr)
    print(m.age)
    
    # output:
    
    <class '__main__.MyClass'>
    MyClass
    (<class 'object'>,)
    {'__module__': '__main__', '__qualname__': 'MyClass', '__init__': <function MyClass.__init__ at 0x1100cb268>, 'addr': 'gz', 'age': <function MyMeta.__init__.<locals>.<lambda> at 0x1100cb2f0>}
    hh
    male
    gz
    Traceback (most recent call last):
      File "/Users/luengwaiban/Desktop/meta.py", line 32, in <module>
        print(m.age)
    AttributeError: 'MyClass' object has no attribute 'age'
    

    上面的代码里,MyMeta 是元类,MyClass 是使用元类实例化的类。 我在元类中的__new__()方法中的 addr 参数里添加 age 元素后,在 MyClass 实例化后是可以正常访问到 age 的。 但是现在屏蔽掉__new__()方法中的往 addr 参数里添加 age 元素的语句,将它放到__init__()方法中的 addr 参数里,却发现在 MyClass 实例化后是访问不到 age,但是将 age 绑在__init__()的 cls 上却可以访问(类似于 gender 的绑定)。

    这样子我就不是很理解了,为什么在元类中的__init__()方法里,将属性添加到 attr 后,MyClass 实例化完成后却访问不到对应的属性?但是将同样的操作放到元类中的__new__()方法中却可以?

    22 条回复    2019-07-18 17:12:15 +08:00
    Trim21
        1
    Trim21  
       2019-07-12 21:54:29 +08:00 via Android   ❤️ 1
    元类是用来操作类的,所以 myclass 不实例化也可以访问到 age 属性,在这里 age 和 gendar 都是类属性而不是实例属性

    因为 new 的调用在 myclass 被创建之前,修改了创建类的参数(就是调用 type.__new__的那一句),而 init 的调用在 myclass 被创建之后,类对象已经创建完了
    txy3000
        2
    txy3000  
       2019-07-13 11:56:27 +08:00
    你在元类定义的属性都是以其为元类的类属性啊 你类的实例不能访问 这样就可以访问 MyClass.age MyClass.addr
    waibunleung
        3
    waibunleung  
    OP
       2019-07-13 15:03:50 +08:00
    感谢前面的回复,我的疑问是普通类可以在__init__()中初始化成员,但是为什么在元类中的__init__()却不可以?@Trim21
    @txy3000
    Trim21
        4
    Trim21  
       2019-07-13 15:17:30 +08:00 via iPhone
    元类不是普通类的父类,普通类是实例化的元类
    你在元类 init 的时候修改的类属性修改的是普通类的类属性变量,而不是像普通类一样修改的是普通类实例的属性变量
    Trim21
        5
    Trim21  
       2019-07-13 15:20:49 +08:00 via iPhone
    @Trim21 元类 init 对应的 cls 就是那个普通类,你在这里的确修改了普通类的类属性,跟你在普通类的 init 里面修改 self.attribute 是同一个道理
    因为元类里面的普通类就对应普通类里面的类实例
    txy3000
        6
    txy3000  
       2019-07-13 15:58:33 +08:00
    感觉你是想了解 python 底层的运行机制 https://github.com/zpoint/CPython-Internals/blob/master/BasicObject/type/type_cn.md 配合 https://github.com/python/cpython 源码 应该能满足你的需求
    waibunleung
        7
    waibunleung  
    OP
       2019-07-13 20:04:35 +08:00
    @Trim21 就算我在元类 init 方法里修改的是普通类的属性,那为什么 m.age 访问不了?
    Trim21
        8
    Trim21  
       2019-07-13 20:10:00 +08:00
    可以访问啊,你访问不了是因为你把__new__里面的 attr['age']里面给注释掉了

    waibunleung
        9
    waibunleung  
    OP
       2019-07-13 20:24:24 +08:00
    @Trim21 所以老哥你误解了我的问题了,我描述里面已经说了我在__new__方法里注释掉了那个 age 的赋值,把它挪到—__init__方法去了,我想问的就是为什么放在__new__方法里可以访问,但是放到__init__方法里却不行....
    (__new__方法里那个 age 的赋值是我故意注释掉的)
    Trim21
        10
    Trim21  
       2019-07-13 20:28:32 +08:00
    @waibunleung #9 我没理解错啊, 我上面解释过了, 你在__init__里面要修改 age 属性的话, 要通过 cls.age, 不能通过 attr['age']
    waibunleung
        11
    waibunleung  
    OP
       2019-07-13 20:51:40 +08:00
    @Trim21 我不是没有尝试去理解你的话,但是我还是不能明白为什么我 在__init__里面要修改 age 属性的话, 要通过 cls.age, 不能通过 attr['age']....
    在普通类里的 init 都可以修改,还是说我不能这样子对比?
    Trim21
        12
    Trim21  
       2019-07-13 21:04:38 +08:00
    可以这样子对比啊

    普通类里面, 赋值是给 self.age 赋值, self 是__init__的第一个参数

    所以在元类里面,也是给__init__的第一个参数的 age 属性赋值, 也就是修改 cls.age, 这不是一样的吗

    区别在于一个赋值是给普通类的实例属性, 一个是类属性
    waibunleung
        13
    waibunleung  
    OP
       2019-07-13 21:18:23 +08:00
    @Trim21 我懂你的意思,但是我的问题是,为什么在元类的__init__方法中的第四个参数,这里我写作 attr,添加 age 元素后,没办法访问到 age 属性呢?
    Trim21
        14
    Trim21  
       2019-07-13 21:31:16 +08:00
    @waibunleung #13 init 里面的 attr 参数是从你 new 里面那个 attr 参数传递过来的,如果你在 new 里面没加 age 属性,在 init 里面也找不到 attr['age'
    frostming
        15
    frostming  
       2019-07-13 22:55:43 +08:00
    我明白楼主的意思了,楼主的意思是为什么可以在__new__里面动态给 attr 添加属性而__init__里面不可以

    type.__init__具体做了什么我也不清楚。结论就是,设计就是这样,如果要给 attr 添加属性,就要在__new__里面做。至于为什么这就要看源码实现了。
    waibunleung
        16
    waibunleung  
    OP
       2019-07-14 14:20:33 +08:00
    @frostming 这个是通过观察得来的结论了,就是想知道具体原因才这么问的
    waibunleung
        17
    waibunleung  
    OP
       2019-07-14 14:31:59 +08:00
    @Trim21 我不需要从 init 里面找到 age 属性,我就是想通过 init 方法去给它加上 age 这个属性
    todd7zhang
        18
    todd7zhang  
       2019-07-15 11:26:28 +08:00
    元类的__init__调用的时候, 类已经创建完毕, 你要给 MyClass 赋新的类属性 age, 当然是要用 cls.age = 0 咯, 在元类__init__里面, 你对原来的参数 attr 里面赋值, 又有什么用呢?

    对 Myclass 增加属性, 要么是在类创建之前, 对参数修改, 然后被 type.__new__调用. 要么在 type.__init__里面, 类已经创建, 再对 cls 赋值属性
    telnetning
        19
    telnetning  
       2019-07-16 19:30:22 +08:00   ❤️ 1
    我的一点简单理解,供参考,对 C 不太熟,不一定对,楼主也可以自己看一下,逻辑在 Objects/typeobject.c 中。

    __new__ 和 __init__ 中的 attr 本身就只是个 dict,并没有什么特殊的意义,区别在于 type.__new__ 和 type.__init__ 对 attr 的处理。
    在 type_new 中:

    ```py
    static PyObject *
    type_new(PyTypeObject *metatype, PyObject *args, PyObject *kwds)

    /* Check arguments: (name, bases, dict) */
    if (!PyArg_ParseTuple(args, "UO!O!:type.__new__", &name, &PyTuple_Type,
    &bases, &PyDict_Type, &orig_dict))
    return NULL;

    ...
    dict = PyDict_Copy(orig_dict);
    ....

    type->tp_dict = dict;
    ```

    即 attr 中最终传入到 tp_dict 中,也就是作为了 类的 member。

    在 type_init 中,源码中并未对 attr 做特殊处理。要想修改类,只能修改 cls。

    ```py
    static int
    type_init(PyObject *cls, PyObject *args, PyObject *kwds)
    ```
    waibunleung
        20
    waibunleung  
    OP
       2019-07-16 23:29:30 +08:00
    @telnetning 良心解答,以感谢
    zpoint
        21
    zpoint  
       2019-07-18 17:09:49 +08:00   ❤️ 1
    @telnetning 这位老哥已经解答了, 我再补充下


    执行到如下这行的时候

    class MyClass(object, metaclass=MyMeta):

    会调用 type(MyMeta).__call__ 去创建这个类, 这个 __call__ 函数在 C 里面的流程可以

    简单的理解为 1. 调用 MyMeta.__new__ 生成一个类, 叫做 MyClass, __new__ 是上面你自己定义的, 其中你调用了 type.__new__(cls, name, bases, attr), 这一步会把 attr 中的值都复制到 MyMeta 对应的属性中, attr 只是个字典而已

    2. 判断一下 issubclass(type(MyClass), MyMeta) 是否为 True, 是的话再调用一下 type(MyClass).__init__(MyClass, name, bases, attr), 这里你没有写任何代码处理 attr 和自身属性的关联, 同样的, attr 还是同一个字典

    到这里, 类已经创建完了, 接下来创建实例, 过程类似
    区别就是 __new__ 你写了一行代码 type.__new__(cls, name, bases, attr) 创建了一个类, 创建的过程中会把 attr 中的值都复制到新创建的类中对应的属性上

    而 __init__ 你没有做对应的操作


    还有, metaclass 的 __new__ 的第一个参数应该是 mcs, 为你定义的 metaclass 本身
    而 metaclass 的 __init__ 的第一个参数应该是 cls, 为 metaclass 的 __new__ 函数创建并返回的新的类, 并不是 metaclass 本身 你定义的时候重名了
    zpoint
        22
    zpoint  
       2019-07-18 17:12:15 +08:00
    更正一下错别字

    调用 MyMeta.__new__ 生成一个类, 这里生成的类名称叫做 MyClass, __new__ 是上面你自己定义的, 其中你调用了 type.__new__(cls, name, bases, attr), 这一步会把 attr 中的值都复制到 MyClass 对应的属性中, attr 只是个字典
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5336 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 06:02 · PVG 14:02 · LAX 22:02 · JFK 01:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.