Python中的元类是什么(具体的)用例?

我有一个喜欢使用元类的朋友,经常提供他们作为解决scheme。

我想到你几乎不需要使用元类。 为什么? 因为我认为如果你正在做类似的事情,你可能应该做一个对象。 而一个小的重新devise/重构是为了。

能够使用元类使很多地方的人把类作为某种二级对象,这对我来说似乎是灾难性的。 编程是否被元编程取代? 不幸的是,类装饰器的添加使得它更加可以接受。

所以,请绝望的去了解Python中元类的有效(具体)用例。 或者开悟一下为什么变异类比变异对象更好,有时候。

我将开始:

有时在使用第三方库时,能够以某种方式改变这个类是很有用的。

(这是我能想到的唯一的情况,而不是具体的)

我有一个类来处理非交互式绘图,作为Matplotlib的前端。 然而,有时候想做互动式阴谋。 只有一些function,我发现我能够增加数字,手动调用绘图等,但是我需要在每次绘图调用之前和之后执行这些操作。 所以要创build一个交互式绘图包装器和一个屏幕外包装器,我发现通过元类来包装适当的方法比做类似的事情更有效率:

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__ ,但这是我find的最直接的解决scheme。

元类的目的不是用元类/类来replace类/对象的区别 – 而是以某种方式改变类定义(以及它们的实例)的行为。 实际上,它是以比默认更适合您的特定域的方式来改变类语句的行为。 我用过的东西是:

  • 跟踪子类,通常用于注册处理程序。 这在使用插件样式设置时非常方便,您只需通过子类化和设置一些类属性就可以注册特定事件的处理程序。 例如。 假设你为各种音乐格式编写处理程序,其中每个类为其types实现适当的方法(播放/获取标记等)。 为新types添加处理程序变成:

     class Mp3File(MusicFile): extensions = ['.mp3'] # Register this type as a handler for mp3 files ... # Implementation of mp3 methods go here 

    然后元类维护一个{'.mp3' : MP3File, ... }等字典,当你通过一个工厂函数请求一个处理程序时,构造一个适当types的对象。

  • 改变行为。 你可能想给特定的属性附加一个特殊的含义,当它们出现时会导致改变的行为。 例如,您可能希望查找名为_get_foo_set_foo方法,并将它们透明地转换为属性。 作为一个真实世界的例子, 这里给出了一个给我更多类C结构定义的方法。 元类用于将声明的项目转换为结构格式string,处理inheritance等,并生成一个能够处理它的类。

    对于其他现实世界的例子,请看看各种ORM,比如sqlalchemy 的 ORM或sqlobject 。 同样,目的是用特定的含义来解释定义(这里是SQL列定义)。

最近我被问到同样的问题,并提出了几个答案。 我希望可以重新启动这个线程,因为我想详细介绍一些提到的用例,并添加一些新的用例。

我见过的大多数元类都做了以下两件事之一:

  1. 注册(向数据结构中添加一个类):

     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 

    或者用一个显式的注册function:

     models = {} def register_model(cls): models[cls.__name__] = cls class A(object): pass register_model(A) 

    实际上,这几乎是相同的:你提到类装饰器是不利的,但它实际上只不过是一个类的函数调用的语法糖,所以没有什么魔力。

    无论如何,在这种情况下,元类的优点是inheritance,因为它们适用于任何子类,而其他解决scheme仅适用于明确装饰或注册的子类。

     >>> class B(A): ... pass ... >>> models {'A': <__main__.A class at 0x...> # No B :( 
  2. 重构(修改类属性或添加新的属性):

     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')} 

    再次,这可以用类装饰器来完成(没有inheritance):

     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! 

    尽pipe与您倡导可读和可维护的非元编程相反,这更麻烦,多余且容易出错:

     class B(A): bar = String() # vs. class B(A): bar = String('bar') _fields = {'B.bar': bar, 'A.foo': A.foo} 

在考虑了最常见和具体的用例之后,你绝对必须使用元类的唯一情况是当你想修改类名或基类列表时,因为一旦定义了这些参数就会被烘焙到类中,并且没有装饰器或function可以取消它们。

 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 

这个在框架中可能是有用的,只要有相似名字的类或者不完整的inheritance树被定义,就可以发出警告,但是我不能想象除了拖动之外的一个理由来改变这些值。 也许大卫Beazley可以。

无论如何,在Python 3中,元类还有__prepare__方法,它允许您将类体评估为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 [<function foo at 0x...>, <function foo at 0x...>] rather than <function foo at 0x...> 

您可能会争论有序的属性可以通过创build计数器来实现,并且可以使用默认参数来模拟重载:

 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) 

