如何自动将类装饰器应用于所有子类?
我有以下代码:
from dataclasses import dataclass
class Expr:
def __repr__(self):
fields = ', '.join(f'{v!r}' for v in self.__dict__.values())
return f'{self.__class__.__name__}({fields})'
@dataclass(repr=False)
class Literal(Expr):
value: object
@dataclass(repr=False)
class Binary(Expr):
lhs: Expr
op: str
rhs: Expr
此代码允许我定义表达式树并匹配
它们,例如:
def fmt_ast(expr: Expr) -> str:
match expr:
case Literal(x):
return str(x)
case Binary(lhs, op, rhs):
return f'({op} {fmt_ast(lhs)} {fmt_ast(rhs)})'
assert False, 'Unreachable'
expr = Binary(Literal(42), '+', Literal(23))
print(fmt_ast(expr))
# (+ 42 23)
但是,我想避免手动装饰每个Expr
子类。即我想跳过编写 @dataclass(repr=False)
。 根据另一个答案,我重写了 Expr
如下:
class Expr:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
dataclass(repr=False)(cls)
def __repr__(self):
fields = ', '.join(f'{v!r}' for v in self.__dict__.values())
return f'{self.__class__.__name__}({fields})'
...这似乎有效(甚至从子类中删除装饰器之后)。问题是,我不明白它为什么起作用:当然,我正在调用 dataclass 装饰器,但我没有做任何事情与结果,即装饰类型。我不会将其分配给任何东西(分配给什么?!),并且不会返回它,因为 __init_subclass__
没有有意义的返回值。然而,代码继续运行,就像子类已用 dataclass
修饰一样。
然而,mypy
(0.942) 揭开了我的虚张声势:
test/__init__.py:29:错误:类“test.Literal”未定义“__match_args__” test/__init__.py:31:错误:类“test.Binary”未定义“__match_args__” test/__init__.py:36:错误:“二进制”参数太多 test/__init__.py:36:错误:“Literal”的参数太多
我相信 mypy
是正确的,这给我留下了两个问题:
- 为什么这似乎有效?
- 如何正确地执行此操作,即满足
mypy
的方式,无需从头开始重新实现dataclass
功能Expr
或其元类?
I have the following code:
from dataclasses import dataclass
class Expr:
def __repr__(self):
fields = ', '.join(f'{v!r}' for v in self.__dict__.values())
return f'{self.__class__.__name__}({fields})'
@dataclass(repr=False)
class Literal(Expr):
value: object
@dataclass(repr=False)
class Binary(Expr):
lhs: Expr
op: str
rhs: Expr
This code allows me to define expression trees and to match
against them, e.g.:
def fmt_ast(expr: Expr) -> str:
match expr:
case Literal(x):
return str(x)
case Binary(lhs, op, rhs):
return f'({op} {fmt_ast(lhs)} {fmt_ast(rhs)})'
assert False, 'Unreachable'
expr = Binary(Literal(42), '+', Literal(23))
print(fmt_ast(expr))
# (+ 42 23)
However, I would like to avoid having to decorate each Expr
subclass manually. I.e. I want to skip writing @dataclass(repr=False)
. Based on another answer, I rewrote Expr
as follows:
class Expr:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
dataclass(repr=False)(cls)
def __repr__(self):
fields = ', '.join(f'{v!r}' for v in self.__dict__.values())
return f'{self.__class__.__name__}({fields})'
… and this seems to work (even after removing the decorators from the subclasses). The thing is, I don’t understand why it works: sure, I’m invoking the dataclass
decorator but I’m not doing anything with the result, i.e.with the decorated type. I don’t assign it to anything (to what?!) and I don’t return it, since __init_subclass__
has no meaningful return value. And yet, the code continues to run as if the subclasses had been decorated with dataclass
.
However, mypy
(0.942) calls my bluff:
test/__init__.py:29: error: Class "test.Literal" doesn't define "__match_args__" test/__init__.py:31: error: Class "test.Binary" doesn't define "__match_args__" test/__init__.py:36: error: Too many arguments for "Binary" test/__init__.py:36: error: Too many arguments for "Literal"
I believe that mypy
is correct, which leaves me with two questions:
- Why does this seem to work at all?
- How to do this properly, i.e. in a way that satisfies
mypy
, without reimplementing thedataclass
functionality from scratch insideExpr
or its metaclass?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
@juanpa.arrivilillaga 在评论中完美地解释了正在发生的事情,如下所示:
dataclass
调用会就地修改类:同一个对象发生变异,以便获得新方法和新行为。mypy
无法知道新方法,因为它不运行代码:它是dataclass
装饰器的特殊情况,因此在普通使用中它确实知道新方法属性。解决方法是将 dataclass 创建的方法添加到 Repr 中,这样 mypy 就会被愚弄。slots
参数时,在这种情况下,__init_subclass__
将无济于事,因为 dataclass 返回一个新的类,已构建以传递给它的基础为基础。对于相同的工作方法,您将必须使用元类,并重写其 __new__ 方法,而不是(或连同)普通继承。metaclass.__new__
方法必须返回将用作类的实际对象。@juanpa.arrivillaga has explained in the comments perfectly what is taking place, as put:
dataclass
call modifies the class in place: the same object is mutated so that it gains the new methods and new behaviors.mypy
can't know about the new methods, because it does not run the code: it special cases thedataclass
decorator, so that in ordinary uses it does know about the new attributes. The workaround there would be adding the methods that dataclass create toRepr
so that mypy would be fooled.slots
parameter, in that case,__init_subclass__
won't help, as dataclass returns a new class, built with base on the one that was passed to it. For the same approach to work, you will have to use a metaclass, and override its__new__
method, instead (or along with) ordinary inheritance. Themetaclass.__new__
method has to return the actual object that will be used as the class.