«Le moindre étonnement» et l'argument par défaut de Mutable

2597

Quiconque bricole avec Python depuis assez longtemps a été mordu (ou déchiré en morceaux) par le problème suivant:

def foo(a=[]):
    a.append(5)
    return a

Novices Python s'attendent cette fonction pour revenir toujours une liste avec un seul élément: [5]. Le résultat est au contraire très différent, et très étonnant (pour un novice):

>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()

Un de mes directeurs a eu sa première rencontre avec cette fonctionnalité et l'a qualifiée de "faille de conception dramatique" du langage. J'ai répondu que le comportement avait une explication sous-jacente, et c'est en effet très déroutant et inattendu si vous ne comprenez pas les internes. Cependant, je n'ai pas pu répondre (à moi-même) à la question suivante: quelle est la raison de la liaison de l'argument par défaut à la définition de la fonction, et non à l'exécution de la fonction? Je doute que le comportement expérimenté ait une utilité pratique (qui a vraiment utilisé des variables statiques en C, sans multiplier les bugs?)

Modifier :

Baczek en a fait un exemple intéressant. Avec la plupart de vos commentaires et Utaal en particulier, j'ai développé davantage:

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Pour moi, il semble que la décision de conception était relative à l'endroit où mettre la portée des paramètres: à l'intérieur de la fonction ou "ensemble" avec elle?

