Python 元类:为啥在类定义期间不调用 __setattr__ 来设置属性?

Posted

技术标签:

【中文标题】Python 元类:为啥在类定义期间不调用 __setattr__ 来设置属性?【英文标题】:Python metaclasses: Why isn't __setattr__ called for attributes set during class definition?Python 元类:为什么在类定义期间不调用 __setattr__ 来设置属性? 【发布时间】:2012-06-01 11:44:17 【问题描述】:

我有以下 python 代码:

class FooMeta(type):
    def __setattr__(self, name, value):
        print name, value
        return super(FooMeta, self).__setattr__(name, value)

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass

我本来希望 __setattr__ 的元类同时被 FOOa 调用。但是,它根本没有被调用。当我在定义类之后将某些内容分配给Foo.whatever 时,方法 is 被调用。

这种行为的原因是什么?有没有办法拦截在创建类期间发生的分配?在__new__ 中使用attrs 将不起作用,因为我想使用check if a method is being redefined。

【问题讨论】:

这个确切的问题是 py3 中元类语法发生变化的原因! 【参考方案1】:

类块大致是构建字典,然后调用元类来构建类对象的语法糖。

这个:

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass

看起来就像你写的一样:

d = 
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
    pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)

只有在没有命名空间污染的情况下(实际上还需要搜索所有基础以确定元类,或者是否存在元类冲突,但我在这里忽略了这一点)。

元类'__setattr__ 可以控制当您尝试在其实例之一(类对象)上设置属性时发生的情况,但在类块内您没有这样做,您正在插入字典对象,因此 dict 类控制正在发生的事情,而不是您的元类。所以你运气不好。


除非您使用的是 Python 3.x!在 Python 3.x 中,您可以在元类上定义 __prepare__ 类方法(或静态方法),它控制在将类块中的属性集传递给元类构造函数之前使用什么对象来累积属性集。默认的__prepare__ 只是返回一个普通的字典,但您可以构建一个不允许重新定义键的自定义类字典,并使用它来累积您的属性:

from collections import MutableMapping


class SingleAssignDict(MutableMapping):
    def __init__(self, *args, **kwargs):
        self._d = dict(*args, **kwargs)

    def __getitem__(self, key):
        return self._d[key]

    def __setitem__(self, key, value):
        if key in self._d:
            raise ValueError(
                'Key !r already exists in SingleAssignDict'.format(key)
            )
        else:
            self._d[key] = value

    def __delitem__(self, key):
        del self._d[key]

    def __iter__(self):
        return iter(self._d)

    def __len__(self):
        return len(self._d)

    def __contains__(self, key):
        return key in self._d

    def __repr__(self):
        return '(!r)'.format(type(self).__name__, self._d)


class RedefBlocker(type):
    @classmethod
    def __prepare__(metacls, name, bases, **kwargs):
        return SingleAssignDict()

    def __new__(metacls, name, bases, sad):
        return super().__new__(metacls, name, bases, dict(sad))


class Okay(metaclass=RedefBlocker):
    a = 1
    b = 2


class Boom(metaclass=RedefBlocker):
    a = 1
    b = 2
    a = 3

运行它给了我:

Traceback (most recent call last):
  File "/tmp/redef.py", line 50, in <module>
    class Boom(metaclass=RedefBlocker):
  File "/tmp/redef.py", line 53, in Boom
    a = 3
  File "/tmp/redef.py", line 15, in __setitem__
    'Key !r already exists in SingleAssignDict'.format(key)
ValueError: Key 'a' already exists in SingleAssignDict

一些注意事项:

    __prepare__ 必须是 classmethodstaticmethod,因为它在元类的实例(您的类)存在之前被调用。 type 仍然需要它的第三个参数才能成为真正的 dict,因此您必须有一个 __new__ 方法将 SingleAssignDict 转换为普通参数 我可以继承dict,这可能会避免(2),但我真的不喜欢这样做,因为像update这样的非基本方法不尊重你对像@这样的基本方法的覆盖987654338@。所以我更喜欢继承collections.MutableMapping 并包装字典。 实际的Okay.__dict__ 对象是一个普通的字典,因为它是由type 设置的,而type 对它想要的字典类型很挑剔。这意味着在类创建之后覆盖类属性不会引发异常。如果要保持类对象的字典强制不覆盖,可以在 __new__ 中的超类调用之后覆盖 __dict__ 属性。

遗憾的是,这种技术在 Python 2.x 中不可用(我检查过)。 __prepare__ 方法没有被调用,这很有意义,因为在 Python 2.x 中,元类由 __metaclass__ 魔法属性而不是类块中的特殊关键字确定;这意味着在元类已知时,用于累积类块属性的 dict 对象已经存在。

比较 Python 2:

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass

大致相当于:

d = 
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
    pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)

要调用的元类是从字典中确定的,而不是 Python 3:

class Foo(metaclass=FooMeta):
    FOO = 123
    def a(self):
        pass

大致相当于:

d = FooMeta.__prepare__('Foo', ())
d['Foo'] = 123
def a(self):
    pass
d['a'] = a
Foo = FooMeta('Foo', (), d)

要使用的字典在哪里由元类确定。

【讨论】:

【参考方案2】:

在创建课程期间没有发生任何作业。或者:它们正在发生,但不是在您认为的上下文中。所有的类属性都是从类体范围收集的,并作为最后一个参数传递给元类的__new__

class FooMeta(type):
    def __new__(self, name, bases, attrs):
        print attrs
        return type.__new__(self, name, bases, attrs)

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123

原因:当类体中的代码执行时,还没有类。这意味着元类没有机会拦截任何东西

【讨论】:

【参考方案3】:

类属性作为单个字典传递给元类,我的假设是这用于一次更新类的__dict__ 属性,例如像cls.__dict__.update(dct) 这样的东西,而不是在每个项目上做setattr()。更重要的是,这一切都是在 C-land 中处理的,根本不是为了调用自定义 __setattr__() 而编写的。

在元类的__init__() 方法中对类的属性做任何你想做的事情都很容易,因为你是作为dict 传递类命名空间的,所以就这样做吧。

【讨论】:

【参考方案4】:

在类创建期间,您的命名空间被评估为一个字典,并作为参数传递给元类,连同类名和基类。因此,在类定义中分配类属性不会按您期望的方式工作。它不会创建一个空类并分配所有内容。您也不能在 dict 中有重复的键,因此在类创建期间,属性已经被重复数据删除。只有在类定义之后设置一个属性,你才能触发你的自定义 __setattr__。

因为命名空间是一个字典,所以您无法检查重复的方法,正如您的其他问题所建议的那样。唯一可行的方法是解析源代码。

【讨论】:

以上是关于Python 元类:为啥在类定义期间不调用 __setattr__ 来设置属性?的主要内容,如果未能解决你的问题,请参考以下文章

Python从门到精通:元类-01-元类

在类中的所有实例方法自动调用之后调用方法:元类还是装饰器?

Python 元类行为(不调用 __new__),有解释吗?

python元类深入解析

元类上的拦截运算符查找

Day 5-8 自定义元类控制类的实例化行为