3213 votes

"Moins d'Étonnement" en Python: La Mutable Argument par Défaut

Quelqu'un bricolage avec Python assez longtemps a été mordu (ou déchiré) par le problème suivant:

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

Python novices attendre cette fonction retourne toujours une liste avec un seul élément: [5]. Le résultat est plutôt très différents, et très étonnant (pour un débutant):

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

Un gestionnaire de la mine une fois eu sa première rencontre avec cette fonctionnalité, et l'a appelé "une dramatique défaut de conception" de la langue. J'ai répondu que le comportement a eu une explication, et il est en effet très surprenant et inattendu si vous ne comprenez pas le fonctionnement interne. Cependant, je n'étais pas en mesure de répondre (pour moi) à 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 les expérimentés comportement a une utilisation pratique (qui a vraiment utilisé les variables statiques en C, sans reproduction des bugs ?)

Edit:

Baczek fait un exemple intéressant. Avec la plupart de vos commentaires et Utaal en particulier, j'ai précisé:

>>> 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 a été par rapport à l'endroit où placer la portée de paramètres: l'intérieur de la fonction ou "ensemble"?

Faire la liaison à l'intérieur de la fonction signifierait qu' x est effectivement lié à la valeur par défaut lorsque la fonction est appelée, ne sont pas définis, ce qui permettrait de présenter une profonde faille: l' def ligne serait "hybride" dans le sens qu'une partie de la liaison (de la fonction de l'objet) arriverait-il à la définition, et la partie (affectation de paramètres par défaut) lors de l'invocation de la fonction de temps.

Le comportement réel est plus cohérent: tout de qui ligne est évaluée lorsque cette ligne est exécutée, un sens à la définition de la fonction.

Guido est un designer fantastique.

Modifier

J'ai relu toutes les très intéressantes et de bonnes réponses que vous avez fournies, et il est difficile d'attribuer une "correcte tickmark", que tout le monde avait de bons points dans la réponse. J'ai marqué Roberto réponse comme correcte parce que c'était plus simple et de révéler, de sorte que les nouveaux arrivants de la navigation à cette question peut commencer à partir de sa réponse et se plonger dans l'autre plus complexe (mais très perspicace) des réponses.

1843voto

Roberto Liffredo Points 15265

En fait, ce n'est pas un défaut de conception, et il n'est pas à cause de interne, ou de la performance.
Il s'agit 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 vous mettez à réfléchir de cette façon, alors il complètement de sens: une fonction est un objet évalué sur sa définition; les paramètres par défaut sont des sortes de "données" et, par conséquent, leur état peut changer d'un appel à l'autre, exactement comme dans n'importe quel autre objet.

En tout cas, Effbot a une très belle explication des raisons de ce comportement dans les Valeurs de Paramètre par Défaut de Python.
Je l'ai trouvé très clair, et je suggère la lecture pour une meilleure connaissance de la façon dont les objets de fonction de travail.

320voto

Eli Courtwright Points 53071

Supposons que vous avez le code suivant

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

def eat(food=fruits):
    ...

Quand je vois la déclaration de manger, le moins étonnant, c'est de penser que si le premier paramètre n'est pas donné, qui doit être égale à la tuple ("apples", "bannanas", "loganberries")

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

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

ensuite, si les paramètres par défaut ont été liés à l'exécution de la fonction plutôt que la fonction de déclaration-là, je serais étonné (très mal) à découvrir que les fruits ont été modifiés. Ce serait plus étonnant de l'OMI, que de découvrir que votre foo de la fonction ci-dessus a été la mutation de la liste.

Le vrai problème réside avec 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, est-ce ma carte, utiliser la valeur de la StringBuffer clé lorsqu'il a été placé dans la carte, ou faut-il stocker la clé par référence? De toute façon, quelqu'un est étonné; soit la personne qui a essayé d'obtenir l'objet de la Carte à l'aide d'une valeur identique à celle qu'ils ont mis dans, ou à la personne qui ne peut pas sembler récupérer leur ovject même si la clé de l'utilisation est littéralement le même objet qui a été utilisé pour le mettre dans la carte. (C'est en fait pourquoi Python ne permet pas son mutable builtin types de données pour être utilisés comme clés de dictionnaire.)

Votre exemple est un bon un des cas où Python les nouveaux arrivants seront surpris et mordu. Mais je dirais que si nous avons corrigé ce, alors que cela ne ferait que créer une situation différente, où ils avaient d'être mordu au lieu de cela, et que l'on serait encore moins intuitif. Par ailleurs, c'est toujours le cas lorsque l'on traite avec des variables mutables; vous courez toujours dans le cas où quelqu'un pourrait attendez un ou l'inverse comportement en fonction de ce code qu'ils écrivent.

Personnellement, j'aime Python approche actuelle: la valeur par défaut des arguments de la fonction sont évalués lorsque la fonction est définie et que l'objet est toujours la valeur par défaut. Je suppose qu'ils pouvaient cas à l'aide d'une liste vide, mais ce genre de revêtement spécial, causerait encore plus de l'étonnement, pour ne pas mentionner être incompatibles.

302voto

glglgl Points 35668

AFAICS personne n'a encore posté la partie pertinente de la documentation:

Valeurs par défaut des paramètres sont évalués lors de la définition de la fonction est exécutée. Cela signifie que l'expression est évaluée une seule fois, lorsque la fonction est définie, et que le même "pré-calculé" valeur est utilisée pour chaque appel. Cela est particulièrement important de comprendre que lorsque un paramètre par défaut est une mutable objet, 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. Une façon de contourner cela est d'Aucun usage en tant que par défaut, et explicitement test pour elle dans le corps de la fonction [...]

139voto

Utaal Points 2063

Je ne sais rien à propos de l'interpréteur Python rouages (et je ne suis pas un expert dans les compilateurs et interprètes) alors ne me blâmez pas si je propose de ne rien unsensible ou impossible.

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

a = []

vous prévoyez d'obtenir une nouvelle liste référencée par un.

Pourquoi l'a=[]

def x(a=[]):

instancier une nouvelle liste de définition de la fonction et non pas sur l'invocation? C'est juste comme vous êtes en demandant "si l'utilisateur ne fournit pas l'argument puis instancier une nouvelle liste et l'utiliser comme s'il avait été produit par l'appelant". Je pense que c'est ambigu, à la place:

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

l'utilisateur, voulez-vous l' un à défaut de l'datetime correspondant lorsque vous êtes à la définition ou de l'exécution de x? Dans ce cas, comme dans la précédente, je vais garder le même comportement que si l'argument par défaut "mission" était la première instruction de la fonction (datetime.now() est appelée sur l'invocation de la fonction). D'autre part, si l'utilisateur veut que la définition de la cartographie en temps il pouvait écrire:

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

Je sais, je sais: c'est une fermeture. Sinon Python pourrait fournir un mot-clé à force de définition de la liaison:

def x(static a=b):

94voto

Lennart Regebro Points 52510

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

Comparer ceci:

class BananaBunch:
    bananas = []

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

Ce code souffre de la même exacte inattendu hasard. la banane est un attribut de classe, et par conséquent, lorsque vous ajoutez des choses, il est ajouté à toutes les classes. La raison en est exactement de même.

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

Oui, c'est inattendu. Mais une fois que la pièce tombe, il s'adapte parfaitement à la façon Python fonctionne en général. En fait, c'est un bon outil pédagogique, et une fois que vous comprenez pourquoi cela se produit, vous aurez grok python beaucoup mieux.

Cela dit, il devrait figurer en bonne place dans toute bonne tutoriel Python. Parce que, comme vous l'avez mentionné, tout le monde court dans ce problème tôt ou tard.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X