簡體   English   中英

檢查對象是否是打字的正確方法是什么。通用?

[英]What's the correct way to check if an object is a typing.Generic?

我正在嘗試編寫驗證類型提示的代碼,為了做到這一點,我必須找出注釋是什么類型的對象。 例如,考慮這個片段,它應該告訴用戶期望什么樣的值:

import typing

typ = typing.Union[int, str]

if issubclass(typ, typing.Union):
    print('value type should be one of', typ.__args__)
elif issubclass(typ, typing.Generic):
    print('value type should be a structure of', typ.__args__[0])
else:
    print('value type should be', typ)

這應該打印“值類型應該是(int,str)之一”,但它會引發異常:

Traceback (most recent call last):
  File "untitled.py", line 6, in <module>
    if issubclass(typ, typing.Union):
  File "C:\Python34\lib\site-packages\typing.py", line 829, in __subclasscheck__
    raise TypeError("Unions cannot be used with issubclass().")
TypeError: Unions cannot be used with issubclass().

isinstance也不起作用:

>>> isinstance(typ, typing.Union)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Python34\lib\site-packages\typing.py", line 826, in __instancecheck__
    raise TypeError("Unions cannot be used with isinstance().")
TypeError: Unions cannot be used with isinstance().

檢查typ是否是typing.Generic的正確方法是什么?

如果可能的話,我希望看到一個由文檔或 PEP 或其他資源支持的解決方案。 通過訪問未記錄的內部屬性來“工作”的“解決方案”很容易找到。 但更有可能的是,它會成為一個實現細節,並且會在未來的版本中發生變化。 我正在尋找“正確的方法”來做到這一點。

您可能正在尋找__origin__

 # * __origin__ keeps a reference to a type that was subscripted,
#   e.g., Union[T, int].__origin__ == Union;`

import typing

typ = typing.Union[int, str]

if typ.__origin__ is typing.Union:
    print('value type should be one of', typ.__args__)
elif typ.__origin__ is typing.Generic:
    print('value type should be a structure of', typ.__args__[0])
else:
    print('value type should be', typ)

>>>value type should be one of (<class 'int'>, <class 'str'>)

我能找到的最好的提倡使用這個無證屬性的是 Guido Van Rossum 的這句令人放心的引述(日期為 2016 年):

我可以推薦的最好的方法是使用__origin__ ——如果我們要更改這個屬性,仍然必須有其他方法來訪問相同的信息,並且很容易在代碼中__origin__的出現。 (與__origin__相比,我不太擔心對__extra__的更改。)您還可以查看內部函數_gorg()_geqv() (顯然,這些名稱不會成為任何公共 API 的一部分,但它們的實現非常簡單且在概念上有用)。

文檔中的這一警告似乎表明尚未在大理石中設置任何內容:

如果核心開發人員認為有必要,可能會添加新功能,即使在次要版本之間,API 也可能會發生變化。

沒有官方方法可以獲取此信息。 typing模塊仍在大量開發中,沒有公開的 API 可言。 (事實上​​,它可能永遠不會有。)

我們所能做的就是查看模塊的內部結構並找到最簡單的方法來獲取我們想要的信息。 而且由於該模塊仍在開發中,其內部結構將發生變化。 很多。


在 python 3.5 和 3.6 中,泛型有一個__origin__屬性,該屬性包含對原始泛型基類的引用(即List[int].__origin__本來是List ),但在 3.7 中改變了這一點。 現在找出某個東西是否是泛型的最簡單方法可能是檢查它的__parameters____args__屬性。

下面是一組可用於檢測泛型的函數:

import typing


if hasattr(typing, '_GenericAlias'):
    # python 3.7
    def _is_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            return True

        if isinstance(cls, typing._SpecialForm):
            return cls not in {typing.Any}

        return False


    def _is_base_generic(cls):
        if isinstance(cls, typing._GenericAlias):
            if cls.__origin__ in {typing.Generic, typing._Protocol}:
                return False

            if isinstance(cls, typing._VariadicGenericAlias):
                return True

            return len(cls.__parameters__) > 0

        if isinstance(cls, typing._SpecialForm):
            return cls._name in {'ClassVar', 'Union', 'Optional'}

        return False
