python - Le «moindre étonnement» et l'argument invariable par défaut




language-design least-astonishment (20)

Toute personne bricolant avec Python assez longtemps a été piquée (ou déchirée) par le problème suivant:

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

Les novices en Python s'attendent à ce que cette fonction retourne toujours une liste avec un seul élément: [5] . Le résultat est plutôt 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 responsables a déjà rencontré pour la première fois cette fonctionnalité et l’a qualifiée de «défaut de conception spectaculaire» de la langue. Je lui ai répondu que le comportement avait une explication sous-jacente, et il est en effet très curieux et inattendu si vous ne comprenez pas les entrailles. Cependant, je n'ai pas été en mesure de répondre à la question suivante: quelle est la raison pour laquelle l'argument par défaut est lié lors de la définition de la fonction et non lors de son exécution? Je doute que le comportement expérimenté ait une utilisation pratique (qui a vraiment utilisé des variables statiques en C, sans générer de bugs?)

Modifier :

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

>>> def a():
...     print("a executed")
...     return []
... 
>>>            
>>> def b(x=a()):
...     x.append(5)
...     print(x)
... 
a executed
>>> b()
[5]
>>> b()
[5, 5]

Pour moi, il semble que la décision 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 que x est effectivement lié à la valeur par défaut spécifiée lorsque la fonction est appelée, non définie, ce qui présenterait un défaut profond: la ligne def 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 le contenu de cette ligne est évalué lors de son exécution, c'est-à-dire lors de la définition de la fonction.


5 points en défense de Python

  1. Simplicité: Le comportement est simple dans le sens suivant: La plupart des gens tombent dans ce piège qu'une seule fois, et non à plusieurs reprises.

  2. Cohérence : Python transmet toujours des objets, pas des noms. Le paramètre par défaut fait évidemment partie de l'en-tête de la fonction (pas du corps de la fonction). Il doit donc être évalué au moment du chargement du module (et uniquement au moment du chargement du module, sauf si imbriqué), et non au moment de l'appel de la fonction.

  3. Utilité : Comme le souligne Frederik Lundh dans son explication de "Valeurs de paramètre par défaut en Python" , le comportement actuel peut être très utile pour la programmation avancée. (Utiliser avec modération.)

  4. Documentation suffisante : dans la documentation la plus élémentaire de Python, le didacticiel, le problème est annoncé comme un "avertissement important" dans la première sous-section de la section "Pour en savoir plus sur la définition de fonctions" . L'avertissement utilise même des caractères gras, rarement appliqués en dehors des titres. RTFM: Lisez le manuel détaillé.

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


Architecture

L'attribution de valeurs par défaut dans un appel de fonction est une odeur de code.

def a(b=[]):
    pass

C'est la signature d'une fonction qui ne sert à rien. Pas seulement à cause des problèmes décrits par d'autres réponses. Je ne vais pas aller à cela ici.

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

Les fonctions qui font deux choses sont mauvaises, comme l’apprend la pratique du code propre.

En s'attaquant à ce problème avec le polymorphisme, nous étendrions la liste python ou en encapsulions une dans une classe, puis nous y exécuterions notre fonction.

Mais attendez vous dites, j'aime mes one-liners.

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, travailler sur le même code.

  • être capable de changer le comportement du matériel lorsque de nouvelles exigences apparaissent.

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

Ne laissez pas les bombes à retardement à ramasser plus tard.

En séparant cette fonction en deux choses, 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 mélanger 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 faire ça?

Sauf si vous échouez dans votre projet, votre code continuera à vivre. Très probablement, votre fonction fera plus que cela. La manière appropriée de créer un code maintenable consiste à 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 quiconque a effectué une programmation orientée objet. Placer la logique qui gère l'instanciation de liste dans le constructeur réduit la charge cognitive de compréhension de ce que le code fait.

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

En renvoyant une liste séparée, vous pouvez supposer qu'il est prudent de faire ce que vous voulez. Mais ce n'est peut-être pas le cas, puisqu'il est également partagé par l'objet a. Forcer l'utilisateur à s'y référer comme pour lui abrappeler à quel endroit appartient la liste. Tout nouveau code qui veut être modifié absera naturellement placé dans la classe, à laquelle il appartient.

la def dontdothis(b=None):fonction de signature n'a aucun de ces avantages.


Pourquoi ne pas vous introspecter?

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

Étant donné une petite fonction simple func définie comme:

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

Lorsque Python rencontre, la première chose qu'il va faire est de compiler afin de créer un code de l' 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 le dit la réponse principale: la liste a peut maintenant être considérée comme un membre de la fonction func .

