元类的一些(具体)用例是什么?

时间:2008-12-24 20:13:06

标签: python metaclass

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

我很想你几乎不需要使用元类。为什么?因为我认为如果你正在对一个类做类似的事情,你应该把它做成一个对象。并且需要一个小的重新设计/重构。

能够使用元类导致许多地方的很多人使用类作为某种二流对象,这对我来说似乎是灾难性的。编程是否被元编程取代?遗憾的是,类装饰器的添加使其更加可以接受。

那么请,我非常想知道Python中元类的有效(具体)用例。或者开悟为什么变异类有时比变异对象更好。

我将开始:

  

有时使用第三方时   库是有用的   以某种方式改变阶级。

(这是我能想到的唯一一个案例,并不是具体的)

20 个答案:

答案 0 :(得分:75)

最近我被问到同样的问题,并提出了几个答案。我希望恢复这个帖子是可以的,因为我想详细说明一些提到的用例,并添加一些新的用例。

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

  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
    

    或使用显式注册功能:

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

    同样,这可以使用类装饰器完成(没有继承):

    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}
    
  3. 在考虑了最常见和具体的用例之后,您绝对必须使用元类的唯一情况是您想要修改类名或基类列表,因为一旦定义,这些参数就会被烘焙到类中,没有装饰器或功能可以解开它们。

    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__方法,它允许您将类体评估为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...>
    

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

    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)
    

    除了更难看之外,它也不那么灵活:如果你想要有序的文字属性,如整数和字符串怎么办?如果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,编程应该很有趣。使用标准的编程结构和设计模式总是令人厌烦和缺乏灵感,并且阻碍了你的想象力。坚持一下!这是一个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
    

答案 1 :(得分:35)

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

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

    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的方法,并将它们透明地转换为属性。作为一个真实世界的例子,here's我写的一个配方给出了更多类似C的结构定义。元类用于将声明的项转换为结构格式字符串,处理继承等,并生成一个能够处理它的类。

    对于其他实际示例,请查看各种ORM,例如sqlalchemy's ORM或sqlobject。同样,目的是解释具有特定含义的定义(此处为SQL列定义)。

答案 2 :(得分:23)

我有一个处理非交互式绘图的类,作为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__中完成,但这是我发现最直接的解决方案。

答案 3 :(得分:17)

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

  

元素比99%更神奇   用户应该担心。如果   你想知道你是否需要它们   不(实际需要的人)   他们肯定地知道他们   需要他们,而不需要   解释为什么)。蒂姆彼得斯   (c.l.p post 2002-12-22)

话虽如此,我(定期)遇到了元类的真实用法。想到的是Django,你的所有模型都继承自models.Model。反过来,models.Model用Django的ORM优点来包装你的数据库模型。这种魔法通过元类来实现。它创建了各种异常类,管理器类等等。

有关故事的开头,请参阅django / db / models / base.py,类ModelBase()。

答案 4 :(得分:6)

合理的元类使用模式是在定义类时执行一次,而不是在实例化同一个类时重复执行。

当多个类共享相同的特殊行为时,重复__metaclass__=X显然比重复特殊用途代码和/或引入ad-hoc共享超类更好。

但即使只有一个特殊的类而没有可预见的扩展,元类的__new____init__是一种更简洁的方法来初始化类变量或其他全局数据,而不是混合使用专用代码和普通{类定义体中的{1}}和def语句。

答案 5 :(得分:6)

元类可以方便地在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()

其他一些技巧:Ingredients for Building a DSL in Python(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,])
]

它并不完美,但已经几乎没有魔法,不需要元类,并且提高了均匀度。

答案 6 :(得分:5)

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

我的目标是抓取flickr's api site并动态生成一个完整的类层次结构,以允许使用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,所以我真的不知道运行时的类定义。能够动态生成类型非常有用。

答案 7 :(得分:5)

我昨天也在考虑同样的事情并完全同意。由于试图使其更具说明性而导致的代码复杂性通常会使代码库更难维护,更难以阅读并且在我看来更少pythonic。 它通常还需要大量copy.copy()ing(以保持继承并从一个类复制到实例)并且意味着你必须在许多地方查看最新情况(总是从元类向上看)这与蟒蛇纹也。 我一直在挑选formencode和sqlalchemy代码,看看这样的声明风格是否值得,而且显然不是。这种样式应该留给描述符(例如属性和方法)和不可变数据。 Ruby对这种声明性样式有更好的支持,我很高兴核心python语言不会沿着那条路走下去。

我可以看到它们用于调试,为所有基类添加元类以获得更丰富的信息。 我也看到他们只在(非常)大型项目中使用以摆脱一些样板代码(但是在清晰度方面)。 sq example的sqlalchemy确实在别处使用它们,根据类定义中的属性值为所有子类添加特定的自定义方法 例如玩具示例

class test(baseclass_with_metaclass):
    method_maker_value = "hello"

