Comment les fonctions Python gèrent-elles les types de paramètres que vous transmettez?

305

Sauf erreur, la création d'une fonction en Python fonctionne comme ceci:

def my_func(param1, param2):
    # stuff

Cependant, vous ne donnez pas réellement les types de ces paramètres. De plus, si je me souviens bien, Python est un langage fortement typé, en tant que tel, il semble que Python ne devrait pas vous laisser passer un paramètre d'un type différent de celui attendu par le créateur de la fonction. Cependant, comment Python sait-il que l'utilisateur de la fonction transmet les types appropriés? Le programme va-t-il simplement mourir s'il n'est pas du bon type, en supposant que la fonction utilise réellement le paramètre? Devez-vous spécifier le type?

Leif Andersen
la source
15
Je pense que la réponse acceptée dans cette question devrait être mise à jour pour être plus en ligne avec les capacités actuelles qu'offre Python. Je pense que cette réponse fait l'affaire.
code_dredd

Réponses:

173

Python est fortement typé car chaque objet a un type, chaque objet connaît son type, il est impossible d'utiliser accidentellement ou délibérément un objet d'un type "comme si" c'était un objet d'un type différent , et toutes les opérations élémentaires sur l'objet sont déléguée à son type.

Cela n'a rien à voir avec les noms . Un nom en Python n'a pas "de type": si et quand un nom est défini, le nom fait référence à un objet , et l' objet a un type (mais cela ne force pas en fait un type sur le nom : a nom est un nom).

Un nom en Python peut parfaitement faire référence à différents objets à des moments différents (comme dans la plupart des langages de programmation, mais pas tous) - et il n'y a pas de contrainte sur le nom de telle sorte que, s'il a déjà fait référence à un objet de type X, il est alors contraint forevermore de se référer uniquement à d' autres objets de type X. les contraintes sur les noms ne font pas partie du concept de « frappe fort », bien que certains amateurs de statique taper (où les noms ne se pressèrent, et dans une statique, Alias compile- le temps, la mode aussi) abusent du terme de cette façon.

Alex Martelli
la source
71
Il semble donc que le typage fort ne soit pas aussi fort, dans ce cas particulier, il est plus faible que le typage statique. sur cet aspect. Corrigez-moi si j'ai tort, s'il-vous plait.
liang
19
@liang C'est une opinion, donc vous ne pouvez pas avoir tort ou raison. C'est certainement aussi mon avis, et j'ai essayé plusieurs langues. Le fait que je ne puisse pas utiliser mon IDE pour connaître le type (et donc les membres) des paramètres est un inconvénient majeur de python. Si cet inconvénient est plus important que les avantages de la frappe de canard, cela dépend de la personne que vous demandez.
Maarten Bodewes
6
Mais cela ne répond à aucune des questions: "Cependant, comment Python sait-il que l'utilisateur de la fonction transmet les types appropriés? Le programme va-t-il simplement mourir s'il est de type incorrect, en supposant que la fonction utilise réellement le paramètre? Devez-vous spécifier le type? " ou ..
qPCR4vir
4
@ qPCR4vir, tout objet peut être passé en argument. L'erreur (une exception, le programme ne "meurt" pas s'il est codé pour l'attraper, voir try/ except) se produira quand et si une opération est tentée que l'objet ne prend pas en charge. Dans Python 3.5, vous pouvez désormais éventuellement "spécifier les types" d'arguments, mais aucune erreur ne se produit, en soi, si la spécification est violée; la notation de frappe est uniquement destinée à aider à séparer les outils qui effectuent l'analyse, etc., elle ne modifie pas le comportement de Python lui-même.
Alex Martelli
2
@AlexMartelli. Remercier! Pour moi, c'est la bonne réponse: "L'erreur (une exception, le programme ne" mourra "pas s'il est codé pour l'attraper, voir try / except)."
qPCR4vir
753

Les autres réponses ont bien expliqué le typage du canard et la réponse simple par tzot :

Python n'a pas de variables, comme d'autres langages où les variables ont un type et une valeur; il a des noms pointant vers des objets qui connaissent leur type.

