"Le moins D'étonnement" et L'Argument par défaut Mutable

quiconque bricolant avec Python assez longtemps a été mordu (ou déchiré en morceaux) par le numéro suivant:

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

les novices Python s'attendraient à ce que cette fonction renvoie 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 ce dispositif, et l'a appelé "un défaut de conception dramatique" de la langue. J'ai répondu que le comportement avait une explication sous-jacente, et il est en effet très déroutant et inattendu si vous ne comprenez pas les internes. Cependant, je n'ai pas été en mesure de répondre (à moi-même) à la question suivante: Quelle est la raison pour lier 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 un usage pratique (qui a vraiment utilisé des variables statiques en C, sans créer de bogues?)

Edit :

Baczek fait un exemple intéressant. Avec la plupart de vos commentaires et ceux D'Utaal en particulier, j'ai élaboré plus loin:

>>> 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 que x est effectivement lié à la valeur par défaut lorsque le la fonction est appelée, non définie, quelque chose qui présenterait un défaut profond: la ligne def serait "hybride" dans le sens qu'une partie de la liaison (de l'objet de la fonction) se produirait à la définition, et une partie (assignation des paramètres par défaut) au moment de l'invocation de la fonction.

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

2114
demandé sur Matt 2009-07-15 22:00:37
la source

30 ответов

en fait, ce n'est pas un défaut de conception, et ce n'est pas à cause des intérieurs, 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 pensez 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 en quelque sorte des "données membres" et donc leur état peut changer d'un appel à l'autre. l'autre-exactement comme dans tout autre objet.

dans tous les cas, Effbot a une très belle explication des raisons de ce comportement dans valeurs par défaut des paramètres en Python .

Je l'ai trouvé très clair, et je suggère vraiment de le lire pour une meilleure connaissance de la façon dont les objets de fonction fonctionnent.

1386
répondu rob 2009-07-18 01:29:39
la source

supposons que vous ayez le code suivant

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

def eat(food=fruits):
    ...

quand je vois la déclaration d'eat, la chose la moins étonnante est de penser que si le premier paramètre n'est pas donné, qu'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é (très mal) de découvrir que les fruits avaient changé. Ce serait plus étonnant IMO que de découvrir que votre foo fonction ci-dessus était la mutation de 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, est-ce que ma carte utilise la valeur de la clé StringBuffer quand elle a été placée dans la carte, ou faut-il stocker la clé par référence? Quoi qu'il en soit, quelqu'un est étonné; soit la personne qui a essayé de sortir l'objet du Map en utilisant une valeur identique à celle avec laquelle ils l'ont mis, soit la personne qui ne semble pas pouvoir récupérer son objet même si la clé qu'ils utilisent 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égrables mutables d'être utilisés comme des clés de dictionnaires).

Votre exemple est un bon d'un cas où les nouveaux venus en Python seront surpris et mordus. Mais je dirais que si nous "corrigions" cela, alors cela ne créerait qu'une situation différente où ils seraient mordus à la place, et que l'on serait encore moins intuitif. De plus, c'est toujours le cas lorsqu'il s'agit de variables mutables; vous rencontrez toujours des cas où quelqu'un pourrait s'attendre intuitivement à un comportement ou le contraire selon le code qu'il écrit.

I personnellement comme l'approche actuelle de Python: les arguments de fonction par défaut sont évalués lorsque la fonction est définie et que l'objet est toujours la fonction par défaut. Je suppose qu'ils pourraient utiliser une liste vide, mais ce genre de boîtier spécial causerait encore plus d'étonnement, sans parler d'être incompatible à l'envers.

238
répondu Eli Courtwright 2016-11-27 17:24:50
la source

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

les valeurs par défaut des paramètres sont évaluées lorsque la définition de la 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 que 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 d'éviter cela est D'utiliser None comme valeur par défaut, et de tester explicitement cette valeur dans le corps de la fonction [...]

201
répondu glglgl 2012-07-10 18:50:42
la source

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

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

a = []

vous vous attendez à obtenir un nouveau liste référencée par un .

Pourquoi l'a=[] dans

def x(a=[]):

instancier une nouvelle liste de définition de la fonction et non pas sur l'invocation? C'est tout comme vous demandez "si l'utilisateur ne fournit pas l'argument alors instantiate une nouvelle liste et l'utiliser comme si elle a été produite par l'appelant". Je pense que c'est plutôt ambigu:

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

