Argument par défaut mutable en Python: Pourquoi?


20

Je sais que les arguments par défaut sont créés au moment de l'initialisation de la fonction et non à chaque appel de la fonction. Voir le code suivant:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

Ce que je ne comprends pas, c'est pourquoi cela a été mis en œuvre de cette façon. Quels avantages ce comportement offre-t-il par rapport à une initialisation à chaque appel?


Ceci est déjà discuté avec des détails étonnants sur Stack Overflow: stackoverflow.com/q/1132941/5419599
Wildcard

Réponses:


14

Je pense que la raison en est la simplicité de mise en œuvre. Permettez-moi de développer.

La valeur par défaut de la fonction est une expression que vous devez évaluer. Dans votre cas, c'est une expression simple qui ne dépend pas de la fermeture, mais elle peut être quelque chose qui contient des variables libres - def ook(item, lst = something.defaultList()). Si vous devez concevoir Python, vous aurez le choix - l'évaluez-vous une fois lorsque la fonction est définie ou à chaque fois que la fonction est appelée. Python choisit la première (contrairement à Ruby, qui va avec la deuxième option).

Il y a certains avantages à cela.

Tout d'abord, vous obtenez des gains de vitesse et de mémoire. Dans la plupart des cas, vous aurez des arguments par défaut immuables et Python peut les construire une seule fois, au lieu de chaque appel de fonction. Cela économise (une partie) de la mémoire et du temps. Bien sûr, cela ne fonctionne pas très bien avec des valeurs mutables, mais vous savez comment vous pouvez contourner.

Un autre avantage est la simplicité. Il est assez facile de comprendre comment l'expression est évaluée - elle utilise la portée lexicale lorsque la fonction est définie. S'ils allaient dans l'autre sens, la portée lexicale pourrait changer entre la définition et l'invocation et la rendre un peu plus difficile à déboguer. Python va un long chemin pour être extrêmement simple dans ces cas.


3
Point intéressant - bien que ce soit généralement le principe de la moindre surprise avec Python. Certaines choses sont simples dans un sens formel de complexité du modèle, mais non évidentes et surprenantes, et je pense que cela compte.
Steve314

1
La chose à propos de la moindre surprise ici est la suivante: si vous avez la sémantique d'évaluation à chaque appel, vous pouvez obtenir une erreur si la fermeture change entre deux appels de fonction (ce qui est tout à fait possible). Cela peut être plus surprenant que de savoir qu'il est évalué une fois. Bien sûr, on peut affirmer que lorsque vous venez d'autres langues, vous excluez la sémantique d'évaluation à chaque appel et c'est la surprise, mais vous pouvez voir comment cela se passe dans les deux sens :)
Stefan Kanev

Bon point sur la portée
0xc0de

Je pense que la portée est en fait l'élément le plus important. Étant donné que vous n'êtes pas limité aux constantes par défaut, vous pouvez avoir besoin de variables qui ne sont pas dans la portée sur le site d'appel.
Mark Ransom

5

Une façon de le dire est que le lst.append(item) ne mute pas le lstparamètre. lstfait toujours référence à la même liste. C'est juste que le contenu de cette liste a été modifié.

Fondamentalement, Python n'a pas (que je me souvienne) de variables constantes ou immuables - mais il a des types constants et immuables. Vous ne pouvez pas modifier une valeur entière, vous pouvez seulement la remplacer. Mais vous pouvez modifier le contenu d'une liste sans la remplacer.

Comme un entier, vous ne pouvez pas modifier une référence, vous pouvez seulement la remplacer. Mais vous pouvez modifier le contenu de l'objet référencé.

Quant à la création de l'objet par défaut une fois, j'imagine que c'est principalement une optimisation, pour économiser sur les frais généraux de création d'objet et de récupération de place.


+1 Exactement. Il est important de comprendre la couche d'indirection - qu'une variable n'est pas une valeur; il fait plutôt référence à une valeur. Pour modifier une variable, la valeur peut être échangée ou mutée (si elle est mutable).
Joonas Pulakka

Face à quelque chose de délicat impliquant des variables en python, je trouve utile de considérer "=" comme "l'opérateur de liaison de nom"; le nom est toujours rebondi, que la chose à laquelle nous le lions soit nouvelle (objet frais ou instance de type immuable) ou non.
StarWeaver

4

Quels avantages ce comportement offre-t-il par rapport à une initialisation à chaque appel?