除了更丑陋之外,它也不那么灵活:如果你想要命令字面属性,比如整数和string,怎么办? 如果Nonex的有效值,该怎么办?

这是解决第一个问题的创造性方法:

 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,编程应该很有趣。 一直使用标准的编程结构和devise模式是无聊和不愉快的,并阻碍你的想象力。 坚持一下! 这是一个metameta类,只为你。

 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 

让我们从蒂姆·彼得的经典引语开始吧:

元类比99%的用户应该担心的更深。 如果你想知道你是否需要他们,你不需要(那些真正需要他们的人肯定知道他们需要他们,不需要为什么解释)。 蒂姆·彼得斯(clp post 2002-12-22)

话虽如此,我(定期)运行元类的真正用途。 想到的是在Django中,所有的模型都从models.Modelinheritance。 models.Model,反过来,做一些严重的魔术,用Django的ORM善良包装你的数据库模型。 这种魔法是通过元类的方式发生的。 它创build所有的exception类,经理类等等。

请参阅django / db / models / base.py,类ModelBase()作为故事的开始。

元类可以方便地在Python中构build领域特定语言。 具体的例子是Django,SQLObject的数据库模式的声明式语法。

Ian Bicking的一个保守元Metaclass的基本例子:

我用过的元类主要是为了支持一种声明式的编程风格。 例如,考虑一个validation模式:

 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构buildDSL的原料 (pdf)。

编辑(由阿里):这样做使用集合和实例的例子是我更喜欢。 重要的事实是这些实例给你更多的权力,并消除使用元类的理由。 进一步值得注意的是,你的例子使用了类和实例的混合,这肯定表明你不能用元类来完成所有的事情。 并创build一个真正的非统一的方式来做到这一点。

 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,]) ] 

这不是完美的,但已经几乎没有魔法,不需要元类,并提高了均匀性。

我昨天刚才也是这样想,完全同意。 由于试图使其更具说明性而导致的代码中的复杂性通常会使得代码更难以维护,难以阅读,而且在我看来,pythonic更less。 它通常还需要大量的copy.copy()(维护inheritance和从类复制到实例),这意味着你必须在很多地方查看发生了什么事情(总是从元类上看),这违背了蟒纹也。 我一直在通过formencode和sqlalchemy代码来看看这样的声明式风格是否值得,而且显然不是。 这样的风格应该留给描述符(如属性和方法)和不可变的数据。 Ruby对这种声明式样式有更好的支持,我很高兴核心python语言不会走这条路线。

我可以看到他们用于debugging,添加一个元类到所有的基类,以获得更丰富的信息。 我也看到他们只在(非常)大的项目中使用,以摆脱一些样板代码(但失去了清晰度)。 例如 ,sqlalchemy确实在其他地方使用它们,以便根据类定义中的属性值向所有子类添加特定的自定义方法,例如玩具示例

 class test(baseclass_with_metaclass): method_maker_value = "hello" 

可以有一个元类,在那个类中生成一个基于“hello”的特殊属性的方法(比如在string末尾添加“hello”的方法)。 可维护性可能很好,以确保您不必在每个子类中编写方法,而只需要定义method_maker_value即可。

对此的需求是非常罕见的,只是减less了一些input,除非你有足够大的代码库,否则不值得考虑。

一个合理的元类使用模式是,当一个类被定义而不是重复的时候,只要同一个类被实例化,就做一次事情。

当多个类共享相同的特殊行为时,重复__metaclass __ = X显然比重复专用代码和/或引入即席共享超类更好。

但是,即使只有一个特殊的类,也没有可预见的扩展名,元类的__new____init__是在类定义主体中混合特殊用途代码和普通的defclass语句的初始化类variables或其他全局数据的更简洁的方法。

你永远不需要使用一个元类,因为你总是可以构造一个类,使用你想要修改的类的inheritance或聚合来做你想要的。

也就是说,在Smalltalk和Ruby中可以很方便地修改一个已经存在的类,但是Python不喜欢直接这样做。

在Python中有一个关于元类的优秀的DeveloperWorks文章可能会有所帮助。 维基百科的文章也相当不错。

我在Python中唯一使用元类的时候是为Flickr API编写包装器。

我的目标是刮flickr的api网站,并dynamic地生成一个完整的类层次结构,以允许使用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,所以我真的不知道运行时的类定义。 能够dynamic生成types是非常有用的。

