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

时间:2012-05-25 22:35: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

我原本期望为__setattr__FOO调用元类a。但是,它根本没有被调用。在定义了类之后,当我为Foo.whatever分配了一些东西时,方法调用。

这种行为的原因是什么,是否有办法拦截在创建课程期间发生的作业?使用attrs中的__new__无效,因为我想check if a method is being redefined

4 个答案:

答案 0 :(得分:21)

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

此:

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__类方法(或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

运行它给了我:

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对它想要的字典类型很挑剔。这意味着在创建类之后覆盖类属性不会引发异常。如果要保持类对象字典所强制的无覆盖,则可以在__dict__中的超类调用后覆盖__new__属性。

  5. 遗憾的是,这项技术在Python 2.x中没有用(我检查过)。不调用__prepare__方法,这在Python 2.x中是有意义的,元类由__metaclass__ magic属性而不是classblock中的特殊关键字确定;这意味着用于累积类块属性的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)
    

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

答案 1 :(得分: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

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

答案 2 :(得分:2)

类属性作为单个字典传递给元类,我的假设是这用于同时更新类的__dict__属性,例如:类似于cls.__dict__.update(dct)而不是对每个项目执行setattr()。更重要的是,它全部都是在C-land中处理的,并且不是为了调用自定义__setattr__()而编写的。

在元类的__init__()方法中,您可以轻松地对类的属性执行任何操作,因为您将类命名空间作为dict传递,所以就这样做。

答案 3 :(得分:0)

在类创建期间,您的命名空间将被计算为dict,并作为参数传递给元类,以及类名和基类。因此,在类定义中分配类属性将不会按预期方式工作。它不会创建一个空类并分配所有内容。您也不能在dict中拥有重复的键,因此在类创建期间,属性已经过重复数据删除。只有在类定义后设置属性,才能触发自定义__setattr __。

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