Python 元类 : Why isn't __setattr__ called for attributes set during class definition?

标签 python metaclass

我有以下 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 时, 方法被调用。

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

最佳答案

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

这个:

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__ 可以控制当您尝试在其实例之一(类对象)上设置属性时发生的情况,但是在类 block 内您'如果不这样做,您将插入到字典对象中,因此 dict 类控制正在发生的事情,而不是您的元类。所以你运气不好。


除非您使用的是 Python 3.x!在 Python 3.x 中,您可以在元类上定义一个 __prepare__ 类方法(或静态方法),它控制在将属性集传递给元类构造函数之前使用什么对象来累积类 block 中的属性集。默认的 __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

一些注意事项:

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

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

比较 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)

要使用的词典由元类确定。

关于Python 元类 : Why isn't __setattr__ called for attributes set during class definition?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10762088/

相关文章:

python - 如何从列中获取日期时间类型而不是时间戳?

python - 包 __init__.py 中导入的模块不可通过点访问

Delphi 类引用...又名元类...何时使用它们

Python 元类为类生成正确的枚举值

Python:元类或 `__init_subclass__`:错误子类创建期间的惯用异常?

python - 使用selenium脚本测试URL,但浏览器打开后,它没有指向请求的URL

python - 错误 "unsupported operand type(s) for -: ' str' 和 'float' ?

python - 动态向数组添加新行

python - 使用元类来跟踪 python 中的实例

python - 习惯从类型继承元类?