元类的唯一合法用例是让其他有趣的开发者不要触碰你的代码。 一旦有一个八卦的开发者掌握了元类,并开始和你一起探索,那么再投入一两个层次来阻止他们。 如果这不起作用,请使用type.__new__或者使用recursion元类的一些scheme。

(写在脸颊上的舌头,但我已经看到了这种模糊处理,Django就是一个很好的例子)

元类不会取代编程! 他们只是一个可以自动化或者做出更优雅的任务的技巧。 一个很好的例子是Pygments语法高亮库。 它有一个名为RegexLexer的类,它可以让用户在一个类上定义一组学习规则作为正则expression式。 元类用于将定义转换为有用的parsing器。

他们就像盐; 很容易使用太多。

我使用元类的方式是为类提供一些属性。 举个例子:

 class NameClass(type): def __init__(cls, *args, **kwargs): type.__init__(cls, *args, **kwargs) cls.name = cls.__name__ 

将把每个类中的名称属性放置在将被设置为指向NameClass的元类上。

有些GUI库在multithreading尝试与它们交互时遇到麻烦。 tkinter就是这样的一个例子。 虽然人们可以用事件和队列来明确地处理这个问题,但是以一种完全忽略问题的方式来使用这个库要简单得多。 看吧 – 元类的魔力。

能够无缝地dynamic重写整个库,以便在multithreading应用程序中按预期正常工作,这在某些情况下非常有用。 safetkinter模块在threadbox模块提供的元类的帮助下做到这一点 – 不需要事件和队列。

threadbox一个threadbox方面是它不关心它克隆的类是什么。 它提供了一个例子,说明如果需要的话,所有的基类可以被元类触及。 元类带来的另一个好处是它们也在inheritance类上运行。 编写自己的程序 – 为什么不呢?

这是一个小用途,但是…我发现有用的元类是每当创build一个子类时调用一个函数。 我把它__initsubclass__成一个查找__initsubclass__属性的元类:无论何时创build子类,定义该方法的所有父类都用__initsubclass__(cls, subcls)调用。 这允许创build一个父类,然后用一些全局registry注册所有的子类,每当定义子类时运行不变检查,执行后期绑定操作等等,所有这些都不需要手动调用函数创build自定义元类履行这些单独的职责。

请注意,我已经慢慢认识到这种行为的隐含的不可思议性是有些不可取的,因为如果从一个上下文的angular度来看待类定义,这是意料之外的……所以我已经不再使用这个解决scheme来处理任何严重的事情初始化每个类和实例的__super属性。

我最近不得不使用元类来帮助声明性地定义一个数据库表的SQLAlchemy模型,这个数据库表是用来自http://census.ire.org/data/bulkdata.html的美国人口普查数据;

IRE为人口普查数据表提供数据库shell ,根据人口普查局p012015,p012016,p012017等的命名惯例创build整数列。

我想要a)能够使用model_instance.p012017语法来访问这些列,b)对于我正在做什么以及c)没有必要显式地定义模型上的几十个字段,所以我将SQLAlchemy的DeclarativeMeta子类化为迭代通过一系列的列自动创build与列对应的模型字段:

 from sqlalchemy.ext.declarative.api import DeclarativeMeta class CensusTableMeta(DeclarativeMeta): def __init__(cls, classname, bases, dict_): table = 'p012' for i in range(1, 49): fname = "%s%03d" % (table, i) dict_[fname] = Column(Integer) setattr(cls, fname, dict_[fname]) super(CensusTableMeta, cls).__init__(classname, bases, dict_) 

然后,我可以使用这个元类用于我的模型定义,并访问模型上的自动枚举字段:

 CensusTableBase = declarative_base(metaclass=CensusTableMeta) class P12Tract(CensusTableBase): __tablename__ = 'ire_p12' geoid = Column(String(12), primary_key=True) @property def male_under_5(self): return self.p012003 ... 

似乎这里描述了一个合法的用法 – 用Metaclass重写Python Docstrings。

我不得不使用它们一次为二进制parsing器,使它更容易使用。 您定义了一个消息类,其中包含导线上的字段的属性。 他们需要按照他们声明的方式进行sorting,以便从中构build最终的线路格式。 如果你使用有序的命名空间字典,你可以用元类来实现。 事实上,它在Metaclasses的例子中:

https://docs.python.org/3/reference/datamodel.html#metaclass-example

但总的来说:非常仔细地评估,如果你真的需要增加元类的复杂性。