代码之家  ›  专栏  ›  技术社区  ›  Ali Afshar

元类有哪些(具体)用例?

  •  92
  • Ali Afshar  · 技术社区  · 17 年前

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

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

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

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

    我将开始:

    有时使用第三方 图书馆能够 以某种方式改变类。

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

    17 回复  |  直到 8 年前
        1
  •  23
  •   Ali Afshar    17 年前

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

        2
  •  64
  •   Dan Gittik    9 年前

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

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

    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
      

      每当你子类 模型 并定义一些 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
    

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

    无论如何,在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)
    

    除了更难看之外,它也不那么灵活:如果你想要有序的文字属性,比如整数和字符串,怎么办?如果…怎么办 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,编程应该很有趣。使用标准的编程结构和设计模式一直都是无聊和无趣的,并且阻碍了你的想象力。多活一点!这是一个元类,只为您。

    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
    
        3
  •  35
  •   S.Lott    17 年前

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

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

      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的结构定义。元类用于将声明的项转换为结构格式字符串、处理继承等,并生成能够处理它的类。

      对于其他现实世界的例子,请查看各种各样的表单,例如 sqlalchemy's ORM或 sqlobject . 同样,目的是解释具有特定含义的定义(这里是SQL列定义)。

        4
  •  17
  •   Peter Rowell    17 年前

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

    元类比99%更神奇 用户永远都应该担心。如果 你想知道你是否需要它们,你 不要(真正需要的人) 他们确信自己 需要它们,而不需要 解释原因)。提姆彼得斯 (C.L.P 2002-12-22之后)

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

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

        5
  •  6
  •   Tripp Lilley    13 年前

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

    一个基本的例子 A Conservative Metaclass Ian Bicking:

    我用过的元类 主要是为了支持 声明式编程风格。为了 实例,考虑验证 图式:

    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
  •   Kenan Banks    17 年前

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

    我的目标是刮 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
  •   David Raznick    17 年前

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

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

    class test(baseclass_with_metaclass):
        method_maker_value = "hello"
    

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

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

        8
  •  5
  •   martineau    8 年前

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

    当多个类共享相同的特殊行为时,重复 __metaclass__=X 显然比重复特殊目的代码和/或引入特别的共享超类要好。

    但即使只有一个特殊的班级,而且没有可预见的延期, __new__ __init__ 元类是初始化类变量或其他全局数据的更干净的方法,而不是混合特殊目的代码和普通代码 def class 类定义主体中的语句。

        9
  •  4
  •   Charlie Martin    17 年前

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

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

    有一个很好的 DeveloperWorks article 在python中进行元分类可能会有所帮助。这个 Wikipedia article 也不错。

        10
  •  3
  •   Benjamin Peterson    17 年前

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

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

        11
  •  3
  •   hyperboreean    17 年前

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

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

    会把 名称 将元类设置为指向NameClass的每个类的属性。

        12
  •  3
  •   Noctis Skytower    13 年前

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

    在某些情况下,能够动态地重写整个库,从而使其在多线程应用程序中正常工作,这是非常有用的。这个 safetkinter 模块在由 threadbox 模块——不需要事件和队列。

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

        13
  •  3
  •   shuttle87 Bhargav Boda    9 年前

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

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

        14
  •  2
  •   Eli Collins    14 年前

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

    请注意,我慢慢地意识到这种行为的隐式魔力有点不受欢迎,因为从上下文中看类定义是不可预料的…所以除了初始化一个 __super 每个类和实例的属性。

        15
  •  1
  •   Geoffrey Hing    12 年前

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

    艾尔提供 database shells 对于人口普查数据表,该表根据人口普查局的命名约定(p012015、p012016、p012017等)创建整数列。

    我想a)能够使用 model_instance.p012017 语法,b)对我正在做的事情相当明确,c)不必在模型上显式定义几十个字段,因此我将sqlacalchemy的 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
    
        ...
    
        16
  •  1
  •   mistermarko    12 年前

    似乎有一个合法的使用描述 here -用元类重写python docstrings。

        17
  •  0
  •   GeeF    9 年前

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

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

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