, voulez-vous un à défaut pour le type datetime correspondant lorsque vous êtes à la définition ou de l'exécution de x ? Dans ce cas, comme dans le précédent, je garderai le même comportement que si l'argument par défaut "assignment" é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 voulait la définition-time mapping il pourrait écrire:

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

je sais, je sais: c'est une fermeture. Sinon, Python pourrait fournir un mot-clé pour forcer la définition - time binding:

def x(static a=b):
100
répondu Utaal 2014-07-05 11:33:47
la source

Eh bien, la raison est tout simplement que les reliures sont faites quand le code est exécuté, et 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 exactement du même événement inattendu. bananes est un attribut de classe, et donc, quand vous ajoutez des choses à elle, il est ajouté à toutes les instances de cette classe. La raison est exactement la même.

c'est juste "Comment cela fonctionne", 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 l'instanciation de l'objet beaucoup, comme vous auriez à garder le code de classe autour et l'exécuter quand les objets sont créés.

Oui, c'est inattendu. Mais une fois que le penny tombe, il s'inscrit parfaitement dans la façon dont Python fonctionne en général. En fait, c'est un bon outil d'enseignement, et une fois que vous comprenez pourquoi cela se produit, vous grok python bien mieux.

qui dit qu'il devrait figurer bien en évidence dans tout bon tutoriel Python. Parce que comme vous le mentionnez, tout le monde se heurte à ce problème tôt ou tard.

75
répondu Lennart Regebro 2014-12-20 01:53:35
la source

je pense que la création d'objets lors de l'exécution serait la meilleure approche. Je suis moins sûr maintenant, puisque vous ne perdez pas certaines fonctionnalités utiles, bien qu'il puisse être la peine indépendamment tout simplement pour éviter la confusion des débutants. Les inconvénients sont:

1. Performance

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

si l'évaluation du temps d'appel est utilisée, alors la fonction coûteuse est appelée chaque fois que votre fonction est utilisée sans argument. Soit vous payez un prix élevé pour chaque appel, soit vous devez mettre en cache manuellement la valeur à l'extérieur, polluant votre espace de noms et ajoutant de la verbosité.

2. Forçant les paramètres liés

un truc utile est de lier les paramètres d'une lambda à la liaison courant d'une variable lorsque la lambda est créée. Par exemple:

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

ceci retourne une liste de fonctions qui rendent 0,1,2,3... respectivement. Si le comportement est modifié, ils lieront plutôt i à la valeur call-time de i, de sorte que vous obtiendrez une liste de fonctions qui ont toutes retourné 9 .

la seule façon de mettre en oeuvre ceci autrement serait de créer une nouvelle fermeture avec le I bound, i.e.:

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

3. Introspection

considérer 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 module inspect , qui

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

cette information est très utile pour 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é de sorte qu'il 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 retourner sous forme de chaîne de caractères.

