Python元类:为什么在类定义期间没有为__setattr__调用属性集?

Thi*_*ter 12 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
Run Code Online (Sandbox Code Playgroud)

我本来期望__setattr__的元类被称为两个的FOOa.但是,它根本没有被调用.当我给你的东西Foo.whatever类已经被定义之后,该方法调用.

这种行为的原因是什么,有没有办法拦截在创建类时发生的任务?因为我想检查一个方法是否被重新定义,因此使用attrsin __new__将不起作用.

Ben*_*Ben 21

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

这个:

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass
Run Code Online (Sandbox Code Playgroud)

几乎像你写的一样出来:

d = {}
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
    pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)
Run Code Online (Sandbox Code Playgroud)

只有没有命名空间污染(实际上还有搜索所有基础来确定元类,或者是否存在元类冲突,但我忽略了这一点).

__setattr__当你尝试在其中一个实例(类对象)上设置属性时,元类' 可以控制会发生什么,但是在类块中你没有这样做,你要插入一个字典对象,所以dict类控件发生了什么,而不是你的元类.所以你运气不好.


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

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
Run Code Online (Sandbox Code Playgroud)

运行这个给了我:

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
Run Code Online (Sandbox Code Playgroud)

一些说明:

  1. __prepare__必须是a classmethod或者staticmethod,因为它是在元类'实例(你的类)存在之前被调用的.
  2. type仍然需要它的第三个参数是真实的dict,所以你必须有一个__new__方法将其转换SingleAssignDict为正常的
  3. 我可以有子类dict,这可能已经避免了(2),但我真的不喜欢这样做,因为非基本方法update如何不尊重你的基本方法的覆盖,如__setitem__.所以我更喜欢子类化collections.MutableMapping并包装字典.
  4. 实际的Okay.__dict__对象是一个普通的字典,因为它是由它设置的,type并且type对它想要的字典类型很挑剔.这意味着在创建类之后覆盖类属性不会引发异常.如果要保持类对象的字典强制不进行覆盖,可以__dict__在超类调用后覆盖该属性__new__.

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

比较Python 2:

class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass
Run Code Online (Sandbox Code Playgroud)

大致相当于:

d = {}
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
    pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)
Run Code Online (Sandbox Code Playgroud)

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

class Foo(metaclass=FooMeta):
    FOO = 123
    def a(self):
        pass
Run Code Online (Sandbox Code Playgroud)

大致相当于:

d = FooMeta.__prepare__('Foo', ())
d['Foo'] = 123
def a(self):
    pass
d['a'] = a
Foo = FooMeta('Foo', (), d)
Run Code Online (Sandbox Code Playgroud)

要使用的字典是从元类确定的.


Cat*_*lus 7

在创建课程期间没有任何作业.或者:它们正在发生,但不是在你认为它们的背景下.所有类属性都从类体范围收集并传递给元类' __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
Run Code Online (Sandbox Code Playgroud)

原因:当类体中的代码执行时,还没有类.这意味着没有机会元类截取任何尚未.