可能有一个元类,它在该类中生成一个方法,该方法具有基于“hello”的特殊属性(比如在字符串末尾添加“hello”的方法)。可维护性确保您不必在您创建的每个子类中编写方法,而您必须定义的是method_maker_value。

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

答案 8 :(得分:4)

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

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

在Python中有一个关于metaclassing的优秀DeveloperWorks article可能有所帮助。 Wikipedia article也很不错。

答案 9 :(得分:3)

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

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

会将 name 属性放在每个将metaclass设置为指向NameClass的类上。

答案 10 :(得分:3)

元类的唯一合法用例是让其他爱管闲事的开发人员不要接触你的代码。一旦爱管闲事的开发人员掌握了元类并开始与你的朋友闲聊,就可以投入另外一两个级别来阻止它们。如果这不起作用,请使用递归元类开始使用type.__new__或某些方案。

(书面的舌头,但我已经看到了这种混淆.Django是一个完美的例子)

答案 11 :(得分:3)

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

他们就像盐;它太容易使用了。

答案 12 :(得分:3)

当多个线程尝试与它们交互时,某些GUI库会出现问题。 tkinter就是这样一个例子;虽然可以明确地处理事件和队列的问题,但以完全忽略问题的方式使用库可能要简单得多。看哪 - 元类的神奇之处。

能够无缝地动态重写整个库,以便在多线程应用程序中按预期正常工作,在某些情况下非常有用。 safetkinter模块在​​threadbox模块提供的元类的帮助下完成了这一操作 - 不需要事件和队列。

threadbox的一个简洁方面是它不关心它克隆的类。它提供了一个示例,说明如果需要,元类可以触及所有基类。元类带来的另一个好处是它们也可以继承类。写自己的程序 - 为什么不呢?

答案 13 :(得分:2)

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

请注意,我慢慢地意识到这种行为的隐含魔法在某种程度上是不可取的,因为如果在上下文中查看类定义时出乎意料......所以我已经不再使用该解决方案了除了为每个类和实例初始化__super属性之外还有什么严重的。

答案 14 :(得分:1)

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

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

我想a)能够使用model_instance.p012017语法访问这些列,b)相当明确我正在做的事情和c)不必在模型上明确定义数十个字段,所以我子类化SQLAlchemy的DeclarativeMeta来迭代一系列列并自动创建与列对应的模型字段:

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

    ...

答案 15 :(得分:1)

似乎有合法的用法描述here - 用元类重写Python Docstrings。

答案 16 :(得分:0)

我必须使用它们一次才能使二进制解析器更容易使用。您可以定义一个消息类,其中包含线路上存在的字段的属性。 它们需要按照声明的方式进行排序,以便从中构建最终的电线格式。如果使用有序的命名空间字典,则可以使用元类执行此操作。事实上,它在Metaclasses的例子中:

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

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

答案 17 :(得分:0)

@Dan Gittik的回答很酷

最后的示例可以澄清很多事情,我将其更改为python 3并给出了一些解释:

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)

#China is metaclass and it's __new__ method would be changed by MetaMetaclass(metaclass)
class China(MetaMetaclass, metaclass=MetaMetaclass):
    __metaclass__ = MetaMetaclass

#Taiwan is metaclass and it's __new__ method would be changed by MetaMetaclass(metaclass)
class Taiwan(MetaMetaclass, metaclass=MetaMetaclass):
    __metaclass__ = MetaMetaclass

#A is a normal class and it's __new__ method would be changed by China(metaclass)
class A(metaclass=China):
    __metaclass__ = China

#B is a normal class and it's __new__ method would be changed by Taiwan(metaclass)
class B(metaclass=Taiwan):
    __metaclass__ = Taiwan


print(A._label)  # Made in China
print(B._label)  # Made in Taiwan

  • 一切都是对象,所以类就是对象
  • 类对象是由元类创建的
  • 所有从类型继承的类都是元类
  • 元类可以控制类的创建
  • 元类也可以控制元类的创建(因此它可以永远循环)
  • 这是元编程...您可以在运行时控制类型系统
  • 再次,一切都是对象,这是一个统一的系统,创建类型,然后创建实例

答案 18 :(得分:0)

另一个用例是当您希望能够修改类级别的属性并确保它仅影响手头的对象时。实际上,这意味着“合并”元类和类实例化的阶段,从而使您仅处理它们自己(唯一)种类的类实例。

当(出于对readibilitypolymorphism的关注而想要 动态定义 property时,我还必须这样做哪些返回值(可能)是基于(经常更改的)实例级属性的计算结果的,只能在元类实例化之后的类级 ie 上完成在类实例化之前。

答案 19 :(得分:0)

Pydantic 是一个用于数据验证和设置管理的库,它在运行时强制执行类型提示,并在数据无效时提供用户友好的错误。它为 BaseModel 和数字范围验证使用元类。

在工作中,我遇到了一些代码,其中的流程具有由类定义的多个阶段。这些步骤的顺序由元类控制,元类在定义类时将步骤添加到列表中。这被抛出并通过将它们添加到列表来设置顺序。