Cependant , une chose intéressante a changé depuis 2010 (lorsque la question a été posée pour la première fois), à savoir l'implémentation de PEP 3107 (implémenté en Python 3). Vous pouvez maintenant réellement spécifier le type d'un paramètre et le type du type de retour d'une fonction comme ceci:

def pick(l: list, index: int) -> int:
    return l[index]

Nous pouvons voir ici que cela pickprend 2 paramètres, une liste let un entier index. Il doit également renvoyer un entier.

Il s'agit donc ici d' lune liste d'entiers que nous pouvons voir sans trop d'effort, mais pour des fonctions plus complexes, cela peut être un peu déroutant quant à ce que la liste doit contenir. Nous voulons également que la valeur par défaut indexsoit 0. Pour résoudre ce problème, vous pouvez choisir d'écrire pickcomme ceci à la place:

def pick(l: "list of ints", index: int = 0) -> int:
    return l[index]

Notez que nous mettons maintenant une chaîne comme type de l, ce qui est autorisé syntaxiquement, mais ce n'est pas bon pour l'analyse par programme (que nous reviendrons plus tard).

Il est important de noter que Python n'augmentera pas TypeErrorsi vous passez un flotteur index, la raison en est l'un des principaux points de la philosophie de conception de Python: "Nous sommes tous des adultes consentants ici" , ce qui signifie que vous êtes censé soyez conscient de ce que vous pouvez passer à une fonction et de ce que vous ne pouvez pas. Si vous voulez vraiment écrire du code qui lance TypeErrors, vous pouvez utiliser la isinstancefonction pour vérifier que l'argument passé est du type approprié ou d'une sous-classe de celui-ci comme ceci:

def pick(l: list, index: int = 0) -> int:
    if not isinstance(l, list):
        raise TypeError
    return l[index]

Plus d'informations sur les raisons pour lesquelles vous devriez rarement faire cela et sur ce que vous devriez faire à la place sont discutées dans la section suivante et dans les commentaires.

Le PEP 3107 améliore non seulement la lisibilité du code, mais propose également plusieurs cas d'utilisation appropriés que vous pouvez lire ici .


L'annotation de type a attiré beaucoup plus d'attention dans Python 3.5 avec l'introduction de PEP 484 qui introduit un module standard pour les indications de type.

Ces indices de type provenaient du vérificateur de type mypy ( GitHub ), qui est maintenant conforme au PEP 484 .

Avec le module de frappe est livré avec une collection assez complète d'indices de type, y compris:

  • List, Tuple, Set, Map- pour list, tuple, setet maprespectivement.
  • Iterable - utile pour les générateurs.
  • Any - quand ça pourrait être n'importe quoi.
  • Union- quand il pourrait être n'importe quoi dans un ensemble spécifié de types, par opposition à Any.
  • Optional- quand ce pourrait être Aucun. Sténographie pour Union[T, None].
  • TypeVar - utilisé avec des génériques.
  • Callable - utilisé principalement pour les fonctions, mais pourrait être utilisé pour d'autres callables.

Ce sont les indices de type les plus courants. Une liste complète se trouve dans la documentation du module de saisie .

Voici l'ancien exemple utilisant les méthodes d'annotation introduites dans le module de frappe:

from typing import List

def pick(l: List[int], index: int) -> int:
    return l[index]

Une fonctionnalité puissante est la Callablequi vous permet de taper des méthodes d'annotation qui prennent une fonction en argument. Par exemple:

from typing import Callable, Any, Iterable

def imap(f: Callable[[Any], Any], l: Iterable[Any]) -> List[Any]:
    """An immediate version of map, don't pass it any infinite iterables!"""
    return list(map(f, l))

L'exemple ci-dessus pourrait devenir plus précis avec l'utilisation de TypeVarau lieu de Any, mais cela a été laissé au lecteur car je pense avoir déjà rempli ma réponse avec trop d'informations sur les merveilleuses nouvelles fonctionnalités activées par l'indication de type.


Auparavant, lorsque l'on documentait du code Python avec par exemple Sphinx, certaines des fonctionnalités ci-dessus pouvaient être obtenues en écrivant des docstrings formatés comme ceci:

