繁体   English   中英

我如何输入提示初始化参数与数据类中的字段相同?

[英]How can i type hint the init params are the same as fields in a dataclass?

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

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

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

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

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)

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


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

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

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

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

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

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

如果您对只需要 kwargs 的要求有所放松,那么您似乎可以通过使用默认工厂来巧妙地解决这个问题。

from dataclasses import dataclass, field


def foo_default() -> str:
    # logic to handle foo here, raise if needed
    ...


def apple_default() -> int:
    # logic to handle apple here, raise if needed
    ...


@dataclass
class MyClass:
    foo: str = field(default_factory=foo_default)
    apple: int = field(default_factory=apple_default)


print(MyClass())
print(MyClass(apple=2))

输出

MyClass(foo=None, apple=None)
MyClass(foo=None, apple=2)

此外,这仍然使 mypy 之类的工具能够捕获错误。

MyClass(apple="a")
$ mypy test.py
test.py:17: error: Argument "apple" to "MyClass" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)

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

TL;博士

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

理论

我相信您知道,术语static类型检查器并非巧合。 static 类型检查器未执行您编写的代码。 它只是通过将某些规则应用于从您的代码派生的图形来解析它并根据自己的内部逻辑推断类型。

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

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

为什么你的提议行不通

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

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

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

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

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

同样,static 类型检查器无法推断出这一点,尤其是因为您想要的 class 应该只是一个基本 class 并且任何后代都可以在任何点引入其自己的属性/类型。

但是dataclasses有效,不是吗?

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

答案是,他们不是。

即使dataclasses_init_fn中动态构造方法时,也没有任何神奇的方式告诉 static 类型检查器__init__方法期望哪些参数类型,即使它们确实注释了它们。

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

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

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

因此,您可以争辩说数据类在dataclasses类型检查方面是“作弊”。 虽然我当然没有抱怨。

务实的解决方案

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

如果您真的想更进一步,这种方法将是您最好的选择。 请注意,这将(从最好的意义上说)是一种允许特定类型检查器捕获“错误”的技巧,否则它们将无法捕获。

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

结论

我不是要劝阻你。 但重要的是要了解环境所施加的限制。 它要么是动态类型和不完美的类型检查(仍然喜欢mypy ),要么是 static 类型,并且没有kwargs can be anything”的行为。

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

为了

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

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

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

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

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

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

演示

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM