«Least Astonishment» et l'argument Mutable par défaut
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 x
est 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 def
ligne 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
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.
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 foo
fonction 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 StringBuffer
clé 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 Map
utilisant une valeur identique à celle avec laquelle elle l'a mis, soit la personne qui ne semble pas pouvoir récupérer son objet même si la clé qu'elle utilise est littéralement le même objet qui a été utilisé pour le mettre dans la carte (c'est 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.
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
None
par 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
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 a
par 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):
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.
Pourquoi ne pas introspecter?
Je suis vraiment surpris que personne n'ait effectué l'introspection perspicace offerte par Python ( 2
et 3
appliquer) sur les callables.
Étant donné une petite fonction simple func
dé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 code
objet 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 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 la liste est développée à l' intérieur de l'objet fonction. J'utilise Python 3.x
pour cela, 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)
...
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 list
objet 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 None
comme 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, func
vous pouvez simplement modifier votre fonction pour renvoyer id
la liste a
utilisé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 bar
soit fait.
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 inspect
module, 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.
5 points pour la défense de Python
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.
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.
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.)
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é.
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.
Ce comportement s'explique facilement par:
- La déclaration de fonction (classe etc.) est exécutée une seule fois, créant tous les objets de valeur par défaut
- 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
a
ne change pas - chaque appel d'affectation crée un nouvel objet int - un nouvel objet est impriméb
ne change pas - le nouveau tableau est construit à partir de la valeur par défaut et impriméc
changements - l'opération est effectuée sur le même objet - et il est imprimé
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.
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 copy
ou deepcopy
l'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 .
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 foo
est 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 a
s'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.
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
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.
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
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' def
instruction 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.
Ce comportement n'est pas surprenant si vous prenez en compte les éléments suivants:
- Le comportement des attributs de classe en lecture seule lors des tentatives d'affectation, et que
- 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 foo
un objet et a
un attribut de foo
(disponible sur foo.func_defs[0]
). Puisque a
est une liste, a
est 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 foo
sans 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é a
dans la portée du code de l'objet fonction. Modifications à a
modifier foo.func_defs[0]
, qui fait partie de l' foo
objet 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'
foo
objet de fonction est instancié,foo.func_defs[0]
est défini surNone
, un objet immuable. - Lorsque la fonction est exécutée avec les valeurs par défaut (sans paramètre spécifié
L
dans l'appel de fonction),foo.func_defs[0]
(None
) est disponible dans la portée locale sous la formeL
. - Ensuite
L = []
, l'affectation ne peut pas aboutirfoo.func_defs[0]
, car cet attribut est en lecture seule. - Par (1) , une nouvelle variable locale également nommé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 futures invocations defoo
.
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]
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 None
et 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
None
et 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.
Les solutions ici sont:
- Utilisez
None
comme valeur par défaut (ou nonceobject
), et activez-la pour créer vos valeurs au moment de l'exécution; ou - Utilisez a
lambda
comme 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
)
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.
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 a
c'est, nous réutilisons pavlo
.
Si pavlo
est mutable (modifiable), et foo
finit par le modifier, un effet que nous remarquerons la prochaine fois foo
est appelé sans le spécifier a
.
Voici donc ce que vous voyez (Souvenez-vous, pavlo
est initialisé à []):
>>> foo()
[5]
Maintenant, pavlo
c'est [5].
Le rappel foo()
modifie à pavlo
nouveau:
>>> foo()
[5, 5]
La spécification a
lors de l'appel foo()
garantit que ce 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
c'est toujours [5, 5]
.
>>> foo()
[5, 5, 5]
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
n'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.
Il est peut-être vrai que:
- Quelqu'un utilise toutes les fonctionnalités de langue / bibliothèque, et
- 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:
- 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 .
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:
- 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. - Vous avez l'intention de ne pas modifier l'argument, mais vous l'avez accidentellement modifié. C'est un bug, corrigez-le.
- 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.
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
Changez simplement la fonction en:
def notastonishinganymore(a = []):
'''The name is just a joke :)'''
a = a[:]
a.append(5)
return a
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 a
n'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. a
est é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, items
il 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 x
est 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
.
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 name
pièce doit être évaluée dans le cadre de définition - nous voulons name
y être disponibles, après tout. Évaluer la fonction uniquement à l'intérieur d'elle-même la rendrait inaccessible.
Puisqu'il parameter
s'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 default
est évalué au moment de la définition, cela peut toujours affecter le temps d'exécution. Si default
est é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
...
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;)