Как получить доступ к аргументам типа typing.Generic?

typing модуль обеспечивает базовый класс для общих подсказок типа: typing.Generic класса.

Подклассы аргументов типа Generic accept в квадратных скобках, например:

list_of_ints = typing.List[int]
str_to_bool_dict = typing.Dict[str, bool]

Мой вопрос: как я могу получить доступ к этим аргументам типа?

То есть, учитывая str_to_bool_dict качестве входных данных, как я могу получить str и bool качестве вывода?

В основном я ищу такую функцию, что

>>> magic_function(str_to_bool_dict)
(<class 'str'>, <class 'bool'>)

Ответы

Ответ 1

Возможность 1

Начиная с Python 3.6. есть общедоступное __args__ и (__parameters__). Например:

print( typing.List[int].__args__ )

Он содержит общие параметры (т. __parameters__ int), а __parameters__ содержит сам общий __parameters__ (т. __parameters__ ~T).

Возможность 2

Использовать typing_inspect.getargs

Что использовать

typing следует за PEP8. Как PEP8, так и typing являются соавторами Guido van Rossum. Двойное ведущее и заключительное подчеркивание определяется как: "волшебные" объекты или атрибуты, которые живут в управляемых пользователем пространствах имен ".

Глубины также прокомментированы в строке; из официального репозитория для ввода мы можем видеть: * " __args__ - кортеж всех аргументов, используемых в __args__, например, Dict[T, int].__args__ == (T, int) ".

Тем не менее, авторы также отмечают: * "Типичный модуль имеет временный статус, поэтому он не покрывается высокими стандартами обратной совместимости (хотя мы стараемся как можно больше сохранить его), это особенно верно для (еще недокументированных) такие как __union_params__. Если вы хотите работать с типизированными типами в контексте выполнения, тогда вам может быть интересен проект typing_inspect (часть которого может закончиться впечатыванием позже). "

Я, генерал, что бы вы ни делали с typing, должен быть обновлен до настоящего времени. Если вам нужны передовые совместимые изменения, я бы рекомендовал писать собственные классы аннотаций.

Ответ 2

Кажется, что этот внутренний метод сделает трюк

typing.List[int]._subs_tree()

который возвращает кортеж:

(typing.List, <class 'int'>)

Но это частный API, возможно, есть лучший ответ.

Ответ 3

Насколько мне известно, счастливого ответа здесь нет.

Что приходит в голову - это __args__ недокументированный, который хранит эту информацию:

list_of_ints.__args__
>>>(<class 'int'>,)

str_to_bool_dict.__args__
>>>(<class 'str'>, <class 'bool'>)

но об этом не упоминается в документации модуля typing.

Стоит отметить, что в документации было очень близко указано:

Вероятно, нам также следует обсудить, нужно ли документировать все аргументы ключевого слова для GenericMeta.__new__. Существуют tvars, args, origin, extra и orig_bases. Я думаю, мы могли бы сказать что-то о первых трех (они соответствуют __parameters__, __args__ и __origin__ и они используются большинством вещей при наборе текста).

Но это не совсем так:

Я добавил GenericMeta в __all__ и добавил docstrings в GenericMeta и GenericMeta.__new__ после обсуждения в проблеме. Я решил не описывать __origin__ и друзей в докстерах. Вместо этого я просто добавил комментарий в том месте, где они впервые используются.

Оттуда у вас все еще есть три взаимоисключающих варианта:

  • дождитесь, пока модуль typing достигнет полной зрелости, и надеемся, что эти функции будут задокументированы в ближайшее время

  • присоединиться к списку рассылки идей Python и посмотреть, может ли быть собрана достаточная поддержка, чтобы сделать эти внутренние публикации общедоступными/частью API

  • тем временем работайте с недокументированными внутренностями, делая азартные игры, что не будет изменений к тем или иным изменениям.

Обратите внимание, что третья точка вряд ли можно избежать, так как даже API может быть изменен:

Типовой модуль был включен в стандартную библиотеку на временной основе. Новые функции могут быть добавлены, и API может измениться даже между незначительными версиями, если это будет сочтено необходимым для основных разработчиков.

Ответ 4

Используйте .__args__ в ваших конструкциях. Итак, вам нужна волшебная функция -

get_type_args = lambda genrc_type: getattr(genrc_type, '__args__')

Мой вопрос: как я могу получить доступ к этим аргументам типа?

В таких ситуациях - как мне получить доступ...

Используйте мощные возможности интроспекции Python.

Даже в качестве программиста, не являющегося программистом, я знаю, что пытаюсь проверить материал, а dir - это функция, похожая на IDE в терминале. Так после

