«Least Astonishment» et l'argument Mutable par défaut

Jul 16 2009

Toute personne bricolant Python assez longtemps a été mordue (ou déchirée) 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 gérants a eu sa première rencontre avec cette fonctionnalité et l'a qualifiée de «défaut 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 éléments internes. Cependant, je n'ai pas été en mesure de 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 engendrer de bugs?)

Modifier :

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

>>> 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ù placer 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, ce qui présenterait une faille profonde: la defligne serait "hybride" dans le sens où une partie de la liaison (de l'objet fonction) se produirait à la définition, et part (affectation des paramètres par défaut) au moment de l'appel de la fonction.

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

Réponses

1658 rob Jul 18 2009 at 04:29

En fait, ce n'est pas un défaut de conception et ce n'est pas à cause des composants 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 pensez de cette manière, alors cela prend tout son sens: une fonction est un objet évalué sur sa définition; les paramètres par défaut sont des sortes de "données membres" et donc leur état peut changer d'un appel à l'autre - exactement comme dans n'importe quel autre objet.

Dans tous les cas, Effbot a une très belle explication des raisons de ce comportement dans les valeurs des paramètres par défaut en 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.

278 EliCourtwright Jul 16 2009 at 01:11

Supposons que vous ayez le code suivant

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

def eat(food=fruits):
    ...

Quand je vois la déclaration de eat, 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 la fonction, alors je serais étonné (d'une très mauvaise manière) de découvrir que les fruits ont été modifiés. Ce serait plus étonnant IMO que de découvrir que votre foofonction ci-dessus était en train de muter 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é d'extraire l'objet en Maputilisant 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 en fait pourquoi Python ne permet pas à ses types de données intégrés mutables d'être 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 à un comportement ou à un comportement opposé en fonction du code qu'il écrit.

J'aime personnellement 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 une liste vide dans des cas spéciaux, mais ce genre de cas particulier causerait encore plus d'étonnement, sans parler d'être rétrocompatible.

244 glglgl Jul 10 2012 at 21:50

La partie pertinente de la documentation :

Les valeurs de paramètre 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 mutable, tel qu'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 ce problème consiste à utiliser Nonepar défaut et à 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
121 Utaal Jul 16 2009 at 06:21

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 d'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-vous utiliser apar défaut la date / heure correspondant à la définition ou à l'exécution x? Dans ce cas, comme dans le précédent, je garderai le même comportement que si l'argument par défaut "affectation" était la première instruction de la fonction ( datetime.now()appelée lors de l'invocation 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):
83 LennartRegebro Jul 16 2009 at 01:54

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, enfin ... lorsque les fonctions sont définies.

Comparez ceci:

class BananaBunch:
    bananas = []

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

Ce code souffre du même événement inattendu. 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 d'objet, car vous auriez à garder le code de classe autour et l'exécuter lorsque les objets sont créés.

Oui, c'est inattendu. Mais une fois que le sou tombe, cela correspond parfaitement au fonctionnement de Python en général. En fait, c'est un bon outil pédagogique, et une fois que vous comprendrez pourquoi cela se produit, vous allez beaucoup mieux grok python.

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

69 DimitrisFasarakisHilliard Dec 09 2015 at 14:13

Pourquoi ne pas introspecter?

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

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

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

Lorsque Python le rencontre, la première chose qu'il fera est de le compiler afin de créer un codeobjet pour cette fonction. Pendant que cette étape de compilation est terminée, Python évalue * puis stocke les arguments par défaut (une liste vide []ici) dans l'objet fonction lui-même . Comme la réponse principale l'a mentionné: la liste apeut maintenant ê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_defaultsen Python 2; oui, deux noms pour la même chose).

Fonction avant exécution:

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

Une fois que Python a 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 ceux- ci:

>>> 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 incorporé :

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

Donc, là 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 ne se passe rien de bizarre ici, c'est juste un peu surprenant.

La solution courante pour lutter contre cela consiste à utiliser Nonecomme valeur par défaut, puis à initialiser dans le corps de la fonction:

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

Comme 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é pour a.


Pour vérifier davantage que la liste dans __defaults__est la même que celle utilisée dans la fonction, funcvous pouvez simplement modifier votre fonction pour renvoyer idla liste autilisée dans le corps de la fonction. Ensuite, comparez-le à la liste dans __defaults__(position [0]in __defaults__) et vous verrez comment ceux-ci font effectivement référence à 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 que le processus de construction de la fonction et de la liaison au nom ne barsoit fait.

59 Brian Jul 16 2009 at 17:05

J'avais l'habitude de penser que la création des objets au moment de l'exécution serait la meilleure approche. Je suis moins sûr maintenant, car vous perdez certaines fonctionnalités utiles, bien que cela puisse en valoir 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 au moment de l'appel est utilisée, la fonction coûteuse est appelée chaque fois que votre fonction est utilisée sans argument. Vous paieriez un prix élevé à chaque appel, ou vous deviez mettre manuellement en cache 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, de sorte que vous obtiendrez une liste de fonctions qui ont toutes renvoyées 9.

La seule façon d'implémenter cela autrement serait de créer une autre 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.

Maintenant, supposons que le comportement des valeurs par défaut puisse être modifié pour 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 obtenir 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.

55 LutzPrechelt Mar 30 2015 at 18:18

5 points pour la 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 de l'en-tête de la fonction (pas du corps de la fonction). Il doit donc être évalué au moment du chargement du module (et uniquement au moment du chargement du module, sauf s'il est imbriqué), pas 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 basique, le didacticiel, le problème est annoncé bruyamment 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 titres. RTFM: Lisez le manuel détaillé.

  5. Méta-apprentissage : Tomber dans le piège est en fait un moment très utile (du moins si vous êtes un apprenant réfléchi), car vous comprendrez par la suite mieux le point «Cohérence» ci-dessus et cela vous en apprendra beaucoup sur Python.

