如何动态测试一个值是否符合Python字面类型?

发布于 2025-02-08 15:23:29 字数 901 浏览 1 评论 0 原文

我想了解如何动态测试一个值是否符合Python中的字面类型。假设我有以下代码:

from typing import Literal

PossibleValues = Literal["a", "b"]

x = input()

我想使用 isInstance ,但是这样做给了我一个类型:

>>> isinstance(x, PossibleValues)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../typing.py", line 720, in __instancecheck__
    return self.__subclasscheck__(type(obj))
  File "/.../typing.py", line 723, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

我发现我可以使用 get_args ,但是这种方法似乎似乎有点骇客。

>>> from typing import get_args
>>> x = input()
'c'
>>> x in get_args(PossibleValues)
False

有更好的方法吗?

I would like to understand how to dynamically test whether a value conforms to a Literal type in Python. Suppose I have the following code:

from typing import Literal

PossibleValues = Literal["a", "b"]

x = input()

I would like to use isinstance, but doing so gives me a TypeError:

>>> isinstance(x, PossibleValues)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../typing.py", line 720, in __instancecheck__
    return self.__subclasscheck__(type(obj))
  File "/.../typing.py", line 723, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

I've found that I could use get_args, but this approach seems a little hacky.

>>> from typing import get_args
>>> x = input()
'c'
>>> x in get_args(PossibleValues)
False

Is there a better way?

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

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

发布评论

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

评论(4

↘紸啶 2025-02-15 15:23:29

来自在这里通过这样做的字面类型:

>>> from typing import Literal
>>> ATYPE = Literal["a", "b"]
>>> "a" in ATYPE.__args__
True
>>> "c" in ATYPE.__args__
False
>>>

使用 typing.get_args 而不是 __ args __ args __ 可能更有Pythonic。我必须使用它来解决mypy错误,同时将文字存储到 typealias

From here I got the idea of testing if a string is of a literal type by doing this:

>>> from typing import Literal
>>> ATYPE = Literal["a", "b"]
>>> "a" in ATYPE.__args__
True
>>> "c" in ATYPE.__args__
False
>>>

It is probably more pythonic to use typing.get_args instead of __args__. I had to use that to get around mypy errors while storing the literal to a TypeAlias.

感性 2025-02-15 15:23:29

typing.get_args 在运行时完全可以接受,但这不会影响该类型Checker的类型(例如:Mypy和Pyright)。

typeform typeform 使用 eNableExperimentionFeatures = true pr ):

from __future__ import annotations  # Required as TypeForm is not yet released in typing_extensions

from typing import TYPE_CHECKING, Literal, TypeAliasType, TypeIs, assert_type, get_args, get_origin

if TYPE_CHECKING:  # Required as TypeForm is not yet released in typing_extensions
    from typing_extensions import TypeForm

type PossibleValues = Literal["a", "b"]


def is_literal_value[T](value: object, typx: TypeForm[T]) -> TypeIs[T]:
    if isinstance(typx, TypeAliasType):
        typx = typx.__value__
    if get_origin(typx) is Literal:
        return value in get_args(typx)
    return False


def check(x: str):
    if is_literal_value(x, PossibleValues):
        assert_type(x, PossibleValues)
        print(f"{x} is in PossibleValues")
    else:
        assert_type(x, str)
        print(f"{x} is not in PossibleValues")


check("a")
check("z")

这将返回:

$ python3 literal.py
a is in PossibleValues
z is not in PossibleValues

$ pyright literal.py
0 errors, 0 warnings, 0 informations

typing.get_args is perfectly acceptable at runtime, but that won't affect the type that type checkers (eg: mypy and pyright) see.

TypeForm introduced in the draft of PEP 747 will allow us to narrow the type though, allowing the (narrowed) value to be used in other code expecting a value of the Literal's type. Pyright supports TypeForm when configured with enableExperimentalFeatures = true (PR):

from __future__ import annotations  # Required as TypeForm is not yet released in typing_extensions

from typing import TYPE_CHECKING, Literal, TypeAliasType, TypeIs, assert_type, get_args, get_origin

if TYPE_CHECKING:  # Required as TypeForm is not yet released in typing_extensions
    from typing_extensions import TypeForm

type PossibleValues = Literal["a", "b"]


def is_literal_value[T](value: object, typx: TypeForm[T]) -> TypeIs[T]:
    if isinstance(typx, TypeAliasType):
        typx = typx.__value__
    if get_origin(typx) is Literal:
        return value in get_args(typx)
    return False


def check(x: str):
    if is_literal_value(x, PossibleValues):
        assert_type(x, PossibleValues)
        print(f"{x} is in PossibleValues")
    else:
        assert_type(x, str)
        print(f"{x} is not in PossibleValues")


check("a")
check("z")

This will return:

$ python3 literal.py
a is in PossibleValues
z is not in PossibleValues

$ pyright literal.py
0 errors, 0 warnings, 0 informations
梦里的微风 2025-02-15 15:23:29

这是我刚刚想到,我对此感到非常满意。

"""
seems like hard coding is the only way to make literal type to accept anything to be literal
(which makes sense if you think about it)
"""

from typing import Any, Literal


my_literal_type = Literal["a", "b", "c"]


# I used `Any` type here but you could use `str` if you want to restrict the dynamic value's type to be `str` only
def guard_mlt(value: Any) -> my_literal_type:
    """
    only allow values in my_literal_type to pass
    and otherwise raise TypeError
    this requires hard coding the return values as it's explained at the top

    >>> guard_mlt('a')
    'a'
    >>> guard_mlt('b')
    'b'
    >>> guard_mlt('c')
    'c'
    >>> guard_mlt('d')
    Traceback (most recent call last):
        ...
    TypeError: d is not in typing.Literal['a', 'b', 'c']
    """
    match value:
        case "a":
            return "a"
        case "b":
            return "b"
        case "c":
            return "c"

    raise TypeError(f"{value} is not in {my_literal_type}")


if __name__ == "__main__":
    import doctest

    doctest.testmod()

(不可避免的)缺点是您需要为每种字面类型提出警卫功能。

Here is what I just came up with and I'm pretty happy with it.

"""
seems like hard coding is the only way to make literal type to accept anything to be literal
(which makes sense if you think about it)
"""

from typing import Any, Literal


my_literal_type = Literal["a", "b", "c"]


# I used `Any` type here but you could use `str` if you want to restrict the dynamic value's type to be `str` only
def guard_mlt(value: Any) -> my_literal_type:
    """
    only allow values in my_literal_type to pass
    and otherwise raise TypeError
    this requires hard coding the return values as it's explained at the top

    >>> guard_mlt('a')
    'a'
    >>> guard_mlt('b')
    'b'
    >>> guard_mlt('c')
    'c'
    >>> guard_mlt('d')
    Traceback (most recent call last):
        ...
    TypeError: d is not in typing.Literal['a', 'b', 'c']
    """
    match value:
        case "a":
            return "a"
        case "b":
            return "b"
        case "c":
            return "c"

    raise TypeError(f"{value} is not in {my_literal_type}")


if __name__ == "__main__":
    import doctest

    doctest.testmod()

The (inevitable) downside is that you will need to come up with the guard function for every literal type.

合久必婚 2025-02-15 15:23:29

您可以尝试访问 __ args __ ,但仍然不确定是否要明确调用魔术方法是一种方法。

get_args 似乎是正确的选择。

from typing import Literal, get_args

PossibleValues = Literal["a", "b"]

"c" in PossibleValues.__args__
"c" in get_args(PossibleValues)

You can try to access __args__ but still not sure if calling magic method explicitly is a way to go.

get_args seems to be the right option to go with.

from typing import Literal, get_args

PossibleValues = Literal["a", "b"]

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