何时在Python中内联元类的定义?

15

今天我在Python中遇到了一个关于元类的令人惊讶的定义,这里有详细说明,并且元类的定义被内联了。 相关部分如下:

class Plugin(object):
    class __metaclass__(type):
        def __init__(cls, name, bases, dict):
            type.__init__(name, bases, dict)
            registry.append((name, cls))

何时使用这样的内联定义才有意义呢?

更进一步的论点:

支持该技术的一个论点是,使用这种技术创建的元类无法在其他地方重用。反驳的论点是,在使用元类的常见模式中,定义一个元类并在一个类中使用它,然后从那个类继承。例如,在保守元类中,有这样一个定义:

class DeclarativeMeta(type):
    def __new__(meta, class_name, bases, new_attrs):
        cls = type.__new__(meta, class_name, bases, new_attrs)
        cls.__classinit__.im_func(cls, new_attrs)
        return cls
class Declarative(object):
    __metaclass__ = DeclarativeMeta
    def __classinit__(cls, new_attrs): pass

可以写成以下方式:

class Declarative(object):  #code not tested!
    class __metaclass__(type):
        def __new__(meta, class_name, bases, new_attrs):
            cls = type.__new__(meta, class_name, bases, new_attrs)
            cls.__classinit__.im_func(cls, new_attrs)
            return cls
    def __classinit__(cls, new_attrs): pass

还有其他需要考虑的问题吗?

1个回答

20

像其他形式的嵌套类定义一样,一个嵌套元类可能更加"简洁和方便"(只要你不介意除了继承外不重用该元类),适用于许多种类的"生产使用",但对于调试和内省来说可能有些不便。

基本上,你将不会为元类提供一个正确的顶级名称,而是会得到在模块中定义的所有自定义元类都无法区分彼此的情况,这是基于它们的__module____name__属性(这是Python用来形成其repr的方式,如果需要)。考虑以下示例:

>>> class Mcl(type): pass
... 
>>> class A: __metaclass__ = Mcl
...
>>> class B:
...   class __metaclass__(type): pass
... 
>>> type(A)
<class '__main__.Mcl'>
>>> type(B)
<class '__main__.__metaclass__'>

换言之,如果你想检查"类A的类型是什么"(记住,元类就是类的类型),你将获得一个清晰而有用的答案——在主模块中它是Mcl。然而,如果你想检查"类B的类型是什么",答案并不是很有用:它所说的是在main模块中它是__metaclass__,但这甚至都不是真的:

>>> import __main__
>>> __main__.__metaclass__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute '__metaclass__'
>>> 

实际上,并没有这种东西;那个 repr 很容易误导人,也不是很有用;-)。

一个类的 repr 实际上是 '%s.%s' % (c.__module__, c.__name__) -- 这是一个简单、有用和一致的规则。但在许多情况下,例如 class 语句在模块范围内不唯一、根本不在模块范围内(而是在函数或类体内),或者根本不存在(类当然可以在没有 class 语句的情况下构建,通过显式调用它们的元类),这可能会有些误导性(除非在使用它们时可以获得实质性的好处,否则尽可能避免这些特殊情况)。例如,请考虑:

>>> class A(object):
...   def foo(self): print('first')
... 
>>> x = A()
>>> class A(object):
...   def foo(self): print('second')
... 
>>> y = A()
>>> x.foo()
first
>>> y.foo()
second
>>> x.__class__
<class '__main__.A'>
>>> y.__class__
<class '__main__.A'>
>>> x.__class__ is y.__class__
False
在同一作用域中使用两个class语句时,第二个会重新绑定名称(这里是A),但现有实例引用对象的第一个绑定而不是名称--因此两个类对象都将保留,其中一个只能通过其实例的type(或__class__属性)访问(如果有实例-如果没有,则第一个对象消失了)-这两个类具有相同的名称和模块(因此具有相同的表示形式),但它们是不同的对象。在类或函数体内嵌套的类,或通过直接调用元类(包括type)创建的类,可能会导致类似的混乱,如果需要调试或内省。

因此,如果您永远不需要调试或以其他方式内省代码,则嵌套元类是可以的,并且如果谁正在这样做理解这些怪癖,那么可以使用该方法(尽管它永远不可能像使用一个好听的真名那样方便,当然就像调试使用lambda编写的函数永远不可能像调试使用def编写的函数那样方便)。通过lambdadef之间的类比,您可以合理地声称对于元类非常简单的匿名“嵌套”定义是可以接受的,因为永远不会需要调试或内省。

在Python 3中,“嵌套定义”根本无效--在那里,必须将元类作为关键字参数传递给类,如class A(metaclass=Mcl):,因此在主体中定义__metaclass__没有效果。我认为这也意味着,在Python 2代码中嵌套的元类定义可能只适用于您确定该代码永远不需要移植到Python 3(因为您让该移植变得更加困难,而且需要取消嵌套元类的定义)-换句话说,这是“一次性”的代码,几年后当某个版本的Python 3具有比Python 2.7(Python 2的最终版本)更快的速度,更多的功能或第三方支持时,它将不存在。

正如计算机历史所表明的那样,您“预计”成为一次性的代码往往会令你感到惊讶,并且在20年后仍然存在(而可能是您在同一时间编写的“面向未来”的代码已经被完全遗忘了;-))。这肯定似乎建议避免嵌套定义元类。


1
@Muhammad,一个类的repr是模块名称、点和类名:就这样。你似乎想要一个不同的规则,但我认为你不理解问题(一个类没有对其“class”语句(如果有)所在的函数或类(如果有)的引用,从而避免创建无用的循环引用)。你关于pickling的观点是正确的,我之前做的错误编辑也证明了这一点(这就是为什么我错误地保留了pickle的提及),现在进行修正。 - Alex Martelli
@Alex,谢谢你的建议。我看不到很多框架使用module.classname作为惰性加载的基础而没有使用eval,但我可以看到相当多的doctest和测试套件受到影响。我想我会放弃这个方案。 - Muhammad Alkarouri
@Muhammad,例如,Django是目前最受欢迎的Python Web框架,它在URL分发器中使用点符号字符串(用于惰性加载视图)-- 在这种情况下,视图通常是函数,但此类框架不需要特别关心或担心,当然也不会使用eval进行惰性加载(那将非常愚蠢)-- 它们将使用__import__getattr!-) - Alex Martelli
@Alex,我实际上是指那些依赖于表示为“module.classname”的框架。像Django这样的框架不受对repr的更改影响(至少在url调度程序中使用时)。因此,我认为它们并不会在repr问题上倾向于任何一方。 - Muhammad Alkarouri
对于新访客来说,整个论点现在受到PEP 3155的影响。在3.3+中,函数和类的repr()使用完全限定名称__qualname__而不是__name__。答案仍然有效,因为如上所述,__metaclass__属性不再是Python 3+中使用元类的方式。 - Muhammad Alkarouri
显示剩余12条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接