Faisons donc un peu d’introspection, un avant et un après, pour examiner comment la liste est développée dans l’objet fonction. J'utilise Python 3.x pour cela, la même chose s'applique à Python 2 (utilisez __defaults__ ou func_defaults dans 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 prend les paramètres par défaut spécifiés ( a = [] ici) et les __defaults__ dans l'attribut __defaults__ pour l'objet de fonction (section pertinente: Callables):

>>> func.__defaults__
([],)

Ok, donc une liste vide comme seule entrée dans __defaults__ , comme prévu.

Fonction après exécution:

Exécutons maintenant cette fonction:

>>> func()

Maintenant, __defaults__ ces __defaults__ :

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

Étonné? La valeur à l'intérieur de l'objet change! Les appels consécutifs à la fonction seront simplement ajoutés à cet objet de list incorporé:

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

Donc, là vous l' avez, la raison pour laquelle cette « faille » se produit, parce que les arguments par défaut font partie de l'objet de la fonction. Il n'y a rien de bizarre ici, c'est juste un peu surprenant.

Pour résoudre ce problème, la solution courante consiste à utiliser None comme valeur par défaut, puis à l'initialiser dans le corps de la fonction:

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

Étant donné que le corps de la fonction est exécutée à nouveau à chaque fois, vous obtenez toujours une nouvelle 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 changer votre fonction pour renvoyer l' id de la liste utilisée dans le corps de la fonction. Ensuite, comparez-le à la liste dans __defaults__ (position [0] dans __defaults__ ) et vous verrez comment ils 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 les tâches suivantes:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

comme vous remarquerez, input() est appelée avant le processus de construction de la fonction et la liaison au nom bar est fait.


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

Les valeurs de paramètre par défaut 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. Cela est particulièrement important à comprendre lorsqu'un paramètre par défaut est un objet modifiable, 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 fait modifiée. Ce n'est généralement pas ce qui était prévu. Une solution consiste à utiliser None comme valeur par défaut et à le tester explicitement dans le corps de la fonction [...]


Ce que vous demandez, c'est pourquoi ceci:

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

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


Eh bien, la raison en est tout simplement que les liaisons sont effectuées lorsque le code est exécuté, et la définition de la fonction est exécutée, ainsi ... 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 incident imprévu. 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 du moins ralentir beaucoup l'instanciation d'objet, car vous auriez à garder le code de classe autour et l'exécuter lorsque des objets sont créés.

Oui, c'est inattendu. Mais une fois que le penny est tombé, cela correspond parfaitement au fonctionnement de Python en général. En fait, c'est une bonne aide à l'enseignement, et une fois que vous comprenez pourquoi cela se produit, vous Grok python beaucoup mieux.

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


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, puisque vous ne perdez quelques fonctionnalités utiles, mais il peut valoir la peine quel que soit simplement pour éviter la confusion chez les débutants. Les inconvénients sont les suivants:

1. la performance

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

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. Soit vous payez un prix élevé pour chaque appel, soit vous devez mettre en cache manuellement la valeur, en polluant votre espace-noms et en 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)]

Ceci retourne une liste de fonctions qui retournent 0,1,2,3 ... respectivement. Si le comportement est modifié, ils lieront plutôt i à la valeur de i de la durée de l' appel , vous obtiendrez ainsi une liste de fonctions renvoyant toutes 9 .

Autrement, la seule façon de mettre en œuvre cette solution serait de créer une nouvelle fermeture avec le i lié, c'est-à-dire:

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

3. Introspection

Considérons 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.

Supposons maintenant que le comportement des valeurs par défaut puisse être modifié de sorte que cela équivaut à:

_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 de voir quels sont les arguments par défaut. Comme 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.


Je ne connais rien au fonctionnement interne de l'interprète 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 options par défaut. Lorsque vous instanciez une liste:

a = []

vous attendez une nouvelle liste référencée par un .

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 ambigu à la place:

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

utilisateur, voulez-vous définir par défaut la date et l'heure correspondant à la définition ou à 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 "assignation" était la première instruction de la fonction (datetime.now () appelé lors de l'appel de la fonction). D'autre part, si l'utilisateur souhaitait le mappage définition-temps, il pouvait écrire:

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

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

def x(static a=b):

Ce n'est pas un défaut de conception . Quiconque trébuche 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 en tant qu'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 des arguments pour avoir des attributs de fonction, par exemple cache={}, et que vous n'êtes pas censé appeler la fonction avec un argument réel.
  2. Vous avez l'intention de ne pas modifier l'argument, mais vous l'avez modifié par inadvertance . C'est un bug, corrigez-le.
  3. Vous avez l'intention de modifier l'argument pour l'utiliser dans la fonction, mais vous ne vous attendiez 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 ou non de la valeur par défaut! Python n'est pas un langage d'appel par valeur, il ne crée donc pas la copie pour vous, vous devez être explicite à ce sujet.