Il vous permet de sélectionner le comportement que vous souhaitez, comme vous l'avez démontré dans votre exemple. Donc, si vous voulez que l'argument par défaut soit immuable, vous utilisez une valeur immuable , telle que Noneou 1. Si vous souhaitez rendre l'argument par défaut mutable, vous utilisez quelque chose de mutable, tel que []. C'est juste de la flexibilité, certes, il peut mordre si vous ne le savez pas.


2

Je pense que la vraie réponse est: Python a été écrit comme un langage procédural et n'a adopté les aspects fonctionnels qu'après coup. Ce que vous recherchez, c'est que la valeur par défaut des paramètres soit effectuée comme une fermeture, et les fermetures en Python ne sont vraiment qu'à moitié cuites. Pour preuve de cet essai:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

ce qui donne [2, 2, 2]où vous vous attendez à une véritable fermeture [0, 1, 2].

Il y a beaucoup de choses que j'aimerais si Python avait la capacité d'envelopper les paramètres par défaut dans les fermetures. Par exemple:

def foo(a, b=a.b):
    ...

Ce serait extrêmement pratique, mais "a" n'est pas dans la portée au moment de la définition de la fonction, donc vous ne pouvez pas le faire et à la place vous devez faire le maladroit:

def foo(a, b=None):
    if b is None:
        b = a.b

Ce qui est presque la même chose ... presque.



1

Un énorme avantage est la mémorisation. Ceci est un exemple standard:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

et pour comparaison:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Mesures de temps en ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s

0

Cela se produit car la compilation en Python est effectuée en exécutant le code descriptif.

Si on disait

def f(x = {}):
    ....

il serait assez clair que vous vouliez un nouveau tableau à chaque fois.

Mais si je dis:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Ici, je suppose que je veux créer des trucs dans diverses listes et avoir un seul fourre-tout global lorsque je ne spécifie pas de liste.

Mais comment le compilateur le devinerait-il? Alors pourquoi essayer? Nous pourrions compter sur le fait que cela ait été nommé ou non, et cela pourrait parfois aider, mais en réalité, ce serait juste deviner. Dans le même temps, il y a une bonne raison de ne pas essayer - la cohérence.

En l'état, Python exécute simplement le code. La variable list_of_all se voit déjà attribuer un objet, de sorte que cet objet est passé par référence dans le code par défaut x de la même manière qu'un appel à n'importe quelle fonction obtiendrait une référence à un objet local nommé ici.

Si nous voulions distinguer le cas sans nom du cas nommé, cela impliquerait que le code lors de la compilation exécute l'affectation d'une manière significativement différente de celle qui est exécutée au moment de l'exécution. Nous ne faisons donc pas le cas particulier.


-5

Cela se produit car les fonctions en Python sont des objets de première classe :

Les valeurs des paramètres par défaut sont évaluées lors de l'exécution de la définition de fonction. 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 .

Il explique ensuite que la modification de la valeur du paramètre modifie la valeur par défaut pour les appels suivants, et qu'une solution simple consistant à utiliser None comme valeur par défaut, avec un test explicite dans le corps de la fonction, est tout ce qui est nécessaire pour éviter toute surprise.

Ce qui signifie que cela def foo(l=[])devient une instance de cette fonction lors de son appel et est réutilisé pour d'autres appels. Considérez les paramètres de fonction comme une séparation des attributs d'un objet.

Les pro pourraient inclure de tirer parti de cela pour que les classes aient des variables statiques de type C. Il est donc préférable de déclarer les valeurs par défaut None et de les initialiser selon les besoins:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

rendements:

[5] [5] [5] [5]

au lieu de l'inattendu:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]


5
Non. Vous pouvez définir des fonctions (de première classe ou non) différemment pour évaluer à nouveau l'expression d'argument par défaut pour chaque appel. Et tout ce qui suit, soit environ 90% de la réponse, est complètement à côté de la question. -1

1
Alors partagez avec nous cette connaissance de la façon de définir des fonctions pour évaluer l'argument par défaut pour chaque appel, je voudrais connaître une manière plus simple que celle recommandée par Python Docs .
inverser le

2
Au niveau de la conception linguistique, je veux dire. La définition du langage Python indique actuellement que les arguments par défaut sont traités comme ils sont; il pourrait tout aussi bien indiquer que les arguments par défaut sont traités d'une autre manière. IOW vous répondez "c'est comme ça que les choses sont" à la question "pourquoi les choses sont-elles comme elles sont".

Python aurait pu implémenter des paramètres par défaut similaires à la façon dont Coffeescript le fait. Il insérerait du bytecode pour vérifier les paramètres manquants, et s'ils manquaient, évaluerait l'expression.
Winston Ewert
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.