53
répondu Brian 2009-07-16 23:13:35
la source

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 toujours passe les objets, pas les noms. Le paramètre par défaut fait évidemment partie de la fonction position (et non le corps fonctionnel). Il par conséquent, devrait être évalué au moment de la charge du module (et seulement au moment de la charge du module, sauf s'il est imbriqué), en fonction du temps d'appel.

  3. utilité : comme le souligne Frederik Lundh dans son explication de "valeurs des paramètres par défaut en Python" , le comportement actuel peut être très utile pour la programmation avancée. (À utiliser avec parcimonie.)

  4. suffisant documentation : dans la documentation Python la plus basique, le tutoriel, le numéro est annoncé à haute voix comme un "attention" dans le première paragraphe de l'Article "Plus d'informations sur la Définition des" Fonctions . L'avertissement utilise même en gras, qui est rarement appliqué en dehors des rubriques. RTFM: lire le manuel fine.

  5. Méta-apprentissage : Tomber dans le piège est en fait un très moment utile (au moins si vous êtes un apprenant réfléchi), parce que vous comprendrez mieux par la suite "La cohérence" ci-dessus et qui vous en apprendre beaucoup sur Python.

50
répondu Lutz Prechelt 2016-08-26 11:33:17
la source

pourquoi ne pas vous introspecter?

je suis vraiment surpris personne n'a effectué l'perspicace introspection offert par Python ( 2 et 3 appliquer) sur callables.

donne une fonction simple func défini comme:

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

lorsque Python le rencontre, la première chose à faire est de le compiler afin de créer un objet code pour ce fonction. Alors que cette étape de compilation est faite, Python évalue * puis stocke les arguments par défaut (une liste vide [] ici) dans l'objet de fonction lui-même . Comme la première réponse l'a mentionné: la liste a peut 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 le liste est étendue à l'intérieur de l'objet de fonction. J'utilise Python 3.x pour ceci, pour Python 2 la même chose s'applique (utilisez __defaults__ ou func_defaults en Python 2; Oui, deux noms pour la même chose).

Fonction Avant Exécution:

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

après que Python exécute cette définition, il va prendre tous les paramètres par défaut spécifiés ( a = [] ici) et cram eux dans l'attribut __defaults__ pour l'objet de fonction (section pertinente: Callables):

>>> func.__defaults__
([],)

o. k, donc une liste vide comme la seule entrée dans __defaults__ , comme prévu.

Fonction Après Exécution:

exécutons maintenant cette fonction:

>>> func()

maintenant, voyons ces __defaults__ encore une fois:

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

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

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

Donc, voilà, la raison pour laquelle ce " flaw " arrive, c'est parce que les arguments par défaut font partie de l'objet de fonction. Il n'y a rien de bizarre ici, c'est juste un peu surprenant.

la solution courante pour combattre cela est d'utiliser None comme la valeur par défaut et puis initialiser dans le corps de 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é de nouveau à chaque fois, vous obtenez toujours une nouvelle liste vide si aucun argument n'a été passé pour a .


pour vérifier que la liste dans __defaults__ est la même que celle utilisée dans la fonction func vous pouvez juste changer votre fonction pour retourner le id de la liste a utilisé à l'intérieur du 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 effectivement à la même instance de liste:

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

tous 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 remarquerez que input() est appelé avant que le processus de construction de la fonction et de la lier au nom bar soit fait.

47
répondu Jim Fasarakis Hilliard 2018-10-11 19:33:49
la source

ce comportement s'explique facilement par:

  1. fonction (classe, etc.) déclaration est exécutée qu'une seule fois, la création de tous par défaut des objets de valeur
  2. tout ce qui est passé par référence

:

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 - toute cession d'appel crée un nouvel objet int - 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-opération est effectuée sur le même objet - et il est imprimé
42
répondu ymv 2017-10-24 09:34:34
la source

ce que vous demandez est pourquoi ceci:

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

n'est pas en interne équivalent à 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 le cas de l'appel explicite func(None, None), que nous ignorerons.

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

une réponse est probablement juste là-elle tournerait effectivement chaque fonction avec des paramètres par défaut dans une 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. Il serait plus lent et plus de mémoire.

32
répondu Glenn Maynard 2009-07-16 00:18:14
la source

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 ,"

Cela va à l'encontre des règles de la programmation fonctionnelle, généralement non souhaitable et devrait être fixé 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 : un copie

Une solution absolument sûre est de copy ou deepcopy l'objet d'entrée d'abord et ensuite de faire n'importe quoi avec la copie.

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

beaucoup de types mutables 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 aussi être copié par copy.copy(any_object) ou plus complet par copy.deepcopy() (ce dernier utile si l'objet mutable est composé à partir d'objets mutables). Certains objets sont fondamentalement basés sur des effets secondaires comme l'objet "file" et ne peuvent pas être reproduits de manière significative par copie. copie

exemple de problème pour une question de 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 devrait pas être ni enregistrés dans le attribut public d'une instance retournée par cette fonction. (À supposer que les attributs privé d'instance ne devraient pas être modifiés par convention par rapport à l'extérieur de cette classe ou de ces sous-classes. c'est à dire _var1 est un attribut privé )

Conclusion:

Les objets de paramètres d'entrée ne doivent pas être modifiés en place (mutés) ni être liés dans un objet retourné par la fonction. (Si nous visez plutôt programmation sans effets secondaires ce qui est fortement recommandé. voir Wiki à propos de" side effect " (les deux premiers paragraphes sont pertinents dans ce contexte.) .)

2)

Seulement si l'effet secondaire sur le paramètre réel est nécessaire mais non désiré sur le paramètre par défaut alors la solution utile est def ...(var1=None): if var1 is None: var1 = [] encore..

3) dans certains cas est la mutable comportement par défaut des paramètres utiles .

31
répondu hynekcer 2017-05-23 14:47:32
la source

cela n'a en fait rien à voir avec les valeurs par défaut, sauf 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 foo est modifiant une variable mutable transmise par l'appelant, lorsque celui-ci ne s'y attend pas. Un Code comme ce serait très bien si la fonction était appelée quelque chose comme append_5 ; alors l'appelant appellerait la fonction afin de modifier la valeur qu'ils passent dans, 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'elle vient de transmettre).

votre original foo , avec un argument par défaut, ne devrait pas être modifier a s'il a été explicitement passé ou a obtenu la valeur par défaut. Votre code devrait 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 passées en arguments comme temporaireslocaux 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 façon destructive un temporaire local dans le cours de calculer quelque chose, et vous avez besoin pour commencer votre manipulation à partir d'une valeur d'argument, vous devez faire une copie.

26
répondu Ben 2011-05-23 08:24:30
la source

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

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 du comportement expérimenté a une utilisation pratique (qui a vraiment utilisé des variables statiques en C, sans créer de bogues ?)

comme vous pouvez le voir, il est un avantage de performance lorsque vous utilisez des arguments par défaut immuables. Cela peut faire une différence si c'est une fonction fréquemment appelée ou si l'argument par défaut prend beaucoup de temps à construire. Aussi, gardez à l'esprit que Python n'est pas C. En C, vous avez des constantes qui sont assez libres. En Python, vous n'avez pas cet avantage.

25
répondu Jason Baker 2013-04-03 01:52:30
la source

sujet déjà occupé, mais d'après ce que j'ai lu ici, ce qui suit m'a aidé à réaliser comment il fonctionne à l'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
23
répondu Stéphane 2015-03-27 02:14:01
la source

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 de fonction. Lorsqu'il est utilisé par la fonction, plusieurs fois, par cette fonction, ils sont et restent le même objet.

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

ils restent mutés parce qu'ils sont les mêmes objet à chaque fois.

code équivalent:

puisque la liste est liée à la fonction lorsque l'objet de la 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 l'équivalent de 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

Démonstration

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

  • voyant que la liste est créée avant que la fonction ait fini de compiler vers un objet de fonction,
  • observant que l'id est le même chaque fois que la liste est référencée,
  • observant que la liste reste modifiée lorsque la fonction qui l'utilise est appelée une deuxième fois,
  • observant l'ordre dans lequel la sortie est imprimée à partir de la source (que je numérotais commodément 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']

est-ce que cela viole 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, alors il devient très attendu.

l'instruction habituelle pour les nouveaux utilisateurs de Python:

mais ce 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 None singleton comme un objet sentinelle pour dire à la fonction si oui ou non nous avons eu un argument autre que le défaut. Si nous n'avons pas d'argument, alors nous voulons utiliser une nouvelle liste vide, [] , comme valeur par défaut.

comme la section de tutoriel sur le flux de contrôle dit:

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
19
répondu Aaron Hall 2017-12-24 00:18:35
la source

une solution de contournement simple 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]
18
répondu hugo24 2013-02-28 15:10:16
la source

ce comportement n'est pas surprenant si vous tenez compte de ce qui suit:

  1. Le comportement de lire uniquement les attributs de classe lors de la tente d'assignation, et que
  2. les fonctions sont des objets (bien expliquées dans la réponse acceptée).

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

(1) est décrit dans le tutoriel Python sur les classes . Dans une tentative 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'écrire à une telle variable créera simplement un nouveau variable locale dans la portée la plus interne, laissant l'identité nom de la variable externe inchangé ).

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

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

ici foo est un objet et a est un attribut de foo (disponible à foo.func_defs[0] ). Comme a est une liste, a est mutable et est donc un attribut" read-write " de foo . Il est initialisé à la liste vide spécifiée par la signature lorsque la fonction est instanciée, et est disponible pour la lecture et l'écriture aussi longtemps que l'objet de la fonction existe.