else:
    # python <3.7
    if hasattr(typing, '_Union'):
        # python 3.6
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union, typing._Optional, typing._ClassVar)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing._Union)):
                return cls.__args__ in {None, ()}

            if isinstance(cls, typing._Optional):
                return True

            return False
    else:
        # python 3.5
        def _is_generic(cls):
            if isinstance(cls, (typing.GenericMeta, typing.UnionMeta, typing.OptionalMeta, typing.CallableMeta, typing.TupleMeta)):
                return True

            return False


        def _is_base_generic(cls):
            if isinstance(cls, typing.GenericMeta):
                return all(isinstance(arg, typing.TypeVar) for arg in cls.__parameters__)

            if isinstance(cls, typing.UnionMeta):
                return cls.__union_params__ is None

            if isinstance(cls, typing.TupleMeta):
                return cls.__tuple_params__ is None

            if isinstance(cls, typing.CallableMeta):
                return cls.__args__ is None

            if isinstance(cls, typing.OptionalMeta):
                return True

            return False


def is_generic(cls):
    """
    Detects any kind of generic, for example `List` or `List[int]`. This includes "special" types like
    Union and Tuple - anything that's subscriptable, basically.
    """
    return _is_generic(cls)


def is_base_generic(cls):
    """
    Detects generic base classes, for example `List` (but not `List[int]`)
    """
    return _is_base_generic(cls)


def is_qualified_generic(cls):
    """
    Detects generics with arguments, for example `List[int]` (but not `List`)
    """
    return is_generic(cls) and not is_base_generic(cls)

所有這些函數都應該在所有 <= 3.7 的 python 版本中工作(包括任何 <3.5 使用typing模塊后向端口的版本)。

正如sonny -garcia在評論中指出的那樣, get_origin()從python 3.8 開始工作

import typing
from typing import get_origin

typ = typing.Union[int, str]
get_origin(typ) == typing.Union
#True

您可以在文檔中找到更多詳細信息

我在我的一個項目中對此進行了編程:

def is_type(arg: object) -> bool:
    """Return True if `arg` is a type, including those for generic typing."""
    from typing import Any, get_origin

    generic_base = type(Any)
    if (aux := generic_base.__base__) is not object:
        generic_base = aux
    return isinstance(arg, (type, generic_base)) or get_origin(arg)

Python 3.8開始, typing.getOrigin(tp)是正確的方法

文檔字符串非常具有指導意義(如果不是從typing返回,則特別是返回 None ):

def get_origin(tp):
    """Get the unsubscripted version of a type.

    This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar
    and Annotated. Return None for unsupported types. Examples::

        get_origin(Literal[42]) is Literal
        get_origin(int) is None
        get_origin(ClassVar[int]) is ClassVar
        get_origin(Generic) is Generic
        get_origin(Generic[T]) is Generic
        get_origin(Union[T, int]) is Union
        get_origin(List[Tuple[T, T]][int]) == list
    """

在您的用例中,它將類似於:

import typing

def func(typ):
    if typing.get_origin(typ) is typing.Union:
        print('value type should be one of', typing.get_args(typ))
    elif typing.get_origin(typ) is typing.Generic:
        print('value type should be a structure of', typing.get_args(typ))
    else:
        print('value type should be', typ)

A = typing.TypeVar("A")
B = typing.TypeVar("B")

func(typing.Union[int, str])
func(typing.Generic[A, B])
func(int)

>>> "value type should be one of (<class 'int'>, <class 'str'>)"
>>> "value type should be a structure of (~A, ~B)"
>>> "value type should be <class 'int'>"

我認為,您最多可以做的就是在變量上使用您的typ ,在其上使用typing.get_type_hints並從返回的__annotations__類字典中提取您需要的信息。

PEP-484說:

get_type_hints() ,一個實用函數,用於從函數或方法中檢索類型提示。 給定一個函數或方法對象,它返回一個與__annotations__格式相同的字典,但在原始函數或方法定義的上下文中將前向引用(以字符串文字給出)作為表達式進行計算。

26.1.7. 類、函數和裝飾器說:

在運行時, isinstance(x, T)將引發TypeError 一般來說, isinstance()issubclass()不應該與類型一起使用。

然而, PEP-526在“非目標”中說:

雖然該提案附帶了用於運行時檢索注解的typing.get_type_hints標准庫函數的擴展,但變量注解不是為運行時類型檢查而設計的。 必須開發第三方軟件包來實現此類功能。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM