144

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

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

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

所以,我很想知道你在 Python 中元类的有效(具体)用例。或者了解为什么变异类有时比变异对象更好。

我将开始:

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

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

4

21 回答 21

129

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

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

  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
    

    每当您创建 subclassModel时,您的类都会在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}
    

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

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

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

无论如何,在 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 [<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到方法,而不是常规和静态函数?最好对你的论点保持明确和详细”。但是,一旦你掌握了面向对象编程,它就会变得更加强大;这也是如此,呃......我猜是准面向方面的编程。一旦你了解元类,它们实际上非常简单,那么为什么不在方便的时候使用它们呢?

最后,元类很受欢迎,编程应该很有趣。一直使用标准的编程结构和设计模式既乏味又乏味,还会阻碍您的想象力。坚持一下!这是一个元元类,专为您准备。

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

编辑

这是一个很老的问题,但它仍然得到了支持,所以我想我会添加一个链接到更全面的答案。如果您想了解更多关于元类及其用途的信息,我刚刚在此处发表了一篇关于它的文章。

于 2015-06-25T22:26:32.893 回答
37

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

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

    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,例如 sqlalchemyORM 或sqlobject。同样,目的是解释具有特定含义的定义(此处为 SQL 列定义)。

于 2008-12-24T21:43:05.087 回答
27

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

于 2008-12-26T01:35:56.520 回答
20

让我们从 Tim Peter 的经典名言开始:

元类是比 99% 的用户所担心的更深层次的魔法。如果你想知道你是否需要它们,你就不需要(真正需要它们的人肯定知道他们需要它们,并且不需要解释为什么)。蒂姆·彼得斯 (clp post 2002-12-22)

话虽如此,我(定期)遇到了元类的真正用途。想到的是在 Django 中,所有模型都继承自 models.Model。反过来,models.Model 用 Django 的 ORM 特性包装了你的 DB 模型。这种神奇是通过元类发生的。它创建各种异常类、管理器类等。

见 django/db/models/base.py, class ModelBase() 为故事的开始。

于 2008-12-25T02:23:47.610 回答
8

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

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

但是,即使只有一个特殊类且没有可预见的扩展, 元类__new__也是__init__一种初始化类变量或其他全局数据的更干净的方法,而不是在类定义主体中混合专用代码和普通defclass语句。

于 2011-08-14T14:49:02.157 回答
7

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

它并不完美,但已经几乎为零魔法,不需要元类,并且提高了一致性。

于 2008-12-24T22:54:53.117 回答
6

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

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

class test(baseclass_with_metaclass):
    method_maker_value = "hello"

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

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

于 2008-12-28T03:20:15.823 回答
5

我唯一一次在 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,所以我真的不知道运行时的类定义。能够动态生成类型非常有用。

于 2008-12-24T21:19:39.847 回答
5

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

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

于 2008-12-24T22:15:22.353 回答
4

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

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

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

于 2008-12-24T21:17:25.717 回答
4

元类的唯一合法用例是防止其他爱管闲事的开发人员接触您的代码。一旦一个爱管闲事的开发人员掌握了元类并开始使用你的元类,再增加一两个级别以将它们排除在外。如果这不起作用,请开始使用type.__new__或使用递归元类的某种方案。

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

于 2011-03-16T19:13:50.780 回答
4

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

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

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

于 2013-06-04T19:29:42.267 回答
3

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

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

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

于 2008-12-25T11:47:43.557 回答
2

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

请注意,我已经慢慢意识到这种行为的隐含魔法有些不可取,因为如果脱离上下文查看类定义是出乎意料的......所以我已经不再使用该解决方案来解决任何严重问题__super为每个类和实例初始化一个属性。

于 2011-08-14T16:44:08.423 回答
1

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

IRE 为人口普查数据表提供数据库外壳,这些外壳按照人口普查局 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

    ...
于 2013-11-08T17:59:56.063 回答
1

这里描述的似乎有一个合法的用途- Rewriting Python Docstrings with a Metaclass。

于 2014-05-25T08:44:58.130 回答
1

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

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

于 2021-05-18T21:53:01.967 回答
0

我不得不将它们用于二进制解析器以使其更易于使用。您定义一个消息类,其中包含在线路上存在的字段的属性。需要按照声明的方式对它们进行排序,以便从中构造最终的电汇格式。如果您使用有序的命名空间字典,您可以使用元类来做到这一点。事实上,它在 Metaclasses 的示例中:

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

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

于 2016-09-02T12:47:37.237 回答
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

  • 一切都是对象,所以类是对象
  • 类对象由元类创建
  • 从 type 继承的所有类都是元类
  • 元类可以控制类创建
  • 元类也可以控制元类的创建(因此它可以永远循环)
  • 这是元编程......你可以在运行时控制类型系统
  • 再次,一切都是对象,这是一个统一的系统,类型创建类型,类型创建实例
于 2019-07-23T11:58:26.853 回答
0

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

当(出于可读性多态性的考虑)我们想要动态定义 propertys时,我也必须这样在元类实例化之后和类实例化之前。

于 2019-08-17T20:53:15.887 回答
0

我知道这是一个老问题,但是如果只想基于传递给构造函数的参数创建一个类的单个实例,这里有一个非常宝贵的用例。

实例单例 我使用此代码在 Z-Wave 网络上创建设备的单例实例。无论我创建实例多少次,如果将相同的值传递给构造函数,如果存在具有完全相同值的实例,那么这就是返回的内容。

import inspect


class SingletonMeta(type):
    # only here to make IDE happy
    _instances = {}

    def __init__(cls, name, bases, dct):
        super(SingletonMeta, cls).__init__(name, bases, dct)
        cls._instances = {}

    def __call__(cls, *args, **kwargs):
        sig = inspect.signature(cls.__init__)
        keywords = {}

        for i, param in enumerate(list(sig.parameters.values())[1:]):
            if len(args) > i:
                keywords[param.name] = args[i]
            elif param.name not in kwargs and param.default != param.empty:
                keywords[param.name] = param.default
            elif param.name in kwargs:
                keywords[param.name] = kwargs[param.name]
        key = []
        for k in sorted(list(keywords.keys())):
            key.append(keywords[k])
        key = tuple(key)

        if key not in cls._instances:
            cls._instances[key] = (
                super(SingletonMeta, cls).__call__(*args, **kwargs)
            )

        return cls._instances[key]


class Test1(metaclass=SingletonMeta):

    def __init__(self, param1, param2='test'):
        pass


class Test2(metaclass=SingletonMeta):

    def __init__(self, param3='test1', param4='test2'):
        pass


test1 = Test1('test1')
test2 = Test1('test1', 'test2')
test3 = Test1('test1', 'test')

test4 = Test2()
test5 = Test2(param4='test1')
test6 = Test2('test2', 'test1')
test7 = Test2('test1')

print('test1 == test2:', test1 == test2)
print('test2 == test3:', test2 == test3)
print('test1 == test3:', test1 == test3)
print('test4 == test2:', test4 == test2)
print('test7 == test3:', test7 == test3)
print('test6 == test4:', test6 == test4)
print('test7 == test4:', test7 == test4)
print('test5 == test6:', test5 == test6)

print('number of Test1 instances:', len(Test1._instances))
print('number of Test2 instances:', len(Test2._instances))

输出

test1 == test2: False
test2 == test3: False
test1 == test3: True
test4 == test2: False
test7 == test3: False
test6 == test4: False
test7 == test4: True
test5 == test6: False
number of Test1 instances: 2
number of Test2 instances: 3

现在有人可能会说它可以在不使用元类的情况下完成,我知道如果 __init__ 方法被修饰就可以完成。我不知道另一种方法。下面的代码虽然将返回一个包含所有相同数据的类似实例,但它不是单例实例,但会创建一个新实例。因为它创建了一个具有相同数据的新实例,所以需要采取额外的步骤来检查实例的相等性。最后,它比使用元类消耗更多的内存,并且使用元类不需要采取额外的步骤来检查相等性。

class Singleton(object):
    _instances = {}

    def __init__(self, param1, param2='test'):
        key = (param1, param2)
        if key in self._instances:
            self.__dict__.update(self._instances[key].__dict__)
        else:
            self.param1 = param1
            self.param2 = param2
            self._instances[key] = self


test1 = Singleton('test1', 'test2')
test2 = Singleton('test')
test3 = Singleton('test', 'test')

print('test1 == test2:', test1 == test2)
print('test2 == test3:', test2 == test3)
print('test1 == test3:', test1 == test3)

print('test1 params', test1.param1, test1.param2)
print('test2 params', test2.param1, test2.param2)
print('test3 params', test3.param1, test3.param2)

print('number of Singleton instances:', len(Singleton._instances))

输出

test1 == test2: False
test2 == test3: False
test1 == test3: False
test1 params test1 test2
test2 params test test
test3 params test test
number of Singleton instances: 2

如果还需要检查新实例的删除或添加,元类方法非常好用。

    import inspect


class SingletonMeta(type):
    # only here to make IDE happy
    _instances = {}

    def __init__(cls, name, bases, dct):
        super(SingletonMeta, cls).__init__(name, bases, dct)
        cls._instances = {}

    def __call__(cls, *args, **kwargs):
        sig = inspect.signature(cls.__init__)
        keywords = {}

        for i, param in enumerate(list(sig.parameters.values())[1:]):
            if len(args) > i:
                keywords[param.name] = args[i]
            elif param.name not in kwargs and param.default != param.empty:
                keywords[param.name] = param.default
            elif param.name in kwargs:
                keywords[param.name] = kwargs[param.name]
        key = []
        for k in sorted(list(keywords.keys())):
            key.append(keywords[k])
        key = tuple(key)

        if key not in cls._instances:
            cls._instances[key] = (
                super(SingletonMeta, cls).__call__(*args, **kwargs)
            )

        return cls._instances[key]


class Test(metaclass=SingletonMeta):

    def __init__(self, param1, param2='test'):
        pass


instances = []

instances.append(Test('test1', 'test2'))
instances.append(Test('test1', 'test'))

print('number of instances:', len(instances))

instance = Test('test2', 'test3')
if instance not in instances:
    instances.append(instance)

instance = Test('test1', 'test2')
if instance not in instances:
    instances.append(instance)

print('number of instances:', len(instances))

输出

number of instances: 2
number of instances: 3

这是一种在实例不再使用后删除已创建的实例的方法。

    import inspect
import weakref


class SingletonMeta(type):
    # only here to make IDE happy
    _instances = {}

    def __init__(cls, name, bases, dct):
        super(SingletonMeta, cls).__init__(name, bases, dct)

        def remove_instance(c, ref):
            for k, v in list(c._instances.items())[:]:
                if v == ref:
                    del cls._instances[k]
                    break
                    
        cls.remove_instance = classmethod(remove_instance)
        cls._instances = {}

    def __call__(cls, *args, **kwargs):
        sig = inspect.signature(cls.__init__)
        keywords = {}

        for i, param in enumerate(list(sig.parameters.values())[1:]):
            if len(args) > i:
                keywords[param.name] = args[i]
            elif param.name not in kwargs and param.default != param.empty:
                keywords[param.name] = param.default
            elif param.name in kwargs:
                keywords[param.name] = kwargs[param.name]
        key = []
        for k in sorted(list(keywords.keys())):
            key.append(keywords[k])
        key = tuple(key)

        if key not in cls._instances:
            instance = super(SingletonMeta, cls).__call__(*args, **kwargs)

            cls._instances[key] = weakref.ref(
                instance,
                instance.remove_instance
            )

        return cls._instances[key]()


class Test1(metaclass=SingletonMeta):

    def __init__(self, param1, param2='test'):
        pass


class Test2(metaclass=SingletonMeta):

    def __init__(self, param3='test1', param4='test2'):
        pass


test1 = Test1('test1')
test2 = Test1('test1', 'test2')
test3 = Test1('test1', 'test')

test4 = Test2()
test5 = Test2(param4='test1')
test6 = Test2('test2', 'test1')
test7 = Test2('test1')

print('test1 == test2:', test1 == test2)
print('test2 == test3:', test2 == test3)
print('test1 == test3:', test1 == test3)
print('test4 == test2:', test4 == test2)
print('test7 == test3:', test7 == test3)
print('test6 == test4:', test6 == test4)
print('test7 == test4:', test7 == test4)
print('test5 == test6:', test5 == test6)

print('number of Test1 instances:', len(Test1._instances))
print('number of Test2 instances:', len(Test2._instances))


print()
del test1
del test5
del test6

print('number of Test1 instances:', len(Test1._instances))
print('number of Test2 instances:', len(Test2._instances))

输出

test1 == test2: False
test2 == test3: False
test1 == test3: True
test4 == test2: False
test7 == test3: False
test6 == test4: False
test7 == test4: True
test5 == test6: False
number of Test1 instances: 2
number of Test2 instances: 3

number of Test1 instances: 2
number of Test2 instances: 1

如果您查看输出,您会注意到 Test1 实例的数量没有改变。那是因为 test1 和 test3 是同一个实例,我只删除了 test1 所以代码中仍然有对 test1 实例的引用,因此 test1 实例没有被删除。

另一个不错的特性是,如果实例仅使用提供的参数来完成它的任务,那么您可以使用元类来促进在完全不同的计算机上或在同一台机器上的不同进程中远程创建实例. 参数可以简单地通过套接字或命名管道传递,并且可以在接收端创建类的副本。

于 2021-11-20T14:42:36.927 回答