appelant foo sans passer outre une valeur par défaut utilise la valeur par défaut de foo.func_defs . Dans ce cas, foo.func_defs[0] est utilisé pour a dans le champ d'application du code de l'objet de fonction. Changements à a changer foo.func_defs[0] , qui fait partie de l'objet foo et persiste entre l'exécution du code dans foo .

maintenant, comparez ceci à l'exemple de la documentation sur émulant le comportement d'argument par défaut d'autres langues , de sorte que la fonction signature par défaut sont utilisés à 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 prenant en compte (1) et (2) comportement souhaité:

  • lorsque l'objet de fonction foo est instancié, foo.func_defs[0] est défini à None , un objet immuable.
  • lorsque la fonction est exécutée par défaut (sans paramètre spécifié pour L dans l'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] parce que l'attribut est en lecture seule.
  • par (1) , une nouvelle variable locale aussi appelée L est 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 .
18
répondu Dmitry Minkovsky 2013-04-01 19:30:56
la source

la réponse La plus courte serait probablement "définition d'exécution", donc l'argument ne fait pas de sens strict. Comme un exemple plus élaboré, vous pouvez citer ceci:

def a(): return []

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

avec un peu de chance il suffit de montrer que ne pas exécuter les expressions d'argument par défaut au moment d'exécution de la déclaration def n'est pas facile ou n'a pas de sens, ou les deux.

je suis d'accord que c'est un gotcha quand vous essayez d'utiliser des constructeurs par défaut, cependant.

18
répondu Baczek 2018-05-21 02:22:19
la source

les solutions ici sont:

  1. utilisez None comme valeur par défaut( ou un nonce object ), et activez-le pour créer vos valeurs à l'exécution; ou
  2. utilisez un lambda comme paramètre par défaut, et appelez-le dans un bloc d'essai pour obtenir la valeur par défaut (c'est le genre de chose que lambda abstraction est pour).