>>> import typing
>>> str_to_bool_dict = typing.Dict[str, bool]

Я хочу посмотреть, есть ли что-нибудь, что делает волшебство, которое вы хотите,

>>> methods = dir(str_to_bool_dict)
>>> methods
['__abstractmethods__', '__args__', .....]

Я вижу слишком много информации, чтобы проверить, правильно ли я проверяю

>>> len(methods)
53
>>> len(dir(dict))
39

Теперь давайте найдем методы, которые были разработаны специально для общих типов

>>> set(methods).difference(set(dir(dict)))
{'__slots__', '__parameters__', '_abc_negative_cache_version', '__extra__',
'_abc_cache', '__args__', '_abc_negative_cache', '__origin__',
'__abstractmethods__', '__module__', '__next_in_mro__', '_abc_registry',
'__dict__', '__weakref__'}

среди них __parameters__, __extra__, __args__ и __origin__ полезны. __extra__ и __origin__ не будут работать без себя, поэтому мы остаемся с __parameters__ и __args__.

>>> str_to_bool_dict.__args__
(<class 'str'>, <class 'bool'>)

Отсюда и ответ.


Introspection позволяет py.test assert выводить устаревшие структуры тестирования JUnit. Даже такие языки, как JavaScript/Elm/Clojure, не имеют прямолинейной вещи, как dir Python. Соглашение об именах Python позволяет вам открывать язык без фактического чтения (в некоторых случаях, таких как эти), документации.

Итак, охота с помощью самоанализа и чтения документации/списков рассылки, чтобы подтвердить ваши выводы.

PS К OP - этот метод также отвечает на ваш вопрос. Каков правильный способ проверить, является ли объект типизацией. Generic? если вы не можете зафиксировать список рассылки или быть занятым разработчиком, - это способ сделать это в python.

Ответ 5

Вопрос задается конкретно о typing.Generic, но оказывается, что (по крайней мере, в более ранних версиях модуля typing) не все подписываемые типы являются подклассами Generic. В более новых версиях все подписываемые типы хранят свои аргументы в __args__:

>>> List[int].__args__
(<class 'int'>,)
>>> Tuple[int, str].__args__
(<class 'int'>, <class 'str'>)

Однако в Python 3.5 некоторые классы, такие как typing.Tuple, typing.Union и typing.Callable хранят их в различных атрибутах, таких как __tuple_params__, __union_params__ или, как правило, в __parameters__. Для полноты здесь приведена функция, которая может извлекать аргументы типа из любого подписываемого типа в любой версии Python:

import typing


if hasattr(typing, '_GenericAlias'):
    # python 3.7
    def _get_base_generic(cls):
        # subclasses of Generic will have their _name set to None, but
        # their __origin__ will point to the base generic
        if cls._name is None:
            return cls.__origin__
        else:
            return getattr(typing, cls._name)
else:
    # python <3.7
    def _get_base_generic(cls):
        try:
            return cls.__origin__
        except AttributeError:
            pass

        name = type(cls).__name__
        if not name.endswith('Meta'):
            raise NotImplementedError("Cannot determine base of {}".format(cls))

        name = name[:-4]
        try:
            return getattr(typing, name)
        except AttributeError:
            raise NotImplementedError("Cannot determine base of {}".format(cls))


if hasattr(typing.List, '__args__'):
    # python 3.6+
    def _get_subtypes(cls):
        subtypes = cls.__args__

        if _get_base_generic(cls) is typing.Callable:
            if len(subtypes) != 2 or subtypes[0] is not ...:
                subtypes = (subtypes[:-1], subtypes[-1])

        return subtypes
else:
    # python 3.5
    def _get_subtypes(cls):
        if isinstance(cls, typing.CallableMeta):
            if cls.__args__ is None:
                return ()

            return cls.__args__, cls.__result__

        for name in ['__parameters__', '__union_params__', '__tuple_params__']:
            try:
                subtypes = getattr(cls, name)
                break
            except AttributeError:
                pass
        else:
            raise NotImplementedError("Cannot extract subtypes from {}".format(cls))

        subtypes = [typ for typ in subtypes if not isinstance(typ, typing.TypeVar)]
        return subtypes


def get_subtypes(cls):
    """
    Given a qualified generic (like List[int] or Tuple[str, bool]) as input, return
    a tuple of all the classes listed inside the square brackets.
    """
    return _get_subtypes(cls)

Демонстрация:

>>> get_subtypes(List[int])
(<class 'int'>,)
>>> get_subtypes(Tuple[str, bool])
(<class 'str'>, <class 'bool'>)