53 ymv Jul 16 2009 at 02:15

Ce comportement s'explique facilement par:

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

Alors:

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 changements - l'opération est effectuée sur le même objet - et il est imprimé
35 GlennMaynard Jul 16 2009 at 03:18

Ce que vous demandez, c'est pourquoi:

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 de l'appel explicite de 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'entre eux et les évaluer lorsque la fonction est appelée?

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

35 hynekcer Nov 23 2012 at 01:09

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 la programmation fonctionnelle, généralement indésirable et devraient être fixés 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 copyou deepcopyl'objet d'entrée d' abord, puis de faire tout 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 minutieusement par copy.deepcopy()(ce dernier est 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 être ni enregistré dans aucun attribut public d'une instance renvoyée par cette fonction. (En supposant que les attributs privés de l'instance ne doivent pas être modifiés de l'extérieur de cette classe ou des sous-classes par convention. C'est-à _var1- dire qu'il s'agit d'un attribut privé)

Conclusion: les
objets paramètres d'entrée ne doivent pas être modifiés sur place (mutés) ni ê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 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, la solution utile est def ...(var1=None): if var1 is None: var1 = [] Plus ..

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

31 Ben May 23 2011 at 11:24

Cela n'a en fait rien à voir avec les valeurs par défaut, à part cela, cela 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 fooest en train de modifier une variable mutable passé depuis l'appelant, lorsque l'appelant ne s'attend pas. Un code comme celui-ci conviendrait si la fonction était appelée quelque chose comme append_5; alors l'appelant appellerait la fonction afin de modifier la valeur qu'ils transmettent, et le comportement serait attendu. Mais il est très peu probable qu'une telle fonction prenne un argument par défaut, et ne renverrait 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 passé ou s'il a obtenu la valeur par défaut. Votre code doit laisser les arguments mutables seuls à moins qu'il ne soit clair d'après le contexte / nom / documentation que les arguments sont censés être modifiés. Utiliser des valeurs mutables transmises 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 avez besoin de 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.

27 Stéphane Mar 27 2015 at 06:14

Sujet déjà chargé, mais d'après ce que j'ai lu ici, ce qui suit m'a 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
25 JasonBaker Jul 16 2009 at 06:18

C'est une optimisation des performances. En raison de cette fonctionnalité, lequel de ces deux appels de fonction pensez-vous est 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 (voirhttp://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 engendrer de bugs?)

Comme vous pouvez le voir, l' utilisation d'arguments par défaut immuables présente un avantage en termes de performances. 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.