L'exemple de la question pourrait appartenir à la catégorie 1 ou 3. Il est étrange qu'il modifie et retourne la liste transmise; vous devriez choisir l'un ou l'autre.


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 cohérent de conserver les deux fonctionnalités ci-dessus tout en soulignant un autre point:

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

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

Il est peut-être vrai que changer de monture en cours de route demanderait une casse importante et que le fait de changer Python pour gérer intuitivement l'extrait d'ouverture de Stefano pourrait créer davantage de problèmes. 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 actuel n'est pas Pythonic et Python est un succès parce que très peu de choses sur la langue constitue une violation du principe de moindre étonnement partout prèssi mal. C'est un problème réel, s'il serait sage ou non de le déraciner. C'est un défaut de conception. Si vous comprenez beaucoup mieux le langage en essayant de tracer le comportement, je peux dire que le C ++ fait tout cela et plus encore; vous apprenez beaucoup en naviguant, par exemple, dans les erreurs de pointeur subtiles. Mais ce n’est pas Pythonic: les personnes qui s’intéressent suffisamment à Python pour persévérer face à ce comportement sont des personnes attirées par le langage, car Python réserve beaucoup moins de surprises que d’autres langages. Dabblers et les curieux deviennent des pythonistes quand ils s’étonnent du peu de temps qu’il faut pour que quelque chose fonctionne - et non à cause d’un design fl - je veux dire d’une logique cachée - qui tranche avec les intuitions des programmeurs attirés par Python parce que ça marche .


Cela n’a en réalité rien à voir avec les valeurs par défaut, à part le fait qu’il se présente souvent comme un comportement inattendu lorsque vous écrivez des fonctions avec des valeurs par défaut modifiables.

>>> def foo(a):
    a.append(5)
    print a

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

Aucune valeur par défaut en vue dans ce code, mais vous obtenez exactement le même problème.

Le problème est que fooest en train de modifier une variable mutable passé depuis l'appelant, lorsque l'appelant ne s'attend pas. Un code comme celui-ci conviendrait si la fonction s'appelait quelque chose comme append_5; alors l'appelant appellerait la fonction afin de modifier la valeur transmise et le comportement serait attendu. Mais il est très peu probable qu'une telle fonction prenne un argument par défaut et ne renvoie 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 permettre de asavoir s'il a été explicitement passé ou s'il a reçu la valeur par défaut. Votre code doit laisser les arguments mutables seuls à moins qu'il ne soit clairement indiqué dans le contexte / nom / documentation que les arguments sont supposé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 ou non des arguments par défaut.

Si vous devez manipuler de manière destructive un temporaire local en cours de calcul, et si vous devez commencer votre manipulation à partir d'une valeur d'argument, vous devez en faire une copie.


Il suffit de changer la fonction pour être:

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 façon dont python transmet les données au paramètre (transmission par valeur ou par référence), pas de mutabilité, ni comment python gère l'instruction "def".

Une brève introduction. Premièrement, il existe deux types de types de données en python: le type de données élémentaire simple, comme les nombres, et le 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émentaire par valeur, c’est-à-dire crée une copie locale de la valeur dans une variable locale, mais transmet objet par référence, c'est-à-dire des pointeurs sur 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 peut-être avec le fait que la déclaration "def" n’est exécutée qu’une fois lorsqu’elle est définie.

[] étant un objet, python transmettra la référence de [] à a, c’est- à -dire aqu’un pointeur sur [] qui se trouve dans la mémoire en tant qu’objet. Il n'y a qu'un seul exemplaire de [] avec, toutefois, 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 . Lorsque vous exécutez le second foo (), la page Web effbot (les éléments ne sont plus évalués) est fausse. aest évalué comme étant l'objet 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, examinons 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, il en est ainsi None(le premier est mutable et le dernier est immuable. Mais la mutabilité n’a rien à voir avec la question). None est quelque part dans l'espace mais nous savons qu'il est là et qu'il n'y a qu'un seul exemplaire de None ici. Ainsi, chaque fois que foo est appelé, les éléments sont évalués (par opposition à une réponse indiquant qu’il n’est évalué qu’une fois) comme étant Aucun, pour être clair, la référence (ou l’adresse) de Aucun. Ensuite, dans l'objet foo, l'élément est remplacé par [], c.-à-d. Qu'il pointe vers un autre objet ayant 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'invocation de foo (1) fait que les éléments pointent vers un objet de liste [] avec une adresse, par exemple 11111111. le contenu de la liste est remplacé par 1 dans la fonction foo de la suite, mais l'adresse reste inchangée Alors 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! Étant donné que nous fournissons explicitement le paramètre, itemsdoit prendre l'adresse de cette nouvelle [], par exemple 2222222, et la renvoyer après avoir apporté des modifications. Maintenant foo (3) est exécuté. depuis seulementxest fourni, les éléments doivent reprendre 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 liste situé dans 11111111. Par conséquent, les éléments sont considérés comme l'adresse 11111111 ayant un élément 1. La liste située en 2222222 contient également un élément 2, mais elle n'est pas plus. En conséquence, un appendice de 3 fera items[1,3].

