web-dev-qa-db-fra.com

Forcer / assurer python doivent être de type spécifique

Comment restreindre une variable membre de classe pour être un type spécifique en Python?


Version plus longue:

J'ai une classe qui a plusieurs variables membres qui sont définies à l'extérieur de la classe. En raison de la façon dont ils sont utilisés, ils doivent être de types spécifiques, int ou list. S'il s'agissait de C++, je les rendrais simplement privés et ferais une vérification de type dans la fonction 'set'. Étant donné que cela n'est pas possible, existe-t-il un moyen de restreindre le type des variables afin qu'une erreur/exception se produise lors de l'exécution si une valeur de type incorrect leur est affectée? Ou dois-je vérifier leur type dans chaque fonction qui les utilise?

Merci.

36
thornate

Vous pouvez utiliser une propriété comme les autres réponses le disent - donc, si vous voulez contraindre un seul attribut, dites "bar" et le contraindre à un entier, vous pouvez écrire du code comme ceci:

class Foo(object):
    def _get_bar(self):
        return self.__bar
    def _set_bar(self, value):
        if not isinstance(value, int):
            raise TypeError("bar must be set to an integer")
        self.__bar = value
    bar = property(_get_bar, _set_bar)

Et cela fonctionne:

>>> f = Foo()
>>> f.bar = 3
>>> f.bar
3
>>> f.bar = "three"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in _set_bar
TypeError: bar must be set to an integer
>>> 

