【问题标题】:How can i type hint the init params are the same as fields in a dataclass?我如何输入提示初始化参数与数据类中的字段相同?
【发布时间】:2022-09-27 16:09:15
【问题描述】:

假设我有一个自定义用例,我需要为数据类动态创建或定义__init__ 方法。

例如,假设我需要像@dataclass(init=False) 一样装饰它,然后修改__init__() 方法以获取关键字参数,例如**kwargs。但是,在kwargs 对象中,我只检查已知数据类字段的存在,并相应地设置这些属性(示例如下)

我想向我的 IDE (PyCharm) 输入提示,修改后的 __init__ 只接受列出的数据类字段作为参数或关键字参数。我不确定是否有办法解决这个问题,使用 typing 库或其他方式。我知道 PY3.11 有 dataclass transforms 计划,这可能会也可能不会做我想要的(我的直觉是)。

这是我正在玩的示例代码,这是一个基本案例,说明了我遇到的问题:

from dataclasses import dataclass


# get value from input source (can be a file or anything else)
def get_value_from_src(_name: str, tp: type):
    return tp()  # dummy value


@dataclass
class MyClass:
    foo: str
    apple: int

    def __init__(self, **kwargs):
        for name, tp in self.__annotations__.items():
            if name in kwargs:
                value = kwargs[name]
            else:
                # here is where I would normally have the logic
                # to read the value from another input source
                value = get_value_from_src(name, tp)
                if value is None:
                    raise ValueError

            setattr(self, name, value)


c = MyClass(apple=None)
print(c)

c = MyClass(foo=\'bar\',  # here, I would like to auto-complete the name
                        # when I start typing `apple`
            )
print(c)

如果我们假设字段的数量或名称不固定,我很好奇是否有一种通用方法基本上可以对类型检查器说,\"这个类的__init__ 只接受与数据类本身定义的字段匹配的(可选)关键字参数\".


附录,基于以下 cmets 中的注释:

  • 传递@dataclass(kw_only=True) 将不起作用,因为想象我正在为一个库编写这个,并且需要支持 Python 3.7+。此外,在实现自定义 __init__() 时,kw_only 无效,如本例所示。

  • 以上只是一个存根__init__ 方法。它可能有更复杂的逻辑,例如基于文件源设置属性。基本上以上只是一个更大用例的示例实现。

  • 我无法将每个字段更新为foo: Optional[str] = None,因为该部分将在用户代码中实现,我无法控制。此外,当您知道将为您生成自定义 __init__() 方法时,以这种方式对其进行注释是没有意义的 - 这意味着不是由 dataclasses 生成的。最后,设置一个默认值每个字段,以便班级可以 不带参数进行实例化,例如MyClass(),看起来不像 对我来说最好的主意。

  • dataclasses 自动生成__init__ 并实现__post_init__() 是行不通的。这是行不通的,因为我需要能够构建没有参数的类,例如MyClass(),因为字段值将从另一个输入源(考虑本地文件或其他地方)设置;这意味着所有字段都是必需的,因此在这种情况下将它们注释为 Optional 将是错误的。我仍然需要能够支持用户输入可选的关键字参数,但是这些**kwargs 将始终与数据类字段名称匹配,因此我希望通过某种方式让自动完成功能与我的 IDE (PyCharm) 一起使用

希望这篇文章能澄清期望和期望的结果。如果有任何问题或任何含糊不清的地方,请告诉我。

  • 如果您这样做只是为了有关键字参数,请使用@dataclass(kw_only=True)
  • @user2357112 我不能使用 kw_only 有几个不同的原因。例如,kw_only 仍然根据需要制作所有参数,但我需要它们作为可选参数。此外,如果我们动态生成 __init__ 方法,kw_only 将不起作用。
  • 看起来您生成自己的__init__ 的唯一原因是复制dataclass 可以为您提供的功能,如果您没有生成自己的__init__
  • 好的,但听起来大部分都由__post_init__ 处理得更好。您可以让dataclass 为您生成__init__ 并获得您正在寻找的所有IDE 自动完成优势。
  • 看,我和下一个人一样喜欢重新发明轮子,但除此之外,在我看来,您的要求只是不一致。甚至如果有一种方法可以根据需要神奇地动态注释您的 __init__ 方法,您的类属性上的类型注释仍然是错误的。 foo: str 表示 foo 应该是一个字符串,并且绝不None。所以这些类型提示一开始就已经错了。不管你喜不喜欢,typing.Optional[str]str | None 是唯一正确的方法,如果一个实例的foo 属性可以同时是strNone

标签: python python-typing python-dataclasses


【解决方案1】:

您所描述的在理论上是不可能的,在实践中也不可能是可行的。

TL;博士

类型检查器不运行您的代码,他们只是阅读它。动态类型注释在术语上是矛盾的。

理论

我相信你知道,这个词静止的类型检查器并非巧合。静态类型检查器不会执行您编写的代码。它只是解析它并根据它自己的通过将某些规则应用于从您的代码派生的图形来实现内部逻辑。

这很重要,因为与其他一些语言不同,Python 是动态的类型化,如您所知,这意味着“事物”(变量)的类型可以完全改变在任何时候.一般来说,有理论上如果不实际执行整个算法,即运行代码,就无法知道代码中所有变量的类型。

作为一个愚蠢但说明性的示例,您可以决定将类型的名称放入文本文件中以在运行时读取,然后用于注释代码中的某些变量。你能用有效的 Python 代码和打字来做到这一点吗?当然。但我认为这是非常清楚的,静态类型检查器永远不会知道该变量的类型。