Faire la liaison à l'intérieur de la fonction signifierait qu'elle xest effectivement liée à la valeur par défaut spécifiée lorsque la fonction est appelée, non définie, quelque chose qui présenterait un défaut profond: la defligne serait "hybride" dans le sens où une partie de la liaison (de l'objet fonction) se produirait à la définition, et une partie (affectation des paramètres par défaut) au moment de l'appel de la fonction.

Le comportement réel est plus cohérent: tout de cette ligne est évalué lorsque cette ligne est exécutée, c'est-à-dire lors de la définition de la fonction.

Stefano Borini
la source
43
Question complémentaire - Bonnes utilisations des arguments par défaut mutables
Jonathan
4
Je ne doute pas que des arguments mutables violent le principe du moindre étonnement pour une personne moyenne, et j'ai vu des débutants s'y mettre, puis remplacer héroïquement les listes de diffusion par des tuples de diffusion. Néanmoins, les arguments mutables sont toujours en ligne avec Python Zen (Pep 20) et tombent dans la clause "évidente pour le néerlandais" (comprise / exploitée par les programmeurs hard core python). La solution de contournement recommandée avec la chaîne doc est la meilleure, mais la résistance aux chaînes doc et à toute documentation (écrite) n'est pas si rare de nos jours. Personnellement, je préférerais un décorateur (disons @fixed_defaults).
Serge
5
Mon argument lorsque je rencontre ceci est: "Pourquoi avez-vous besoin de créer une fonction qui renvoie une mutable qui pourrait éventuellement être une mutable que vous transmettriez à la fonction? Soit elle modifie une mutable, soit elle en crée une nouvelle. Pourquoi avez-vous besoin pour faire les deux avec une seule fonction? Et pourquoi l'interprète devrait être réécrit pour vous permettre de le faire sans ajouter trois lignes à votre code? " Parce que nous parlons de réécrire la façon dont l'interpréteur gère les définitions de fonction et les évocations ici. C'est beaucoup à faire pour un cas d'utilisation à peine nécessaire.
Alan Leuthard
12
"Les novices de Python s'attendraient à ce que cette fonction retourne toujours une liste avec un seul élément:. [5]" Je suis un novice Python, et je ne m'attendrais pas à cela, car évidemment, foo([1])je reviendrai [1, 5], non [5]. Ce que vous vouliez dire, c'est qu'un novice s'attendrait à ce que la fonction appelée sans paramètre revienne toujours [5].
symplectomorphe
2
Cette question demande "Pourquoi est-ce que [la mauvaise façon] a été implémentée ainsi?" Il ne demande pas "Quelle est la bonne façon?" , qui est couvert par [ Pourquoi l'utilisation d'arg = None corrige-t-elle le problème d'argument par défaut mutable de Python? ] * ( stackoverflow.com/questions/10676729/… ). Les nouveaux utilisateurs sont presque toujours moins intéressés par les premiers et beaucoup plus par les seconds, c'est donc parfois un lien / dupe très utile à citer.
smci

Réponses:

1614

En fait, ce n'est pas un défaut de conception, et ce n'est pas à cause des internes ou des performances.
Cela vient simplement du fait que les fonctions en Python sont des objets de première classe, et pas seulement un morceau de code.

Dès que vous arrivez à penser de cette façon, alors cela prend tout son sens: une fonction est un objet évalué sur sa définition; les paramètres par défaut sont une sorte de "données de membre" et par conséquent leur état peut changer d'un appel à l'autre - exactement comme dans tout autre objet.

En tout cas, Effbot a une très belle explication des raisons de ce comportement dans Default Parameter Values ​​in Python .
Je l'ai trouvé très clair et je suggère vraiment de le lire pour une meilleure connaissance du fonctionnement des objets fonctionnels.

Rob
la source
80
À tous ceux qui lisent la réponse ci-dessus, je vous recommande fortement de prendre le temps de lire l'article Effbot lié. En plus de toutes les autres informations utiles, la partie sur la façon dont cette fonctionnalité de langue peut être utilisée pour la mise en cache / mémorisation des résultats est très pratique à savoir!
Cam Jackson
85
Même s'il s'agit d'un objet de première classe, on peut toujours imaginer une conception où le code de chaque valeur par défaut est stocké avec l'objet et réévalué à chaque appel de la fonction. Je ne dis pas que ce serait mieux, juste que les fonctions étant des objets de première classe ne l'empêchent pas complètement.
gerrit
313
Désolé, mais tout ce qui est considéré comme "le plus grand WTF en Python" est certainement un défaut de conception . C'est une source de bugs pour tout le monde à un moment donné, car personne ne s'attend à ce comportement au début - ce qui signifie qu'il n'aurait pas dû être conçu de cette façon pour commencer. Peu m'importe quels cerceaux ils devaient traverser, ils auraient dû concevoir Python pour que les arguments par défaut ne soient pas statiques.
BlueRaja - Danny Pflughoeft
192
Qu'il s'agisse ou non d'un défaut de conception, votre réponse semble impliquer que ce comportement est en quelque sorte nécessaire, naturel et évident étant donné que les fonctions sont des objets de première classe, et ce n'est tout simplement pas le cas. Python a des fermetures. Si vous remplacez l'argument par défaut par une affectation sur la première ligne de la fonction, il évalue l'expression à chaque appel (en utilisant potentiellement des noms déclarés dans une portée englobante). Il n'y a aucune raison qu'il ne soit pas possible ou raisonnable d'avoir des arguments par défaut évalués chaque fois que la fonction est appelée exactement de la même manière.
Mark Amery
24
Le design ne découle pas directement de functions are objects. Dans votre paradigme, la proposition serait d'implémenter les valeurs par défaut des fonctions en tant que propriétés plutôt qu'en tant qu'attributs.
bukzor
273

Supposons que vous ayez le code suivant

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Quand je vois la déclaration de manger, le moins étonnant est de penser que si le premier paramètre n'est pas donné, il sera égal au tuple ("apples", "bananas", "loganberries")

Cependant, supposé plus tard dans le code, je fais quelque chose comme

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

alors si les paramètres par défaut étaient liés à l'exécution de la fonction plutôt qu'à la déclaration de fonction, alors je serais étonné (de très mauvaise façon) de découvrir que les fruits avaient été modifiés. Ce serait plus étonnant IMO que de découvrir que votrefoo fonction ci-dessus mutait la liste.

Le vrai problème réside dans les variables mutables, et toutes les langues ont ce problème dans une certaine mesure. Voici une question: supposons qu'en Java j'ai le code suivant:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Maintenant, ma carte utilise-t-elle la valeur de la StringBufferclé lorsqu'elle a été placée dans la carte, ou stocke-t-elle la clé par référence? De toute façon, quelqu'un est étonné; soit la personne qui a essayé de sortir l'objet duMap valeur en utilisant une valeur identique à celle avec laquelle elle l'a mis, soit la personne qui ne semble pas pouvoir récupérer son objet même si la clé qu'elle utilise est littéralement le même objet qui a été utilisé pour le mettre dans la carte (c'est pourquoi Python ne permet pas que ses types de données intégrés mutables soient utilisés comme clés de dictionnaire).

Votre exemple est un bon exemple d'un cas où les nouveaux arrivants Python seront surpris et mordus. Mais je dirais que si nous «corrigions» cela, cela ne ferait que créer une situation différente où ils seraient mordus à la place, et celle-là serait encore moins intuitive. De plus, c'est toujours le cas lorsqu'il s'agit de variables mutables; vous rencontrez toujours des cas où quelqu'un pourrait intuitivement s'attendre à l'un ou au comportement opposé en fonction du code qu'il écrit.

Personnellement, j'aime l'approche actuelle de Python: les arguments de fonction par défaut sont évalués lorsque la fonction est définie et cet objet est toujours la valeur par défaut. Je suppose qu'ils pourraient utiliser des cas spéciaux en utilisant une liste vide, mais ce type de boîtier spécial provoquerait encore plus d'étonnement, sans parler d'être incompatible en arrière.

Eli Courtwright
la source
30
Je pense que c'est une question de débat. Vous agissez sur une variable globale. Toute évaluation effectuée n'importe où dans votre code impliquant votre variable globale fera désormais (correctement) référence à ("myrtilles", "mangues"). le paramètre par défaut pourrait être comme n'importe quel autre cas.
Stefano Borini
47
En fait, je ne pense pas être d'accord avec votre premier exemple. Je ne suis pas sûr que j'aime l'idée de modifier un initialiseur comme ça en premier lieu, mais si je le faisais, je m'attendrais à ce qu'il se comporte exactement comme vous le décrivez - en changeant la valeur par défaut en ("blueberries", "mangos").
Ben Blank
12
Le paramètre par défaut est comme tout autre cas. Ce qui est inattendu, c'est que le paramètre est une variable globale et non locale. Ce qui à son tour est dû au fait que le code est exécuté lors de la définition de la fonction, et non à l'appel. Une fois que vous obtenez cela, et que c'est la même chose pour les cours, c'est parfaitement clair.
Lennart Regebro le
17
Je trouve l'exemple trompeur plutôt que brillant. Si some_random_function()ajoute à au fruitslieu d'attribuer à lui, le comportement eat() va changer. Voilà pour le merveilleux design actuel. Si vous utilisez un argument par défaut référencé ailleurs, puis modifiez la référence depuis l'extérieur de la fonction, vous demandez des problèmes. Le vrai WTF est lorsque les gens définissent un nouvel argument par défaut (un littéral de liste ou un appel à un constructeur), et obtiennent toujours un bit.
alexis
13
Vous venez de déclarer globalet de réaffecter explicitement le tuple - il n'y a absolument rien de surprenant si cela eatfonctionne différemment après cela.
user3467349
241

La partie pertinente de la documentation :

Les valeurs des paramètres par défaut sont évaluées de gauche à droite lorsque la définition de fonction est exécutée. Cela signifie que l'expression est évaluée une fois, lorsque la fonction est définie, et que la même valeur «pré-calculée» est utilisée pour chaque appel. Ceci est particulièrement important pour comprendre quand un paramètre par défaut est un objet modifiable, comme une liste ou un dictionnaire: si la fonction modifie l'objet (par exemple en ajoutant un élément à une liste), la valeur par défaut est en effet modifiée. Ce n'est généralement pas ce qui était prévu. Un moyen de contourner cela est de l'utiliser Nonepar défaut et de le tester explicitement dans le corps de la fonction, par exemple:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
glglgl
la source
181
Les expressions «ce n'est généralement pas ce qui était prévu» et «un moyen de contourner cela» sentent comme si elles documentaient un défaut de conception.
bukzor
4
@Matthew: Je suis bien conscient, mais ça ne vaut pas l'écueil. Vous verrez généralement les guides de style et les linters marquer inconditionnellement les valeurs par défaut modifiables comme incorrectes pour cette raison. La manière explicite de faire la même chose est de bourrer un attribut sur la fonction ( function.data = []) ou mieux encore, de créer un objet.
bukzor
6
@bukzor: Les pièges doivent être notés et documentés, c'est pourquoi cette question est bonne et a reçu tant de votes positifs. Dans le même temps, les écueils n'ont pas nécessairement besoin d'être supprimés. Combien de débutants Python ont passé une liste à une fonction qui l'a modifiée et ont été choqués de voir les changements apparaître dans la variable d'origine? Pourtant, les types d'objets mutables sont merveilleux, lorsque vous comprenez comment les utiliser. Je suppose que cela se résume à l'opinion sur cet écueil particulier.
Matthew
33
L'expression "ce n'est généralement pas ce qui était prévu" signifie "pas ce que le programmeur voulait réellement se produire," pas "pas ce que Python est censé faire".
holdenweb
4
@holdenweb Wow, je suis en retard à la fête. Compte tenu du contexte, bukzor a tout à fait raison: ils documentent un comportement / conséquence qui n'était pas "prévu" lorsqu'ils ont décidé que le langage devait exécuter la définition de la fonction. Comme c'est une conséquence involontaire de leur choix de conception, c'est un défaut de conception. S'il ne s'agissait pas d'un défaut de conception, il ne serait même pas nécessaire de proposer "un moyen de contourner ce problème".
code_dredd
118

Je ne sais rien du fonctionnement interne de l'interpréteur Python (et je ne suis pas non plus un expert en compilateurs et interprètes), alors ne me blâmez pas si je propose quelque chose d'insensible ou impossible.

À condition que les objets python soient mutables, je pense que cela devrait être pris en compte lors de la conception des arguments par défaut. Lorsque vous instanciez une liste:

a = []

vous vous attendez à obtenir une nouvelle liste référencée par a.

Pourquoi le a=[]dans

def x(a=[]):

instancier une nouvelle liste sur la définition de la fonction et non sur l'invocation? C'est comme si vous demandiez "si l'utilisateur ne fournit pas l'argument, instanciez une nouvelle liste et utilisez-la comme si elle avait été produite par l'appelant". Je pense que c'est plutôt ambigu:

def x(a=datetime.datetime.now()):

utilisateur, voulez- avous définir par défaut le datetime correspondant à la définition ou à l'exécution x? Dans ce cas, comme dans le précédent, je conserverai le même comportement que si l'argument par défaut "assignation" était la première instruction de la fonction ( datetime.now()appelée lors de l'appel de la fonction). D'un autre côté, si l'utilisateur voulait le mappage définition-temps, il pouvait écrire:

b = datetime.datetime.now()
def x(a=b):

Je sais, je sais: c'est une fermeture. Alternativement, Python peut fournir un mot clé pour forcer la liaison au moment de la définition:

def x(static a=b):
Utaal
la source
11
Vous pouvez faire: def x (a = None): Et puis, si a est None, définissez a = datetime.datetime.now ()
Anon
20
Merci pour ça. Je ne pouvais pas vraiment mettre le doigt sur pourquoi cela me contrarie sans fin. Vous l'avez magnifiquement fait avec un minimum de fuzz et de confusion. En tant que personne issue de la programmation de systèmes en C ++ et parfois de la traduction naïve de fonctionnalités de langage, ce faux ami m'a donné un coup de pied dans le creux de la tête, tout comme les attributs de classe. Je comprends pourquoi les choses sont ainsi, mais je ne peux pas m'empêcher de détester, peu importe ce qui pourrait en résulter. Au moins, c'est tellement contraire à mon expérience, que je ne l'oublierai probablement pas (espérons-le) ...
AndreasT
5
@Andreas une fois que vous utilisez Python assez longtemps, vous commencez à voir à quel point il est logique pour Python d'interpréter les choses comme des attributs de classe comme il le fait - c'est uniquement en raison des bizarreries et des limitations particulières de langages comme C ++ (et Java, et C # ...) qu'il est logique que le contenu du class {}bloc soit interprété comme appartenant aux instances :) Mais lorsque les classes sont des objets de première classe, la chose naturelle est évidemment que leur contenu (en mémoire) reflète leur contenu (dans du code).
Karl Knechtel
6
La structure normative n'est pas une bizarrerie ou une limitation dans mon livre. Je sais que cela peut être maladroit et laid, mais vous pouvez appeler cela une "définition" de quelque chose. Les langages dynamiques me semblent un peu anarchistes: bien sûr tout le monde est libre, mais il faut une structure pour amener quelqu'un à vider la poubelle et à paver la route. Je suppose que je suis vieux ... :)
AndreasT
4
La définition de la fonction est exécutée au moment du chargement du module. Le corps de la fonction est exécuté au moment de l'appel de la fonction. L'argument par défaut fait partie de la définition de la fonction, pas du corps de la fonction. (Cela devient plus compliqué pour les fonctions imbriquées.)
Lutz Prechelt
84

Eh bien, la raison est tout simplement que les liaisons sont effectuées lorsque le code est exécuté et que la définition de la fonction est exécutée, eh bien ... lorsque les fonctions sont définies.

Comparez ceci:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Ce code souffre de la même situation inattendue. bananas est un attribut de classe et, par conséquent, lorsque vous y ajoutez des éléments, il est ajouté à toutes les instances de cette classe. La raison est exactement la même.

C'est juste "Comment ça marche", et le faire fonctionner différemment dans le cas de la fonction serait probablement compliqué, et dans le cas de la classe probablement impossible, ou au moins ralentir beaucoup l'instanciation des objets, car vous devriez garder le code de la classe autour et l'exécuter lorsque des objets sont créés.

Oui, c'est inattendu. Mais une fois que le sou tombe, il s'intègre parfaitement avec le fonctionnement de Python en général. En fait, c'est une bonne aide pédagogique, et une fois que vous comprendrez pourquoi cela se produit, vous améliorerez beaucoup le python.

Cela dit, il devrait figurer en bonne place dans tout bon tutoriel Python. Parce que, comme vous le mentionnez, tout le monde rencontre ce problème tôt ou tard.

Lennart Regebro
la source
Comment définissez-vous un attribut de classe différent pour chaque instance d'une classe?
Kieveli
19
S'il est différent pour chaque instance, ce n'est pas un attribut de classe. Les attributs de classe sont des attributs de la classe. D'où le nom. Ils sont donc les mêmes pour toutes les instances.
Lennart Regebro le
1
Comment définissez-vous un attribut dans une classe qui est différent pour chaque instance d'une classe? (Redéfini pour ceux qui n'ont pas pu déterminer qu'une personne qui ne connaît pas les fonctions de nommage de Python pourrait poser des questions sur les variables membres normales d'une classe).
Kieveli
@Kievieli: Vous parlez de variables membres normales d'une classe. :-) Vous définissez les attributs d'instance en disant self.attribute = value dans n'importe quelle méthode. Par exemple __init __ ().
Lennart Regebro
@Kieveli: Deux réponses: vous ne pouvez pas, car toute chose que vous définissez au niveau d'une classe sera un attribut de classe, et toute instance qui accédera à cet attribut accédera au même attribut de classe; vous pouvez, en quelque sorte /, en utilisant propertys - qui sont en fait des fonctions de niveau classe qui agissent comme des attributs normaux mais sauvegardent l'attribut dans l'instance au lieu de la classe (en utilisant self.attribute = valuecomme l'a dit Lennart).
Ethan Furman
66