(Il existe également une nouvelle façon d'écrire des propriétés, en utilisant la "propriété" intégrée comme décorateur à la méthode getter - mais je préfère l'ancienne, comme je l'ai mise ci-dessus).

Bien sûr, si vous avez beaucoup d'attributs dans vos classes et que vous souhaitez tous les protéger de cette manière, cela commence à devenir verbeux. Rien à craindre - les capacités d'introspection de Python permettent de créer un décorateur de classe qui pourrait automatiser cela avec un minimum de lignes.

def getter_setter_gen(name, type_):
    def getter(self):
        return getattr(self, "__" + name)
    def setter(self, value):
        if not isinstance(value, type_):
            raise TypeError("%s attribute must be set to an instance of %s" % (name, type_))
        setattr(self, "__" + name, value)
    return property(getter, setter)

def auto_attr_check(cls):
    new_dct = {}
    for key, value in cls.__dict__.items():
        if isinstance(value, type):
            value = getter_setter_gen(key, value)
        new_dct[key] = value
    # Creates a new class, using the modified dictionary as the class dict:
    return type(cls)(cls.__name__, cls.__bases__, new_dct)

Et vous utilisez simplement auto_attr_checken tant que décorateur de classe, et déclarez que les attributs que vous souhaitez dans le corps de classe sont égaux aux types que les attributs doivent également contraindre:

...     
... @auto_attr_check
... class Foo(object):
...     bar = int
...     baz = str
...     bam = float
... 
>>> f = Foo()
>>> f.bar = 5; f.baz = "hello"; f.bam = 5.0
>>> f.bar = "hello"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bar attribute must be set to an instance of <type 'int'>
>>> f.baz = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: baz attribute must be set to an instance of <type 'str'>
>>> f.bam = 3 + 2j
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in setter
TypeError: bam attribute must be set to an instance of <type 'float'>
>>> 
40
jsbueno

En général, ce n'est pas une bonne idée pour les raisons mentionnées par @yak dans son commentaire. Vous empêchez essentiellement l'utilisateur de fournir des arguments valides qui ont les attributs/comportements corrects mais qui ne se trouvent pas dans l'arbre d'héritage dans lequel vous avez codé en dur.

Exclusion de responsabilité, il existe quelques options disponibles pour ce que vous essayez de faire. Le principal problème est qu'il n'y a pas d'attributs privés en Python. Donc, si vous avez juste une ancienne référence d'objet, disons self._a, Vous ne pouvez pas garantir que l'utilisateur ne la définira pas directement même si vous avez fourni un setter qui vérifie le type. Les options ci-dessous montrent comment réellement appliquer la vérification de type.

Remplacer __setattr__

Cette méthode ne sera pratique que pour un (très) petit nombre d'attributs auxquels vous effectuez cette opération. La méthode __setattr__ Est ce qui est appelé lorsque vous utilisez la notation par points pour attribuer un attribut régulier. Par exemple,

class A:
    def __init__(self, a0):
        self.a = a0

Si nous faisons maintenant A().a = 32, cela appellerait A().__setattr__('a', 32)sous le capot . En fait, self.a = a0 Dans __init__ Utilise également self.__setattr__. Vous pouvez l'utiliser pour appliquer la vérification de type:

 class A:
    def __init__(self, a0):
        self.a = a0
    def __setattr__(self, name, value):
        if name == 'a' and not isinstance(value, int):
            raise TypeError('A.a must be an int')
        super().__setattr__(name, value)

L'inconvénient de cette méthode est que vous devez avoir un if name == ... Distinct pour chaque type que vous souhaitez vérifier (ou if name in ... Pour vérifier plusieurs noms pour un type donné). L'avantage est que c'est le moyen le plus simple de rendre presque impossible pour l'utilisateur de contourner la vérification de type.

Faire une propriété

Les propriétés sont des objets qui remplacent votre attribut normal par un objet descripteur (généralement en utilisant un décorateur). Les descripteurs peuvent avoir des méthodes __get__ Et __set__ Qui personnalisent l'accès à l'attribut sous-jacent. C'est un peu comme prendre la branche if correspondante dans __setattr__ Et la placer dans une méthode qui s'exécutera juste pour cet attribut. Voici un exemple:

class A:
    def __init__(self, a0):
        self.a = a0
    @property
    def a(self):
        return self._a
    @a.setter
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self._a = value

Une manière légèrement différente de faire la même chose peut être trouvée dans la réponse de @ jsbueno.

Bien que l'utilisation d'une propriété de cette façon soit astucieuse et résout principalement le problème, elle présente quelques problèmes. La première est que vous avez un attribut "privé" _a Que l'utilisateur peut modifier directement, en contournant votre vérification de type. C'est presque le même problème que d'utiliser un getter et un setter simples, sauf que maintenant a est accessible en tant qu'attribut "correct" qui redirige vers le setter en arrière-plan, ce qui rend moins probable que l'utilisateur gâche avec _a. Le deuxième problème est que vous avez un getter superflu pour faire fonctionner la propriété en lecture-écriture. Ces problèmes font l'objet de this question.

Créer un descripteur True Setter uniquement

Cette solution est probablement la plus robuste dans l'ensemble. Il est suggéré dans le réponse acceptée à la question mentionnée ci-dessus. Fondamentalement, au lieu d'utiliser une propriété, qui a un tas de fioritures et de commodités dont vous ne pouvez pas vous débarrasser, créez votre propre descripteur (et décorateur) et utilisez-le pour tous les attributs qui nécessitent une vérification de type:

class SetterProperty:
    def __init__(self, func, doc=None):
        self.func = func
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        return self.func(obj, value)

class A:
    def __init__(self, a0):
        self.a = a0
    @SetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')
        self.__dict__['a'] = value

Le setter cache la valeur réelle directement dans le __dict__ De l'instance pour éviter de se reproduire indéfiniment. Cela permet d'obtenir la valeur de l'attribut sans fournir un getter explicite. Étant donné que le descripteur a n'a pas la méthode __get__, La recherche se poursuivra jusqu'à ce qu'il trouve l'attribut dans __dict__. Cela garantit que tous les ensembles passent par le descripteur/setter tandis que permet un accès direct à la valeur d'attribut.

Si vous avez un grand nombre d'attributs qui nécessitent une vérification comme celle-ci, vous pouvez déplacer la ligne self.__dict__['a'] = value Dans la méthode __set__ Du descripteur:

class ValidatedSetterProperty:
    def __init__(self, func, name=None, doc=None):
        self.func = func
        self.__= name if name is not None else func.__name__
        self.__doc__ = doc if doc is not None else func.__doc__
    def __set__(self, obj, value):
        ret = self.func(obj, value)
        obj.__dict__[self.__name__] = value

class A:
    def __init__(self, a0):
        self.a = a0
    @ValidatedSetterProperty
    def a(self, value):
        if not isinstance(value, int):
            raise TypeError('A.a must be an int')

Mise à jour

Python3.6 fait cela pour vous presque hors de la boîte: https://docs.python.org/3.6/whatsnew/3.6.html#pep-487-descriptor-protocol-enhancements

TL; DR

Pour un très petit nombre d'attributs nécessitant une vérification de type, remplacez directement __setattr__. Pour un plus grand nombre d'attributs, utilisez le descripteur de définition uniquement comme indiqué ci-dessus. L'utilisation directe des propriétés pour ce type d'application présente plus de problèmes qu'elle n'en résout.

13
Mad Physicist

Depuis Python 3.5, vous pouvez utiliser type-hints pour indiquer qu'un attribut de classe doit être d'un type particulier. Ensuite, vous pouvez inclure quelque chose comme MyPy dans le cadre de votre processus d'intégration continue pour vérifier que tous les types de contrats sont respectés.

Par exemple, pour le script Python suivant:

class Foo:
    x: int
    y: int

foo = Foo()
foo.x = "hello"

MyPy donnerait l'erreur suivante:

6: error: Incompatible types in assignment (expression has type "str", variable has type "int")

Si vous souhaitez que les types soient appliqués lors de l'exécution, vous pouvez utiliser le package enforce . Du README:

>>> import enforce
>>>
>>> @enforce.runtime_validation
... def foo(text: str) -> None:
...     print(text)
>>>
>>> foo('Hello World')
Hello World
>>>
>>> foo(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/william/.local/lib/python3.5/site-packages/enforce/decorators.py", line 106, in universal
    _args, _kwargs = enforcer.validate_inputs(parameters)
  File "/home/william/.local/lib/python3.5/site-packages/enforce/enforcers.py", line 69, in validate_inputs
    raise RuntimeTypeError(exception_text)
enforce.exceptions.RuntimeTypeError: 
  The following runtime type errors were encountered:
       Argument 'text' was not of type <class 'str'>. Actual type was <class 'int'>.
4
ostrokach

Remarque 1: @Blckknght merci pour votre commentaire honnête. J'ai raté le problème de récursivité dans ma suite de tests beaucoup trop simple.

Note 2: J'ai écrit cette réponse quand j'étais au tout début de l'apprentissage de Python. Pour l'instant, je préfère utiliser les descripteurs de Python, voir par exemple link1 , link2 .

Grâce aux articles précédents et à certaines réflexions, je pense avoir trouvé un moyen beaucoup plus convivial de restreindre un attribut de classe à un type spécifique.

Tout d'abord, nous créons une fonction, qui teste universellement le type:

def ensure_type(value, types):
    if isinstance(value, types):
        return value
    else:
        raise TypeError('Value {value} is {value_type}, but should be {types}!'.format(
            value=value, value_type=type(value), types=types))

Ensuite, nous l'utilisons et l'appliquons simplement dans nos classes via setter. Je pense que c'est relativement simple et suivez DRY, surtout une fois que vous l'exportez vers un module séparé pour alimenter l'ensemble de votre projet. Voir l'exemple ci-dessous:

class Product:
    def __init__(self, name, quantity):
        self.name = name
        self.quantity = quantity

    @property
    def name(self):
        return self.__dict__['name']

    @name.setter
    def name(self, value):
        self.__dict__['name'] = ensure_type(value, str)

    @property
    def quantity(self):
        return self.quantity

    @quantity.setter
    def quantity(self, value):
        self.__dict__['quantity'] = ensure_type(value, int)

Les tests produisent des résultats raisonnables. Voir d'abord les tests:

if __== '__main__':
    from traceback import format_exc

    try:
        p1 = Product(667, 5)
    except TypeError as err:
        print(format_exc(1))

    try:
        p2 = Product('Knight who say...', '5')
    except TypeError as err:
        print(format_exc(1))

    p1 = Product('SPAM', 2)
    p2 = Product('...and Love', 7)
    print('Objects p1 and p2 created successfully!')

    try:
        p1.name = -191581
    except TypeError as err:
        print(format_exc(1))

    try:
        p2.quantity = 'EGGS'
    except TypeError as err:
        print(format_exc(1))

Et les résultats des tests:

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 35, in <module>
    p1 = Product(667, 5)
TypeError: Value 667 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 40, in <module>
    p2 = Product('Knights who say...', '5')
TypeError: Value 5 is <class 'str'>, but should be <class 'int'>!

Objects p1 and p2 created successfully!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 49, in <module>
    p1.name = -191581
TypeError: Value -191581 is <class 'int'>, but should be <class 'str'>!

Traceback (most recent call last):
  File "/Users/BadPhoenix/Desktop/Coding/Coders-Lab/Week-2/WAR_PYT_S_05_OOP/2_Praca_domowa/day-1/stackoverflow.py", line 54, in <module>
    p2.quantity = 'EGGS'
TypeError: Value EGGS is <class 'str'>, but should be <class 'int'>!
2
CapedHero

Vous pouvez le faire exactement comme vous dites que vous avez dit que vous le feriez en C++; faites leur assigner passer par une méthode setter, et demandez à la méthode setter de vérifier le type. Les concepts d '"état privé" et d' "interfaces publiques" dans Python se font avec la documentation et les conventions, et il est pratiquement impossible de forcer quiconque d'utiliser votre setter plutôt que d'affecter directement la variable. Mais si vous donnez les noms d'attributs commençant par un trait de soulignement et documentez les setters comme moyen d'utiliser votre classe, cela devrait le faire (n'utilisez pas __names avec deux traits de soulignement; c'est presque toujours plus de problèmes que cela ne vaut à moins que vous ne soyez en fait dans la situation pour laquelle ils sont conçus, ce qui se heurte aux noms d'attributs dans une hiérarchie d'héritage). Seuls les développeurs particulièrement obtus éviteront la manière simple d'utiliser la classe de la manière dont elle est documentée pour travailler en faveur de déterminer quels sont les noms internes et de les utiliser directement; o développeurs frustrés par le comportement inhabituel de votre classe (pour Python) et ne leur permettant pas d'utiliser une classe de type liste personnalisée à la place d'une liste.

Vous pouvez utiliser des propriétés, comme d'autres réponses l'ont décrit, pour ce faire tout en donnant l'impression que vous attribuez directement des attributs.


Personnellement, je trouve que les tentatives d'application de la sécurité des types dans Python sont assez inutiles. Pas parce que je pense que la vérification de type statique est toujours inférieure, mais parce que même si vous pouviez ajouter des exigences de type sur votre Python qui ont fonctionné à 100% du temps, elles ne seront tout simplement pas efficaces pour maintenir l'assurance que votre programme est exempt d'erreurs de type parce que elles ne lèveront des exceptions qu'au moment de l'exécution .

Penses-y; lorsque votre programme compilé statiquement réussit sans erreur, vous savez qu'il est complètement exempt de tous les bogues que le compilateur peut détecter (dans le cas de langages comme Haskell ou Mercury, c'est une assez bonne garantie, mais toujours pas complète; dans le cas de langages comme C++ ou Java ... meh).

Mais en Python, l'erreur de type ne sera remarquée que si elle est exécutée. Cela signifie que, même si vous pouvez obtenir une application de type statique complète partout dans votre programme, vous devez exécuter régulièrement des suites de tests avec une couverture de code à 100% pour réellement sachez que votre programme est exempt d'erreurs de type. Mais si vous aviez régulièrement exécuté des tests avec une couverture complète, vous auriez savoir si vous aviez des erreurs de type, même sans essayer d'appliquer les types! Donc, l'avantage ne me semble vraiment pas en valoir la peine. Vous jetez la force (flexibilité) de Python sans gagner plus qu'une bagatelle dans l'une de ses faiblesses (détection d'erreur statique).

1
Ben

Je sais que cette discussion a été réglée, mais une solution beaucoup plus simple consiste à utiliser le module de structure Python ci-dessous. Cela vous obligerait à créer un conteneur pour vos données avant de lui attribuer une valeur , mais il est très efficace pour maintenir le type de données statique. https://pypi.python.org/pypi/structures

1
Jon

Vous pouvez utiliser le même type de property que vous mentionnez en C++. Vous obtiendrez de l'aide pour la propriété de http://adam.gomaa.us/blog/2008/aug/11/the-python-property-builtin/ .

1
Nilesh