吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1810|回复: 11
收起左侧

[Python 转载] [笔记] Python 元类(metaclass) 学习笔记

  [复制链接]
the_stars 发表于 2020-8-28 02:11
本帖最后由 the_stars 于 2020-8-31 15:17 编辑
前几天重装系统, 之前的笔记不见了. 现在重新把类的创建过程记录一次.

很多原理是不是这样并不是特别的清楚, 但是表面运行的一些东西可以去探究一二. 本质还是需要去看Python的C源代码, 下面代码都是由我自己试出来的, 可能会有很多不合理的地方, 希望各位大神可以指出.

如果有pycharm, 以下所有print打印可以通过pycharm的debug调试来代替来查看各种参数或者过程

1. 关于type

每个类其实也是一个对象, 都是由type类创建出来的对象, 这个类(type的对象), 具有call行为, 因此这个类(type的对象)可以通过

cls() 可以加括号代表具有call行为, (对应方法__call__)来创建实例. type是由自己创建而来的

>>> a = 1
>>> type(a)
<class `int`>
>>> type(int)
<class `type`>
>>> type(type)
<class `type`>
>>> class Test(object):
            pass
>>> test_object = Test()
>>> type(test_object)
<class `Test`>
>>> type(Test)
<class `type`>
  • 先扯一下关于type新手容易犯的错误. 在我刚学python那段时间, 看一个东西是什么类型, 就喜欢用print(type(xxx))来判断类型, 有一次当时判断一个参数是不是int类型,  因为第一次遇到, 立马想到的是 type(1) == "int", 结果条件一直是false, 当时就特别纳闷, 后面才知道, type(x)返回的是x的类, 因此type(1)返回的是int这个类,   (关于为什么print(type(1))打印的是<class int>, 这个了解过__str____repr__方法的应该都很清楚了. 这里不涉及)

    将一个class int  与 字符串"int"做比较自然是false, 既然type(1)是 class int 那么int("213")可不可以用type(1)("213")代替呢?

    当然是可以的

    >>> a = type(1)
    >>> a("123")
    123

    因为type(1)返回的是int 所以type(1)("123") 等价 int("123"), 当然用type(2)什么的都可以 所以如果要判断x是不是int类型, 可以通过这样判断type(a) == int (注意就是int, 没有引号), 或者 type(a) is int,  不过更推荐isinstance(a, int)

    (在最下方已更正), 这里isinstance(a, int)是不可以的, 推荐用type(a) is int

    阅读过一些源码的同学应该见过不少这样的代码, 在一个实例方法里面, 姑且叫他test方法

    class ...:

    ​                ...

    ​                def test(self):

    ​                                ...

    ​                                return type(self)(*args, **kwargs)

    最后的type(self)其实就是当前的类, 后面加括号就是自己这个类加括号, 就是生成自己这个类的对象返回回去. 由于是Python是动态语言, 这里self可能不是自己这个类, 而是被传入其他类. 这里后面再说

  • 既然所有的类都是由type创建的(这句话记住就好了), 那么哪些类的创建过程是怎样的? 现在先贴一下代码

    class Meta(type):
      pass
    
    class Test(object, metaclass=Meta):
      pass

    这里metaclass=Meta是指定以Meta为元类(元类必须继承type或者其余type的子类)来创建Test类, 不指定默认为type, 为什么这么指定我就不清楚了. 记住就好了.

    我猜测可能在type.__new__中会检测Test元类, 有的话type会让元类代{过}{滤}理它创建Test类忽略我的猜测, 免得干扰思路.

    现在我们已经指定了Test这个类由Meta创建, 也就是Test已经是Meta的对象了, 通过打印语句验证一下

    >>> print(type(Test))
    <class '__main__.Meta'>

    打印结果已经不再是<class type>了, 可以通过将metaclass=Meta删去再打印验证. 会发现打印的是<class type>

    既然会看元类, 应该都了解过__new__, __call__, __init__. 没有的话阅读起来可能会有点懵.

    这里注意一下, type的三大方法, new, call, init与object等其余class的三个是完全不同的. 但是意义差不多.

    既然Test是Meta的对象, 那么Test什么时候被创建出来?  定义的完后, 一个class定义完后下面就可以拿来用了, 所以当Test定义完后, 既然它是Meta的对象, 自然会调用Meta.__new__来创建Test这个类(也是Meta的对象), 那么废话不多说, 重写Meta的new方法.

    class Meta(type):
      def __new__(mcs, *args, **kwargs):
          print("Meta.__new__ start")
          return type.__new__(mcs, *args, **kwargs)
    
    class Test(object, metaclass=Meta):
      pass

    直接运行发现打印出了Meta.__new__, 我们在这里只定义了类和方法, 没有执行任何语句, 因此打印信息出现就代表着Meta的new被调用了. 就是在Test定义的时候, 解释器会帮我们调用它的元类去创建它. 那么第二个问题来了, new中的参数分别代表着什么呢? 打印一下就知道了

    class Meta(type):
      def __new__(mcs, *args, **kwargs):
          print(mcs)
          print(args)
          print(kwargs)
          return type.__new__(mcs, *args, **kwargs)
    
    class Test(object, metaclass=Meta):
      pass

    直接运行, 发现是

    <class '__main__.Meta'>
    ('Test', (<class 'object'>,), {'__module__': '__main__', '__qualname__': 'Test'})
    {}

    mcs表示这Meta这个元类,

    args是一个元素有三个, 分别是'Test', object, 和一个字典

    没有关键字参数.

    这里就直接说args了, 第一个Test类的名字, 第二个是Test类的父类, 第三个是Test类的命名空间. 就是它的一些属性, 可以通过类名+.调用, 而且这三个对于一个类来说是必须参数, 那么下面我们把Meta.__new__参数简化一点.

    class Meta(type):
      def __new__(mcs, name, bases, namespaces, **kwargs):
          print(kwargs)
          return type.__new__(mcs, name, bases, namespaces)
    

    这里type.__new__不能传入**kwargs, 因为type.__new__只有4个参数, 关于这个kwargs可以暂时忽略干嘛用的, 只有自定义元类才可以加这个参数.

  • 第二个问题, type.__new__返回的是什么东西?  很简单, 打印一下就可以了, 贴代码

    class Meta(type):
      def __new__(mcs, name, bases, namespaces, **kwargs):
          cls = type.__new__(mcs, name, bases, namespaces)
          print(cls)
          return cls
    
    class Test(object, metaclass=Meta):
      pass
    

    下面是运行结果

    <class '__main__.Test'>

    竟然是Test类, 那就代表着type.__new__通过传入的name, bases, namespaces来创建了一个类. 关于第参数一个mcs, 就是我猜测最终都是由type来为一个类指定元类去创建的原因(忽略).

    那么为Test类定义一些类属性和方法, 看看能不能在Meta.__new__中去调用一下

    class Meta(type):
      def __new__(mcs, name, bases, namespaces, **kwargs):
          cls = type.__new__(mcs, name, bases, namespaces)
          print(cls.a)
          cls.print()
          return cls
    
    class Test(object, metaclass=Meta):
      a = 1
    
      @classmethod
      def print(cls):
          print("我是<class `%s`>" % cls.__name__)
    

    发现被正常调用

    1
    我是<class `Test`>

    既然元类可以在类创建之前进行拦截, 如果我们Meta.__new__中把参数namespaces拦截并修改, 比如加一个叫做__init__的方法, 而Test中不定义__init__, 那么当用Test创建实例后会自动调用__init__吗? 贴代码

    def init(self):
      print("我是<class `%s`>的实例%s" % (type(self), self))
    
    class Meta(type):
      def __new__(mcs, name, bases, namespaces, **kwargs):
          namespaces['__init__'] = init
          cls = type.__new__(mcs, name, bases, namespaces)
          return cls
    
    class Test(object, metaclass=Meta):
      pass
    
    Test()

    发现打印如下

    我是<class `<class '__main__.Test'>`>的实例<__main__.Test object at 0x000002CCED4E7640>

    那就代表可以在创建之前拦截类并为他添加一些方法属性, 甚至嵌入你想要的任何操作.

    __new__到这里也差不多了, 至于拦截后可以干什么就要看具体需求了,比如 通过__new__你可以简单实现如下功能,  定义某一各类, 如果这个类由一个方法是大写开头的, 直接抛出异常说不符合代码规范. 然后终止程序.

  • 关于type.__init__, 其实这个和正常的__init__做的事情差不多, 也是来初始化对象的(也是类), 不过要注意的事它只接受四个参数

    class Meta(type):
      def __new__(mcs, name, bases, namespaces, **kwargs):
          namespaces['__init__'] = init
          cls = type.__new__(mcs, name, bases, namespaces)
          return cls
    
      def __init__(cls, name, bases, namespaces, **kwargs):
          type.__init__(cls, name, bases, namespaces)
    
    class Test(object, metaclass=Meta):
      pass

    关于**kwargs一样, 只有自定义元类可以接受这个参数, __init__可以接受的参数和__new__几乎一样, 唯一不同的是第一个参数, new中传入的是元类Meta, init中传入的事new中返回的创建好的cls(也就是Test类), 在init中进行初始化合情合理. __init__差不多就这里了, 可以通过哪些参数做想要为Test类进行初始化的操作

  • 关于type.__call__,  这个就负责在创建对象的时候进行一些调度了

    如果Test定义了__init__方法和__new__,  这些方法为什么会被调用, 是谁帮我们调用的, 这个其实是type.__call__帮我们调用了这两个方法,

    了解过__call__方法应该可以理解下面的话, 假设一个类Test, 创建实例了, test = Test(), 这里问: 为什么Test可以加括号? 就是因为Test这个类(type的对象)的类(也就是type)定义__call__方法, 使得type的对象(也就是Test类)可以进行加括号调用, 说了这么多, 下面上代码

    class Meta(type):
      def __call__(cls, *args, **kwargs):
          print("Meta.__call__")
          return type.__call__(cls, *args, **kwargs)  # 仅仅是加一个打印语句, 原来的type返回什么我们不去碰它
    
    class Test(object, metaclass=Meta):
      pass
    
    t = Test()

    这里有个 有意思的地方, 如果直接运行, 会打印出来Meta.__call__, 我们似乎并没有执行什么函数或者方法, 仅仅是定义了一个类, 实例化了一个对象, 而这个对象也并没有__init__和__new__方法. 我们只在Meta.__call__中有过打印语句而已, 尝试删掉

    t = Test(), 会发现不会打印了, 那代表着在实例化Test()调用了Meta.__call__方法.

    那么第二个问题, __call__传入的参数是什么,  打印一下就知道了.

    class Meta(type):
      def __call__(cls, *args, **kwargs):
          print(cls)
          print(args)
          print(kwargs)
          return type.__call__(cls, *args, **kwargs)  # 仅仅是加一个打印语句, 原来的type返回什么我们不去碰它
    
    class Test(object, metaclass=Meta):
      pass
    
    Test(1, 2, 3, 4, a=2, b=3)

    打印结果

    <class '__main__.Test'>
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}
    Traceback (most recent call last):
    File "F:/PycharmProjects/Test/meta_test/meta.py", line 16, in <module>
      Test(1, 2, 3, 4, a=2, b=3)
    File "F:/PycharmProjects/Test/meta_test/meta.py", line 9, in __call__
      return type.__call__(cls, *args, **kwargs)
    TypeError: Test() takes no arguments

    其实args和kwargs就是在实例化的时候传入的位置参数和关键字参数, 第一个cls就是代表着Test类. 后面报错因为Test并没有定义

    init方法去接受参数. 而在Meta.__call__中并没有显示的调用Test.__init__啊, 那么久只能在type.__call__type帮我们干了这些事情, 帮我们自动调用__init__, 和__new__等, 那么我们拦截args, kwargs, 不让它传入到type.__call__中, 是不是就可以不会报错了呢?  代码

    class Meta(type):
      def __call__(cls, *args, **kwargs):
          print(cls)
          print(args)
          print(kwargs)
          return type.__call__(cls)  # 移除了参数
    
    class Test(object, metaclass=Meta):
      pass
    
    Test(1, 2, 3, 4, a=2, b=3)

    输出

    <class '__main__.Test'>
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}

    果然没有报错, 我们在call中拦截了传给init的参数, 就不会导致参数不符合情况了

    下一步通过print确认一下是不是

    class Meta(type):
      def __call__(cls, *args, **kwargs):
          print("Meta.__call__ start")
          print(args)
          print(kwargs)
          obj = type.__call__(cls, *args, **kwargs)  # 恢复之前的参数
                  print("Meta.__call__ end")
          return obj
    
    class Test(object, metaclass=Meta):
      def __new__(cls, *args, **kwargs):
          print("Test.__new__ start")
          print(args)
          print(kwargs)
          self = object.__new__(cls)
          print("Test.__new__ end")
          return self
    
      def __init__(self, *args, **kwargs):
          print("Test.__init__ start")
          print(args)
          print(kwargs)
          print("Test.__init__ end")
    
    test_obj = Test(1, 2, 3, 4, a=2, b=3)
    

    打印结果

    Meta.__call__ start
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}
    Test.__new__ start
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}
    Test.__new__ end
    Test.__init__ start
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}
    Test.__init__ end
    Meta.__call__ end

    很清晰的可以看到, 在type.__call__中, 首先帮我们调用了Test__new__, 然后才帮我们调用的Test.__init__, 注意最后是Test.__init__ end后, 才到Meta.__call__ end, 所以test_obj接收的是Meta.__call__中返回的obj, 并不是Test.__new__返回的self, 虽然在这里他们都一样, 并且都是同一个对象和地址., 那么我在Meta.__call__返回一个随意的对象, test_object接收的可不再是Test的实例的, 甚至于Test毫无关系, 代码

    class Meta(type):
      def __call__(cls, *args, **kwargs):
          print("Meta.__call__ start")
          print(args)
          print(kwargs)
          obj = type.__call__(cls, *args, **kwargs)  # 恢复之前的参数
          print("Meta.__call__ end")
          return Demo()
    
    class Test(object, metaclass=Meta):
      def __new__(cls, *args, **kwargs):
          print("Test.__new__ start")
          print(args)
          print(kwargs)
          self = object.__new__(cls)
          print("Test.__new__ end")
          return self
    
      def __init__(self, *args, **kwargs):
          print("Test.__init__ start")
          print(args)
          print(kwargs)
          print("Test.__init__ end")
    
    class Demo(object):
      pass
    
    test_object = Test(1, 2, 3, 4, a=2, b=3)
    print("-" * 20)
    print(test_object)

    打印结果

    Meta.__call__ start
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}
    Test.__new__ start
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}
    Test.__new__ end
    Test.__init__ start
    (1, 2, 3, 4)
    {'a': 2, 'b': 3}
    Test.__init__ end
    Meta.__call__ end
    --------------------
    <__main__.Demo object at 0x0000024DFF1883A0>

    返回的test_object是Demo对象.

    到这来, 总共类的创建过程以及行为差不多就通了, 从类的创建拦截type.__new__, 到给类初始化type.__init__. 到控制类产生对象type.__call__