Pourquoi ne vous introspectez pas?

Je suis vraiment surpris que personne n'ait effectué l'introspection perspicace offerte par Python ( 2et 3s'applique) sur les callables.

Étant donné une petite fonction simple funcdéfinie comme:

>>> def func(a = []):
...    a.append(5)

Lorsque Python le rencontrera, la première chose qu'il fera sera de le compiler afin de créer un codeobjet pour cette fonction. Pendant cette étape de compilation, Python évalue * puis stocke les arguments par défaut (une liste vide []ici) dans l'objet fonction lui-même . Comme la première réponse l'a mentionné: la liste apeut désormais être considérée comme un membre de la fonction func.

Alors, faisons une introspection, un avant et un après pour examiner comment la liste est développée à l' intérieur de l'objet fonction. J'utilise Python 3.xpour cela, pour Python 2, la même chose s'applique (utilisez __defaults__ou func_defaultsdans Python 2; oui, deux noms pour la même chose).

Fonction avant exécution:

>>> def func(a = []):
...     a.append(5)
...     

Après que Python ait exécuté cette définition, il prendra tous les paramètres par défaut spécifiés ( a = []ici) et les entassera dans l' __defaults__attribut de l'objet fonction (section pertinente: Callables):

>>> func.__defaults__
([],)

Ok, donc une liste vide comme entrée unique __defaults__, comme prévu.

Fonction après exécution:

Exécutons maintenant cette fonction:

>>> func()

Maintenant, __defaults__revoyons-les:

>>> func.__defaults__
([5],)

Étonné? La valeur à l'intérieur de l'objet change! Les appels consécutifs à la fonction s'ajouteront désormais simplement à cet listobjet intégré :

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Donc, vous l'avez, la raison pour laquelle cette «faille» se produit, c'est parce que les arguments par défaut font partie de l'objet fonction. Il n'y a rien de bizarre ici, c'est juste un peu surprenant.

La solution courante pour lutter contre cela est d'utiliser Nonepar défaut puis d'initialiser dans le corps de la fonction:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Puisque le corps de la fonction est exécuté à nouveau à chaque fois, vous obtenez toujours une nouvelle liste vide si aucun argument n'a été passé a.


Pour vérifier davantage que la liste dans __defaults__est la même que celle utilisée dans la fonction, funcvous pouvez simplement changer votre fonction pour renvoyer idla liste autilisée dans le corps de la fonction. Ensuite, comparez-le à la liste dans __defaults__(position [0]dans __defaults__) et vous verrez comment ceux-ci se réfèrent en effet à la même instance de liste:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Le tout avec le pouvoir de l'introspection!


* Pour vérifier que Python évalue les arguments par défaut lors de la compilation de la fonction, essayez d'exécuter ce qui suit:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

comme vous le remarquerez, input()est appelé avant le processus de construction de la fonction et de sa liaison au nom bar.

Dimitris Fasarakis Hilliard
la source
1
Est-il id(...)nécessaire pour cette dernière vérification, ou l' isopérateur répondrait-il à la même question?
das-g
1
@ das-g isferait très bien, j'ai juste utilisé id(val)parce que je pense que cela pourrait être plus intuitif.
Dimitris Fasarakis Hilliard
L'utilisation Nonepar défaut limite considérablement l'utilité de l' __defaults__introspection, donc je ne pense pas que cela fonctionne bien comme défense d'avoir un __defaults__travail comme il le fait. L'évaluation différée ferait plus pour garder les valeurs par défaut des fonctions utiles des deux côtés.
Brilliand
58

Je pensais que la création des objets à l'exécution serait la meilleure approche. Je suis moins certain maintenant, car vous perdez certaines fonctionnalités utiles, même si cela en vaut la peine, simplement pour éviter la confusion des débutants. Les inconvénients de le faire sont:

1. Performance

def foo(arg=something_expensive_to_compute())):
    ...

Si l'évaluation du temps d'appel est utilisée, la fonction coûteuse est appelée chaque fois que votre fonction est utilisée sans argument. Vous paierez un prix cher à chaque appel ou vous devrez mettre en cache manuellement la valeur en externe, polluant votre espace de noms et ajoutant de la verbosité.

2. Forcer les paramètres liés

Une astuce utile consiste à lier les paramètres d'un lambda à la liaison actuelle d'une variable lorsque le lambda est créé. Par exemple:

funcs = [ lambda i=i: i for i in range(10)]

Cela renvoie une liste de fonctions qui renvoient respectivement 0,1,2,3 .... Si le comportement est modifié, ils se lieront ià la place à la valeur de temps d'appel de i, vous obtiendrez donc une liste de fonctions que tous ont renvoyées 9.

