如何自动将类装饰器应用于所有子类?

发布于 2025-01-19 14:42:54 字数 2077 浏览 4 评论 0原文

我有以下代码:

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 是正确的,这给我留下了两个问题:

  1. 为什么这似乎有效?
  2. 如何正确地执行此操作,即满足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:

  1. Why does this seem to work at all?
  2. How to do this properly, i.e. in a way that satisfies mypy, without reimplementing the dataclass functionality from scratch inside Expr or its metaclass?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

倾城°AllureLove 2025-01-26 14:42:54

@juanpa.arrivilillaga 在评论中完美地解释了正在发生的事情,如下所示:

  • dataclass 调用会就地修改类:同一个对象发生变异,以便获得新方法和新行为。
  • mypy 无法知道新方法,因为它不运行代码:它是 dataclass 装饰器的特殊情况,因此在普通使用中它确实知道新方法属性。解决方法是将 dataclass 创建的方法添加到 Repr 中,这样 mypy 就会被愚弄。
  • 对 dataclass 的某些调用可能会返回一个新对象,例如使用新的 slots 参数时,在这种情况下,__init_subclass__ 将无济于事,因为 dataclass 返回一个新的类,已构建以传递给它的基础为基础。对于相同的工作方法,您将必须使用元类,并重写其 __new__ 方法,而不是(或连同)普通继承。 metaclass.__new__ 方法必须返回将用作类的实际对象。
def __repr__(self):
   # ...
   # stand alone method which will be injected by the metaclass


class MExpr(type):
    def __new__(mcls, name, bases, ns):
        cls = super().__new__(mcls, name, bases, ns)
        cls.__repr__ = __repr__
        return dataclass(repr=False, slots=...)(cls)  

class Literal(metaclass=MExpr):
    ...

@juanpa.arrivillaga has explained in the comments perfectly what is taking place, as put:

  • a 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 the dataclass decorator, so that in ordinary uses it does know about the new attributes. The workaround there would be adding the methods that dataclass create to Repr so that mypy would be fooled.
  • some calls to dataclass might return a new object, like when using the new 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. The metaclass.__new__ method has to return the actual object that will be used as the class.
def __repr__(self):
   # ...
   # stand alone method which will be injected by the metaclass


class MExpr(type):
    def __new__(mcls, name, bases, ns):
        cls = super().__new__(mcls, name, bases, ns)
        cls.__repr__ = __repr__
        return dataclass(repr=False, slots=...)(cls)  

class Literal(metaclass=MExpr):
    ...
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文