Проверьте переменную на тип Union во время выполнения в Python 3.6

Я пытаюсь написать декоратор функций, который использует подсказки типов Python 3.6, чтобы проверить, соответствует ли словарь аргументов подсказкам типов, и, если не вызывает ошибку с четким описанием проблемы, для использования для HTTP API.

Проблема в том, что когда функция имеет параметр, использующий тип Union, я не могу проверить переменную по нему во время выполнения.

Например, у меня есть эта функция

from typing import Union
def bark(myname: str, descr: Union[int, str], mynum: int = 3) -> str:
    return descr + myname * mynum

Я могу сделать:

isinstance('Arnold', bark.__annotations__['myname'])

Но нет:

isinstance(3, bark.__annotations__['descr'])

Потому что Union нельзя использовать с isinstance или issubclass.

Я не мог найти способ проверить это с помощью объекта типа. Я попытался реализовать проверку самостоятельно, но хотя bark.__annotations__['descr'] отображается как typing.Union[int, str] в REPL, я не могу получить доступ к списку типов во время выполнения, если не использую уродливый хак проверки bark.__annotations__['descr'].__repr__().

Есть ли правильный способ получить доступ к этой информации? Или это намеренно недоступно во время выполнения?


person Jacopofar    schedule 30.08.2017    source источник
comment
Связано: Как получить доступ к аргументам типа typing.Generic?   -  person Aran-Fey    schedule 28.05.2018


Ответы (5)


Вы можете использовать атрибут __args__ для Union, который содержит tuple "возможного содержимого:

>>> from typing import Union

>>> x = Union[int, str]
>>> x.__args__
(int, str)
>>> isinstance(3, x.__args__)
True
>>> isinstance('a', x.__args__)
True

Аргумент __args__ не задокументирован, поэтому его можно рассматривать как «вмешательство в детали реализации», но это кажется лучшим способом, чем разбор repr.

person MSeifert    schedule 30.08.2017
comment
Python 3.5 вместо этого использовал Union[int, str].__union_args__, в 3.6, похоже, нет способа разрешить Union из любого другого Generic, поскольку isinstance не работает :( - person c z; 16.10.2017

В Python 3.8 и более поздних версиях подход, предложенный MSeifert и Richard Xia можно улучшить, не используя недокументированные атрибуты __origin__ и __args__. Эта функциональность обеспечивается новыми функциями typing.get_args(tp) и typing.get_origin(tp):

>> from typing import Union, get_origin, get_args
>> x = Union[int, str]
>> get_origin(x), get_args(x)
(typing.Union, (<class 'int'>, <class 'str'>))
>> get_origin(x) is Union
True
>> isinstance(3, get_args(x))
True
>> isinstance('a', get_args(x))
True
>> isinstance([], get_args(x))
False

P.S.: Я знаю, что вопрос касается Python 3.6 (вероятно, потому что это была самая новая версия на тот момент), но я попал сюда, когда искал решение как пользователь Python 3.8. Я предполагаю, что другие могут оказаться в такой же ситуации, поэтому я подумал, что добавление нового ответа здесь имеет смысл.

person Frank    schedule 02.11.2020
comment
Именно то, что мне было нужно. - person Benoît Dubreuil; 13.04.2021

Существующий принятый ответ MSeifert (https://stackoverflow.com/a/45959000/7433423) не отличает Unions от другие универсальные типы, и во время выполнения трудно определить, является ли аннотация типа Union или каким-либо другим универсальным типом, таким как Mapping, из-за поведения isinstance() и issubclass() на параметризованных типах Union.

Похоже, что универсальные типы будут иметь недокументированный атрибут __origin__, который будет содержать ссылку на исходный универсальный тип, использованный для его создания. Как только вы подтвердите, что аннотация типа является параметризованным Union, вы можете использовать также недокументированный атрибут __args__ для получения параметров типа.

>>> from typing import Union
>>> type_anno = Union[int, str]
>>> type_anno.__origin__ is Union
True
>>> isinstance(3, type_anno.__args__)
True
>>> isinstance('a', type_anno.__args__)
True
person Richard Xia    schedule 24.03.2018

Вы можете использовать модуль typeguard, который можно установить вместе с pip. Он предоставляет вам функцию check_argument_types или декоратор функций @typechecked. который должен выполнять проверку типов во время выполнения для вас: https://github.com/agronholm/typeguard

from typing import Union
from typeguard import check_argument_types, typechecked

def check_and_do_stuff(a: Union[str, int]) -> None:
    check_argument_types() 
    # do stuff ...

@typechecked
def check_decorator(a: Union[str, int]) -> None:
    # do stuff ...

check_and_do_stuff("hello")
check_and_do_stuff(42)
check_and_do_stuff(3.14)  # raises TypeError

Если вы хотите проверить тип одной переменной по другой причине, вы можете напрямую использовать функцию typeguard check_type:

from typing import Union
from typeguard import check_type

MyType = Union[str, int]

check_type("arg", "string", MyType, None)  # OK
check_type("arg", 42, MyType, None)  # OK
check_type("arg", 3.5, MyType, None)  # raises TypeError

Аргументы "arg" и None в этом примере не используются. Обратите внимание, что функция check_type не задокументирована как общедоступная функция этого модуля, поэтому ее API может быть изменен.

person Jindra Helcl    schedule 24.07.2018

Я предполагаю, что Union - это не сам тип, а описание типа.

Но мы можем запросить тип, просто используя type(Union) (также работает в 2.7)

>>> from typing import Union
>>> type(Union)
typing.Union
>>> x = Union[int, str]
>>> isinstance(x, type(Union))
True
person Alex    schedule 30.12.2020
comment
Это не выполняет требуемую проверку — он должен возвращать True тогда и только тогда, когда x является либо int, либо str, а не если x сам является объектом типа Union. - person Ken Williams; 16.03.2021