La seule façon de mettre cela en œuvre autrement serait de créer une nouvelle fermeture avec la borne i, c'est-à-dire:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspection

Considérez le code:

def foo(a='test', b=100, c=[]):
   print a,b,c

Nous pouvons obtenir des informations sur les arguments et les valeurs par défaut en utilisant le inspectmodule, qui

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Ces informations sont très utiles pour des choses comme la génération de documents, la métaprogrammation, les décorateurs, etc.

Supposons maintenant que le comportement des valeurs par défaut puisse être modifié afin que ce soit l'équivalent de:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

Cependant, nous avons perdu la capacité d'introspection, et voir ce que les arguments par défaut sont . Parce que les objets n'ont pas été construits, nous ne pouvons jamais les saisir sans appeler la fonction. Le mieux que nous puissions faire est de stocker le code source et de le renvoyer sous forme de chaîne.

Brian
la source
1
vous pouvez également réaliser l'introspection si pour chacun il y avait une fonction pour créer l'argument par défaut au lieu d'une valeur. le module d'inspection appellera simplement cette fonction.
yairchu
@ SilentGhost: Je parle de savoir si le comportement a été modifié pour le recréer - le créer une fois est le comportement actuel et pourquoi le problème par défaut mutable existe.
Brian
1
@yairchu: Cela suppose que la construction est sûre (donc n'a pas d'effets secondaires). L'introspection des arguments ne devrait rien faire , mais l'évaluation de code arbitraire pourrait bien avoir un effet.
Brian
1
Un langage différent signifie souvent simplement écrire des choses différemment. Votre premier exemple pourrait facilement s'écrire: _expensive = cher (); def foo (arg = _expensive), si vous ne voulez pas spécifiquement qu'il soit réévalué.
Glenn Maynard
@Glenn - c'est ce à quoi je faisais référence avec "cache la variable en externe" - c'est un peu plus verbeux, et vous vous retrouvez avec des variables supplémentaires dans votre espace de noms.
Brian
55

5 points en défense de Python

  1. Simplicité : Le comportement est simple dans le sens suivant: la plupart des gens ne tombent dans ce piège qu'une seule fois, pas plusieurs fois.

  2. Cohérence : Python transmet toujours des objets, pas des noms. Le paramètre par défaut fait évidemment partie du titre de la fonction (et non du corps de la fonction). Elle doit donc être évaluée au moment du chargement du module (et uniquement au moment du chargement du module, sauf si elle est imbriquée), et non au moment de l'appel de la fonction.

  3. Utilité : Comme le souligne Frederik Lundh dans son explication des "Valeurs des paramètres par défaut en Python" , le comportement actuel peut être très utile pour la programmation avancée. (Utiliser avec modération.)

  4. Documentation suffisante : dans la documentation Python la plus élémentaire, le didacticiel, le problème est annoncé à haute voix comme un "avertissement important" dans la première sous-section de la section "En savoir plus sur la définition des fonctions" . L'avertissement utilise même des caractères gras, qui sont rarement appliqués en dehors des en-têtes. RTFM: Lisez le manuel fin.

  5. Méta-apprentissage : tomber dans le piège est en fait un moment très utile (au moins si vous êtes un apprenant réfléchi), car vous comprendrez ensuite mieux le point "cohérence" ci-dessus et cela vous en apprendra beaucoup sur Python.

Lutz Prechelt
la source
18
Il m'a fallu un an pour trouver que ce comportement gâche mon code en production, a fini par supprimer une fonctionnalité complète jusqu'à ce que je tombe sur ce défaut de conception par hasard. J'utilise Django. Étant donné que l'environnement de transfert n'avait pas beaucoup de demandes, ce bogue n'a jamais eu d'impact sur le contrôle qualité. Lorsque nous sommes allés en direct et avons reçu de nombreuses demandes simultanées - certaines fonctions utilitaires ont commencé à remplacer les paramètres les uns des autres! Faire des failles de sécurité, des bugs et autres.
oriadam
7
@oriadam, aucune infraction, mais je me demande comment vous avez appris Python sans avoir rencontré cela auparavant. J'apprends juste Python maintenant et cet écueil possible est mentionné dans le tutoriel officiel Python juste à côté de la première mention des arguments par défaut. (Comme mentionné au point 4 de cette réponse.) Je suppose que la morale est - plutôt antipathique - de lire les documents officiels de la langue que vous utilisez pour créer un logiciel de production.
Wildcard
De plus, il serait surprenant (pour moi) qu'une fonction de complexité inconnue soit appelée en plus de l'appel de fonction que je fais.
Vatine le
52

Ce comportement s'explique facilement par:

  1. La déclaration de fonction (classe, etc.) n'est exécutée qu'une seule fois, créant tous les objets de valeur par défaut
  2. tout est passé par référence

Donc:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a ne change pas - chaque appel d'affectation crée un nouvel objet int - un nouvel objet est imprimé
  2. b ne change pas - le nouveau tableau est construit à partir de la valeur par défaut et imprimé
  3. c modifications - l'opération est effectuée sur le même objet - et elle est imprimée
ymv
la source
(En fait, ajouter est un mauvais exemple, mais les entiers étant immuables est toujours mon point principal.)
Anon
Je l'ai réalisé à mon grand regret après avoir vérifié que, avec b défini sur [], b .__ add __ ([1]) renvoie [1] mais laisse également b encore [] même si les listes sont mutables. Ma faute.
Anon
@ANon: oui __iadd__, mais cela ne fonctionne pas avec int. Bien sûr. :-)
Veky
35

Ce que vous demandez, c'est pourquoi cela:

def func(a=[], b = 2):
    pass

n'est pas équivalent en interne à ceci:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

sauf pour le cas d'appeler explicitement func (None, None), que nous ignorerons.

En d'autres termes, au lieu d'évaluer les paramètres par défaut, pourquoi ne pas stocker chacun d'eux et les évaluer lorsque la fonction est appelée?

Une réponse est probablement là - elle transformerait efficacement chaque fonction avec des paramètres par défaut en fermeture. Même si tout est caché dans l'interpréteur et pas une fermeture complète, les données doivent être stockées quelque part. Ce serait plus lent et utiliserais plus de mémoire.

Glenn Maynard
la source
6
Cela n'aurait pas besoin d'être une fermeture - une meilleure façon de penser serait simplement de faire du bytecode créant les valeurs par défaut la première ligne de code - après tout, vous compilez le corps à ce point de toute façon - il n'y a pas de vraie différence entre le code dans les arguments et le code dans le corps.
Brian
10
Certes, mais cela ralentirait toujours Python, et ce serait en fait assez surprenant, à moins que vous ne fassiez de même pour les définitions de classe, ce qui le rendrait stupidement lent car vous auriez à réexécuter la définition de classe entière chaque fois que vous instanciez un classe. Comme mentionné, la solution serait plus surprenante que le problème.
Lennart Regebro
D'accord avec Lennart. Comme Guido aime à le dire, pour chaque fonctionnalité de langue ou bibliothèque standard, il y a quelqu'un qui l'utilise.
Jason Baker
6
Le changer maintenant serait de la folie - nous explorons simplement pourquoi c'est ainsi. S'il effectuait une évaluation par défaut tardive, cela ne serait pas nécessairement surprenant. Il est tout à fait vrai qu'un tel noyau d'une différence d'analyse aurait des effets étendus, et probablement nombreux et obscurs, sur la langue dans son ensemble.
Glenn Maynard
35

1) Le soi-disant problème de "l'argument par défaut Mutable" est en général un exemple spécial démontrant que:
"Toutes les fonctions avec ce problème souffrent également d'un problème d'effet secondaire similaire sur le paramètre réel ,"
C'est contre les règles de programmation fonctionnelle, généralement négligeable et doit être fixé les deux ensemble.

Exemple:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Solution : une copie
Une solution absolument sûre est d'abord de copyou vers deepcopyl'objet d'entrée, puis de faire quoi que ce soit avec la copie.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

De nombreux types mutables intégrés ont une méthode de copie comme some_dict.copy()ou some_set.copy()ou peuvent être copiés facilement comme somelist[:]ou list(some_list). Chaque objet peut également être copié par copy.copy(any_object)ou plus approfondi par copy.deepcopy()(ce dernier utile si l'objet mutable est composé d'objets mutables). Certains objets sont fondamentalement basés sur des effets secondaires comme l'objet "fichier" et ne peuvent pas être reproduits de manière significative par copie. copier

Exemple de problème pour une question SO similaire

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

Il ne doit pas non plus être enregistré dans aucun attribut public d'une instance retournée par cette fonction. (En supposant que les attributs privés de l'instance ne devraient pas être modifiés depuis l'extérieur de cette classe ou sous-classes par convention._var1 est un attribut privé)

Conclusion: les
objets de paramètres d'entrée ne doivent pas être modifiés sur place (mutés) ni ne doivent être liés à un objet renvoyé par la fonction. (Si nous préférons la programmation sans effets secondaires, ce qui est fortement recommandé. Voir le wiki sur les "effets secondaires" (les deux premiers paragraphes sont pertinents dans ce contexte.).)

2)
Uniquement si l'effet secondaire sur le paramètre réel est requis mais indésirable sur le paramètre par défaut, alors la solution utile est def ...(var1=None): if var1 is None: var1 = [] Plus ..

3) Dans certains cas, le comportement modifiable des paramètres par défaut est utile .

hynekcer
la source
5
J'espère que vous savez que Python n'est pas un langage de programmation fonctionnel.
Veky
6
Oui, Python est un langage multi-paragigm avec quelques fonctionnalités fonctionnelles. ("Ne faites pas que chaque problème ressemble à un clou juste parce que vous avez un marteau.") Beaucoup d'entre eux sont dans les meilleures pratiques Python. Python a une programmation fonctionnelle HOWTO intéressante. Les autres fonctionnalités sont les fermetures et le curry, non mentionnés ici.
hynekcer
1
J'ajouterais également, à ce stade avancé, que la sémantique d'affectation de Python a été conçue explicitement pour éviter la copie de données si nécessaire, de sorte que la création de copies (et en particulier de copies complètes) affectera à la fois l'exécution et l'utilisation de la mémoire. Ils ne devraient donc être utilisés qu'en cas de besoin, mais les nouveaux arrivants ont souvent du mal à comprendre quand c'est le cas.
holdenweb
1
@holdenweb Je suis d'accord. Une copie temporaire est le moyen le plus courant et parfois le seul moyen possible de protéger les données mutables d'origine d'une fonction étrangère qui les modifie potentiellement. Heureusement, une fonction qui modifie déraisonnablement les données est considérée comme un bogue et donc peu courante.
hynekcer
Je suis d'accord avec cette réponse. Et je ne comprends pas pourquoi la def f( a = None )construction est recommandée alors que vous voulez vraiment dire autre chose. La copie est correcte, car vous ne devez pas muter d'arguments. Et quand vous le faites if a is None: a = [1, 2, 3], vous copiez quand même la liste.
koddo
30

Cela n'a en fait rien à voir avec les valeurs par défaut, à part le fait qu'il apparaît souvent comme un comportement inattendu lorsque vous écrivez des fonctions avec des valeurs par défaut modifiables.

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

Aucune valeur par défaut en vue dans ce code, mais vous obtenez exactement le même problème.

Le problème est que l' fooon modifie une variable mutable transmise par l'appelant, lorsque l'appelant ne s'y attend pas. Un code comme celui-ci serait bien si la fonction était appelée quelque chose commeappend_5 ; alors l'appelant appellerait la fonction afin de modifier la valeur qu'ils transmettent, et le comportement serait attendu. Mais une telle fonction serait très peu susceptible de prendre un argument par défaut, et ne retournerait probablement pas la liste (puisque l'appelant a déjà une référence à cette liste; celle qu'il vient de passer).

Votre original foo, avec un argument par défaut, ne devrait pas modifier as'il a été explicitement transmis ou a obtenu la valeur par défaut. Votre code doit laisser les arguments modifiables seuls, sauf s'il ressort clairement du contexte / nom / documentation que les arguments sont censés être modifiés. Utiliser des valeurs mutables passées comme arguments en tant que temporaires locaux est une très mauvaise idée, que nous soyons en Python ou non et qu'il y ait des arguments par défaut impliqués ou non.

Si vous devez manipuler de manière destructive un temporaire local au cours du calcul de quelque chose et que vous devez commencer votre manipulation à partir d'une valeur d'argument, vous devez en faire une copie.

Ben
la source
7
Bien que lié, je pense que c'est un comportement distinct (car nous prévoyons appendde changer a"sur place"). Qu'un mutable par défaut ne soit pas ré-instancié à chaque appel est le bit "inattendu" ... du moins pour moi. :)
Andy Hayden
2
@AndyHayden si la fonction est censée modifier l'argument, pourquoi serait-il logique d'avoir une valeur par défaut?
Mark Ransom
@MarkRansom le seul exemple auquel je peux penser est cache={}. Cependant, je soupçonne que ce "moindre étonnement" survient lorsque vous ne vous attendez pas (ou ne voulez pas) à la fonction que vous appelez pour muter l'argument.
Andy Hayden
1
@AndyHayden J'ai laissé ma propre réponse ici avec une expansion de ce sentiment. Laissez-moi savoir ce que vous pensez. Je pourrais y ajouter votre exemple cache={}pour être complet.
Mark Ransom
1
@AndyHayden Le point de ma réponse est que si vous êtes étonné en mutant accidentellement la valeur par défaut d'un argument, alors vous avez un autre bug, qui est que votre code peut accidentellement muter la valeur d'un appelant lorsque la valeur par défaut n'a pas été utilisée. Et notez que l'utilisation Noneet l'attribution de la valeur par défaut réelle si l'argument None ne résout pas ce problème (je le considère comme un anti-motif pour cette raison). Si vous corrigez l'autre bogue en évitant de muter les valeurs des arguments, qu'elles aient ou non des valeurs par défaut, vous ne remarquerez ou ne vous soucierez jamais de ce comportement "étonnant".
Ben
27

Sujet déjà occupé, mais d'après ce que j'ai lu ici, les éléments suivants m'ont aidé à comprendre comment cela fonctionne en interne:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232
Stéphane
la source
2
en fait, cela pourrait être un peu déroutant pour les nouveaux arrivants car les a = a + [1]surcharges a... envisagez de le modifier b = a + [1] ; print id(b)et d'ajouter une ligne a.append(2). Cela rendra plus évident que +sur deux listes crée toujours une nouvelle liste (affectée à b), tandis qu'une modification apeut toujours avoir la même id(a).
Jörn Hees
25

C'est une optimisation des performances. En raison de cette fonctionnalité, lequel de ces deux appels de fonction pensez-vous être le plus rapide?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Je vais vous donner un indice. Voici le démontage (voir http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Je doute que le comportement expérimenté ait une utilité pratique (qui a vraiment utilisé des variables statiques en C, sans multiplier les bugs?)

Comme vous pouvez le voir, il existe un avantage en termes de performances lors de l'utilisation d'arguments par défaut immuables. Cela peut faire une différence s'il s'agit d'une fonction fréquemment appelée ou si l'argument par défaut prend beaucoup de temps à construire. Gardez également à l'esprit que Python n'est pas C. En C, vous avez des constantes qui sont à peu près gratuites. En Python, vous n'avez pas cet avantage.

Jason Baker
la source
24

Python: l'argument par défaut Mutable

Les arguments par défaut sont évalués au moment où la fonction est compilée dans un objet fonction. Lorsqu'ils sont utilisés par la fonction, plusieurs fois par cette fonction, ils sont et restent le même objet.

Lorsqu'ils sont mutables, lorsqu'ils subissent une mutation (par exemple, en y ajoutant un élément), ils restent mutés lors d'appels consécutifs.

Ils restent mutés car ils sont à chaque fois le même objet.

Code équivalent:

Puisque la liste est liée à la fonction lorsque l'objet fonction est compilé et instancié, ceci:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

est presque exactement équivalent à ceci:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Manifestation