2. 关于**kwargs
  • 上面一直说只有自定义元类可以接收**kwargs, 但是这东西是怎么来的, 看下面代码

    class Meta(type):
      def __new__(mcs, name, bases, namespaces, **kwargs):
          print(kwargs)
          return type.__new__(mcs, name, bases, namespaces)
    
    kws = {"a": 1, "b": lambda: 1}
    
    class Test(object, metaclass=Meta, _root=True, **kws):
      pass
    

    打印结果

    {'_root': True, 'a': 1, 'b': <function <lambda> at 0x000002AFAE146280>}

    其实就是接收在类定义后面的括号里面给的关键字参数而已. 和函数用法相同, 不过metaclass这些有特殊意义的关键字参数并没有被接收, 可能是被type拦截了.

3. 关于__init_subclass__bound method
  • __init_subclass__可以在执行一些简单的拦截类的行为使用, 可以无须自定义元类.

  • bound method一个函数真正绑定在一个对象身上成为方法, 从而省略传入第一个参数, 是在type.__call__中完成的, 在这之前其实那些实例方法都只是函数, 比如

    class Demo(object):
      def f(self):
          print(self)
    
    class Test(object):
      def f(self, x, y):
          self.f()
          print(x, y)
    
    Test.f(Demo(), 1, 2)

    输出

    <__main__.Demo object at 0x0000026F0C827460>
    1 2

    在没有被绑定之前完全就是可以当一个函数使用.

  • 具体的下次有机会在记一记吧.

