Quiconque bricole avec Python depuis assez longtemps a été mordu (ou déchiré en morceaux) par le problème suivant:
def foo(a=[]):
a.append(5)
return a
Novices Python s'attendent cette fonction pour revenir toujours une liste avec un seul élément: [5]
. Le résultat est au contraire très différent, et très étonnant (pour un novice):
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
Un de mes directeurs a eu sa première rencontre avec cette fonctionnalité et l'a qualifiée de "faille de conception dramatique" du langage. J'ai répondu que le comportement avait une explication sous-jacente, et c'est en effet très déroutant et inattendu si vous ne comprenez pas les internes. Cependant, je n'ai pas pu répondre (à moi-même) à la question suivante: quelle est la raison de la liaison de l'argument par défaut à la définition de la fonction, et non à l'exécution de la fonction? Je doute que le comportement expérimenté ait une utilité pratique (qui a vraiment utilisé des variables statiques en C, sans multiplier les bugs?)
Modifier :
Baczek en a fait un exemple intéressant. Avec la plupart de vos commentaires et Utaal en particulier, j'ai développé davantage:
>>> def a():
... print("a executed")
... return []
...
>>>
>>> def b(x=a()):
... x.append(5)
... print(x)
...
a executed
>>> b()
[5]
>>> b()
[5, 5]
Pour moi, il semble que la décision de conception était relative à l'endroit où mettre la portée des paramètres: à l'intérieur de la fonction ou "ensemble" avec elle?
Faire la liaison à l'intérieur de la fonction signifierait qu'elle x
est effectivement liée à la valeur par défaut spécifiée lorsque la fonction est appelée, non définie, quelque chose qui présenterait un défaut profond: la def
ligne serait "hybride" dans le sens où une partie de la liaison (de l'objet fonction) se produirait à la définition, et une partie (affectation des paramètres par défaut) au moment de l'appel de la fonction.
Le comportement réel est plus cohérent: tout de cette ligne est évalué lorsque cette ligne est exécutée, c'est-à-dire lors de la définition de la fonction.
la source
[5]
" Je suis un novice Python, et je ne m'attendrais pas à cela, car évidemment,foo([1])
je reviendrai[1, 5]
, non[5]
. Ce que vous vouliez dire, c'est qu'un novice s'attendrait à ce que la fonction appelée sans paramètre revienne toujours[5]
.Réponses:
En fait, ce n'est pas un défaut de conception, et ce n'est pas à cause des internes ou des performances.
Cela vient simplement du fait que les fonctions en Python sont des objets de première classe, et pas seulement un morceau de code.
Dès que vous arrivez à penser de cette façon, alors cela prend tout son sens: une fonction est un objet évalué sur sa définition; les paramètres par défaut sont une sorte de "données de membre" et par conséquent leur état peut changer d'un appel à l'autre - exactement comme dans tout autre objet.
En tout cas, Effbot a une très belle explication des raisons de ce comportement dans Default Parameter Values in Python .
Je l'ai trouvé très clair et je suggère vraiment de le lire pour une meilleure connaissance du fonctionnement des objets fonctionnels.
la source
functions are objects
. Dans votre paradigme, la proposition serait d'implémenter les valeurs par défaut des fonctions en tant que propriétés plutôt qu'en tant qu'attributs.Supposons que vous ayez le code suivant
Quand je vois la déclaration de manger, le moins étonnant est de penser que si le premier paramètre n'est pas donné, il sera égal au tuple
("apples", "bananas", "loganberries")
Cependant, supposé plus tard dans le code, je fais quelque chose comme
alors si les paramètres par défaut étaient liés à l'exécution de la fonction plutôt qu'à la déclaration de fonction, alors je serais étonné (de très mauvaise façon) de découvrir que les fruits avaient été modifiés. Ce serait plus étonnant IMO que de découvrir que votre
foo
fonction ci-dessus mutait la liste.Le vrai problème réside dans les variables mutables, et toutes les langues ont ce problème dans une certaine mesure. Voici une question: supposons qu'en Java j'ai le code suivant:
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é de sortir l'objet duMap
valeur en utilisant une valeur identique à celle avec laquelle elle l'a mis, soit la personne qui ne semble pas pouvoir récupérer son objet même si la clé qu'elle utilise est littéralement le même objet qui a été utilisé pour le mettre dans la carte (c'est pourquoi Python ne permet pas que ses types de données intégrés mutables soient utilisés comme clés de dictionnaire).Votre exemple est un bon exemple d'un cas où les nouveaux arrivants Python seront surpris et mordus. Mais je dirais que si nous «corrigions» cela, cela ne ferait que créer une situation différente où ils seraient mordus à la place, et celle-là serait encore moins intuitive. De plus, c'est toujours le cas lorsqu'il s'agit de variables mutables; vous rencontrez toujours des cas où quelqu'un pourrait intuitivement s'attendre à l'un ou au comportement opposé en fonction du code qu'il écrit.
Personnellement, j'aime l'approche actuelle de Python: les arguments de fonction par défaut sont évalués lorsque la fonction est définie et cet objet est toujours la valeur par défaut. Je suppose qu'ils pourraient utiliser des cas spéciaux en utilisant une liste vide, mais ce type de boîtier spécial provoquerait encore plus d'étonnement, sans parler d'être incompatible en arrière.
la source
("blueberries", "mangos")
.some_random_function()
ajoute à aufruits
lieu d'attribuer à lui, le comportementeat()
va changer. Voilà pour le merveilleux design actuel. Si vous utilisez un argument par défaut référencé ailleurs, puis modifiez la référence depuis l'extérieur de la fonction, vous demandez des problèmes. Le vrai WTF est lorsque les gens définissent un nouvel argument par défaut (un littéral de liste ou un appel à un constructeur), et obtiennent toujours un bit.global
et de réaffecter explicitement le tuple - il n'y a absolument rien de surprenant si celaeat
fonctionne différemment après cela.La partie pertinente de la documentation :
la source
function.data = []
) ou mieux encore, de créer un objet.Je ne sais rien du fonctionnement interne de l'interpréteur Python (et je ne suis pas non plus un expert en compilateurs et interprètes), alors ne me blâmez pas si je propose quelque chose d'insensible ou impossible.
À condition que les objets python soient mutables, je pense que cela devrait être pris en compte lors de la conception des arguments par défaut. Lorsque vous instanciez une liste:
vous vous attendez à obtenir une nouvelle liste référencée par
a
.Pourquoi le
a=[]
dansinstancier 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:
utilisateur, voulez-
a
vous définir par défaut le datetime correspondant à la définition ou à l'exécutionx
? Dans ce cas, comme dans le précédent, je conserverai le même comportement que si l'argument par défaut "assignation" était la première instruction de la fonction (datetime.now()
appelée lors de l'appel de la fonction). D'un autre côté, si l'utilisateur voulait le mappage définition-temps, il pouvait écrire: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:
la source
class {}
bloc soit interprété comme appartenant aux instances :) Mais lorsque les classes sont des objets de première classe, la chose naturelle est évidemment que leur contenu (en mémoire) reflète leur contenu (dans du code).Eh bien, la raison est tout simplement que les liaisons sont effectuées lorsque le code est exécuté et que la définition de la fonction est exécutée, eh bien ... lorsque les fonctions sont définies.
Comparez ceci:
Ce code souffre de la même situation inattendue. bananas est un attribut de classe et, par conséquent, lorsque vous y ajoutez des éléments, il est ajouté à toutes les instances de cette classe. La raison est exactement la même.
C'est juste "Comment ça marche", et le faire fonctionner différemment dans le cas de la fonction serait probablement compliqué, et dans le cas de la classe probablement impossible, ou au moins ralentir beaucoup l'instanciation des objets, car vous devriez garder le code de la classe autour et l'exécuter lorsque des objets sont créés.
Oui, c'est inattendu. Mais une fois que le sou tombe, il s'intègre parfaitement avec le fonctionnement de Python en général. En fait, c'est une bonne aide pédagogique, et une fois que vous comprendrez pourquoi cela se produit, vous améliorerez beaucoup le python.
Cela dit, il devrait figurer en bonne place dans tout bon tutoriel Python. Parce que, comme vous le mentionnez, tout le monde rencontre ce problème tôt ou tard.
la source
property
s - qui sont en fait des fonctions de niveau classe qui agissent comme des attributs normaux mais sauvegardent l'attribut dans l'instance au lieu de la classe (en utilisantself.attribute = value
comme l'a dit Lennart).Pourquoi ne vous introspectez pas?
Je suis vraiment surpris que personne n'ait effectué l'introspection perspicace offerte par Python (
2
et3
s'applique) sur les callables.Étant donné une petite fonction simple
func
définie comme:Lorsque Python le rencontrera, la première chose qu'il fera sera de le compiler afin de créer un
code
objet pour cette fonction. Pendant cette étape de compilation, Python évalue * puis stocke les arguments par défaut (une liste vide[]
ici) dans l'objet fonction lui-même . Comme la première réponse l'a mentionné: la listea
peut désormais être considérée comme un membre de la fonctionfunc
.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__
oufunc_defaults
dans Python 2; oui, deux noms pour la même chose).Fonction avant exécution:
Après que Python ait exécuté cette définition, il prendra tous les paramètres par défaut spécifiés (
a = []
ici) et les entassera dans l'__defaults__
attribut de l'objet fonction (section pertinente: Callables):Ok, donc une liste vide comme entrée unique
__defaults__
, comme prévu.Fonction après exécution:
Exécutons maintenant cette fonction:
Maintenant,
__defaults__
revoyons-les:Étonné? La valeur à l'intérieur de l'objet change! Les appels consécutifs à la fonction s'ajouteront désormais simplement à cet
list
objet intégré :Donc, vous l'avez, la raison pour laquelle cette «faille» se produit, c'est parce que les arguments par défaut font partie de l'objet fonction. Il n'y a rien de bizarre ici, c'est juste un peu surprenant.
La solution courante pour lutter contre cela est d'utiliser
None
par défaut puis d'initialiser dans le corps de la fonction:Puisque le corps de la fonction est exécuté à nouveau à chaque fois, vous obtenez toujours une nouvelle liste vide si aucun argument n'a été passé
a
.Pour vérifier davantage que la liste dans
__defaults__
est la même que celle utilisée dans la fonction,func
vous pouvez simplement changer votre fonction pour renvoyerid
la listea
utilisée dans le corps de la fonction. Ensuite, comparez-le à la liste dans__defaults__
(position[0]
dans__defaults__
) et vous verrez comment ceux-ci se réfèrent en effet à la même instance de liste: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:
comme vous le remarquerez,
input()
est appelé avant le processus de construction de la fonction et de sa liaison au nombar
.la source
id(...)
nécessaire pour cette dernière vérification, ou l'is
opérateur répondrait-il à la même question?is
ferait très bien, j'ai juste utiliséid(val)
parce que je pense que cela pourrait être plus intuitif.None
par défaut limite considérablement l'utilité de l'__defaults__
introspection, donc je ne pense pas que cela fonctionne bien comme défense d'avoir un__defaults__
travail comme il le fait. L'évaluation différée ferait plus pour garder les valeurs par défaut des fonctions utiles des deux côtés.Je pensais que la création des objets à l'exécution serait la meilleure approche. Je suis moins certain maintenant, car vous perdez certaines fonctionnalités utiles, même si cela en vaut la peine, simplement pour éviter la confusion des débutants. Les inconvénients de le faire sont:
1. Performance
Si l'évaluation du temps d'appel est utilisée, la fonction coûteuse est appelée chaque fois que votre fonction est utilisée sans argument. Vous paierez un prix cher à chaque appel ou vous devrez mettre en cache manuellement la valeur en externe, polluant votre espace de noms et ajoutant de la verbosité.
2. Forcer les paramètres liés
Une astuce utile consiste à lier les paramètres d'un lambda à la liaison actuelle d'une variable lorsque le lambda est créé. Par exemple:
Cela renvoie une liste de fonctions qui renvoient respectivement 0,1,2,3 .... Si le comportement est modifié, ils se lieront
i
à la place à la valeur de temps d'appel de i, vous obtiendrez donc une liste de fonctions que tous ont renvoyées9
.La seule façon de mettre cela en œuvre autrement serait de créer une nouvelle fermeture avec la borne i, c'est-à-dire:
3. Introspection
Considérez le code:
Nous pouvons obtenir des informations sur les arguments et les valeurs par défaut en utilisant le
inspect
module, quiCes informations sont très utiles pour des choses comme la génération de documents, la métaprogrammation, les décorateurs, etc.
Supposons maintenant que le comportement des valeurs par défaut puisse être modifié afin que ce soit l'équivalent de:
Cependant, nous avons perdu la capacité d'introspection, et voir ce que les arguments par défaut sont . Parce que les objets n'ont pas été construits, nous ne pouvons jamais les saisir sans appeler la fonction. Le mieux que nous puissions faire est de stocker le code source et de le renvoyer sous forme de chaîne.
la source
5 points en 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 du titre de la fonction (et non du corps de la fonction). Elle doit donc être évaluée au moment du chargement du module (et uniquement au moment du chargement du module, sauf si elle est imbriquée), et non au moment de l'appel de la fonction.
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 élémentaire, le didacticiel, le problème est annoncé à haute voix comme un "avertissement important" dans la première sous-section de la section "En savoir plus sur la définition des fonctions" . L'avertissement utilise même des caractères gras, qui sont rarement appliqués en dehors des en-têtes. RTFM: Lisez le manuel fin.
Méta-apprentissage : tomber dans le piège est en fait un moment très utile (au moins si vous êtes un apprenant réfléchi), car vous comprendrez ensuite mieux le point "cohérence" ci-dessus et cela vous en apprendra beaucoup sur Python.
la source
Ce comportement s'explique facilement par:
Donc:
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
modifications - l'opération est effectuée sur le même objet - et elle est impriméela source
__iadd__
, mais cela ne fonctionne pas avec int. Bien sûr. :-)Ce que vous demandez, c'est pourquoi cela:
n'est pas équivalent en interne à ceci:
sauf pour le cas d'appeler explicitement func (None, None), que nous ignorerons.
En d'autres termes, au lieu d'évaluer les paramètres par défaut, pourquoi ne pas stocker chacun d'eux et les évaluer lorsque la fonction est appelée?
Une réponse est probablement là - elle transformerait efficacement chaque fonction avec des paramètres par défaut en fermeture. Même si tout est caché dans l'interpréteur et pas une fermeture complète, les données doivent être stockées quelque part. Ce serait plus lent et utiliserais plus de mémoire.
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 ,"
C'est contre les règles de programmation fonctionnelle, généralement négligeable et doit être fixé les deux ensemble.
Exemple:
Solution : une copie
Une solution absolument sûre est d'abord de
copy
ou versdeepcopy
l'objet d'entrée, puis de faire quoi que ce soit avec la copie.De nombreux types mutables intégrés ont une méthode de copie comme
some_dict.copy()
ousome_set.copy()
ou peuvent être copiés facilement commesomelist[:]
oulist(some_list)
. Chaque objet peut également être copié parcopy.copy(any_object)
ou plus approfondi parcopy.deepcopy()
(ce dernier utile si l'objet mutable est composé d'objets mutables). Certains objets sont fondamentalement basés sur des effets secondaires comme l'objet "fichier" et ne peuvent pas être reproduits de manière significative par copie. copierExemple de problème pour une question SO similaire
Il ne doit pas non plus être enregistré dans aucun attribut public d'une instance retournée par cette fonction. (En supposant que les attributs privés de l'instance ne devraient pas être modifiés depuis l'extérieur de cette classe ou sous-classes par convention.
_var1
est un attribut privé)Conclusion: les
objets de paramètres d'entrée ne doivent pas être modifiés sur place (mutés) ni ne doivent être liés à un objet renvoyé par la fonction. (Si nous préférons la programmation sans effets secondaires, ce qui est fortement recommandé. Voir le wiki sur les "effets secondaires" (les deux premiers paragraphes sont pertinents dans ce contexte.).)
2)
Uniquement si l'effet secondaire sur le paramètre réel est requis mais indésirable sur le paramètre par défaut, alors la solution utile est
def ...(var1=None):
if var1 is None:
var1 = []
Plus ..3) Dans certains cas, le comportement modifiable des paramètres par défaut est utile .
la source
def f( a = None )
construction est recommandée alors que vous voulez vraiment dire autre chose. La copie est correcte, car vous ne devez pas muter d'arguments. Et quand vous le faitesif a is None: a = [1, 2, 3]
, vous copiez quand même la liste.Cela n'a en fait rien à voir avec les valeurs par défaut, à part le fait qu'il apparaît souvent comme un comportement inattendu lorsque vous écrivez des fonctions avec des valeurs par défaut modifiables.
Aucune valeur par défaut en vue dans ce code, mais vous obtenez exactement le même problème.
Le problème est que l'
foo
on modifie une variable mutable transmise par l'appelant, lorsque l'appelant ne s'y attend pas. Un code comme celui-ci serait bien si la fonction était appelée quelque chose commeappend_5
; alors l'appelant appellerait la fonction afin de modifier la valeur qu'ils transmettent, et le comportement serait attendu. Mais une telle fonction serait très peu susceptible de prendre un argument par défaut, et ne retournerait probablement pas la liste (puisque l'appelant a déjà une référence à cette liste; celle qu'il vient de passer).Votre original
foo
, avec un argument par défaut, ne devrait pas modifiera
s'il a été explicitement transmis ou a obtenu la valeur par défaut. Votre code doit laisser les arguments modifiables seuls, sauf s'il ressort clairement du contexte / nom / documentation que les arguments sont censés être modifiés. Utiliser des valeurs mutables passées comme arguments en tant que temporaires locaux est une très mauvaise idée, que nous soyons en Python ou non et qu'il y ait des arguments par défaut impliqués ou non.Si vous devez manipuler de manière destructive un temporaire local au cours du calcul de quelque chose et que vous devez commencer votre manipulation à partir d'une valeur d'argument, vous devez en faire une copie.
la source
append
de changera
"sur place"). Qu'un mutable par défaut ne soit pas ré-instancié à chaque appel est le bit "inattendu" ... du moins pour moi. :)cache={}
. Cependant, je soupçonne que ce "moindre étonnement" survient lorsque vous ne vous attendez pas (ou ne voulez pas) à la fonction que vous appelez pour muter l'argument.cache={}
pour être complet.None
et l'attribution de la valeur par défaut réelle si l'argumentNone
ne résout pas ce problème (je le considère comme un anti-motif pour cette raison). Si vous corrigez l'autre bogue en évitant de muter les valeurs des arguments, qu'elles aient ou non des valeurs par défaut, vous ne remarquerez ou ne vous soucierez jamais de ce comportement "étonnant".Sujet déjà occupé, mais d'après ce que j'ai lu ici, les éléments suivants m'ont aidé à comprendre comment cela fonctionne en interne:
la source
a = a + [1]
surchargesa
... envisagez de le modifierb = a + [1] ; print id(b)
et d'ajouter une lignea.append(2)
. Cela rendra plus évident que+
sur deux listes crée toujours une nouvelle liste (affectée àb
), tandis qu'une modificationa
peut toujours avoir la mêmeid(a)
.C'est une optimisation des performances. En raison de cette fonctionnalité, lequel de ces deux appels de fonction pensez-vous être le plus rapide?
Je vais vous donner un indice. Voici le démontage (voir http://docs.python.org/library/dis.html ):
#
1#
2Comme vous pouvez le voir, il existe un avantage en termes de performances lors de l'utilisation d'arguments par défaut immuables. Cela peut faire une différence s'il s'agit d'une fonction fréquemment appelée ou si l'argument par défaut prend beaucoup de temps à construire. Gardez également à l'esprit que Python n'est pas C. En C, vous avez des constantes qui sont à peu près gratuites. En Python, vous n'avez pas cet avantage.
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 fonction. Lorsqu'ils sont utilisés par la fonction, plusieurs fois par cette fonction, ils sont et restent le même objet.
Lorsqu'ils sont mutables, lorsqu'ils subissent une mutation (par exemple, en y ajoutant un élément), ils restent mutés lors d'appels consécutifs.
Ils restent mutés car ils sont à chaque fois le même objet.
Code équivalent:
Puisque la liste est liée à la fonction lorsque l'objet fonction est compilé et instancié, ceci:
est presque exactement équivalent à ceci:
Manifestation
Voici une démonstration - vous pouvez vérifier qu'ils sont le même objet chaque fois qu'ils sont référencés par
example.py
et l'exécuter avec
python example.py
:Cela viole-t-il le principe du "moindre étonnement"?
Cet ordre d'exécution est souvent déroutant pour les nouveaux utilisateurs de Python. Si vous comprenez le modèle d'exécution Python, cela devient tout à fait normal.
L'instruction habituelle pour les nouveaux utilisateurs de Python:
Mais c'est pourquoi l'instruction habituelle pour les nouveaux utilisateurs est de créer leurs arguments par défaut comme ceci à la place:
Cela utilise le singleton None comme objet sentinelle pour indiquer à la fonction si nous avons ou non obtenu un argument autre que la valeur par défaut. Si nous n'obtenons aucun argument, nous voulons en fait utiliser une nouvelle liste vide
[]
, par défaut.Comme le dit la section du didacticiel sur le flux de contrôle :
la source
La réponse la plus courte serait probablement "la définition est l'exécution", donc tout l'argument n'a aucun sens strict. Comme exemple plus artificiel, vous pouvez citer ceci:
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 problème lorsque vous essayez d'utiliser des constructeurs par défaut.
la source
Une solution simple utilisant None
la source
Ce comportement n'est pas surprenant si vous tenez compte des éléments suivants:
Le rôle de (2) a été largement couvert dans ce fil. (1) est probablement le facteur d'étonnement, car ce comportement n'est pas «intuitif» en provenance d'autres langues.
(1) est décrit dans le tutoriel Python sur les classes . Pour tenter d'attribuer une valeur à un attribut de classe en lecture seule:
Revenez à l'exemple original et considérez les points ci-dessus:
Voici
foo
un objet eta
est un attribut defoo
(disponible surfoo.func_defs[0]
). Puisquea
est une liste,a
est mutable et est donc un attribut de lecture-écriture defoo
. Il est initialisé dans la liste vide comme spécifié par la signature lorsque la fonction est instanciée, et est disponible pour la lecture et l'écriture tant que l'objet fonction existe.L'appel
foo
sans remplacer une valeur par défaut utilise la valeur de cette valeur par défaut à partir defoo.func_defs
. Dans ce cas,foo.func_defs[0]
est utilisé poura
la portée de code de l'objet fonction. Modifications àa
modifierfoo.func_defs[0]
, qui fait partie de l'foo
objet et persiste entre l'exécution du code dansfoo
.Maintenant, comparez cela à l'exemple de la documentation sur l' émulation du comportement d'argument par défaut d'autres langages , de telle sorte que les valeurs par défaut de la signature de fonction soient utilisées à chaque exécution de la fonction:
En tenant compte de (1) et (2) , on peut voir pourquoi cela accomplit le comportement souhaité:
foo
objet fonction est instancié,foo.func_defs[0]
est défini surNone
, un objet immuable.L
l'appel de fonction),foo.func_defs[0]
(None
) est disponible dans la portée locale en tant queL
.L = []
, l'affectation ne peut pas réussir àfoo.func_defs[0]
, car cet attribut est en lecture seule.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 defoo
.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 de liste est lié à la fonction lorsqu'il est défini et non lorsqu'il est exécuté. Étant donné que les listes et les dictionnaires sont modifiables, toute modification de ce paramètre affectera les autres appels à cette fonction. Par conséquent, les appels suivants à la fonction recevront cette liste partagée qui peut avoir été modifiée par tout autre appel à la fonction. Pire encore, deux paramètres utilisent en même temps le paramètre partagé de cette fonction, inconscient des modifications apportées par l'autre.
Mauvaise méthode (probablement ...) :
Vous pouvez vérifier qu'ils sont un seul et même objet en utilisant
id
:Selon Brett Slatkin, «Python efficace: 59 façons spécifiques d'écrire mieux en Python», article 20: Utiliser
None
et Docstrings pour spécifier des arguments dynamiques par défaut (p. 48)Cette implémentation garantit que chaque appel à la fonction reçoit la liste par défaut ou bien la liste transmise à la fonction.
Méthode préférée :
Il peut y avoir des cas d'utilisation légitimes pour la «mauvaise méthode» par lesquels le programmeur a voulu que le paramètre de liste par défaut soit partagé, mais c'est plus probablement l'exception que la règle.
la source
Les solutions ici sont:
None
comme valeur par défaut (ou un nonceobject
) et activez-la pour créer vos valeurs lors de l'exécution; oulambda
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 agréable car les utilisateurs de la fonction peuvent passer un appelable, qui peut être déjà existant (comme un
type
)la source
Quand nous faisons cela:
... nous assignons l'argument
a
à une liste sans nom , si l'appelant ne transmet pas la valeur de a.Pour simplifier les choses pour cette discussion, donnons temporairement un nom à la liste sans nom. Et alors
pavlo
?À tout moment, si l'appelant ne nous dit pas ce que
a
c'est, nous le réutilisonspavlo
.Si
pavlo
est mutable (modifiable), etfoo
finit par le modifier, un effet que nous remarquons la prochaine foisfoo
est appelé sans précisera
.Voici donc ce que vous voyez (rappelez-vous,
pavlo
est initialisé à []):Maintenant,
pavlo
c'est [5].Un
foo()
nouvel appel modifie àpavlo
nouveau:Le fait de spécifier
a
lors de l'appelfoo()
garantit qu'ilpavlo
n'est pas touché.Alors,
pavlo
c'est encore[5, 5]
.la source
J'exploite parfois ce comportement comme une alternative au modèle suivant:
Si
singleton
est uniquement utilisé paruse_singleton
, j'aime le modèle suivant en remplacement:Je l'ai utilisé pour instancier des classes clientes qui accèdent à des ressources externes, et aussi pour créer des dictés ou des listes pour la mémorisation.
Comme je ne pense pas que ce modèle soit bien connu, je mets un petit commentaire pour me prémunir contre de futurs malentendus.
la source
_make_singleton
à def time dans l'exemple d'argument par défaut, mais à call time dans l'exemple global. Une véritable substitution utiliserait une sorte de boîte mutable pour la valeur d'argument par défaut, mais l'ajout de l'argument donne l'occasion de passer des valeurs alternatives.Vous pouvez contourner cela en remplaçant l'objet (et donc le lien avec la portée):
Moche, mais ça marche.
la source
Il peut être vrai que:
il est tout à fait cohérent de conserver les deux caractéristiques ci-dessus et de faire un autre point:
Les autres réponses, ou au moins certaines d'entre elles font les points 1 et 2 mais pas 3, ou font le point 3 et minimisent les points 1 et 2. Mais les trois sont vrais.
Il est peut-être vrai que changer de cheval à mi-chemin ici demanderait une rupture importante et qu'il pourrait y avoir plus de problèmes créés en changeant Python pour gérer intuitivement l'extrait d'ouverture de Stefano. Et il peut être vrai que quelqu'un qui connaissait bien les internes de Python pourrait expliquer un champ de mines de conséquences. cependant,
Le comportement existant n'est pas Pythonic, et Python réussit parce que très peu de choses sur le langage violent le principe du moindre étonnement n'importe où prèsmal. C'est un vrai problème, qu'il soit sage ou non de le déraciner. C'est un défaut de conception. Si vous comprenez mieux le langage en essayant de retracer le comportement, je peux dire que C ++ fait tout cela et plus encore; vous apprenez beaucoup en naviguant, par exemple, sur de subtiles erreurs de pointeur. Mais ce n'est pas Pythonic: les gens qui se soucient suffisamment de Python pour persévérer face à ce comportement sont des gens attirés par le langage parce que Python a beaucoup moins de surprises que les autres langages. Les barboteurs et les curieux deviennent des pythonistes lorsqu'ils sont étonnés du peu de temps qu'il faut pour faire fonctionner quelque chose - pas à cause d'un design fl - je veux dire, un puzzle logique caché - qui coupe contre les intuitions des programmeurs attirés par Python. parce que ça marche .
la source
x=[]
signifie «créer un objet de liste vide et y lier le nom« x »». Ainsi, dansdef f(x=[])
, une liste vide est également créée. Il n'est pas toujours lié à x, il est donc lié au substitut par défaut. Plus tard, lorsque f () est appelé, la valeur par défaut est supprimée et liée à x. Étant donné que c'est la liste vide elle-même qui a été écartée, cette même liste est la seule chose disponible pour se lier à x, que quelque chose y soit coincé ou non. Comment pourrait-il en être autrement?Ce n'est pas un défaut de conception . Quiconque trébuche sur cela fait quelque chose de mal.
Il y a 3 cas où je peux rencontrer ce problème:
cache={}
, et qu'on ne devrait pas du tout appeler la fonction avec un argument réel.L'exemple dans la question pourrait tomber dans la catégorie 1 ou 3. Il est étrange qu'il modifie à la fois la liste passée et la renvoie; vous devez choisir l'un ou l'autre.
la source
cache={}
modèle est vraiment une solution d'interview uniquement, en vrai code que vous voulez probablement@lru_cache
!Ce "bug" m'a donné beaucoup d'heures supplémentaires! Mais je commence à en voir une utilisation potentielle (mais j'aurais aimé que ce soit au moment de l'exécution, quand même)
Je vais vous donner ce que je vois comme un exemple utile.
imprime ce qui suit
la source
Changez simplement la fonction pour qu'elle soit:
la source
Je pense que la réponse à cette question réside dans la façon dont python transmet les données au paramètre (passe par valeur ou par référence), pas la mutabilité ou comment python gère la déclaration "def".
Une brève introduction. Tout d'abord, il existe deux types de types de données en python, l'un est un type de données élémentaire simple, comme les nombres, et un autre type de données, les objets. Deuxièmement, lors de la transmission de données à des paramètres, python transmet le type de données élémentaires par valeur, c'est-à-dire, fait une copie locale de la valeur dans une variable locale, mais transmet l'objet par référence, c'est-à-dire des pointeurs vers l'objet.
En admettant les deux points ci-dessus, expliquons ce qui est arrivé au code python. C'est uniquement à cause du passage par référence pour les objets, mais n'a rien à voir avec mutable / immuable, ou sans doute le fait que l'instruction "def" n'est exécutée qu'une seule fois lorsqu'elle est définie.
[] est un objet, donc python passe la référence de [] à
a
, c'est- à -dire qu'ila
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 remplacée par 1 par la méthode append. Mais notez qu'il n'y a qu'une seule copie de l'objet liste et que cet objet devient maintenant 1 . Lors de l'exécution du deuxième foo (), ce que la page Web d'effbot dit (les éléments ne sont plus évalués) est faux.a
est évalué comme étant l'objet de liste, bien que maintenant le contenu de l'objet soit 1 . C'est l'effet de passer par référence! Le résultat de foo (3) peut être facilement dérivé de la même manière.Pour valider davantage ma réponse, jetons un œil à deux codes supplémentaires.
====== No. 2 ========
[]
est un objet, il en est de mêmeNone
(le premier est mutable tandis que le second est immuable. Mais la mutabilité n'a rien à voir avec la question). Aucun n'est quelque part dans l'espace mais nous savons qu'il est là et il n'y a qu'une seule copie de None. Ainsi, chaque fois que foo est invoqué, les éléments sont évalués (par opposition à une réponse selon laquelle ils ne sont évalués qu'une seule fois) pour être None, pour être clair, la référence (ou l'adresse) de None. Ensuite, dans le foo, l'élément est changé en [], c'est-à-dire qu'il pointe vers un autre objet qui a une adresse différente.====== No. 3 =======
L'appel de foo (1) fait pointer les éléments vers un objet de liste [] avec une adresse, par exemple, 11111111. le contenu de la liste est changé à 1 dans la fonction foo dans la suite, mais l'adresse n'est pas modifiée, toujours 11111111 Puis foo (2, []) arrive. Bien que le [] dans foo (2, []) ait le même contenu que le paramètre par défaut [] lors de l'appel de foo (1), leur adresse est différente! Puisque nous fournissons le paramètre explicitement,
items
doit prendre l'adresse de ce nouveau[]
, disons 2222222, et le renvoyer après avoir apporté quelques modifications. Maintenant foo (3) est exécuté. puisque seulementx
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é dans 11111111. Ainsi, les éléments sont évalués comme étant l'adresse 11111111 ayant un élément 1. La liste située à 2222222 contient également un élément 2, mais elle n'est pointée par aucun élément plus. Par conséquent, un ajout de 3 feraitems
[1,3].D'après les explications ci-dessus, nous pouvons voir que la page Web effbot recommandée dans la réponse acceptée n'a pas donné de réponse pertinente à cette question. De plus, je pense qu'un point de la page Web effbot est faux. Je pense que le code concernant l'interface utilisateur est correct:
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:Si nous exécutons,
x[7]()
nous obtiendrons 7 comme prévu etx[9]()
donnerons 9, une autre valeur dei
.la source
x[7]()
c'est le cas9
.TLDR: les valeurs par défaut au moment de la définition sont cohérentes et strictement plus expressives.
La définition d'une fonction affecte deux étendues: l'étendue de définition contenant la fonction et l'étendue d'exécution contenue par la fonction. Bien qu'il soit assez clair comment les blocs sont mappés aux étendues, la question est de savoir où
def <name>(<args=defaults>):
appartient:La
def name
pièce doit être évaluée dans le périmètre de définition - nous voulonsname
y être disponibles, après tout. L'évaluation de la fonction uniquement en elle-même la rendrait inaccessible.Puisque
parameter
c'est un nom constant, on peut "l'évaluer" en même temps quedef name
. Cela a également l'avantage de produire la fonction avec une signature connue commename(parameter=...):
, au lieu d'un nuname(...):
.Maintenant, quand évaluer
default
?La cohérence dit déjà "à la définition": tout le reste
def <name>(<args=defaults>):
est également mieux évalué à la définition. En retarder certaines parties serait le choix étonnant.Les deux choix ne sont pas équivalents non plus: si
default
est évalué au moment de la définition, il peut toujours affecter le temps d'exécution. Sidefault
est évalué au moment de l'exécution, il ne peut pas affecter le temps de définition. Choisir "à la définition" permet d'exprimer les deux cas, tandis que choisir "à l'exécution" ne peut en exprimer qu'un:la source
def <name>(<args=defaults>):
est également mieux évalué à la définition." Je ne pense pas que la conclusion découle de la prémisse. Ce n'est pas parce que deux choses sont sur la même ligne qu'elles doivent être évaluées dans la même portée.default
est une chose différente du reste de la ligne: c'est une expression. L'évaluation d'une expression est un processus très différent de la définition d'une fonction.def
) ou d'une expression (lambda
) ne change pas que la création d'une fonction signifie une évaluation - en particulier de sa signature. Et les valeurs par défaut font partie de la signature d'une fonction. Cela ne signifie pas que les valeurs par défaut doivent être évaluées immédiatement - les indices de type peuvent ne pas l'être, par exemple. Mais cela suggère certainement qu'ils devraient le faire à moins qu'il n'y ait une bonne raison de ne pas le faire.Toutes les autres réponses expliquent pourquoi c'est en fait un comportement agréable et souhaité, ou pourquoi vous ne devriez pas en avoir besoin de toute façon. Le mien est pour les têtus qui veulent exercer leur droit de plier la langue à leur gré, et non l'inverse.
Nous allons "corriger" ce comportement avec un décorateur qui copiera la valeur par défaut au lieu de réutiliser la même instance pour chaque argument positionnel laissé à sa valeur par défaut.
Redéfinissons maintenant notre fonction en utilisant ce décorateur:
C'est particulièrement bien pour les fonctions qui prennent plusieurs arguments. Comparer:
avec
Il est important de noter que la solution ci-dessus se casse si vous essayez d'utiliser des arguments de mots clés, comme ceci:
Le décorateur pourrait être ajusté pour permettre cela, mais nous laissons cela comme un exercice pour le lecteur;)
la source