关于python:元类的一些(具体)用例是什么?

What are some (concrete) use-cases for metaclasses?

我有一个朋友喜欢使用元类,并经常提供它们作为解决方案。

我认为您几乎不需要使用元类。为什么?因为我想如果你对一个类做类似的事情,你应该对一个对象做。一个小的重新设计/重构是有序的。

能够使用元类已经导致很多地方的很多人使用类作为某种二流对象,这对我来说是灾难性的。编程是否被元编程取代?不幸的是,添加了类装饰器使得它更容易被接受。

所以,我非常想知道您在Python中对元类的有效(具体)用例。或者启发我们为什么有时改变类比改变对象更好。

我将开始:

Sometimes when using a third-party
library it is useful to be able to
mutate the class in a certain way.

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


最近有人问我同样的问题,我想出了几个答案。我希望可以重新激活这个线程,因为我想详细说明上面提到的一些用例,并添加一些新的用例。好的。

我见过的大多数元类都做两件事之一:好的。

  • 注册(向数据结构添加类):好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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字典中注册:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    >>> class A(Model):
    ...     pass
    ...
    >>> class B(A):
    ...     pass
    ...
    >>> models
    {'A': <__main__.A class at 0x...>,
     'B': <__main__.B class at 0x...>}

    这也可以通过类修饰符来实现:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    models = {}

    def model(cls):
        models[cls.__name__] = cls
        return cls

    @model
    class A(object):
        pass

    或具有显式注册功能:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    models = {}

    def register_model(cls):
        models[cls.__name__] = cls

    class A(object):
        pass

    register_model(A)

    实际上,这几乎是一样的:你提到类修饰器时很不情愿,但它实际上只是类上函数调用的语法甜头,所以没有什么魔力。好的。

    无论如何,在这种情况下,元类的优点是继承,因为它们适用于任何子类,而其他解决方案只适用于显式修饰或注册的子类。好的。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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字典中(为了便于迭代,不必每次都查看所有类属性及其所有基类属性):好的。

    1
    2
    3
    4
    5
    6
    7
    8
    >>> class A(Model):
    ...     foo = Integer()
    ...
    >>> class B(A):
    ...     bar = String()
    ...
    >>> B._fields
    {'foo': Integer('A.foo'), 'bar': String('B.bar')}

    同样,这可以通过类修饰器(不继承)完成:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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')} :(

    或明确地:好的。

    1
    2
    3
    class A(object):
        foo = Integer('A.foo')
        _fields = {'foo': foo} # Don't forget all the base classes' fields, too!

    尽管与您倡导可读和可维护的非元编程相反,这更为麻烦、冗余和容易出错:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    class B(A):
        bar = String()

    # vs.

    class B(A):
        bar = String('bar')
        _fields = {'B.bar': bar, 'A.foo': A.foo}
  • 考虑到最常见和最具体的用例,您绝对必须使用元类的唯一情况是,当您想要修改类名或基类列表时,因为一旦定义了这些参数,这些参数就会被烘焙到类中,并且没有任何装饰器或函数可以将它们取消绑定。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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

    这可能在框架中很有用,用于在定义具有相似名称或不完整继承树的类时发出警告,但除了Trolling之外,我想不出实际更改这些值的原因。也许大卫比兹利可以。好的。

    无论如何,在python 3中,元类也有__prepare__方法,它允许您将类体评估为dict以外的映射,从而支持有序属性、重载属性和其他邪恶的酷东西:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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']

    nbsp;好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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...>

    您可能认为可以使用创建计数器实现有序属性,并且可以使用默认参数模拟重载:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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)

    nbsp;好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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)

    除了更难看之外,它也不那么灵活:如果你想要有序的文字属性,比如整数和字符串,怎么办?如果Nonex的有效值怎么办?好的。

    以下是解决第一个问题的创造性方法:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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']

    下面是解决第二个问题的创造性方法:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    _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,编程应该很有趣。使用标准的编程结构和设计模式一直都是无聊和无趣的,并且阻碍了你的想象力。多活一点!这是一个元类,只为您。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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

    好啊。


    元类的目的不是用元类/类替换类/对象的区别,而是以某种方式改变类定义(及其实例)的行为。实际上,它是改变类语句的行为,其方式对于特定域可能比默认域更有用。我使用它们的目的是:

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

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

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

    • 改变行为。您可能希望对某些属性附加一个特殊的含义,从而在它们出现时改变行为。例如,您可能希望查找名称为_get_foo_set_foo的方法,并透明地将它们转换为属性。作为一个现实世界的例子,这里有一个我编写的方法来给出更多类似C的结构定义。元类用于将声明的项转换为结构格式字符串、处理继承等,并生成能够处理它的类。

      对于其他现实世界的例子,请看一下各种各样的窗体,比如SQLAlchemy的ORM或SQLObject。同样,目的是解释具有特定含义的定义(这里是SQL列定义)。


    我有一个处理非交互式绘图的类,作为Matplotlib的前端。然而,有时人们想进行交互式绘图。只使用了几个函数,我发现我可以增加数字计数、手动调用绘图等,但我需要在每次绘图调用之前和之后执行这些操作。因此,为了创建一个交互式绘图包装器和一个屏幕外绘图包装器,我发现通过元类(包装适当的方法)这样做比执行以下操作更有效:

    1
    2
    class PlottingInteractive:
        add_slice = wrap_pylab_newplot(add_slice)

    这个方法不跟上API的变化等等,但是在重新设置类属性之前,迭代__init__中的类属性的方法更有效,并且使事情保持最新:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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__中实现,但这是我发现最直接的解决方案。


    让我们从蒂姆·彼得的经典名言开始:

    Metaclasses are deeper magic than 99%
    of users should ever worry about. If
    you wonder whether you need them, you
    don't (the people who actually need
    them know with certainty that they
    need them, and don't need an
    explanation about why). Tim Peters
    (c.l.p post 2002-12-22)

    说到这一点,我(定期)遇到了元类的真正用途。想到的是在Django中,所有模型都继承自models.model。模型。模型,反过来,做了一些严重的魔术,用Django的ORM美德包装您的数据库模型。这种魔力是通过元类产生的。它创建各种各样的异常类、管理器类等。

    请参见django/db/models/base.py,class modelbase()了解故事的开头。


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

    伊恩·比金的保守元类的一个基本例子:

    The metaclasses I've used have been
    primarily to support a sort of
    declarative style of programming. For
    instance, consider a validation
    schema:

    1
    2
    3
    4
    5
    6
    7
    8
    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):使用集合和实例执行此操作的一个示例是我喜欢的。重要的事实是实例,它给了您更多的能力,并消除了使用元类的原因。更值得注意的是,您的示例使用了类和实例的混合,这显然表明您不能只使用元类来完成这一切。并创造出一种真正不统一的方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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明显优于重复特殊目的代码和/或引入特别的共享超类。

    但是,即使只有一个特殊类,并且没有可预见的扩展,元类的__new____init__也是初始化类变量或其他全局数据的一种更干净的方法,而不是将特殊用途代码与类定义体中的正常defclass语句混合在一起。


    我在Python中唯一一次使用元类是在为FlickrAPI编写包装器时。

    我的目标是抓取Flickr的API站点并动态生成完整的类层次结构,以允许使用Python对象访问API:

    1
    2
    3
    4
    # 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,所以我真的不知道运行时的类定义。能够动态生成类型非常有用。


    我昨天也是这么想的,完全同意。在我看来,代码中的复杂之处在于试图使其更具声明性,这通常会使代码库更难维护、更难阅读,也会减少Python。它通常还需要大量的copy.copy()ing(以维护继承和从类到实例的复制),这意味着您必须在许多地方查看发生了什么(总是从元类向上看),这也违背了Python的粒度。我一直在挑选formEncode和sqlacalchemy代码,看看这样的声明性风格是否值得,显然不值得。这样的样式应该留给描述符(如属性和方法)和不可变的数据。Ruby对这种声明性风格有更好的支持,我很高兴核心的Python语言不会走这条路。

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

    1
    2
    class test(baseclass_with_metaclass):
        method_maker_value ="hello"

    可能有一个元类,它在该类中生成了一个具有基于"hello"的特殊属性的方法(比如在字符串末尾添加"hello"的方法)。确保您不必在每个子类中编写一个方法,这对可维护性很有好处,相反,您需要定义的只是方法 maker 值。

    这种需求是非常罕见的,而且只减少了一些输入,除非您有足够大的代码库,否则不值得考虑。


    您绝对不需要使用元类,因为您可以始终使用要修改的类的继承或聚合来构造一个执行所需操作的类。

    也就是说,在Smalltalk和Ruby中,能够修改现有的类非常方便,但Python不喜欢直接这样做。

    有一篇关于Python中元类化的优秀developerWorks文章可能会有所帮助。维基百科的文章也不错。


    我使用元类的方法是为类提供一些属性。例如:

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

    将把name属性放在将要将元类设置为指向name class的每个类上。


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

    在某些情况下,能够动态地重写整个库,从而使其在多线程应用程序中正常工作,这是非常有用的。safetkinter模块通过threadbox模块提供的元类(不需要事件和队列)来实现这一点。

    threadbox的一个很好的方面是它不关心它克隆的是什么类。它提供了一个示例,说明如何在需要时使用元类来访问所有的基类。元类的另一个好处是它们也可以在继承类的基础上运行。自己编写的程序——为什么不呢?


    元类不能取代编程!它们只是一个可以自动化或使某些任务更优雅的技巧。Pygments语法高亮显示库就是一个很好的例子。它有一个名为RegexLexer的类,允许用户将一组词法规则定义为类上的正则表达式。元类用于将定义转换为有用的解析器。

    它们就像盐,很容易用得太多。


    元类唯一合法的用例是防止其他多管闲事的开发人员接触您的代码。一旦一个爱管闲事的开发人员掌握了元类并开始使用您的元类,就可以添加一两个级别来阻止它们。如果这不起作用,就开始使用type.__new__,或者使用递归元类的某些方案。

    (在脸上写着"舌头",但我见过这种混淆。Django就是一个很好的例子)


    这只是一个小用途,但是…我发现有一件事很有用,那就是在创建子类时调用一个函数。我将其编码为一个元类,它查找__initsubclass__属性:每当创建一个子类时,定义该方法的所有父类都会用__initsubclass__(cls, subcls)调用。这允许创建一个父类,然后在一些全局注册表中注册所有子类,在定义子类时对其运行不变检查,执行后期绑定操作等。所有这些都不需要手动调用函数或创建执行这些单独职责的自定义元类。

    请注意,我慢慢地意识到这种行为的隐式魔力有点不受欢迎,因为从上下文中看类定义是不可预料的…因此,除了为每个类和实例初始化__super属性之外,我已经不再将该解决方案用于任何严重的问题。


    最近,我不得不使用一个元类来帮助声明性地在一个数据库表周围定义一个sqlachemy模型,该数据库表由来自http://census.ire.org/data/bulkdata.html的美国人口普查数据填充。

    IRE为人口普查数据表提供数据库shell,该表根据人口普查局的p012015、p012016、p012017等命名约定创建整数列。

    我希望a)能够使用model_instance.p012017语法访问这些列,b)对我正在做的事情相当明确,c)不必在模型上显式定义几十个字段,因此我将sqlacalchemy的DeclarativeMeta子类化,以便遍历一系列列并自动创建与这些列对应的模型字段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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_)

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

        ...

    这里描述了一种合法的用法——用一个元类重写python docstring。


    为了便于使用,我必须对二进制分析器使用它们一次。您可以定义一个消息类,该类具有线中存在的字段的属性。它们需要按照声明的方式进行排序,以从中构造最终的线格式。如果使用有序的名称空间dict,则可以对元类执行此操作。实际上,它在元类示例中有:

    https://docs.python.org/3/reference/datamodel.html元类示例

    但是总的来说:如果您真的需要增加元类的复杂性,请仔细评估。