小编典典

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

python

我有以下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__meta类同时被称为FOOa。但是,根本没有调用它。当我给你的东西Foo.whatever类已经被定义之后,该方法
调用。

这种行为的原因是什么,有没有办法拦截在创建类期间发生的作业?使用attrsin__new__无效,因为我想检查方法是否正在重新定义


阅读 216

收藏
2020-12-20

共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__只是返回一个普通的字典,但是您可以构建一个自定义类似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__则可以在超类调用之后覆盖属性。

遗憾的是,该技术在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)

与字典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)

使用字典的位置由元类确定。

2020-12-20