9
votes

mypy: argument de méthode incompatible avec supertype

Regardez l'exemple de code (mypy_test.py):

import typing

class Base:
   def fun(self, a: typing.Any):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
   def fun(self, a: SomeType):
       pass

maintenant mypy se plaint:

import typing

class Base:
   def fun(self, a: typing.Type[str]):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
    def fun(self, a: SomeType):
        pass

Dans ce cas, comment Je travaille avec une hiérarchie de classes et je suis sûr de type?

Versions du logiciel:

mypy 0.650 Python 3.7.1

Ce que j'ai essayé:

mypy mypy_test.py  
mypy_test.py:10: error: Argument 1 of "fun" incompatible with supertype "Base"

Mais cela n'a pas aidé.

Un utilisateur a commenté: "Regarde comme vous ne pouvez pas restreindre les types acceptés dans la méthode remplacée? "

Mais dans ce cas, si j'utilise le type le plus large possible ( typing.Any ) dans une signature de classe de base, il devrait ça marche pas non plus. Mais c'est le cas:

import typing

class Base:
   def fun(self, a: str):
       pass

SomeType = typing.NewType('SomeType', str)

class Derived(Base):
    def fun(self, a: SomeType):
        pass

Aucune plainte de mypy avec le code directement ci-dessus.


1 commentaires

On dirait que vous ne pouvez pas restreindre les types acceptés dans la méthode remplacée?


3 Réponses :


1
votes

Utilisez une classe générique comme suit:

from typing import Generic
from typing import NewType
from typing import TypeVar


BoundedStr = TypeVar('BoundedStr', bound=str)


class Base(Generic[BoundedStr]):
    def fun(self, a: BoundedStr) -> None:
        pass


SomeType = NewType('SomeType', str)


class Derived(Base[SomeType]):
    def fun(self, a: SomeType) -> None:
        pass

L'idée est de définir une classe de base, avec un type générique. Maintenant vous voulez que ce type générique soit un sous-type de str , d'où la directive bound = str .

Ensuite, vous définissez votre type SomeType , et lorsque vous sous-classez Base , vous spécifiez quelle est la variable de type générique: dans ce cas, c'est SomeType . Ensuite, mypy vérifie que SomeType est un sous-type de str (puisque nous avons déclaré que BoundedStr doit être un délimité par str ), et dans ce cas, mypy est content.

Bien sûr, mypy se plaindra si vous avez défini SomeType = NewType ('SomeType', int) et l'avez utilisé comme variable de type pour Base ou, plus généralement, si vous sous-classez Base [SomeTypeVariable] si SomeTypeVariable n'est pas un sous-type de str .

J'ai lu dans un commentaire que vous vouliez abandonner mypy. Pas! Apprenez plutôt comment fonctionnent les types; Lorsque vous êtes dans des situations où vous sentez que mon cœur est contre vous, il est très probable qu'il y ait quelque chose que vous n'avez pas bien compris. Dans ce cas, demandez l'aide d'autres personnes au lieu d'abandonner!


0 commentaires

0
votes

Les deux fonctions ont le même nom, alors renommez simplement 1 des fonctions. mypy vous donnera la même erreur si vous faites ceci:

class Base:
   def fun1(self, a: str):
       pass

Renommer fun en fun1 résout le problème avec mypy, bien que ce ne soit qu'une solution de contournement pour mypy.

class Derived(Base):
        def fun(self, a: int):


0 commentaires

13
votes

Votre premier exemple est malheureusement légitimement dangereux - il enfreint quelque chose connu sous le nom de "principe de substitution de Liskov".

Pour démontrer pourquoi c'est le cas, permettez-moi de simplifier un peu votre exemple : Je vais demander à la classe de base d'accepter n'importe quel type d ' objet et demander à la classe dérivée enfant d'accepter un int . J'ai également ajouté un peu de logique d'exécution: la classe Base imprime simplement l'argument; la classe Derived ajoute l'argument contre certains int arbitraires.

def run_query(executor: BaseSQLExecutor, query: str) -> None:
    executor.execute(query)

run_query(PostgresSQLExecutor, "my nasty unescaped and dangerous string")

En surface, cela semble parfaitement bien. Qu'est-ce qui pourrait mal tourner?

Eh bien, supposons que nous écrivions l'extrait de code suivant. Cet extrait de code de type vérifie parfaitement bien: Derived est une sous-classe de Base, nous pouvons donc passer une instance de Derived dans n'importe quel programme qui accepte une instance de Base. Et de même, Base.fun peut accepter n'importe quel objet, alors il devrait être sûr de passer une chaîne?

from typing import NewType

class BaseSQLExecutor:
    def execute(self, query: str) -> None: ...

SanitizedSQLQuery = NewType('SanitizedSQLQuery', str)

class PostgresSQLExecutor:
    def execute(self, query: SanitizedSQLQuery) -> None: ...

Vous pourrez peut-être voir où cela va - ce programme est en fait dangereux et plantera lors de l'exécution! Plus précisément, la toute dernière ligne est interrompue: nous passons dans une instance de Derived, et la méthode fun de Derived n'accepte que les ints. Il essaiera ensuite d'ajouter la chaîne qu'il reçoit avec 10, et plantera rapidement avec un TypeError.