Voici une démonstration - vous pouvez vérifier qu'ils sont le même objet chaque fois qu'ils sont référencés par

  • voir que la liste est créée avant que la fonction ait fini de compiler en un objet fonction,
  • en observant que l'identifiant est le même à chaque fois que la liste est référencée,
  • constatant que la liste reste modifiée lorsque la fonction qui l'utilise est appelée une deuxième fois,
  • en observant l'ordre dans lequel la sortie est imprimée à partir de la source (que j'ai commodément numérotée pour vous):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

et l'exécuter avec python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Cela viole-t-il le principe du "moindre étonnement"?

Cet ordre d'exécution est souvent déroutant pour les nouveaux utilisateurs de Python. Si vous comprenez le modèle d'exécution Python, cela devient tout à fait normal.

L'instruction habituelle pour les nouveaux utilisateurs de Python:

Mais c'est pourquoi l'instruction habituelle pour les nouveaux utilisateurs est de créer leurs arguments par défaut comme ceci à la place:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Cela utilise le singleton None comme objet sentinelle pour indiquer à la fonction si nous avons ou non obtenu un argument autre que la valeur par défaut. Si nous n'obtenons aucun argument, nous voulons en fait utiliser une nouvelle liste vide [], par défaut.

Comme le dit la section du didacticiel sur le flux de contrôle :

Si vous ne voulez pas que la valeur par défaut soit partagée entre les appels suivants, vous pouvez écrire la fonction comme ceci à la place:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
Aaron Hall
la source
24

La réponse la plus courte serait probablement "la définition est l'exécution", donc tout l'argument n'a aucun sens strict. Comme exemple plus artificiel, vous pouvez citer ceci:

def a(): return []

def b(x=a()):
    print x

J'espère que cela suffit pour montrer que ne pas exécuter les expressions d'argument par défaut au moment de l'exécution de l' definstruction n'est pas facile ou n'a pas de sens, ou les deux.

Je suis d'accord que c'est un problème lorsque vous essayez d'utiliser des constructeurs par défaut.

Baczek
la source
20

Une solution simple utilisant None

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
hugo24
la source
19

Ce comportement n'est pas surprenant si vous tenez compte des éléments suivants:

  1. Le comportement des attributs de classe en lecture seule lors des tentatives d'affectation, et que
  2. Les fonctions sont des objets (bien expliqué dans la réponse acceptée).

Le rôle de (2) a été largement couvert dans ce fil. (1) est probablement le facteur d'étonnement, car ce comportement n'est pas «intuitif» en provenance d'autres langues.

(1) est décrit dans le tutoriel Python sur les classes . Pour tenter d'attribuer une valeur à un attribut de classe en lecture seule:

... toutes les variables trouvées en dehors de la portée la plus interne sont en lecture seule ( une tentative d'écriture dans une telle variable créera simplement une nouvelle variable locale dans la portée la plus interne, en laissant inchangée la variable externe portant le même nom ).

Revenez à l'exemple original et considérez les points ci-dessus:

def foo(a=[]):
    a.append(5)
    return a

Voici fooun objet et aest un attribut de foo(disponible sur foo.func_defs[0]). Puisque aest une liste, aest mutable et est donc un attribut de lecture-écriture de foo. Il est initialisé dans la liste vide comme spécifié par la signature lorsque la fonction est instanciée, et est disponible pour la lecture et l'écriture tant que l'objet fonction existe.

L'appel foosans remplacer une valeur par défaut utilise la valeur de cette valeur par défaut à partir de foo.func_defs. Dans ce cas, foo.func_defs[0]est utilisé pour ala portée de code de l'objet fonction. Modifications à amodifier foo.func_defs[0], qui fait partie de l' fooobjet et persiste entre l'exécution du code dans foo.

Maintenant, comparez cela à l'exemple de la documentation sur l' émulation du comportement d'argument par défaut d'autres langages , de telle sorte que les valeurs par défaut de la signature de fonction soient utilisées à chaque exécution de la fonction:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

En tenant compte de (1) et (2) , on peut voir pourquoi cela accomplit le comportement souhaité:

  • Lorsque l' fooobjet fonction est instancié, foo.func_defs[0]est défini sur None, un objet immuable.
  • Lorsque la fonction est exécutée avec des valeurs par défaut (sans paramètre spécifié pour Ll'appel de fonction), foo.func_defs[0]( None) est disponible dans la portée locale en tant que L.
  • Sur L = [], l'affectation ne peut pas réussir à foo.func_defs[0], car cet attribut est en lecture seule.
  • Selon (1) , une nouvelle variable locale également nommée Lest créée dans la portée locale et utilisée pour le reste de l'appel de fonction. foo.func_defs[0]reste donc inchangé pour les invocations futures de foo.
Dmitry Minkovsky
la source
19

Je vais démontrer une structure alternative pour passer une valeur de liste par défaut à une fonction (cela fonctionne aussi bien avec les dictionnaires).

Comme d'autres l'ont longuement commenté, le paramètre de liste est lié à la fonction lorsqu'il est défini et non lorsqu'il est exécuté. Étant donné que les listes et les dictionnaires sont modifiables, toute modification de ce paramètre affectera les autres appels à cette fonction. Par conséquent, les appels suivants à la fonction recevront cette liste partagée qui peut avoir été modifiée par tout autre appel à la fonction. Pire encore, deux paramètres utilisent en même temps le paramètre partagé de cette fonction, inconscient des modifications apportées par l'autre.

Mauvaise méthode (probablement ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Vous pouvez vérifier qu'ils sont un seul et même objet en utilisant id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Selon Brett Slatkin, «Python efficace: 59 façons spécifiques d'écrire mieux en Python», article 20: Utiliser Noneet Docstrings pour spécifier des arguments dynamiques par défaut (p. 48)

La convention pour obtenir le résultat souhaité en Python est de fournir une valeur par défaut Noneet de documenter le comportement réel dans la docstring.

Cette implémentation garantit que chaque appel à la fonction reçoit la liste par défaut ou bien la liste transmise à la fonction.

Méthode préférée :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

Il peut y avoir des cas d'utilisation légitimes pour la «mauvaise méthode» par lesquels le programmeur a voulu que le paramètre de liste par défaut soit partagé, mais c'est plus probablement l'exception que la règle.

Alexandre
la source
17

Les solutions ici sont:

  1. Utilisez Nonecomme valeur par défaut (ou un nonce object) et activez-la pour créer vos valeurs lors de l'exécution; ou
  2. Utilisez a lambdacomme paramètre par défaut et appelez-le dans un bloc try pour obtenir la valeur par défaut (c'est le genre de chose à laquelle l'abstraction lambda est destinée).

La deuxième option est agréable car les utilisateurs de la fonction peuvent passer un appelable, qui peut être déjà existant (comme un type)

Marcin
la source
16

Quand nous faisons cela:

def foo(a=[]):
    ...

... nous assignons l'argument aà une liste sans nom , si l'appelant ne transmet pas la valeur de a.

Pour simplifier les choses pour cette discussion, donnons temporairement un nom à la liste sans nom. Et alors pavlo?

def foo(a=pavlo):
   ...

À tout moment, si l'appelant ne nous dit pas ce que ac'est, nous le réutilisons pavlo.

Si pavloest mutable (modifiable), et foofinit par le modifier, un effet que nous remarquons la prochaine fois fooest appelé sans préciser a.

Voici donc ce que vous voyez (rappelez-vous, pavloest initialisé à []):

 >>> foo()
 [5]

Maintenant, pavloc'est [5].

Un foo()nouvel appel modifie à pavlonouveau:

>>> foo()
[5, 5]

Le fait de spécifier alors de l'appel foo()garantit qu'il pavlon'est pas touché.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Alors, pavloc'est encore [5, 5].

>>> foo()
[5, 5, 5]
Saish
la source
16

J'exploite parfois ce comportement comme une alternative au modèle suivant:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Si singletonest uniquement utilisé par use_singleton, j'aime le modèle suivant en remplacement:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Je l'ai utilisé pour instancier des classes clientes qui accèdent à des ressources externes, et aussi pour créer des dictés ou des listes pour la mémorisation.

Comme je ne pense pas que ce modèle soit bien connu, je mets un petit commentaire pour me prémunir contre de futurs malentendus.

bgreen-litl
la source
2
Je préfère ajouter un décorateur pour la mémorisation et mettre le cache de mémorisation sur l'objet de fonction lui-même.
Stefano Borini
Cet exemple ne remplace pas le modèle plus complexe que vous affichez, car vous appelez _make_singletonà def time dans l'exemple d'argument par défaut, mais à call time dans l'exemple global. Une véritable substitution utiliserait une sorte de boîte mutable pour la valeur d'argument par défaut, mais l'ajout de l'argument donne l'occasion de passer des valeurs alternatives.
Yann Vernier
15

Vous pouvez contourner cela en remplaçant l'objet (et donc le lien avec la portée):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

Moche, mais ça marche.

joedborg
la source
3
C'est une bonne solution dans les cas où vous utilisez un logiciel de génération automatique de documentation pour documenter les types d'arguments attendus par la fonction. Mettre a = None puis définir a sur [] si a est None n'aide pas le lecteur à comprendre d'un coup d'œil ce qui est attendu.
Michael Scott Cuthbert
Idée sympa: relier ce nom garantit qu'il ne peut jamais être modifié. J'aime beaucoup ça.
holdenweb
C'est exactement la façon de procéder. Python ne fait pas de copie du paramètre, c'est donc à vous de faire la copie explicitement. Une fois que vous en avez une copie, il vous appartient de la modifier à votre guise sans aucun effet secondaire inattendu.
Mark Ransom
13

Il peut être vrai que:

  1. Quelqu'un utilise toutes les fonctionnalités de langue / bibliothèque, et
  2. Changer de comportement ici serait mal avisé, mais

il est tout à fait cohérent de conserver les deux caractéristiques ci-dessus et de faire un autre point:

  1. C'est une fonctionnalité déroutante et c'est malheureux en Python.

Les autres réponses, ou au moins certaines d'entre elles font les points 1 et 2 mais pas 3, ou font le point 3 et minimisent les points 1 et 2. Mais les trois sont vrais.

Il est peut-être vrai que changer de cheval à mi-chemin ici demanderait une rupture importante et qu'il pourrait y avoir plus de problèmes créés en changeant Python pour gérer intuitivement l'extrait d'ouverture de Stefano. Et il peut être vrai que quelqu'un qui connaissait bien les internes de Python pourrait expliquer un champ de mines de conséquences. cependant,

Le comportement existant n'est pas Pythonic, et Python réussit parce que très peu de choses sur le langage violent le principe du moindre étonnement n'importe où prèsmal. C'est un vrai problème, qu'il soit sage ou non de le déraciner. C'est un défaut de conception. Si vous comprenez mieux le langage en essayant de retracer le comportement, je peux dire que C ++ fait tout cela et plus encore; vous apprenez beaucoup en naviguant, par exemple, sur de subtiles erreurs de pointeur. Mais ce n'est pas Pythonic: les gens qui se soucient suffisamment de Python pour persévérer face à ce comportement sont des gens attirés par le langage parce que Python a beaucoup moins de surprises que les autres langages. Les barboteurs et les curieux deviennent des pythonistes lorsqu'ils sont étonnés du peu de temps qu'il faut pour faire fonctionner quelque chose - pas à cause d'un design fl - je veux dire, un puzzle logique caché - qui coupe contre les intuitions des programmeurs attirés par Python. parce que ça marche .

Christos Hayward
la source
6
-1 Bien qu'une perspective défendable, ce n'est pas une réponse, et je suis en désaccord avec elle. Trop d'exceptions spéciales engendrent leurs propres cas d'angle.
Marcin
3
Alors, il est "incroyablement ignorant" de dire qu'en Python, il serait plus logique qu'un argument par défaut de [] reste [] chaque fois que la fonction est appelée?
Christos Hayward
3
Et il est ignorant de considérer comme un idiome malheureux de définir un argument par défaut sur Aucun, puis dans le corps du corps de la fonction si argument == Aucun: argument = []? Est-il ignorant de considérer cet idiome comme malheureux car souvent les gens veulent ce à quoi un nouveau venu naïf pourrait s'attendre, que si vous affectez f (argument = []), l'argument sera automatiquement réglé par défaut sur une valeur de []?
Christos Hayward
3
Mais en Python, une partie de l'esprit du langage est que vous n'avez pas à faire trop de plongées profondes; array.sort () fonctionne et fonctionne indépendamment du peu de connaissances que vous avez sur le tri, le big-O et les constantes. La beauté de Python dans le mécanisme de tri des tableaux, pour donner l'un des innombrables exemples, est que vous n'êtes pas obligé de plonger profondément dans les internes. Et pour le dire différemment, la beauté de Python est qu'il n'est généralement pas nécessaire de plonger profondément dans l'implémentation pour obtenir quelque chose qui fonctionne correctement. Et il existe une solution de contournement (... si argument == Aucun: argument = []), FAIL.
Christos Hayward
3
En tant que autonome, l'instruction x=[]signifie «créer un objet de liste vide et y lier le nom« x »». Ainsi, dans def f(x=[]), une liste vide est également créée. Il n'est pas toujours lié à x, il est donc lié au substitut par défaut. Plus tard, lorsque f () est appelé, la valeur par défaut est supprimée et liée à x. Étant donné que c'est la liste vide elle-même qui a été écartée, cette même liste est la seule chose disponible pour se lier à x, que quelque chose y soit coincé ou non. Comment pourrait-il en être autrement?
Jerry B
10

Ce n'est pas un défaut de conception . Quiconque trébuche sur cela fait quelque chose de mal.

Il y a 3 cas où je peux rencontrer ce problème:

  1. Vous avez l'intention de modifier l'argument comme effet secondaire de la fonction. Dans ce cas, il n'a jamais de sens d'avoir un argument par défaut. La seule exception est lorsque vous abusez de la liste d'arguments pour avoir des attributs de fonction, par exemple cache={}, et qu'on ne devrait pas du tout appeler la fonction avec un argument réel.
  2. Vous avez l' intention de quitter l'argument non modifié, mais vous avez accidentellement ne le modifier. C'est un bug, corrigez-le.
  3. Vous avez l'intention de modifier l'argument à utiliser dans la fonction, mais vous ne vous attendiez pas à ce que la modification soit visible en dehors de la fonction. Dans ce cas, vous devez faire une copie de l'argument, que ce soit la valeur par défaut ou non! Python n'est pas un langage d'appel par valeur, il ne fait donc pas la copie pour vous, vous devez être explicite à ce sujet.

L'exemple dans la question pourrait tomber dans la catégorie 1 ou 3. Il est étrange qu'il modifie à la fois la liste passée et la renvoie; vous devez choisir l'un ou l'autre.

Mark Ransom
la source
"Faire quelque chose de mal" est le diagnostic. Cela dit, je pense qu'il y a des moments où = aucun motif n'est utile, mais généralement vous ne voulez pas modifier s'il est passé un mutable dans ce cas (2). Le cache={}modèle est vraiment une solution d'interview uniquement, en vrai code que vous voulez probablement @lru_cache!
Andy Hayden
9

Ce "bug" m'a donné beaucoup d'heures supplémentaires! Mais je commence à en voir une utilisation potentielle (mais j'aurais aimé que ce soit au moment de l'exécution, quand même)

Je vais vous donner ce que je vois comme un exemple utile.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

imprime ce qui suit

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
Norfeldt
la source
8

Changez simplement la fonction pour qu'elle soit:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
ytpillai
la source
7

Je pense que la réponse à cette question réside dans la façon dont python transmet les données au paramètre (passe par valeur ou par référence), pas la mutabilité ou comment python gère la déclaration "def".

Une brève introduction. Tout d'abord, il existe deux types de types de données en python, l'un est un type de données élémentaire simple, comme les nombres, et un autre type de données, les objets. Deuxièmement, lors de la transmission de données à des paramètres, python transmet le type de données élémentaires par valeur, c'est-à-dire, fait une copie locale de la valeur dans une variable locale, mais transmet l'objet par référence, c'est-à-dire des pointeurs vers l'objet.

En admettant les deux points ci-dessus, expliquons ce qui est arrivé au code python. C'est uniquement à cause du passage par référence pour les objets, mais n'a rien à voir avec mutable / immuable, ou sans doute le fait que l'instruction "def" n'est exécutée qu'une seule fois lorsqu'elle est définie.

[] est un objet, donc python passe la référence de [] à a, c'est- à -dire qu'il an'est qu'un pointeur vers [] qui se trouve dans la mémoire en tant qu'objet. Il n'y a qu'une seule copie de [] avec cependant de nombreuses références. Pour le premier foo (), la liste [] est remplacée par 1 par la méthode append. Mais notez qu'il n'y a qu'une seule copie de l'objet liste et que cet objet devient maintenant 1 . Lors de l'exécution du deuxième foo (), ce que la page Web d'effbot dit (les éléments ne sont plus évalués) est faux. aest évalué comme étant l'objet de liste, bien que maintenant le contenu de l'objet soit 1 . C'est l'effet de passer par référence! Le résultat de foo (3) peut être facilement dérivé de la même manière.

Pour valider davantage ma réponse, jetons un œil à deux codes supplémentaires.

====== No. 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]est un objet, il en est de même None(le premier est mutable tandis que le second est immuable. Mais la mutabilité n'a rien à voir avec la question). Aucun n'est quelque part dans l'espace mais nous savons qu'il est là et il n'y a qu'une seule copie de None. Ainsi, chaque fois que foo est invoqué, les éléments sont évalués (par opposition à une réponse selon laquelle ils ne sont évalués qu'une seule fois) pour être None, pour être clair, la référence (ou l'adresse) de None. Ensuite, dans le foo, l'élément est changé en [], c'est-à-dire qu'il pointe vers un autre objet qui a une adresse différente.

====== No. 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

L'appel de foo (1) fait pointer les éléments vers un objet de liste [] avec une adresse, par exemple, 11111111. le contenu de la liste est changé à 1 dans la fonction foo dans la suite, mais l'adresse n'est pas modifiée, toujours 11111111 Puis foo (2, []) arrive. Bien que le [] dans foo (2, []) ait le même contenu que le paramètre par défaut [] lors de l'appel de foo (1), leur adresse est différente! Puisque nous fournissons le paramètre explicitement, itemsdoit prendre l'adresse de ce nouveau[] , disons 2222222, et le renvoyer après avoir apporté quelques modifications. Maintenant foo (3) est exécuté. puisque seulementxest fourni, les éléments doivent reprendre leur valeur par défaut. Quelle est la valeur par défaut? Il est défini lors de la définition de la fonction foo: l'objet liste situé dans 11111111. Ainsi, les éléments sont évalués comme étant l'adresse 11111111 ayant un élément 1. La liste située à 2222222 contient également un élément 2, mais elle n'est pointée par aucun élément plus. Par conséquent, un ajout de 3 fera items[1,3].

D'après les explications ci-dessus, nous pouvons voir que la page Web effbot recommandée dans la réponse acceptée n'a pas donné de réponse pertinente à cette question. De plus, je pense qu'un point de la page Web effbot est faux. Je pense que le code concernant l'interface utilisateur est correct:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Chaque bouton peut contenir une fonction de rappel distincte qui affichera une valeur différente de i. Je peux fournir un exemple pour montrer ceci:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Si nous exécutons, x[7]()nous obtiendrons 7 comme prévu et x[9]()donnerons 9, une autre valeur de i.

user2384994
la source
5
Votre dernier point est faux. Essayez-le et vous verrez que x[7]()c'est le cas 9.
Duncan
2
"python passe le type de données élémentaires par valeur, c'est-à-dire, faire une copie locale de la valeur dans une variable locale" est complètement incorrect. Je suis étonné que quelqu'un puisse évidemment bien connaître Python, tout en ayant une horrible incompréhension des fondamentaux. :-(
Veky
6

TLDR: les valeurs par défaut au moment de la définition sont cohérentes et strictement plus expressives.


La définition d'une fonction affecte deux étendues: l'étendue de définition contenant la fonction et l'étendue d'exécution contenue par la fonction. Bien qu'il soit assez clair comment les blocs sont mappés aux étendues, la question est de savoir où def <name>(<args=defaults>):appartient:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

La def namepièce doit être évaluée dans le périmètre de définition - nous voulons namey être disponibles, après tout. L'évaluation de la fonction uniquement en elle-même la rendrait inaccessible.

Puisque parameterc'est un nom constant, on peut "l'évaluer" en même temps que def name. Cela a également l'avantage de produire la fonction avec une signature connue comme name(parameter=...):, au lieu d'un nu name(...):.

Maintenant, quand évaluer default?

La cohérence dit déjà "à la définition": tout le reste def <name>(<args=defaults>):est également mieux évalué à la définition. En retarder certaines parties serait le choix étonnant.

Les deux choix ne sont pas équivalents non plus: si defaultest évalué au moment de la définition, il peut toujours affecter le temps d'exécution. Si defaultest évalué au moment de l'exécution, il ne peut pas affecter le temps de définition. Choisir "à la définition" permet d'exprimer les deux cas, tandis que choisir "à l'exécution" ne peut en exprimer qu'un:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...
MonsieurMiyagi
la source
"La cohérence dit déjà" à la définition ": tout le reste def <name>(<args=defaults>):est également mieux évalué à la définition." Je ne pense pas que la conclusion découle de la prémisse. Ce n'est pas parce que deux choses sont sur la même ligne qu'elles doivent être évaluées dans la même portée. defaultest une chose différente du reste de la ligne: c'est une expression. L'évaluation d'une expression est un processus très différent de la définition d'une fonction.
LarsH
Les définitions des fonctions @LarsH sont évaluées en Python. Que cela provienne d'une instruction ( def) ou d'une expression ( lambda) ne change pas que la création d'une fonction signifie une évaluation - en particulier de sa signature. Et les valeurs par défaut font partie de la signature d'une fonction. Cela ne signifie pas que les valeurs par défaut doivent être évaluées immédiatement - les indices de type peuvent ne pas l'être, par exemple. Mais cela suggère certainement qu'ils devraient le faire à moins qu'il n'y ait une bonne raison de ne pas le faire.
MisterMiyagi
OK, créer une fonction signifie une évaluation dans un certain sens, mais évidemment pas dans le sens où chaque expression qu'elle contient est évaluée au moment de la définition. La plupart ne le sont pas. Je ne sais pas dans quel sens la signature est particulièrement "évaluée" au moment de la définition, pas plus que le corps de la fonction n'est "évalué" (analysé en une représentation appropriée); tandis que les expressions dans le corps de la fonction ne sont clairement pas évaluées au sens plein. De ce point de vue, la cohérence voudrait dire que les expressions dans la signature ne devraient pas non plus être "entièrement" évaluées.
LarsH
Je ne veux pas dire que vous vous trompez, mais seulement que votre conclusion ne découle pas uniquement de la cohérence.
LarsH
Les valeurs par défaut de @LarsH ne font pas partie du corps, et je ne prétends pas que la cohérence est le seul critère. Pouvez-vous suggérer comment clarifier la réponse?
MisterMiyagi
3

Toutes les autres réponses expliquent pourquoi c'est en fait un comportement agréable et souhaité, ou pourquoi vous ne devriez pas en avoir besoin de toute façon. Le mien est pour les têtus qui veulent exercer leur droit de plier la langue à leur gré, et non l'inverse.

Nous allons "corriger" ce comportement avec un décorateur qui copiera la valeur par défaut au lieu de réutiliser la même instance pour chaque argument positionnel laissé à sa valeur par défaut.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Redéfinissons maintenant notre fonction en utilisant ce décorateur:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

C'est particulièrement bien pour les fonctions qui prennent plusieurs arguments. Comparer:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

avec

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Il est important de noter que la solution ci-dessus se casse si vous essayez d'utiliser des arguments de mots clés, comme ceci:

foo(a=[4])

Le décorateur pourrait être ajusté pour permettre cela, mais nous laissons cela comme un exercice pour le lecteur;)

Przemek D
la source