D'après les explications ci-dessus, nous pouvons constater que la page Web d' effbot recommandée dans la réponse acceptée n'a pas permis de fournir une 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 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 aurons 7 comme prévu, et nous x[9]()donnerons 9, une autre valeur de i.


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 abondamment commenté, le paramètre list est lié à la fonction lorsqu'elle est définie et non lorsqu'elle est exécutée. Comme les listes et les dictionnaires sont mutables, toute modification de ce paramètre affectera les autres appels de cette fonction. En conséquence, les appels ultérieurs à la fonction recevront cette liste partagée qui pourrait 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, inconscients 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 du même objet en utilisant id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Per Brett Slatkin, "Effective Python: 59 façons spécifiques d'écrire un meilleur python", élément 20: Utilisation Noneet Docstrings pour spécifier des arguments dynamiques par défaut (p. 48).

La convention permettant d'obtenir le résultat souhaité en Python consiste à fournir une valeur par défaut Noneet à documenter le comportement réel dans la chaîne de documentation.

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 :

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 «méthode incorrecte» dans lesquels le programmeur souhaitait que le paramètre de liste par défaut soit partagé, mais il est plus probable que l’exception que la règle.


Quand on fait ça:

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

... nous assignons l'argument aà une liste non nommée , si l'appelant ne transmet pas la valeur de a.

Pour simplifier la discussion, donnons temporairement un nom à la liste non nommée. Que diriez- pavlovous

def foo(a=pavlo):
   ...

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

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

Donc, voici ce que vous voyez (rappelez-vous, pavloest initialisé à []):

 >>> foo()
 [5]

Maintenant, pavloc'est [5].

Appeler à foo()nouveau modifie à pavlonouveau:

>>> foo()
[5, 5]

Spécifier aquand appeler foo()garantit pavlon'est pas touché.

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

Alors, pavloest toujours [5, 5].

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

Sujet déjà occupé, 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

Vous pouvez contourner ce problème 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.


1) Le problème dit de "Argument par défaut mutable" est en général un exemple particulier démontrant que:
"Toutes les fonctions avec ce problème souffrent également d'un problème similaire d'effet secondaire sur le paramètre actuel ,"
Cela va à l'encontre des règles de la programmation fonctionnelle, généralement non désirable et devrait être fixé les deux ensemble.

Exemple:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Solution : une copie
Une solution absolument sûre est copyou deepcopyl'entrée objet 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]"

Beaucoup de 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 approfondi copy.deepcopy()(ce dernier est utile si l'objet mutable est composé à partir d'objets mutables). Certains objets sont fondamentalement basés sur des effets secondaires tels que l’objet "fichier" et ne peuvent pas être reproduits de manière significative par copie. copying

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 devrait pas être sauvegardé 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 de ces sous-classes par convention. Il _var1s'agit d'un attribut privé)

Conclusion:
Les objets de paramètres d'entrée ne doivent pas être modifiés à la place (mutés) ni liés à un objet renvoyé par la fonction. (Si nous préférons programmer sans effets secondaires, c'est vivement recommandé. Voir Wiki sur "effets secondaires" (Les deux premiers paragraphes sont pertinents dans ce contexte.).)

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

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


Ce "bug" m'a donné beaucoup d'heures supplémentaires! Mais je commence à 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 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

La réponse la plus courte serait probablement "définition est exécution", par conséquent tout l'argument n'a pas de sens strict. Comme exemple plus artificiel, vous pouvez citer ceci:

def a(): return []

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

Espérons qu'il suffira de montrer que le fait de ne pas exécuter les expressions d'argument par défaut au moment de l'exécution de l' definstruction n'est pas facile ou n'a pas de sens, ou les deux.

Je conviens que c'est un piège quand vous essayez d'utiliser des constructeurs par défaut, cependant.





least-astonishment