la deuxième option est agréable parce que les utilisateurs de la fonction peuvent passer dans un appelable, qui peut être déjà existant (tel qu'un type )

17
répondu Marcin 2013-06-30 20:20:35
la source

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 singleton est utilisé uniquement par use_singleton , j'aime le modèle suivant comme un remplacement:

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

j'ai utilisé ceci pour instancier des classes de clients qui accèdent à des ressources externes, et aussi pour créer des dicts ou des listes pour la mémorisation.

comme je ne pense pas que ce modèle est bien connu, Je ne mets un court commentaire pour se prémunir contre de futurs malentendus.

16
répondu bgreen-litl 2015-02-06 15:56:53
la source

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 list est lié à la fonction lorsqu'il est défini par opposition à lorsqu'il est exécuté. Comme les listes et les dictionnaires sont mutables, toute modification de ce paramètre affectera les autres appels à cette fonction. Par conséquent, les appels subséquents à la fonction recevront cette liste partagée qui peut avoir été modifiée par d'autres appels à la fonction. Pire encore, deux paramètres utilisent le paramètre partagé de cette fonction en même temps, ignorant les changements apportés par l'autre.

méthode erronée (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

Par Brett Slatkin "Efficace Python: 59 Moyens spécifiques d'écrire mieux Python", Point 20: utilisez None et Docstrings pour spécifier les arguments dynamiques par défaut (p. 48)

la convention pour atteindre le résultat désiré en Python est de fournir une valeur par défaut de None et documenter le comportement réel dans la docstring.

cette implémentation garantit que chaque appel à la fonction soit reçoit la liste par défaut ou sinon, la liste passe à 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égitime de la "mauvaise méthode" par laquelle le programmeur a voulu partager le paramètre de liste par défaut, mais c'est plus probablement l'exception que la règle.

16
répondu Alexander 2015-09-12 23:41:53
la source

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

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

laid, mais ça marche.

15
répondu jdborg 2013-01-15 15:02:03
la source

Lorsque nous faisons cela:

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

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

pour simplifier la discussion, donnons temporairement un nom à la liste anonyme. Et pavlo ?

def foo(a=pavlo):
   ...

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

si pavlo est mutable (modifiable), et foo finit par le modifier, un effet que nous remarquons la prochaine fois foo est appelé sans spécifier a .

donc c'est ce que vous voyez (rappelez-vous, pavlo est initialisé à []):

 >>> foo()
 [5]

maintenant, pavlo est [5].

Appel foo() modifie à nouveau pavlo de nouveau:

>>> foo()
[5, 5]

spécifiant a lors de l'appel foo() assure que pavlo n'est pas touché.

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

donc, pavlo est toujours [5, 5] .

>>> foo()
[5, 5, 5]
14
répondu Saish 2014-09-12 02:05:43
la source

C'est peut-être vrai que:

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

il est tout à fait conforme à détenir à la fois des fonctionnalités ci-dessus et encore faire un autre point:

  1. c'est une caractéristique confuse et c'est malheureux en Python.

les autres réponses, ou du moins certains d'entre eux font les points 1 et 2 mais pas 3, ou font le point 3 et les points 1 et 2. mais tous les trois sont vrais.

il est peut-être vrai que changer de cheval à mi-course ici demanderait un bris important, et qu'il pourrait y avoir plus de problèmes créés par le changement de Python pour gérer intuitivement l'extrait d'ouverture de Stefano. Et il est peut-être vrai que quelqu'un qui connaissait bien les internes de Python pourrait expliquer un champ de mines conséquence. cependant,

le comportement existant n'est pas pythonique, et Python est réussi parce que très peu sur le langage viole le principe de moindre étonnement n'importe où près de ce mal. 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 le langage beaucoup mieux en essayant de tracer le comportement, je peux dire que C++ fait tout cela et plus encore; vous apprenez beaucoup en naviguant, par exemple, des erreurs de pointeur subtiles. Mais ce N'est pas pythonique: les gens qui se soucient assez de Python pour persévérer face à ce comportement sont des gens qui sont attirés par le langage parce que Python a beaucoup moins de surprises que d'autres langues. Les Dabblers et les curieux deviennent des Pythonistas quand ils sont étonnés du peu de temps qu'il faut pour que quelque chose fonctionne--pas à cause d'un design fl--je veux dire, un puzzle logique caché--qui va à l'encontre des intuitions des programmeurs qui sont attirés par Python car il fonctionne Juste .

11
répondu JonathanHayward 2009-07-16 23:17:59
la source

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

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
9
répondu Norfeldt 2013-07-23 14:07:58
la source

je pense que la réponse à cette question réside dans la façon dont python passe les données au paramètre (passer par la 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 y a deux 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, lorsque vous passez des données à des paramètres, python passe le type de données élémentaires par la valeur, i.e., faire une copie locale de la valeur à un variable locale, mais passer objet par référence, i.e., pointeurs vers l'objet.

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

[] est un objet, donc python passer la référence de [] à a , i.e., a est seulement un pointeur sur [] qui se trouve dans la mémoire comme un 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 d'ajout. Mais notez qu'il n'y a qu'une copie de l'objet list et que cet objet devient maintenant 1 . Lors de l'exécution du second foo(), ce que la page Web effbot dit (les éléments ne sont plus évalués) est erroné. a est évalué comme l'objet de la liste, bien que maintenant le le contenu de l'objet est 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 ma réponse, regardons 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, ainsi est None (le premier est mutable tandis que le second est immuable. Mais le changement 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 copie de aucun. 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 fois) pour être Aucun, pour être clairs, la référence (ou l'adresse) de aucun. Ensuite, dans la foo, l'élément est remplacé par [], 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]

le invocation de foo(1) faire des articles pointer vers un objet de la liste [] avec une adresse, dire, 11111111. le contenu de la liste est changé à 1 dans la fonction foo dans la suite, mais l'adresse n'est pas changé, toujours 11111111. Puis foo(2,[]) est à venir. Bien que [] in 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 fournir le paramètre explicitement, items doit prendre l'adresse de ce nouveau [] , dites 2222222, et retournez-le après avoir fait quelques changements. Maintenant foo (3) est exécuté. puisque seulement x est fourni, les articles doivent prendre à nouveau sa 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 list situé dans 11111111. Ainsi, les éléments sont évalués comme étant l'adresse 111111 ayant un élément 1. La liste située au 2222222 contient aussi un élément 2, mais il n'est plus pointé par des articles. Par conséquent, un ajout de 3 fera items [1,3].

à partir des 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 erroné. Je pense que le code en ce qui concerne l'INTERFACE utilisateur.Bouton 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]() donnera 9, une autre valeur de i .

8
répondu user2384994 2013-08-22 09:58:41
la source

il ne s'agit pas d'un défaut de conception . Quiconque trébuche là-dessus fait quelque chose de mal.

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

  1. Vous avez l'intention de modifier l'argument comme un 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 on ne s'attend pas à ce que vous appeliez la fonction avec un argument réel du tout.
  2. vous avez l'intention de laisser l'argument non modifié, mais vous avez accidentellement fait le modifier. C'est un bug, corrigé.
  3. Vous avez l'intention de modifier l'argument pour l'utilisation à l'intérieur de la fonction, mais n'attendez pas que la modification soit visible à l'extérieur de la fonction. Dans ce cas, vous devez faire une copie de la argument, que ce soit par défaut ou non! Python n'est pas un langage call-by-value Donc il ne fait pas la copie pour vous, vous devez être explicite à ce sujet.

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

6
répondu Mark Ransom 2017-10-17 21:04:59
la source
>>> def a():
>>>    print "a executed"
>>>    return []
>>> x =a()
a executed
>>> def b(m=[]):
>>>    m.append(5)
>>>    print m
>>> b(x)
[5]
>>> b(x)
[5, 5]
3
répondu jai 2013-10-05 13:49:47
la source

il suffit de changer la fonction pour être:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
3
répondu ytpillai 2018-09-07 00:29:08
la source

Architecture

Attribution des valeurs par défaut dans un appel de fonction est une odeur de code.

def a(b=[]):
    pass

il s'agit de la signature d'une fonction qui n'est pas bonne. Pas seulement à cause des problèmes décrits par d'autres réponses. Je ne vais pas entrer dans les ici.

Cette fonction vise à faire deux choses. Créez une nouvelle liste, et exécutez une fonctionnalité, très probablement sur ladite liste.

fonctions qui font deux les choses sont de mauvaises fonctions, comme nous apprenons des pratiques de code propre.

en attaquant ce problème avec le polymorphisme, nous étendons la liste des pythons ou en enveloppons une dans une classe, puis nous exécutons notre fonction sur elle.

mais attendez, dites-vous, j'aime mes doublures.

Eh bien, devinez quoi. Le Code est plus qu'un simple moyen de contrôler le comportement du matériel. C'est une façon de:

  • communiquer avec d'autres développeurs, travaillant sur le même code.

  • possibilité de modifier le comportement du matériel lorsque de nouvelles exigences apparaissent.

  • être capable de comprendre le flux du programme après avoir récupéré le code après deux ans pour effectuer le changement mentionné ci-dessus.

ne laissez pas de bombes à retardement.

séparer cette fonction en deux choses qu'elle fait, nous avons besoin d'une classe

class ListNeedsFives(object):
    def __init__(self, b=None):
        if b is None:
            b = []
        self.b = b

    def foo():
        self.b.append(5)

exécuté par

a = ListNeedsFives()
a.foo()
a.b

et pourquoi est-ce mieux que de brosser tout le code ci-dessus en une seule fonction.

def dontdothis(b=None):
    if b is None:
        b = []
    b.append(5)
    return b

Pourquoi ne pas le faire?

sauf si vous échouez dans votre projet, votre code vivra. Il est fort probable que votre fonction en fera plus que cela. La bonne façon de faire le code maintenable est de séparer le code en parties atomiques avec une portée correctement limitée.

le constructeur d'une classe est un composant très communément reconnu par tous ceux qui ont fait de la programmation orientée objet. Placer la logique qui gère l'instanciation de liste dans le constructeur rend la charge cognitive de comprendre ce que le code fait plus petit.

La méthode foo() ne renvoie pas la liste, pourquoi pas?

In de retour une liste autonome, vous pourriez supposer qu'il est sûr de faire ce que vous voulez à elle. Mais peut-être pas, car elle est aussi partagée par l'objet a . Forcer l'utilisateur à l'appeler a.b lui rappelle où la liste doit être placée. Tout nouveau code qui veut modifier a.b sera naturellement placé dans le catégorie, où il appartient.

la fonction de signature def dontdothis(b=None): ne présente aucun de ces avantages.

-3
répondu firelynx 2017-11-10 18:43:47
la source