Python - 将类标头中的位置参数传递给元类



在形式*(1, 2, 3)中传递位置参数在语法上是在关键字参数之后分配的。此代码:

class Meta(type):
def __new__(meta, *args, **kwargs):
return super().__new__(meta, *args)
class A(metaclass=Meta, sandwich='tasty', *(10,)): pass

给我一个错误:

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

为什么位置参数与超类列表混合在一起? 我认为 Python 应该能够处理这种特殊情况,因为这样的实现迫使元类用户以仅关键字的方式传递参数,并且这种接口选择被剥夺了元类程序员。

tl;博士

类定义中带有单个星号*的带星标的表达式将在关键字参数之前进行处理,即使它们出现在关键字参数之后也是如此。这会导致*(10,)被解释为基类,并且由于10不是有效的基类,因此会出现错误消息。

解释

第一点是元类中__new__方法的参数列表包含*args。为了更好地理解发生了什么,*args可以替换为name, bases, namespace这些参数是在类定义中引用元类时传递给元类的参数。

所以更换

class Meta(type):
def __new__(meta, *args, **kwargs):
return super().__new__(meta, *args)

class Meta(type):
def __new__(self, name, bases, namespace, **kwargs):
return super().__new__(self, name, bases, namespace)

第二点是类定义包含带星标的表达式。

class A(metaclass=Meta, sandwich='tasty', *(10,)): pass

但是,定义不是函数定义。它们可能看起来相似,但它们遵循不同的规则,因为它们服务于不同的目的。

函数定义允许任意位置参数、关键字参数和带有单星号*(可变数量的位置参数(和双星号**(可变数量的关键字参数(的带星标的表达式。

类定义通常包括所有基的列表。这里的一个特例是能够定义一个元类,其中包含关键字参数metaclass=Meta和任意数量的其他关键字参数,而没有任何特殊含义。还支持带有双星号**(可变数量的关键字参数(的带星标的表达式。

类定义是一个可执行语句。继承列表通常给出基类的列表(有关更高级的用法,请参阅元类(,因此列表中的每个项目都应计算为允许子类化的类对象。

https://docs.python.org/3/reference/compound_stmts.html#class-definitions

我没有找到在类定义中使用带星标的表达式时定义规则的官方文档,但是在调用中使用它们的规则恰好符合可观察的行为。

(...( 尽管 *expression 语法可能出现在显式关键字参数之后,但它会在关键字参数之前处理 – 见下文(。所以:

def f(a, b):
...     print(a, b)
...
>>> f(b=1, *(2,))
2 1
>>> f(a=1, *(2,))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() got multiple values for keyword argument 'a'
>>> f(1, *(2,))
1 2

https://docs.python.org/dev/reference/expressions.html#calls

由于元类是一个关键字参数,因此在元类之前处理带星标的表达式,因此使用

class A(metaclass=Meta, sandwich='tasty', *(10,)):
pass

对应于

class A(10, metaclass=Meta, sandwich='tasty'):
pass

可以在抽象语法树中看到

>>> import ast
>>> source = '''
... class Meta(type):
...     def __new__(meta, *args, **kwargs):
...         return super().__new__(meta, *args)
... 
... class A(metaclass=Meta, sandwich='tasty', *(10,)): pass
... '''
>>> 
>>> ast.dump(ast.parse(source))
"Module(body=[ClassDef(name='Meta', bases=[Name(id='type', ctx=Load())], keywords=[], body=[FunctionDef(name='__new__', args=arguments(posonlyargs=[], args=[arg(arg='meta')], vararg=arg(arg='args'), kwonlyargs=[], kw_defaults=[], kwarg=arg(arg='kwargs'), defaults=[]), body=[Return(value=Call(func=Attribute(value=Call(func=Name(id='super', ctx=Load()), args=[], keywords=[]), attr='__new__', ctx=Load()), args=[Name(id='meta', ctx=Load()), Starred(value=Name(id='args', ctx=Load()), ctx=Load())], keywords=[]))], decorator_list=[])], decorator_list=[]), ClassDef(name='A', bases=[Starred(value=Tuple(elts=[Constant(value=10)], ctx=Load()), ctx=Load())], keywords=[keyword(arg='metaclass', value=Name(id='Meta', ctx=Load())), keyword(arg='sandwich', value=Constant(value='tasty'))], body=[Pass()], decorator_list=[])], type_ignores=[])"

其中bases定义为包含元组(10,)的带星标的表达式

bases=[Starred(value=Tuple(elts=[Constant(value=10)], ctx=Load()), ctx=Load())]

因此,类定义中的任何非关键字参数都被解释为基类,并且由于10不是有效的基类,这会导致上述错误消息。

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

将参数传递给元类

我不确定它是否考虑了最佳实践,但将变量位置参数定义为类属性可能是处理事情的有意义的方式。

class Meta(type):
def __new__(self, name, bases, classdict, *args, **kwargs):
for key in kwargs:
print(key + '=' + kwargs[key])
print('*args', *classdict['args'])
return super().__new__(self, name, bases, classdict)
class A(metaclass=Meta, sandwich='tasty'):
args = (10,)

我很清楚这个问题的年龄,但由于前两点可能更普遍感兴趣,我选择回答答案。