Comment spécifier que le type de retour d'une méthode est le même que la classe elle-même?

410

J'ai le code suivant en python 3:

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: Position) -> Position:
        return Position(self.x + other.x, self.y + other.y)

Mais mon éditeur (PyCharm) dit que la position de référence ne peut pas être résolue (dans la __add__méthode). Comment dois-je spécifier que je m'attends à ce que le type de retour soit de type Position?

Edit: Je pense que c'est en fait un problème avec PyCharm. Il utilise en fait les informations dans ses avertissements et l'achèvement du code

Mais corrigez-moi si je me trompe et que je dois utiliser une autre syntaxe.

Michael van Gerwen
la source

Réponses:

576

TL; DR : si vous utilisez Python 4.0, cela fonctionne. À compter d'aujourd'hui (2019) dans la version 3.7+, vous devez activer cette fonctionnalité à l'aide d'une future déclaration ( from __future__ import annotations) - pour Python 3.6 ou inférieur, utilisez une chaîne.

Je suppose que vous avez cette exception:

NameError: name 'Position' is not defined

En effet, vous Positiondevez le définir avant de pouvoir l'utiliser dans une annotation, sauf si vous utilisez Python 4.

Python 3.7+: from __future__ import annotations

Python 3.7 introduit PEP 563: évaluation différée des annotations . Un module qui utilise la future instruction from __future__ import annotationsstockera automatiquement les annotations sous forme de chaînes:

from __future__ import annotations

class Position:
    def __add__(self, other: Position) -> Position:
        ...

Il est prévu que cela devienne la valeur par défaut dans Python 4.0. Étant donné que Python est toujours un langage typé dynamiquement, aucune vérification de type n'est donc effectuée au moment de l'exécution, les annotations de frappe ne devraient pas avoir d'impact sur les performances, non? Faux! Avant python 3.7, le module de typage était l' un des modules python les plus lents du cœur, donc si vous import typingconstaterez une augmentation des performances jusqu'à 7 fois lors de la mise à niveau vers la version 3.7.

Python <3.7: utilisez une chaîne

Selon PEP 484 , vous devez utiliser une chaîne au lieu de la classe elle-même:

class Position:
    ...
    def __add__(self, other: 'Position') -> 'Position':
       ...

