web-dev-qa-db-fra.com

Quelle est la bonne façon de vérifier si un objet est une frappe. Générique?

J'essaie d'écrire du code qui valide les indications de type, et pour ce faire, je dois savoir quel type d'objet est l'annotation. Par exemple, considérez cet extrait qui est censé indiquer à l'utilisateur le type de valeur attendu:

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)

Cela devrait afficher "le type de valeur doit être l'un de (int, str)", mais à la place, il lève une exception:

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 ne fonctionne pas non plus:

>>> 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().

Quelle est la bonne façon de vérifier si typ est un typing.Generic?

Si possible, j'aimerais voir une solution soutenue par de la documentation ou un PEP ou une autre ressource. Une "solution" qui "fonctionne" en accédant à des attributs internes non documentés est facile à trouver. Mais plus probable qu'improbable, cela se révélera être un détail d'implémentation et changera dans les futures versions. Je cherche "la bonne façon" pour le faire.

25
Aran-Fey

Il n'y a aucun moyen officiel d'obtenir ces informations. Le module typing est toujours en développement lourd et n'a pas d'API publique à proprement parler. (En fait, il n'en aura probablement jamais.)

Tout ce que nous pouvons faire est d'examiner les éléments internes du module et de trouver le moyen le moins grossier d'obtenir les informations que nous recherchons. Et parce que le module est toujours en cours d'élaboration, ses internes vont changer. Beaucoup.


Dans python 3.5 et 3.6, les génériques avaient un __Origin__ attribut contenant une référence à la classe de base générique d'origine (c'est-à-dire List[int].__Origin__ aurait été List), mais cela a été modifié dans 3.7. Maintenant, le moyen le plus simple de savoir si quelque chose est un générique est probablement de vérifier son __parameters__ et __args__ les attributs.

Voici un ensemble de fonctions qui peuvent être utilisées pour détecter des génériques:

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)

Toutes ces fonctions devraient fonctionner dans toutes les versions python <= 3.7 (y compris tout élément <3.5 qui utilise le backport du module typing)).

13
Aran-Fey

Vous recherchez peut-être __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'>)

Le mieux que j'ai pu trouver pour préconiser l'utilisation de cet attribut non documenté est ce rassurant citation de Guido Van Rossum (il y a 2 ans):

Le mieux que je puisse recommander est d'utiliser __Origin__ - si nous devions changer cet attribut, il devrait encore y avoir un autre moyen d'accéder aux mêmes informations, et il serait facile de récupérer votre code pour les occurrences de __Origin__. (Je serais moins préoccupé par les modifications de __Origin__ Que de __extra__.) Vous pouvez également consulter les fonctions internes _gorg() et _geqv() ( ces noms ne feront évidemment partie d'aucune API publique, mais leur implémentation est très simple et conceptuellement utile).

Cette mise en garde dans la documentation semble indiquer que rien n'est encore gravé dans le marbre:

De nouvelles fonctionnalités peuvent être ajoutées et l'API peut changer même entre les versions mineures si les développeurs principaux le jugent nécessaire.

21
Jacques Gaudin