我有一个朋友喜欢使用元类,并定期提供它们作为解决方案.
我很想你几乎不需要使用元类.为什么?因为我认为如果你正在对一个类做类似的事情,你应该把它做成一个对象.并且需要一个小的重新设计/重构.
能够使用元类导致许多地方的很多人使用类作为某种二流对象,这对我来说似乎是灾难性的.编程是否被元编程取代?遗憾的是,类装饰器的添加使其更加可接受.
所以,我非常想知道Python中元类的有效(具体)用例.或者开悟为什么变异类有时比变异对象更好.
我将开始:
有时,在使用第三方库时,能够以某种方式改变类是有用的.
(这是我能想到的唯一一个案例,并不具体)
最近我被问到了同样的问题,并提出了几个答案.我希望恢复这个线程是可以的,因为我想详细说明一些提到的用例,并添加一些新的用例.
我见过的大多数元类都做两件事之一:
注册(向数据结构添加类):
models = {} class ModelMetaclass(type): def __new__(meta, name, bases, attrs): models[name] = cls = type.__new__(meta, name, bases, attrs) return cls class Model(object): __metaclass__ = ModelMetaclass
无论何时子类化Model
,您的类都在models
字典中注册:
>>> class A(Model): ... pass ... >>> class B(A): ... pass ... >>> models {'A': <__main__.A class at 0x...>, 'B': <__main__.B class at 0x...>}
这也可以使用类装饰器完成:
models = {} def model(cls): models[cls.__name__] = cls return cls @model class A(object): pass
或者使用显式注册功能:
models = {} def register_model(cls): models[cls.__name__] = cls class A(object): pass register_model(A)
实际上,这几乎是一样的:你提到类装饰器是不利的,但它实际上只不过是对一个类上的函数调用的语法糖,所以对它没有任何魔力.
无论如何,在这种情况下元类的优点是继承,因为它们适用于任何子类,而其他解决方案仅适用于显式修饰或注册的子类.
>>> class B(A): ... pass ... >>> models {'A': <__main__.A class at 0x...> # No B :(
重构(修改类属性或添加新属性):
class ModelMetaclass(type): def __new__(meta, name, bases, attrs): fields = {} for key, value in attrs.items(): if isinstance(value, Field): value.name = '%s.%s' % (name, key) fields[key] = value for base in bases: if hasattr(base, '_fields'): fields.update(base._fields) attrs['_fields'] = fields return type.__new__(meta, name, bases, attrs) class Model(object): __metaclass__ = ModelMetaclass
无论何时子类Model
和定义某些Field
属性,都会注入它们的名称(例如,用于提供更多信息性错误消息),并将其分组到一个_fields
字典中(以便于迭代,无需查看所有类属性及其所有基类)属性每次):
>>> class A(Model): ... foo = Integer() ... >>> class B(A): ... bar = String() ... >>> B._fields {'foo': Integer('A.foo'), 'bar': String('B.bar')}
同样,这可以使用类装饰器完成(没有继承):
def model(cls): fields = {} for key, value in vars(cls).items(): if isinstance(value, Field): value.name = '%s.%s' % (cls.__name__, key) fields[key] = value for base in cls.__bases__: if hasattr(base, '_fields'): fields.update(base._fields) cls._fields = fields return cls @model class A(object): foo = Integer() class B(A): bar = String() # B.bar has no name :( # B._fields is {'foo': Integer('A.foo')} :(
或明确:
class A(object): foo = Integer('A.foo') _fields = {'foo': foo} # Don't forget all the base classes' fields, too!
虽然与您倡导可读和可维护的非元编程相反,但这更加麻烦,冗余且容易出错:
class B(A): bar = String() # vs. class B(A): bar = String('bar') _fields = {'B.bar': bar, 'A.foo': A.foo}
在考虑了最常见和最具体的用例之后,您绝对必须使用元类的唯一情况是,当您想要修改类名或基类列表时,因为一旦定义,这些参数将被烘焙到类中,并且没有装饰器或功能可以解开它们.
class Metaclass(type): def __new__(meta, name, bases, attrs): return type.__new__(meta, 'foo', (int,), attrs) class Baseclass(object): __metaclass__ = Metaclass class A(Baseclass): pass class B(A): pass print A.__name__ # foo print B.__name__ # foo print issubclass(B, A) # False print issubclass(B, int) # True
这可能在用于在定义具有相似名称或不完整继承树的类时发出警告的框架中有用,但我想不出除了实际更改这些值之外的原因.也许David Beazley可以.
无论如何,在Python 3中,元类也有这个__prepare__
方法,它允许你将类体评估为除了a之外的映射dict
,从而支持有序属性,重载属性和其他邪恶的酷东西:
import collections class Metaclass(type): @classmethod def __prepare__(meta, name, bases, **kwds): return collections.OrderedDict() def __new__(meta, name, bases, attrs, **kwds): print(list(attrs)) # Do more stuff... class A(metaclass=Metaclass): x = 1 y = 2 # prints ['x', 'y'] rather than ['y', 'x']
class ListDict(dict): def __setitem__(self, key, value): self.setdefault(key, []).append(value) class Metaclass(type): @classmethod def __prepare__(meta, name, bases, **kwds): return ListDict() def __new__(meta, name, bases, attrs, **kwds): print(attrs['foo']) # Do more stuff... class A(metaclass=Metaclass): def foo(self): pass def foo(self, x): pass # prints [, ] rather than
您可能会认为可以使用创建计数器实现有序属性,并且可以使用默认参数模拟重载:
import itertools class Attribute(object): _counter = itertools.count() def __init__(self): self._count = Attribute._counter.next() class A(object): x = Attribute() y = Attribute() A._order = sorted([(k, v) for k, v in vars(A).items() if isinstance(v, Attribute)], key = lambda (k, v): v._count)
class A(object): def _foo0(self): pass def _foo1(self, x): pass def foo(self, x=None): if x is None: return self._foo0() else: return self._foo1(x)
除了更难看之外,它也不太灵活:如果你想要有序的文字属性,如整数和字符串,该怎么办?如果None
是有效值x
怎么办?
这是解决第一个问题的创造性方法:
import sys class Builder(object): def __call__(self, cls): cls._order = self.frame.f_code.co_names return cls def ordered(): builder = Builder() def trace(frame, event, arg): builder.frame = frame sys.settrace(None) sys.settrace(trace) return builder @ordered() class A(object): x = 1 y = 'foo' print A._order # ['x', 'y']
这是解决第二个问题的创造性方法:
_undefined = object() class A(object): def _foo0(self): pass def _foo1(self, x): pass def foo(self, x=_undefined): if x is _undefined: return self._foo0() else: return self._foo1(x)
但这比一个简单的元类(特别是第一个真正融化你的大脑)的伏都教更多.我的观点是,你认为元类是不熟悉和反直觉的,但你也可以将它们视为编程语言演变的下一步:你只需要调整你的思维方式.毕竟,你可能在C中做所有事情,包括用函数指针定义一个结构并将它作为第一个参数传递给它的函数.第一次看到C++的人可能会说,"这是什么魔法?为什么编译器会隐式传递this
给方法,而不是传递给常规函数和静态函数?最好是对你的参数进行明确和冗长".但是,一旦你得到它,面向对象编程就会变得更加强大; 我猜是这样的,呃......面向方面的编程.一旦你理解了元类,它们实际上非常简单,那么为什么不在方便的时候使用它们呢?
最后,元类是rad,编程应该很有趣.使用标准的编程结构和设计模式总是令人厌烦和缺乏灵感,并且阻碍了你的想象力.坚持一下!这是一个metametaclass,只为你.
class MetaMetaclass(type): def __new__(meta, name, bases, attrs): def __new__(meta, name, bases, attrs): cls = type.__new__(meta, name, bases, attrs) cls._label = 'Made in %s' % meta.__name__ return cls attrs['__new__'] = __new__ return type.__new__(meta, name, bases, attrs) class China(type): __metaclass__ = MetaMetaclass class Taiwan(type): __metaclass__ = MetaMetaclass class A(object): __metaclass__ = China class B(object): __metaclass__ = Taiwan print A._label # Made in China print B._label # Made in Taiwan
元类的目的不是用metaclass/class替换类/对象的区别 - 它是以某种方式改变类定义(以及它们的实例)的行为.实际上,改变类语句的行为的方式可能对您的特定域比默认值更有用.我用过的东西是:
跟踪子类,通常用于注册处理程序.这在使用插件样式设置时非常方便,您只希望通过子类化和设置一些类属性来为特定事物注册处理程序.例如.假设您为各种音乐格式编写处理程序,其中每个类为其类型实现适当的方法(播放/获取标记等).为新类型添加处理程序变为:
class Mp3File(MusicFile): extensions = ['.mp3'] # Register this type as a handler for mp3 files ... # Implementation of mp3 methods go here
然后,元类维护{'.mp3' : MP3File, ... }
etc 的字典,并在通过工厂函数请求处理程序时构造适当类型的对象.
改变行为.您可能希望为某些属性附加特殊含义,从而导致存在时行为发生改变.例如,你可能想寻找一个名为方法_get_foo
和_set_foo
和透明地转换为性能.作为一个真实的例子,这里是我写的一个配方,用于提供更多类似C的结构定义.元类用于将声明的项转换为结构格式字符串,处理继承等,并生成一个能够处理它的类.
对于其他实际示例,请查看各种ORM,例如sqlalchemy 的 ORM或sqlobject.同样,目的是解释具有特定含义的定义(此处为SQL列定义).
我有一个处理非交互式绘图的类,作为Matplotlib的前端.但是,有时人们想要进行交互式绘图.只有几个函数,我发现我能够增加数字计数,手动调用绘图等,但我需要在每次绘图调用之前和之后执行这些操作.因此,要创建交互式绘图包装器和屏幕外绘图包装器,我发现通过元类包装适当的方法来执行此操作更有效,而不是执行以下操作:
class PlottingInteractive: add_slice = wrap_pylab_newplot(add_slice)
此方法无法跟上API更改等,但__init__
在重新设置类属性之前迭代类属性的方法更有效并且保持最新:
class _Interactify(type): def __init__(cls, name, bases, d): super(_Interactify, cls).__init__(name, bases, d) for base in bases: for attrname in dir(base): if attrname in d: continue # If overridden, don't reset attr = getattr(cls, attrname) if type(attr) == types.MethodType: if attrname.startswith("add_"): setattr(cls, attrname, wrap_pylab_newplot(attr)) elif attrname.startswith("set_"): setattr(cls, attrname, wrap_pylab_show(attr))
当然,可能有更好的方法来做到这一点,但我发现这是有效的.当然,这也可以在__new__
或中完成__init__
,但这是我发现最直接的解决方案.
让我们从蒂姆彼得的经典名言开始:
元类比99%的用户应该担心的更深刻.如果你想知道你是否需要它们,你就不会(实际需要它们的人确切地知道他们需要它们,并且不需要解释为什么).蒂姆彼得斯(clp post 2002-12-22)
话虽如此,我(定期)遇到了元类的真正用法.想到的是Django,你的所有模型都继承自models.Model.反过来,models.Model用Django的ORM优点来包装你的数据库模型.这种魔法通过元类来实现.它创建了各种异常类,管理器类等.
有关故事的开头,请参阅django/db/models/base.py,类ModelBase().
元类可以方便地在Python中构建域特定语言.具体的例子是Django,SQLObject的数据库模式的声明性语法.
Ian Bicking的A Conservative Metaclass的一个基本例子:
我使用过的元类主要是为了支持一种声明式的编程风格.例如,考虑验证模式:
class Registration(schema.Schema): first_name = validators.String(notEmpty=True) last_name = validators.String(notEmpty=True) mi = validators.MaxLength(1) class Numbers(foreach.ForEach): class Number(schema.Schema): type = validators.OneOf(['home', 'work']) phone_number = validators.PhoneNumber()
其他一些技术:用Python构建DSL的成分(pdf).
编辑(由Ali):使用集合和实例执行此操作的示例是我更喜欢的.重要的事实是实例,它们可以为您提供更多功能,并消除使用元类的原因.进一步值得注意的是,您的示例使用了类和实例的混合,这肯定表明您不能只使用元类来完成所有操作.并创造了一种真正非均匀的方式.
number_validator = [ v.OneOf('type', ['home', 'work']), v.PhoneNumber('phone_number'), ] validators = [ v.String('first_name', notEmpty=True), v.String('last_name', notEmpty=True), v.MaxLength('mi', 1), v.ForEach([number_validator,]) ]
它并不完美,但已经几乎没有魔法,不需要元类,并且改善了均匀性.
元类使用的合理模式是在定义类时执行一次,而不是在实例化同一类时重复执行.
当多个类共享相同的特殊行为时,重复__metaclass__=X
显然比重复特殊用途代码和/或引入ad-hoc共享超类更好.
但即使只有一个特殊的类,没有可预见的扩展, __new__
并且__init__
元类是一种更简洁的方法来初始化类变量或其他全局数据,而不是在类定义体中混合使用特殊用途代码和普通def
和class
语句.
我在Python中使用元类的唯一一次是为Flickr API编写包装器.
我的目标是刮掉flickr的api站点并动态生成一个完整的类层次结构,以允许使用Python对象进行API访问:
# Both the photo type and the flickr.photos.search API method # are generated at "run-time" for photo in flickr.photos.search(text=balloons): print photo.description
所以在那个例子中,因为我从网站生成了整个Python Flickr API,所以我真的不知道运行时的类定义.能够动态生成类型非常有用.
我昨天也在考虑同样的事情并完全同意.由于试图使其更具说明性而导致的代码复杂性通常会使代码库更难维护,更难以阅读并且在我看来更少pythonic.它通常还需要大量copy.copy()ing(以保持继承并从一个类复制到实例)并且意味着你必须在许多地方查看最新情况(总是从元类向上看)这与蟒蛇纹也.我一直在挑选formencode和sqlalchemy代码,看看这样的声明风格是否值得,而且显然不是.这种样式应该留给描述符(例如属性和方法)和不可变数据.Ruby对这种声明性样式有更好的支持,我很高兴核心python语言不会沿着那条路走下去.
我可以看到它们用于调试,为所有基类添加元类以获得更丰富的信息.我也看到他们只在(非常)大型项目中使用以摆脱一些样板代码(但是在清晰度方面).sqlalchemy 例如在其他地方使用它们,根据类定义中的属性值向所有子类添加特定的自定义方法,例如玩具示例
class test(baseclass_with_metaclass): method_maker_value = "hello"
可能有一个元类,它在该类中生成一个方法,该方法具有基于"hello"的特殊属性(比如在字符串末尾添加"hello"的方法).可维护性确保您不必在您创建的每个子类中编写方法,而您必须定义的是method_maker_value.
对此的需求是如此罕见,只是减少了一些打字,除非你有足够大的代码库,否则它不值得考虑.