Si vous utilisez le framework Django, cela peut être familier car les modèles Django utilisent également des chaînes pour les références directes (définitions de clés étrangères où le modèle étranger est selfou n'est pas encore déclaré). Cela devrait fonctionner avec Pycharm et d'autres outils.

Sources

Les pièces pertinentes du PEP 484 et PEP 563 , pour vous épargner le voyage:

Références avancées

Lorsqu'un indice de type contient des noms qui n'ont pas encore été définis, cette définition peut être exprimée sous la forme d'un littéral de chaîne, à résoudre ultérieurement.

Une situation où cela se produit couramment est la définition d'une classe de conteneur, où la classe en cours de définition se produit dans la signature de certaines des méthodes. Par exemple, le code suivant (le début d'une implémentation d'arbre binaire simple) ne fonctionne pas:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

Pour y remédier, nous écrivons:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

Le littéral de chaîne doit contenir une expression Python valide (c'est-à-dire que compiler (allumé, '', 'eval') doit être un objet de code valide) et il doit être évalué sans erreur une fois le module entièrement chargé. Les espaces de noms locaux et globaux dans lesquels il est évalué doivent être les mêmes espaces de noms dans lesquels les arguments par défaut de la même fonction seraient évalués.

et PEP 563:

Dans Python 4.0, les annotations de fonction et de variable ne seront plus évaluées au moment de la définition. Au lieu de cela, une forme de chaîne sera conservée dans le __annotations__dictionnaire respectif . Les vérificateurs de type statique ne verront aucune différence de comportement, tandis que les outils utilisant des annotations lors de l'exécution devront effectuer une évaluation différée.

...

La fonctionnalité décrite ci-dessus peut être activée à partir de Python 3.7 à l'aide de l'importation spéciale suivante:

from __future__ import annotations

Des choses que vous pourriez être tenté de faire à la place

A. Définir un mannequin Position

Avant la définition de classe, placez une définition fictive:

class Position(object):
    pass


class Position(object):
    ...

Cela supprimera le NameErroret peut même sembler OK:

>>> Position.__add__.__annotations__
{'other': __main__.Position, 'return': __main__.Position}

Mais est-ce?

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: False
other is Position: False

B. Monkey-patch pour ajouter les annotations:

Vous voudrez peut-être essayer un peu de magie de méta-programmation Python et écrire un décorateur pour patcher la définition de la classe afin d'ajouter des annotations:

class Position:
    ...
    def __add__(self, other):
        return self.__class__(self.x + other.x, self.y + other.y)

Le décorateur devrait être responsable de l'équivalent de ceci:

Position.__add__.__annotations__['return'] = Position
Position.__add__.__annotations__['other'] = Position

Au moins, cela semble juste:

>>> for k, v in Position.__add__.__annotations__.items():
...     print(k, 'is Position:', v is Position)                                                                                                                                                                                                                  
return is Position: True
other is Position: True

Probablement trop de problèmes.

Conclusion

Si vous utilisez la version 3.6 ou inférieure, utilisez un littéral de chaîne contenant le nom de la classe, utilisez la version 3.7 from __future__ import annotationset cela fonctionnera.

Paulo Scardine
la source
2
À droite, il s'agit moins d'un problème PyCharm que d'un problème Python 3.5 PEP 484. Je soupçonne que vous obtiendriez le même avertissement si vous l'exécutiez via l'outil de type mypy.
Paul Everitt
23
> si vous utilisez Python 4.0 ça marche juste au fait, avez-vous vu Sarah Connor? :)
scrutari
@JoelBerkeley Je viens de le tester et les paramètres de type ont fonctionné pour moi sur 3.6, n'oubliez pas d'importer typingcar tout type que vous utilisez doit être dans la portée lorsque la chaîne est évaluée.
Paulo Scardine
ah, mon erreur, je ne faisais que ''tourner la classe, pas les paramètres de type
joelb
5
Remarque importante pour toute personne utilisant from __future__ import annotations- cela doit être importé avant toutes les autres importations.
Artur
16

Spécifier le type en tant que chaîne est très bien, mais cela me grogne toujours un peu que nous contournions fondamentalement l'analyseur. Il vaut donc mieux ne pas mal orthographier l'une de ces chaînes littérales:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Une légère variation consiste à utiliser une police de caractères liée, au moins alors vous ne devez écrire la chaîne qu'une seule fois lors de la déclaration de la police:

from typing import TypeVar

T = TypeVar('T', bound='Position')

class Position:

    def __init__(self, x: int, y: int):
        self.x = x
        self.y = y

    def __add__(self, other: T) -> T:
        return Position(self.x + other.x, self.y + other.y)
vbraun
la source
8
Je souhaite que Python ait un typing.Selfpour le spécifier explicitement.
Alexander Huszagh
2
Je suis venu ici pour voir si quelque chose comme vous typing.Selfexistait. Le renvoi d'une chaîne codée en dur ne renvoie pas le type correct lors de l'exploitation du polymorphisme. Dans mon cas, je voulais implémenter une méthode de classe de désérialisation . J'ai décidé de retourner un dict (kwargs) et d'appeler some_class(**some_class.deserialize(raw_data)).
Scott P.
Les annotations de type utilisées ici sont appropriées lors de l'implémentation correcte de cette option pour utiliser des sous-classes. Cependant, l'implémentation renvoie Position, et non la classe, donc l'exemple ci-dessus est techniquement incorrect. L'implémentation devrait remplacer Position(par quelque chose comme self.__class__(.
Sam Bull
De plus, les annotations indiquent que le type de retour dépend other, mais il dépend très probablement self. Donc, vous devrez mettre l'annotation selfpour décrire le comportement correct (et peut-être otherdevrait simplement Positionmontrer qu'il n'est pas lié au type de retour). Cela peut également être utilisé pour les cas où vous ne travaillez qu'avec self. par exempledef __aenter__(self: T) -> T:
Sam Bull
15

Le nom «Position» n'est pas disponible au moment où le corps de classe lui-même est analysé. Je ne sais pas comment vous utilisez les déclarations de type, mais le PEP 484 de Python - qui est ce que la plupart des modes devraient utiliser si vous utilisez ces conseils de frappe, vous pouvez simplement mettre le nom sous forme de chaîne à ce stade:

def __add__(self, other: 'Position') -> 'Position':
    return Position(self.x + other.x, self.y + other.y)

Consultez https://www.python.org/dev/peps/pep-0484/#forward-references - des outils conformes à celui-ci sauront en déballer le nom de classe et en faire usage (il est toujours important d'avoir en gardant à l'esprit que le langage Python lui-même ne fait rien de ces annotations - elles sont généralement destinées à l'analyse de code statique, ou on pourrait avoir une bibliothèque / un cadre pour la vérification de type lors de l'exécution - mais vous devez le définir explicitement).

mise à jour En outre, à partir de Python 3.8, vérifiez pep-563 - à partir de Python 3.8, il est possible d'écrire from __future__ import annotationspour différer l'évaluation des annotations - les classes de référencement direct devraient fonctionner directement.

jsbueno
la source
9

Lorsqu'un indice de type chaîne est acceptable, l' __qualname__élément peut également être utilisé. Il contient le nom de la classe et il est disponible dans le corps de la définition de classe.

class MyClass:
    @classmethod
    def make_new(cls) -> __qualname__:
        return cls()

En procédant ainsi, renommer la classe n'implique pas de modifier les indications de type. Mais personnellement, je ne m'attendrais pas à ce que les éditeurs de code intelligents gèrent bien ce formulaire.

Yvon DUTAPIS
la source
1
Ceci est particulièrement utile car il ne code pas en dur le nom de la classe, il continue donc de fonctionner dans les sous-classes.
Florian Brucker
Je ne sais pas si cela fonctionnera avec l'évaluation différée des annotations (PEP 563), j'ai donc posé une question à ce sujet .
Florian Brucker