C'est pourquoi mypy vous interdit de restreindre les types d'arguments dans une méthode que vous écrasez. Si Derived est une sous-classe de Base, cela signifie que nous devrions pouvoir remplacer une instance de Derived partout où nous utilisons Base sans rien casser. Cette règle est spécifiquement connue sous le nom de principe de substitution de Liskov.

La restriction des types d'arguments empêche que cela se produise.

(En remarque, le fait que mypy vous oblige à respecter Liskov assez standard. Presque tous les langages à typage statique avec sous-typage font la même chose - Java, C #, C ++ ... Le seul contre-exemple que je connaisse est Eiffel.)


Nous pouvons potentiellement rencontrer des problèmes similaires avec votre exemple d'origine. Pour rendre cela un peu plus évident, permettez-moi de renommer certaines de vos classes pour qu'elles soient un peu plus réalistes. Supposons que nous essayions d'écrire une sorte de moteur d'exécution SQL, et d'écrire quelque chose qui ressemble à ceci:

def accepts_base(b: Base) -> None:
    b.fun("hello!")

accepts_base(Base())
accepts_base(Derived())

Notez que ce code est identique à votre exemple d'origine! La seule chose qui diffère, ce sont les noms.

Nous pouvons à nouveau rencontrer des problèmes d'exécution similaires - supposons que nous ayons utilisé les classes ci-dessus comme suit:

class Base:
    def fun(self, a: object) -> None:
        print("Inside Base", a)

class Derived(Base):
    def fun(self, a: int) -> None:
        print("Inside Derived", a + 10)

Si cette vérification était autorisée, nous avons introduit une vulnérabilité de sécurité potentielle dans notre code! L'invariant que PostgresSQLExecutor ne peut accepter que les chaînes que nous avons explicitement décidé de marquer comme un type "SanitizedSQLQuery" est cassé.


Maintenant, pour répondre à votre autre question: pourquoi mypy cesse de se plaindre si nous faisons accepter à la place un argument de type Any?

Eh bien, c'est parce que le type Any a une signification très spéciale: il représente un type 100% entièrement dynamique. Quand vous dites "la variable X est de type Any", vous dites en fait "Je ne veux pas que vous supposiez quoi que ce soit à propos de cette variable - et je veux pouvoir utiliser ce type cependant Je veux sans que vous vous plaigniez! "

Il est en fait inexact d'appeler Any le" type le plus large possible ". En réalité, c'est à la fois le type le plus large ET le type le plus étroit possible. Chaque type unique est un sous-type de Any AND Any est un sous-type de tous les autres types. Mypy choisira toujours la position qui n'entraîne aucune erreur de vérification de type.

Essentiellement, c'est une trappe d'échappement, une façon de dire au vérificateur de type "Je sais mieux". Chaque fois que vous donnez un type de variable Any, vous désactivez en fait complètement toute vérification de type sur cette variable, pour le meilleur ou pour le pire.

Pour plus d'informations, consultez typing.Any vs object? .


Enfin, que pouvez-vous faire à propos de tout cela?

Eh bien, malheureusement, je ne suis pas forcément sûr d'un moyen facile de contourner cela: vous allez devoir repenser votre code. C'est fondamentalement malsain, et il n'y a pas vraiment d'astuces qui vous permettront de vous en sortir.

La manière exacte dont vous vous y prenez dépend de ce que vous essayez de faire exactement. Vous pourriez peut-être faire quelque chose avec des génériques, comme l'a suggéré un utilisateur. Ou peut-être que vous pourriez simplement renommer l'une des méthodes comme une autre suggérée. Ou bien, vous pouvez modifier Base.fun pour qu'il utilise le même type que Derived.fun ou vice-versa; vous pourriez faire en sorte que Derived n'hérite plus de Base. Tout dépend vraiment des détails de votre situation exacte.

Et bien sûr, si la situation est vraiment insoluble, vous pouvez renoncer à la vérification de type dans ce coin. codebase entièrement et faites en sorte que Base.fun (...) accepte Any (et acceptez que vous puissiez commencer à rencontrer des erreurs d'exécution).

Devoir prendre en compte ces questions et reconcevoir votre code peut sembler un problème incommode - - cependant, je pense personnellement que c'est quelque chose à célébrer! Mypy a réussi à vous empêcher d'introduire accidentellement un bogue dans votre code et vous pousse à écrire un code plus robuste.


3 commentaires

Mec, quelle réponse solide!


Mais qu'en est-il des types de sortie dérivés? Pourquoi la réduction du type de sortie pose-t-elle un problème ou est-elle dangereuse?


@dba - réduire le type de sortie dérivé est en fait sûr! La règle complète est qu'il est dangereux de restreindre les types d'entrée dérivés et non sûr de élargir les types de sortie dérivés. (Ou pour utiliser un langage plus formel, les fonctions sont contravariantes dans leur type d'entrée et covariantes dans leur type de sortie.) Ainsi, par exemple, si le type de sortie parent est int , il serait dangereux que le type de sortie dérivé soit élargi à objet . Ma réponse ne portait que sur la règle des types d'entrées, car c'était ce que OP demandait plus ou moins dans leur question.