python 检查字段是否正在键入,可选

muk1a3rh  于 2023-06-04  发布在  Python
关注(0)|答案(6)|浏览(387)

检查类中的字段是否为键入的最佳方法是什么?可选?
示例代码:

from typing import Optional
import re
from dataclasses import dataclass, fields

@dataclass(frozen=True)
class TestClass:
    required_field_1: str
    required_field_2: int
    optional_field: Optional[str]

def get_all_optional_fields(fields) -> list:
    return [field.name for field in fields if __is_optional_field(field)]

def __is_optional_field(field) -> bool:
    regex = '^typing.Union\[.*, NoneType\]$'
    return re.match(regex, str(field.type)) is not None

print(get_all_optional_fields(fields(TestClass)))

其中fields来自dataclasses,我想列出所有Optional字段。我现在解决这个问题的方法是使用基于Regex的字段名,但我不喜欢这种方法。有没有更好的办法呢?

58wvjzkj

58wvjzkj1#

作为参考,Python 3.8(2019年10月首次发布)在typing模块中添加了get_originget_args函数。
文档中的示例:

assert get_origin(Dict[str, int]) is dict
assert get_args(Dict[int, str]) == (int, str)

assert get_origin(Union[int, str]) is Union
assert get_args(Union[int, str]) == (int, str)

这将允许:

def is_optional(field):
    return typing.get_origin(field) is Union and \
           type(None) in typing.get_args(field)

对于较旧的Python,这里有一些兼容性代码:

# Python >= 3.8
try:
    from typing import Literal, get_args, get_origin
# Compatibility
except ImportError:
    get_args = lambda t: getattr(t, '__args__', ()) \
                         if t is not Generic else Generic
    get_origin = lambda t: getattr(t, '__origin__', None)
dgsult0t

dgsult0t2#

注意:typing.Optional[x]typing.Union[x, None]的别名
现在,可以检查输入字段注解的属性,检查它是否像Union[x,None]那样定义:
您可以读取其属性__module____args____origin__

from typing import *

def print_meta_info(x):
      print(x.__module__, x.__args__, x.__origin__)

x = Optional[int]
print_meta_info(x) # 'typing', (class Int,), typing.Union

x = Union[int, float]
print_meta_info(x) # 'typing', (class int, class float), typing.Union

x = Iterable[str]
print_meta_info(x) # 'typing', (class int,), typing.Iterable

您需要执行以下步骤来定义检查器:
1.确保注解具有键__module____args____origin__

  1. __module__必须设置为“typing”。如果不是,则注解不是由类型化模块定义的对象
  2. __origin__值等于键入。Union
  3. __args__必须是一个包含2个元素的元组,其中第二个元素是类NoneType(type(None)
    如果所有条件的计算结果都为true,则表示您已键入。可选[x]
    您可能还需要知道注解中的可选类是什么:
x = Optional[int].__args__[0]
print(x) # class int
uurity8g

uurity8g3#

另一种方法(适用于python 3.7和3.8)是中继Union操作的工作方式:
union([x,y],[y])= union([x],[y]) = union(union([x],[y]),[x,y])
逻辑是Optional类型不能是Optional er。虽然你不能直接知道type是否是空的/可选的,但Optional[type]将与type相同,type是可选的,其他(确切地说是Union[type,None])。
因此,在我们的案例中:

Union[SomeType,None] == Union[Union[SomeType,None]]

(the第一个等于Optional[SomeType],第二个等于Optional[Optional[SomeType]]
这样可以很容易地检查Optional值:

from dataclasses import dataclass, fields
from typing import Optional

@dataclass()
class DC:
    x: Optional[str] = None
    y: str = "s"

def get_optional_fields(cls):
    fields_list = fields(cls)
    return [
        field.name 
        for field in fields_list if 
        field.type == Optional[field.type]
    ]


if __name__ == '__main__':
    print(get_optional_fields(DC())) # ['x']
gv8xihay

gv8xihay4#

Optional[X]等于Union[X, None]。所以你可以,

import re
from typing import Optional

from dataclasses import dataclass, fields

@dataclass(frozen=True)
class TestClass:
    required_field_1: str
    required_field_2: int
    optional_field: Optional[str]

def get_optional_fields(klass):
    class_fields = fields(klass)
    for field in class_fields:
        if (
            hasattr(field.type, "__args__")
            and len(field.type.__args__) == 2
            and field.type.__args__[-1] is type(None)
        ):
            # Check if exactly two arguments exists and one of them are None type
            yield field.name

print(list(get_optional_fields(TestClass)))
zlhcx6iw

zlhcx6iw5#

Python 3.10添加了符号T | None来指定联合类型(参见PEP 604)。在我的本地Python 3.10.7中,types.get_origin将其转换为types.UnionType(而不是`typing.Union)。所以下面的代码适用于平面联合(但请参阅SimSimY关于嵌套联合的答案)。

import typing
import types

def field_is_optional(cls: type, field_name: str):
    """A field is optional when it has Union type with a NoneType alternative.
    Note that Optional[] is a special form which is converted to a Union with a NoneType option
    """
    field_type = typing.get_type_hints(cls).get(field_name, None)
    origin = typing.get_origin(field_type)
    #print(field_name, ":", field_type, origin)
    if origin is typing.Union:
        return type(None) in typing.get_args(field_type)
    if origin is types.UnionType:
        return type(None) in typing.get_args(field_type)
    return False

这是一个测试:

from dataclasses import dataclass
from typing import Optional, Union

@dataclass
class A:
    foo : Optional[int] = None
    bar : int|None = None
    baz : Union[int, float, None] = None
    x : int = 1

a=A()
assert field_is_optional(type(a), "foo")
assert field_is_optional(type(a), "bar")
assert field_is_optional(type(a), "baz")
assert field_is_optional(type(a), "x") == False
xsuvu9jc

xsuvu9jc6#

我写了一个叫做typedload的库,可以用来做这个。
该库的主要目的是转换为/从json和namedtuple/dataclass/attrs,但由于它需要做这些检查,它公开了函数。
请注意,不同版本的python会改变内部类型API的工作方式,因此检查不会在每个python版本上工作。
我的库在内部解决它,向用户隐藏细节。
使用它,代码是这样的

from typing import *
a = Optional[int]

from typedload import typechecks
typechecks.is_union(a) and type(None) in typechecks.uniontypes(a)

https://github.com/ltworf/typedload
当然,如果你不需要支持多个python版本,你可能不需要依赖一个库,但是未来的版本可能会打破这个检查。他们甚至在小版本之间改变了API。

相关问题