def pick(l, index):
    """
    :param l: list of integers
    :type l: list
    :param index: index at which to pick an integer from *l*
    :type index: int
    :returns: integer at *index* in *l*
    :rtype: int
    """
    return l[index]

Comme vous pouvez le voir, cela prend un certain nombre de lignes supplémentaires (le nombre exact dépend de la façon dont vous voulez être explicite et de la façon dont vous formatez votre docstring). Mais il devrait maintenant être clair pour vous que le PEP 3107 fournit une alternative qui est à bien des égards (toutes?) Supérieure. Cela est particulièrement vrai en combinaison avec PEP 484 qui, comme nous l'avons vu, fournit un module standard qui définit une syntaxe pour ces types d'indications / annotations qui peut être utilisée de telle sorte qu'elle soit sans ambiguïté et précise mais flexible, ce qui en fait un combinaison puissante.

À mon avis, c'est l'une des meilleures fonctionnalités de Python. J'ai hâte que les gens commencent à en exploiter le pouvoir. Désolé pour la longue réponse, mais c'est ce qui se passe quand je m'excite.


Un exemple de code Python qui utilise fortement l'indication de type peut être trouvé ici .

erb
la source
2
@rickfoosusa: Je soupçonne que vous n'exécutez pas Python 3 dans lequel la fonctionnalité a été ajoutée.
erb
26
Attends une minute! Si la définition du paramètre et du type de retour n'augmente pas a TypeError, quel est l'intérêt d'utiliser pick(l: list, index: int) -> intcomme une définition sur une ligne alors? Ou je me suis trompé, je ne sais pas.
Erdin Eray
24
@Eray Erdin: C'est un malentendu commun et pas du tout une mauvaise question. Il peut être utilisé à des fins de documentation, aide les IDE à effectuer une meilleure autocomplétion et à trouver des erreurs avant l'exécution en utilisant l'analyse statique (tout comme mypy que j'ai mentionné dans la réponse). Il y a des espoirs que le runtime puisse tirer parti des informations et accélérer réellement les programmes mais cela va probablement prendre très longtemps pour être implémenté. Vous pourriez également être en mesure de créer un décorateur qui lance les TypeErrors pour vous (les informations sont stockées dans l' __annotations__attribut de l'objet fonction).
erb
2
@ErdinEray Je dois ajouter que lancer TypeErrors est une mauvaise idée (le débogage n'est jamais amusant, peu importe à quel point les exceptions prévues sont levées). Mais n'ayez crainte, l'avantage des nouvelles fonctionnalités décrites dans ma réponse permet une meilleure solution: ne comptez sur aucune vérification à l'exécution, faites tout avant l'exécution avec mypy ou utilisez un éditeur qui fait l'analyse statique pour vous tel que PyCharm .
erb
2
@Tony: Lorsque vous renvoyez deux ou plusieurs objets, vous retournez en fait un tuple, vous devez donc utiliser l'annotation de type Tuple, c'estdef f(a) -> Tuple[int, int]:
erb
14

Vous ne spécifiez pas de type. La méthode échouera (au moment de l'exécution) uniquement si elle essaie d'accéder à des attributs qui ne sont pas définis sur les paramètres transmis.

Donc, cette fonction simple:

def no_op(param1, param2):
    pass

... n'échouera pas, quels que soient les deux arguments passés.

Cependant, cette fonction:

def call_quack(param1, param2):
    param1.quack()
    param2.quack()

... échouera à l'exécution si param1et param2n'ont pas tous les deux des attributs appelables nommés quack.

TM.
la source
+1: Les attributs et les méthodes ne sont pas déterminés statiquement. Le concept de la façon dont ce "type approprié" ou "type incorrect" est établi selon que le type fonctionne correctement ou non dans la fonction.
S.Lott
11

De nombreuses langues ont des variables, qui sont d'un type spécifique et ont une valeur. Python n'a pas de variables; il a des objets, et vous utilisez des noms pour faire référence à ces objets.

Dans d'autres langues, quand vous dites:

a = 1

puis une variable (généralement entière) change son contenu à la valeur 1.

En Python,

a = 1

signifie "utiliser le nom a pour faire référence à l'objet 1 ". Vous pouvez effectuer les opérations suivantes dans une session Python interactive:

>>> type(1)
<type 'int'>

La fonction typeest appelée avec l'objet 1; puisque chaque objet connaît son type, il est facile typede trouver ce type et de le renvoyer.

De même, chaque fois que vous définissez une fonction

def funcname(param1, param2):

la fonction reçoit deux objets, les nomme param1et param2, quel que soit leur type. Si vous voulez vous assurer que les objets reçus sont d'un type spécifique, codez votre fonction comme s'ils appartenaient au (x) type (s) requis et interceptez les exceptions levées si elles ne le sont pas. Les exceptions levées sont généralement TypeError(vous avez utilisé une opération non valide) et AttributeError(vous avez essayé d'accéder à un membre inexistant (les méthodes sont également des membres)).

tzot
la source
8

Python n'est pas fortement typé dans le sens d'une vérification de type statique ou à la compilation.

La plupart du code Python relève de ce qu'on appelle le "Duck Typing" - par exemple, vous recherchez une méthode readsur un objet - vous ne vous souciez pas si l'objet est un fichier sur disque ou une socket, vous voulez juste lire N octets de celui-ci.

Mark Rushakoff
la source
21
Python est fortement typé. Il est également typé dynamiquement.
Daniel Newby
1
Mais cela ne répond à aucune des questions: "Cependant, comment Python sait-il que l'utilisateur de la fonction transmet les types appropriés? Le programme mourra-t-il simplement si ce n'est pas le bon type, en supposant que la fonction utilise réellement le paramètre? Devez-vous spécifier le type? " ou ..
qPCR4vir
6

Comme l' explique Alex Martelli ,

La solution normale, Pythonique, préférée est presque invariablement le "typage du canard": essayez d'utiliser l'argument comme s'il était d'un certain type souhaité, faites-le dans une instruction try / except en capturant toutes les exceptions qui pourraient survenir si l'argument n'était pas en fait de ce type (ou tout autre type imitant bien le canard ;-), et dans la clause except, essayez autre chose (en utilisant l'argument "comme si" c'était d'un autre type).

Lisez le reste de son article pour des informations utiles.

Nick Presta
la source
5

Python ne se soucie pas de ce que vous transmettez à ses fonctions. Lorsque vous appelez my_func(a,b), les variables param1 et param2 contiendront alors les valeurs de a et b. Python ne sait pas que vous appelez la fonction avec les types appropriés et attend du programmeur qu'il s'en occupe. Si votre fonction sera appelée avec différents types de paramètres, vous pouvez encapsuler le code y accédant avec des blocs try / except et évaluer les paramètres comme vous le souhaitez.

Kyle
la source
11
Python n'a pas de variables, comme d'autres langages où les variables ont un type et une valeur; il a des noms pointant vers des objets qui connaissent leur type.
tzot
2

Vous ne spécifiez jamais le type; Python a le concept de typage canard ; Fondamentalement, le code qui traite les paramètres fera certaines hypothèses à leur sujet - peut-être en appelant certaines méthodes qu'un paramètre devrait implémenter. Si le paramètre est du mauvais type, une exception sera levée.

En général, c'est à votre code de s'assurer que vous passez des objets du type approprié - il n'y a pas de compilateur pour appliquer cela à l'avance.

Justin Ethier
la source
2

Il y a une exception notoire à la frappe de canard qui mérite d'être mentionnée sur cette page.

Lorsque la strfonction appelle __str__la méthode de classe, elle vérifie subtilement son type:

>>> class A(object):
...     def __str__(self):
...         return 'a','b'
...
>>> a = A()
>>> print a.__str__()
('a', 'b')
>>> print str(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __str__ returned non-string (type tuple)

Comme si Guido nous indiquait quelle exception un programme devrait lever s'il rencontre un type inattendu.

Antony Hatchkins
la source
1

En Python, tout a un type. Une fonction Python fera tout ce qui lui est demandé si le type d'arguments la prend en charge.

Exemple: fooajoutera tout ce qui peut être __add__édité;) sans trop se soucier de son type. Cela signifie donc que pour éviter l'échec, vous ne devez fournir que les éléments qui prennent en charge l'ajout.

def foo(a,b):
    return a + b

class Bar(object):
    pass

class Zoo(object):
    def __add__(self, other):
        return 'zoom'

if __name__=='__main__':
    print foo(1, 2)
    print foo('james', 'bond')
    print foo(Zoo(), Zoo())
    print foo(Bar(), Bar()) # Should fail
Pratik Deoghare
la source
1

Je n'ai pas vu cela mentionné dans d'autres réponses, alors je vais l'ajouter au pot.

Comme d'autres l'ont dit, Python n'applique pas le type sur les paramètres de fonction ou de méthode. On suppose que vous savez ce que vous faites et que si vous avez vraiment besoin de savoir le type de chose qui a été transmise, vous le vérifierez et déciderez quoi faire par vous-même.

L'un des principaux outils pour ce faire est la fonction isinstance ().

Par exemple, si j'écris une méthode qui s'attend à obtenir des données de texte binaires brutes, plutôt que les chaînes encodées en utf-8 normales, je pourrais vérifier le type des paramètres en chemin et soit m'adapter à ce que je trouve, soit élever un exception à refuser.

def process(data):
    if not isinstance(data, bytes) and not isinstance(data, bytearray):
        raise TypeError('Invalid type: data must be a byte string or bytearray, not %r' % type(data))
    # Do more stuff

Python fournit également toutes sortes d'outils pour creuser dans des objets. Si vous êtes courageux, vous pouvez même utiliser importlib pour créer vos propres objets de classes arbitraires, à la volée. J'ai fait cela pour recréer des objets à partir de données JSON. Une telle chose serait un cauchemar dans un langage statique comme C ++.

Dread Quixadhal
la source
1

Pour utiliser efficacement le module de frappe (nouveau dans Python 3.5), incluez all ( *).

from typing import *

Et vous serez prêt à utiliser:

List, Tuple, Set, Map - for list, tuple, set and map respectively.
Iterable - useful for generators.
Any - when it could be anything.
Union - when it could be anything within a specified set of types, as opposed to Any.
Optional - when it might be None. Shorthand for Union[T, None].
TypeVar - used with generics.
Callable - used primarily for functions, but could be used for other callables.

Cependant, vous pouvez toujours utiliser les noms de type comme int, list, dict, ...

prosti
la source
1

J'ai implémenté un wrapper si quelqu'un souhaite spécifier des types de variables.

import functools
    
def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for i in range(len(args)):
            v = args[i]
            v_name = list(func.__annotations__.keys())[i]
            v_type = list(func.__annotations__.values())[i]
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        v = result
        v_name = 'return'
        v_type = func.__annotations__['return']
        error_msg = 'Variable `' + str(v_name) + '` should be type ('
        error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
        if not isinstance(v, v_type):
                raise TypeError(error_msg)
        return result

    return check

Utilisez-le comme:

@type_check
def test(name : str) -> float:
    return 3.0

@type_check
def test2(name : str) -> str:
    return 3.0

>> test('asd')
>> 3.0

>> test(42)
>> TypeError: Variable `name` should be type (<class 'str'>) but instead is type (<class 'int'>)

>> test2('asd')
>> TypeError: Variable `return` should be type (<class 'str'>) but instead is type (<class 'float'>)

ÉDITER

Le code ci-dessus ne fonctionne pas si aucun type d'argument (ou de retour) n'est déclaré. La modification suivante peut aider, d'autre part, elle ne fonctionne que pour les kwargs et ne vérifie pas les arguments.

def type_check(func):

    @functools.wraps(func)
    def check(*args, **kwargs):
        for name, value in kwargs.items():
            v = value
            v_name = name
            if name not in func.__annotations__:
                continue
                
            v_type = func.__annotations__[name]

            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ') '
            if not isinstance(v, v_type):
                raise TypeError(error_msg)

        result = func(*args, **kwargs)
        if 'return' in func.__annotations__:
            v = result
            v_name = 'return'
            v_type = func.__annotations__['return']
            error_msg = 'Variable `' + str(v_name) + '` should be type ('
            error_msg += str(v_type) + ') but instead is type (' + str(type(v)) + ')'
            if not isinstance(v, v_type):
                    raise TypeError(error_msg)
        return result

    return check
Gergely Papp
la source