我有以下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。
答案 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
一些注意事项:
__prepare__
必须是classmethod
或staticmethod
,因为它是在元类'实例(您的类)存在之前调用的。type
仍然需要其第三个参数为真实dict
,因此您必须使用__new__
方法将SingleAssignDict
转换为正常dict
子类化,这可能已经避免了(2),但我真的不喜欢这样做,因为像update
这样的非基本方法不尊重你的覆盖。像__setitem__
这样的基本方法。所以我更喜欢子类collections.MutableMapping
并包装字典。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)
要从字典中确定要调用的元类,而不是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 __。
因为命名空间是一个字典,所以你无法检查重复的方法,正如你的另一个问题所建议的那样。唯一可行的方法是解析源代码。