25 AaronHall May 01 2016 at 23:20

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 sont mutés (par exemple, en y ajoutant un élément), ils restent mutés lors d'appels consécutifs.

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

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'il s'agit du même objet à chaque fois qu'ils sont référencés par

  • voir que la liste est créée avant que la fonction n'ait fini de se compiler en un objet fonction,
  • en observant que l'identifiant est le même à chaque fois que la liste est référencée,
  • en observant que la liste reste modifiée lorsque la fonction qui l'utilise est appelée une seconde fois,
  • en observant l'ordre dans lequel la sortie est imprimée à partir de la source (que j'ai 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 attendu.

L'instruction habituelle aux nouveaux utilisateurs de Python:

Mais c'est pourquoi l'instruction habituelle aux 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 celui 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
24 Baczek Jul 16 2009 at 19:19

La réponse la plus courte serait probablement "la définition est l'exécution", donc l'argument entier 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 piège lorsque vous essayez d'utiliser des constructeurs par défaut, cependant.

21 DmitryMinkovsky Apr 25 2012 at 02:43

Ce comportement n'est pas surprenant si vous prenez en compte les é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és dans la réponse acceptée).

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

(1) est décrit dans le didacticiel 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, laissant inchangée la variable externe nommée de manière identique ).

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

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

Voici fooun objet et aun attribut de foo(disponible sur foo.func_defs[0]). Puisque aest une liste, aest mutable et est donc un attribut en lecture-écriture de foo. Il est initialisé à 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 par défaut de foo.func_defs. Dans ce cas, foo.func_defs[0]est utilisé adans la portée du 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 sorte que les valeurs par défaut de signature de fonction soient utilisées chaque fois que la fonction est exécutée:

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 de fonction est instancié, foo.func_defs[0]est défini sur None, un objet immuable.
  • Lorsque la fonction est exécutée avec les valeurs par défaut (sans paramètre spécifié Ldans l'appel de fonction), foo.func_defs[0]( None) est disponible dans la portée locale sous la forme L.
  • Ensuite L = [], l'affectation ne peut pas aboutir foo.func_defs[0], car cet attribut est en lecture seule.
  • Par (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 futures invocations de foo.
20 hugo24 Feb 28 2013 at 18:10

Une solution de contournement simple en utilisant Aucun

>>> 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]
19 Alexander Sep 12 2015 at 13:00

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, par opposition à 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. En conséquence, les appels ultérieurs à 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 le paramètre partagé de cette fonction en même temps sans tenir compte 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'il s'agit d'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 un meilleur 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 passée à 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» dans laquelle 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.

17 Marcin Mar 21 2012 at 00:22

Les solutions ici sont:

  1. Utilisez Nonecomme valeur par défaut (ou nonce object), et activez-la pour créer vos valeurs au moment 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 intéressante car les utilisateurs de la fonction peuvent passer un appelable, qui peut être déjà existant (comme a type)

16 joedborg Jan 15 2013 at 18:02

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.

16 Saish Sep 12 2014 at 05:05

Lorsque nous faisons cela:

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

... nous affectons 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 pourquoi pas pavlo?

def foo(a=pavlo):
   ...

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

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

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

 >>> foo()
 [5]

Maintenant, pavloc'est [5].

Le rappel foo()modifie à pavlonouveau:

>>> foo()
[5, 5]

La spécification alors de l'appel foo()garantit que ce pavlon'est pas touché.

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

Donc, pavloc'est toujours [5, 5].

>>> foo()
[5, 5, 5]
16 bgreen-litl Feb 06 2015 at 04:44

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 singletonn'est utilisé que 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 client qui accèdent à des ressources externes, ainsi que pour créer des dictionnaires ou des listes pour la mémorisation.

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

13 ChristosHayward Jul 17 2009 at 02:17

Il est 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 fonctionnalités ci-dessus et de faire encore un autre point:

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

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

Il est peut-être vrai que changer de chevaux à mi-chemin ici demanderait une casse 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 est peut-être vrai que quelqu'un qui connaissait bien les composants internes de Python pourrait expliquer un champ de mines de conséquences. cependant,

Le comportement existant n'est pas pythonique, et Python réussit parce que très peu de choses dans le langage violent le principe du moindre étonnement à un niveau proche de celui-ci. 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 beaucoup 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, dans 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 qui sont attirés par le langage car Python a beaucoup moins de surprises que les autres langages. Les amateurs 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 de logique caché - qui va à l'encontre des intuitions des programmeurs qui sont attirés par Python parce que ça marche .

10 MarkRansom Oct 18 2017 at 00:38

Ce n'est pas un défaut de conception . Quiconque trébuche là-dessus fait quelque chose de mal.

Il y a 3 cas que je vois où vous pourriez rencontrer ce problème:

  1. Vous avez l'intention de modifier l'argument comme effet secondaire de la fonction. Dans ce cas, il n'est jamais logique 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 que vous ne devriez pas du tout appeler la fonction avec un argument réel.
  2. Vous avez l'intention de ne pas modifier l'argument, mais vous l'avez accidentellement modifié. C'est un bug, corrigez-le.
  3. Vous avez l'intention de modifier l'argument à utiliser dans la fonction, mais vous ne vous attendez pas à ce que la modification soit visible en dehors de la fonction. Dans ce cas, vous devez faire une copie de l'argument, qu'il s'agisse de 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 de la question pourrait tomber dans la catégorie 1 ou 3. Il est étrange que cela modifie à la fois la liste transmise et la renvoie; vous devriez choisir l'un ou l'autre.

9 Norfeldt Jul 22 2013 at 14:35

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

Je vais vous donner ce que je considère 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
8 ytpillai May 26 2015 at 06:04

Changez simplement la fonction en:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
7 user2384994 Aug 22 2013 at 12:58

Je pense que la réponse à cette question réside dans la manière dont python transmet les données au paramètre (passer par valeur ou par référence), pas dans la mutabilité ou dans la manière dont python gère l'instruction "def".

Une brève introduction. Premièrement, 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 est des objets. Deuxièmement, lors du passage des données aux paramètres, python passe type de données élémentaires par valeur, c'est-à-dire, fait une copie locale de la valeur vers une variable locale, mais passe 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 cela 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 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 changée en 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 dit la page Web d'effbot (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 du passage 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 coup d'œil à deux codes supplémentaires.

====== N ° 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, 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 il n'est évalué qu'une seule fois) pour être None, pour être clair, la référence (ou l'adresse) de None. Ensuite, dans le toto, l'élément est changé en [], c'est-à-dire qu'il pointe vers un autre objet qui a une adresse différente.

====== N ° 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 que les éléments pointent vers un objet de liste [] avec une adresse, disons, 11111111. le contenu de la liste est changé en 1 dans la fonction foo dans la suite, mais l'adresse n'est pas modifiée, toujours 11111111 Ensuite, foo (2, []) arrive. Bien que [] 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, itemsil faut prendre l'adresse de ce nouveau [], disons 2222222, et le renvoyer après avoir fait quelques changements. Maintenant, foo (3) est exécuté. puisque seul xest 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é en 11111111. Ainsi, les éléments sont évalués comme étant l'adresse 11111111 ayant un élément 1. La liste située en 2222222 contient également un élément 2, mais elle n'est pointée par des éléments. plus. Par conséquent, un append de 3 fera items[1,3].

À partir des explications ci-dessus, nous pouvons voir que la page Web d' 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 d'effbot est faux. Je pense que le code concernant le UI.Button 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 donner 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 nous x[9]()donnerons 9, une autre valeur de i.

6 MisterMiyagi Dec 15 2018 at 19:09

TLDR: Les valeurs par défaut de l'heure de définition sont cohérentes et strictement plus expressives.


La définition d'une fonction affecte deux portées: la portée de définition contenant la fonction et la portée d'exécution contenue par la fonction. Bien qu'il soit assez clair comment les blocs correspondent aux portées, 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 cadre de définition - nous voulons namey être disponibles, après tout. Évaluer la fonction uniquement à l'intérieur d'elle-même la rendrait inaccessible.

Puisqu'il parameters'agit d'un nom constant, nous pouvons "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'une simple 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 un choix étonnant.

Les deux choix ne sont pas non plus équivalents: si defaultest évalué au moment de la définition, cela peut toujours affecter le temps d'exécution. Si defaultest évalué au moment de l'exécution, il ne peut pas affecter le moment de la 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
    ...
4 PrzemekD Jan 03 2019 at 14:38

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

Nous "corrigerons" 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

Ceci est particulièrement intéressant pour les fonctions qui acceptent 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 est interrompue si vous essayez d'utiliser des arguments de mot-clé, comme ceci:

foo(a=[4])

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