更正 isinstance(a, int)

isinstance(obj, type_tuple) 是判断obj是type_tuple中的实例的, 父类也可以,  因此这里isinstance(int, int)是 False, 是我的失误.

那么应该推荐用type(a) is int, 至于为什么不推荐type(a) == int, 这里也拓展一下吧, 因为用is判断是判断内存地址, 而 == 是调用__eq__, 我们这里的需求是a的类型就是int, 在这里无论==还是int都没问题, 毕竟我们不会去更改int这个内置的类, 如果是我们自己写的类或者一些三方库, 且__eq__被重写了, 可能会发生一些预期之外的结果. 比如.

class Meta(type):
    def __eq__(cls, other):
        return cls is not other

class Test(object, metaclass=Meta):
    pass

print(Test == Test)
print(Test is Test)

输出

False
True

是不是很意外? 因为type(t) == Test会去调用Test类的类(就是Meta)的__eq__方法, 返回的自然是False, 但是用is只比较内存地址, 而每个类只有唯一的一个地址, 那么就一定为True, 而且效率也高于==, 因==还回去调用函数, 如果函数里面还有复杂的操作, 那么效率更慢.

PS: __eq__如果定义在Test中, 那么这个eq只在Test生成的对象作==比较的时候才会被调用, 不要混淆eq定义在Meta里面和Test里面.

免费评分

参与人数 7吾爱币 +7 热心值 +7 收起 理由
Zeaf + 1 + 1 我很赞同!
skyward + 1 + 1 不错,有收获~
Alex27933 + 1 + 1 谢谢@Thanks!
rosemaryzed + 1 + 1
喜欢你和酸奶 + 1 + 1 冲鸭
yc0205 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
joneqm + 1 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

喜欢你和酸奶 发表于 2020-8-28 08:38
学习学习  多发发笔记吧 o·(&#730; &#707;&#803;&#803;&#805;&#7620;&#8979;&#706;&#803;&#803;&#805;&#7621; )&#8231;o
米粒米粒 发表于 2020-8-28 04:12
xxscwsrym 发表于 2020-8-28 05:40
a6670950810 发表于 2020-8-28 06:51
我来学习学习
chunfengyidu 发表于 2020-8-28 07:13
学习一下,谢谢楼主分享
没想到吧 发表于 2020-8-28 08:08
看了一下,发现自己不会
xy0225 发表于 2020-8-28 08:28
这个不错,学习借鉴
阿奇大魔王 发表于 2020-8-28 09:22
学习学习
JohnLu 发表于 2020-8-28 09:23
向楼主学习,不断挑战自己
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-25 02:10

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表