为什么你的提议行不通

抽象出所有dataclass 的东西和可能的逻辑里面您的__init__ 方法,您的要求归结为以下内容。

“我想定义一个方法(__init__),但它的参数类型只有在运行时才能知道。”

我为什么要这么说?我是说你注释类属性的类型,对吗?所以你有类型!

当然,但是正如您自己指出的那样,这些通常与您可以传递给__init__ 方法的参数没有任何关系。您希望 __init__ 方法接受任意关键字参数。然而,您还需要一个静态类型检查器来推断那里允许/预期哪些类型。

要连接这两者(属性类型和方法参数类型),您当然可以编写某种逻辑。您甚至可以通过以下方式实现它强制执行坚持这些类型。该逻辑可以读取类属性的类型注释,匹配**kwargs 并在其中一个不匹配时引发TypeError。这是完全可能的,您几乎已经在示例代码中实现了这一点。但这仅在运行时有效!

同样,静态类型检查器具有没门推断,尤其因为您想要的类应该只是一个基类,并且任何后代都可以在任何时候引入自己的属性/类型。

但是dataclasses 工作,不是吗?

您可能会争辩说,这种注解__init__ 方法的动态方式适用于数据类。那么为什么它们如此不同呢?为什么它们被正确推断,但您提出的代码却不能?

答案是,他们不是。

即使dataclasses 也没有任何神奇的方式告诉静态类型检查器__init__ 方法期望哪些参数类型,即使它们注释它们,当它们在_init_fn 中动态构造方法时。

mypy 正确推断这些类型的唯一原因是因为它们实现了一个单独的插件只是对于数据类。这意味着它可以工作,因为他们通读了PEP 557 并为mypy 手工制作了一个插件,该插件专门促进了基于那里描述的规则的类型推断。

您可以看到 DataclassTransformer.transform 方法中发生的魔法。您不能将此行为推广到任意代码,这就是为什么他们必须为此编写一个完整的插件。

我对 PyCharm 如何进行类型检查还不够熟悉,但我强烈怀疑他们使用了类似的东西。

所以你可以说dataclasses 在静态类型检查方面是“作弊”。虽然我当然没有抱怨。

务实的解决方案

即使像我个人喜欢并广泛使用的像 Pydantic 这样“高调”的东西,也需要它自己的 mypy 插件才能正确实现 __init__ 类型推断(参见 here)。对于 PyCharm,它们有自己独立的 Pydantic plugin,没有它,内部类型检查器就无法为初始化等提供很好的自动建议。

如果您真的想更进一步,这种方法将是您最好的选择。请注意,这将是(在最好的意义上)黑客允许具体的键入检查器以捕获“错误”,否则它们将无法捕获。

我认为它不太可能可行的原因是,它本质上会增加您项目的工作量,以涵盖您想要满足的那些类型检查器的特定黑客。如果你有足够的承诺并且有资源,那就去吧。

结论

我不是要劝阻你。但重要的是要了解环境所施加的限制。它要么是动态类型和不完美的类型检查(仍然喜欢mypy),要么是静态类型,没有kwargs 可以是任何东西”行为。

希望这是有道理的。如果我犯了任何错误,请告诉我。这只是基于我对 Python 输入的理解。

【讨论】:

    【解决方案2】:

    为了

    dataclasses 自动生成__init__ 并实现__post_init__() 是行不通的。这是行不通的,因为我需要能够在没有参数的情况下构造类,例如MyClass(),因为字段值将从另一个输入源(考虑本地文件或其他地方)设置;这意味着所有字段都是必需的,因此在这种情况下将它们注释为 Optional 是错误的。我仍然需要能够支持用户输入可选的关键字参数,但这些 **kwargs 将始终与数据类字段名称匹配,因此我希望通过某种方式让自动完成功能与我的 IDE (PyCharm) 一起使用

    dataclasses.field + default_factory 可以是一个解决方案。

    但是,dataclass 字段声明似乎是在用户代码中实现的:

    我无法将每个字段更新为foo: Optional[str] = None,因为该部分将在用户代码中实现,我无法控制。此外,当您知道将为您生成自定义 __init__() 方法时,以这种方式对其进行注释是没有意义的 - 这意味着不是由 dataclasses 生成的。最后,为每个字段设置一个默认值,以便可以在没有参数的情况下实例化类,例如MyClass(),对我来说似乎不是最好的主意。

    如果您的 IDE 支持ParamSpec,则有一个解决方法:不正确(无法通过静态类型检查器),但具有自动完成功能:

    from typing import Callable, Iterable, TypeVar, ParamSpec
    
    from dataclasses import dataclass
    
    T = TypeVar('T')
    P = ParamSpec('P')
    
    # user defined dataclass
    @dataclass
    class MyClass:
        foo: str
        apple: int
    
    
    def wrap(factory: Callable[P, T], annotations: Iterable[tuple[str, type]]) -> Callable[P, T]:
        def default_factory(**kwargs):
            for name, type_ in annotations:
                kwargs.setdefault(name, type_())
            return factory(**kwargs)
        return default_factory
    
    WrappedMyClass = wrap(MyClass, MyClass.__annotations__.items())
    WrappedMyClass() # Okay
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-11-29
      • 2011-02-08
      • 1970-01-01
      • 2021-11-12
      • 2021-09-10
      • 2021-02-23
      • 1970-01-01
      • 2011-03-16
      相关资源
      最近更新 更多