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

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

我有以下python代码:

1
2
3
4
5
6
7
8
9
10
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

我本以为FOOa都需要元类的__setattr__。但是,它根本没有被调用。在类被定义之后,当我给Foo.whatever赋值时,方法被调用。

这种行为的原因是什么?有没有一种方法可以截获在创建课程期间发生的作业?在__new__中使用attrs是行不通的,因为我想检查一个方法是否正在被重新定义。


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

这是:

1
2
3
4
5
class Foo(object):
    __metaclass__ = FooMeta
    FOO = 123
    def a(self):
        pass

就像你写的:

1
2
3
4
5
6
7
d = {}
d['__metaclass__'] = FooMeta
d['FOO'] = 123
def a(self):
    pass
d['a'] = a
Foo = d.get('__metaclass__', type)('Foo', (object,), d)

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

元类"EDOCX1"(0)可以控制当您试图在它的一个实例(类对象)上设置属性时会发生什么,但在您不这样做的类块中,您正在插入字典对象,因此dict类控制正在发生的事情,而不是元类。所以你运气不好。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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

运行这个可以让我:

1
2
3
4
5
6
7
8
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这样的非基本方法不尊重您对__setitem__这样的基本方法的覆盖。所以我更喜欢用collections.MutableMapping子类和包装字典。
  • 实际的Okay.__dict__对象是一个普通的字典,因为它是由typetype设置的,对它想要的字典类型很挑剔。这意味着类创建后重写类属性不会引发异常。如果要维护类对象字典强制的不覆盖,可以在__new__中的超类调用之后覆盖__dict__属性。
  • 遗憾的是,这种技术在python 2.x中不可用(我检查过)。没有调用__prepare__方法,这与python 2.x中的意义一样,元类是由__metaclass__magic属性确定的,而不是类块中的一个特殊关键字;这意味着当元类已知时,用于为类块积累属性的dict对象已经存在。

    比较python 2:

    1
    2
    3
    4
    5
    class Foo(object):
        __metaclass__ = FooMeta
        FOO = 123
        def a(self):
            pass

    大致相当于:

    1
    2
    3
    4
    5
    6
    7
    d = {}
    d['__metaclass__'] = FooMeta
    d['FOO'] = 123
    def a(self):
        pass
    d['a'] = a
    Foo = d.get('__metaclass__', type)('Foo', (object,), d)

    其中要调用的元类是由字典确定的,而不是由python 3确定的:

    1
    2
    3
    4
    class Foo(metaclass=FooMeta):
        FOO = 123
        def a(self):
            pass

    大致相当于:

    1
    2
    3
    4
    5
    6
    d = FooMeta.__prepare__('Foo', ())
    d['Foo'] = 123
    def a(self):
        pass
    d['a'] = a
    Foo = FooMeta('Foo', (), d)

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


    在创建课程的过程中没有作业发生。或者:他们正在发生,但不是在你认为的情况下发生的。所有类属性都是从类主体范围中收集的,并作为最后一个参数传递给元类'__new__

    1
    2
    3
    4
    5
    6
    7
    8
    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

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


    类属性作为一个字典传递给元类,我的假设是,它用于同时更新类的__dict__属性,例如,像cls.__dict__.update(dct)这样的属性,而不是对每个项执行setattr()。更重要的是,它都是在C-Land中处理的,而不是简单地写为一个自定义的__setattr__()

    对元类的__init__()方法中的类的属性进行任何操作都很容易,因为类名称空间被作为dict传递,所以只需这样做即可。


    在类创建过程中,您的名称空间将被计算为dict,并作为参数传递给元类,以及类名和基类。因此,在类定义中指定类属性不会像您期望的那样工作。它不会创建空类并分配所有内容。在dict中也不能有重复的键,因此在类创建期间,属性已经消除了重复。只有在类定义之后设置属性,才能触发自定义的setattr。

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