• python 面向对象专题(十三):元类(二): metaclass魔术方法


    0 魔术方法

    魔术方法是python的一个特点:他们允许程序员重写变量操作符号和对象的行为。调用者需要这样来重写:

    class Funky:
        def __call__(self):
            print("Look at me, I work like a function!")
    f = Funky()
    f()

    返回值就是print的那句话了。像function一样工作。

    metaclass依赖一些魔术方法,所以多了解一些是非常有用的。

    1 slots(定位,跟踪)

    当你在class中定义一个魔术方法的时候,function除了__dict__中的条目之外,在整个类结构中,作为一个描述着这个class的指针一样结束。这个结构对于每一个魔术方法有一个字段。出于一些原因这些字段被称为type slots。

    现在,这里有另一个特征,通过__slots__属性执行,一个拥有__slots__的class创造的实例不包含__dict__(这将使用更少的内存)。副作用是实例不能出现未在__slots__中指定的字段:如果你尝试设置一个不存在于__slots__中的字段,那么将会获得一个报错。

    本文提及的单独的slots都是type slots不是__slots__。(类里的魔术方法)

    class Foobar:
        """
         A class that only allows these attributes: "a", "b" or "c"
        """
        __slots__ = "a", "b", "c"
     
    foo = Foobar()
    foo.a = 1
    # foo.x = 2

    2 对象属性查找

    这里很容易出错,因为和python2的就样式相比有很多细小的不同。

    假设我们有一个类和一个实例,并且实例是类的实例,获取(评估:原文用evaluate)实例的footbar大概相当于下面这样:

    为Class.__getattribute__ (tp_getattro)调用type slot。默认会执行下面:
        Class.__dict__是否有一个foobar元素是一个数据描述符?
            如果有,返回Class.__dict__['foobar'].__get__(instance, Class)
        instance.__dict__是否有一个foobar元素?
            如果有,返回instance.__dict__['foobar']
        Class.__dict__是否有一个foobar元素但并不是数据描述符?
            如果有,返回Class.__dict__['foobar'].__get__(instance, klass)
        Class.__dict__是否有一个foobar元素?
            如果有,返回Class.__dict__['foobar']
    如果属性还没找到,如果有Class.__getattr__,就会调用Class.__getattr__('foobar')

    如果你还不清楚,请看下图:

     为了避免点号'.'带来的混淆,图里用了冒号':'。

    类属性查找

    当你查找(评估:原文用evaluate)一些类似于class的foobar,由于class需要能够支持classmathod和staticmethod装饰器,所以和查找实例的foobar有一点不同。

    假设类是metaclass的实例,查找(评估:原文用evaluate)class的foobar相当于下面这样:

    为Metaclass.__getattribute__ (tp_getattro)调用type slot。默认会执行下面:
        Metaclass.__dict__是否有一个foobar元素是一个数据描述符?
            如果有,返回Metaclass.__dict__['foobar'].__get__(Class, Metaclass)
        Class.__dict__是否有一个foobar元素是一个描述符(任何种类)?
            如果有,返回Class.__dict__['foobar'].__get__(None, Class)
        Class.__dict__是否有一个foobar元素?
            如果有,返回Class.__dict__['foobar']
        Metaclass.__dict__是否有一个foobar元素不是一个数据描述符?
            如果有,返回Metaclass.__dict__['foobar'].__get__(Class, Metaclass)
        Metaclass.__dict__是否有一个foobar元素?
            如果有,返回Metaclass.__dict__['foobar']
    如果属性还没找到,并且有Metaclass.__getattr__,就会调用Metaclass.__getattr__('foobar')

    魔术方法查看

    对于魔术方法来说,查找已经完成了,直接在大结构上用slots。

    对象的类是否有关于魔术方法的slot(大概就像c语言中object->ob_type->tp_<魔术方法>)?如果有,就使用,如果是NULL,那么选项不被支持。
    在C中:
    object->ob_type是对象的类
    ob_type->tp_<魔术方法>是type slot

    这看起来很简单,然而type slots在你function的外包装上到处都是,所以描述符就按照预期工作:

    class Magic:
        @property
        def __repr__(self):
            def inner():
                return "It works!"
            return inner
     
    print(repr(Magic()))

    这是否意味着这些地方并没有遵守规则,并且用不同的方式找到了slot?很遗憾是的,继续。。。

    __new__方法

    __new__方法是class和metaclass之间最容易混淆的方法之一。他有一些非常特别的约定。

    当__init__只是一个初始化装置(当__init__被调用的时候,实例已经被创建了)的时候,__new__方法是一个创造者(因为他返回新的实例)。

    假设有下面的class:

    class Foobar:
        def __new__(cls):
            return super().__new__(cls)

    现在你重新调用之前的部分,你将期待__new__将会在metaclass上查找,但是很遗憾,对于这种情况他并不是很有用,所以他查找的很安静。

    __prepare__方法

    这个方法被第要用在class本体被执行之前并且他必须返回一个类似字典的对象,这个对象被用来作为class本体的所有代码的本地命名空间。(在类中namespace参数可以取到__prepare__的返回值)在python3的时候加入。

    如果你的__prepare__返回一个对象x:

    class Class(metaclass=Meta):
        a = 1
        b = 2
        c = 3

    将对x做如下改变:

    x['a'] = 1
    x['b'] = 2
    x['c'] = 3

    这个x对象需要看起来像个字典。注意这个x对象最终将成为Metaclass.__new__的参数,如果他不是一个dict的实例,你需要在调用super().__new__之前转换它。

    我们用__prepare__返回一个对象,这个对象只能执行__getitem__和__setitem__:

    class DictLike:
        def __init__(self):
            self.data = {}
        def __getitem__(self, name):
            print('__getitem__(%r)' % name)
            return self.data[name]
        def __setitem__(self, name, value):
            print('__setitem__(%r, %r)' % (name, value))
            self.data[name] = value
    class CustomNamespaceMeta(type):
        def __prepare__(name, bases):
            return DictLike()

    然而,__new__将会抱怨:

    class Foobar(metaclass=CustomNamespaceMeta):
        a = 1
        b = 2
    __getitem__('__name__')
    __setitem__('__module__', '__main__')
    __setitem__('__qualname__', 'Foobar')
    __setitem__('a', 1)
    __setitem__('b', 2)
    Traceback (most recent call last):
      File "test.py", line 99, in <module>
        class Foobar(metaclass=CustomNamespaceMeta):
    TypeError: type.__new__() argument 3 must be dict, not DictLike

    我们必须把它转化成真正的字典(或者他的一个子类):

    class FixedCustomNamespaceMeta(CustomNamespaceMeta):
        def __new__(mcs, name, bases, namespace):
            return super().__new__(mcs, name, bases, namespace.data)

    接着,一切跟我期待的一样:

    class Foobar(metaclass=FixedCustomNamespaceMeta):
        a = 1
        b = 2
    __getitem__('__name__')
    __setitem__('__module__', '__main__')
    __setitem__('__qualname__', 'Foobar')
    __setitem__('a', 1)
    __setitem__('b', 2)

    下面这段代码我添了点东西,上面理解了你可以不看:

    class DictLike:
        def __init__(self):
            self.data = {}
        def __getitem__(self, name):
            print('__getitem__(%r)' % name)
            return self.data[name]
        def __setitem__(self, name, value):
            print('__setitem__(%r, %r)' % (name, value))
            self.data[name] = value
    class CustomNamespaceMeta(type):
        def __prepare__(name, bases):
            d = DictLike()
            print(d)
            print(d.__dict__)
            return d
     
    class FixedCustomNamespaceMeta(CustomNamespaceMeta):
        def __new__(mcs, name, bases, namespace):
            print(mcs)
            print(name)
            print(namespace)
            print(namespace.__dict__)
     
            return super().__new__(mcs, name, bases, namespace.data)
    class Foobar(metaclass=FixedCustomNamespaceMeta):
        a = 1
        b = 2

    返回值

    <__main__.DictLike object at 0x04F53790>
    {'data': {}}
    __getitem__('__name__')
    __setitem__('__module__', '__main__')
    __setitem__('__qualname__', 'Foobar')
    __setitem__('a', 1)
    __setitem__('b', 2)
    <class '__main__.FixedCustomNamespaceMeta'>
    Foobar
    <__main__.DictLike object at 0x04F53790>
    {'data': {'__module__': '__main__', '__qualname__': 'Foobar', 'a': 1, 'b': 2}}

    返回值中可以看出namespace和__prepare__的返回值是一个东西。

    把他们放在一起

    先介绍一下实例是如何构建的:

    如何读这个泳道图:

    水平的两块泳道代表你定义function的地方。

    实心的线意味着function被调用了。

    从Metaclass.__call__到Class.__new__的线意味着Metaclass.__call__将调用Class.__new__。

    虚线意味着有一些东西要返回。

    Class.__new__返回了一个Class的实例。

    Metaclass.__call__返回了一切Class.__new__返回的东西(如果他返回了一个class实例,他也要在上面调用class.__init__)。

    写数字红圆圈记录了调用顺序。

    创造一个class也非常的相似:

    简单的写下:

    Metaclass.__prepare__只是返回命名空间对象(一个类似字典的对象,像之前解释的那样)。

    Metaclass.__new__返回Class对象

    Metaclass.__call__返回一切Metaclass__new__ 返回的(返回一个metaclass的实例,他同样在实例上调用了Metaclass.__init__)。

    无论是metaclass还是class,如果__new__没有返回实例,那么就不会触发__init__

    所以,你会发现metaclass允许你定制对象生命周期中几乎所有的部分。

  • 相关阅读:
    如何在maven项目中使用spring
    Maven之项目搭建与第一个helloworld(多图)
    Extjs Grid 各种Demo
    关于HTML与CSS与class
    两个乒乓球队进行比赛,各出三人。甲队为a,b,c三人,乙队为x,y,z三人。已抽签决定比赛名单。有人向队员打听比赛的名单。a说他不和x比,c说他不和x,z比,请编程序找出三队赛手的名单。
    猴子吃桃问题:猴子第一天摘下若干个桃子,当即吃了一半,还不瘾,又多吃了一个     第二天早上又将剩下的桃子吃掉一半,又多吃了一个。以后每天早上都吃了前一天剩下     的一半零一个。到第10天早上想再吃时,见只剩下一个桃子了。求第一天共摘了多少。
    一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数,请问该数是多少?
    :判断101-200之间有多少个素数,并输出所有素数。 程序分析:判断素数的方法:用一个数分别去除2到sqrt(这个数),如果能被整除, 则表明此数不是素数,反之是素数。
    设有一数据库,包括四个表:学生表(Student)、课程表(Course)、成绩表(Score)以及教师信息表(Teacher)。四个表的结构分别如表1-1的表(一)~表(四)所示,数据如表1-2的表(一)~表(四)所示。用SQL语句创建四个表并完成相关题目。
    企业发放的奖金根据利润提成
  • 原文地址:https://www.cnblogs.com/qiu-hua/p/14732386.html
Copyright © 2020-2023  润新知