Pourquoi les arguments par défaut sont-ils évalués au moment de la définition en Python?

StackOverflow https://stackoverflow.com/questions/1651154

  •  22-07-2019
  •  | 
  •  

Question

J'ai eu beaucoup de difficulté à comprendre la cause fondamentale d'un problème rencontré dans un algorithme. Ensuite, en simplifiant les fonctions étape par étape, j’ai découvert que l’évaluation des arguments par défaut dans Python ne se comportait pas comme prévu.

Le code est le suivant:

class Node(object):
    def __init__(self, children = []):
        self.children = children

Le problème est que chaque instance de la classe Node partage le même attribut children , si l'attribut n'est pas donné explicitement, tel que:

>>> n0 = Node()
>>> n1 = Node()
>>> id(n1.children)
Out[0]: 25000176
>>> id(n0.children)
Out[0]: 25000176

Je ne comprends pas la logique de cette décision de conception? Pourquoi les concepteurs Python ont-ils décidé que les arguments par défaut devaient être évalués au moment de la définition? Cela me semble très contre-intuitif.

Était-ce utile?

La solution

L'alternative serait plutôt épaisse - stocker les "valeurs d'argument par défaut". dans l'objet de fonction en tant que " thunks " de code à exécuter maintes et maintes fois à chaque fois que la fonction est appelée sans valeur spécifiée pour cet argument - et rendrait beaucoup plus difficile l'obtention d'une liaison anticipée (binding at def time), ce qui est souvent ce que vous souhaitez. Par exemple, en Python tel qu’il existe:

def ack(m, n, _memo={}):
  key = m, n
  if key not in _memo:
    if m==0: v = n + 1
    elif n==0: v = ack(m-1, 1)
    else: v = ack(m-1, ack(m, n-1))
    _memo[key] = v
  return _memo[key]

... écrire une fonction mémoize comme celle ci-dessus est une tâche assez élémentaire. De même:

for i in range(len(buttons)):
  buttons[i].onclick(lambda i=i: say('button %s', i))

... le simple i = i , qui repose sur la liaison anticipée (temps de définition) des valeurs arg par défaut, constitue un moyen simple- ment simple d'obtenir une liaison anticipée. Ainsi, la règle actuelle est simple, directe et vous permet de faire tout ce que vous voulez d'une manière extrêmement facile à expliquer et à comprendre: si vous souhaitez une liaison tardive de la valeur d'une expression, évaluez cette expression dans le corps de la fonction; si vous souhaitez une liaison anticipée, évaluez-la comme valeur par défaut d'un argument.

L'alternative, imposant une liaison tardive dans les deux situations, n'offrirait pas cette flexibilité et vous obligerait à effectuer des étapes (telles que l'intégration de votre fonction dans une usine de fermeture) chaque fois que vous avez besoin d'une liaison anticipée, comme dans les exemples ci-dessus. - Encore plus de passe-partout lourds imposés au programmeur par cette décision de conception hypothétique (au-delà des décisions "invisibles" consistant à générer et à évaluer de manière répétée des thunks un peu partout).

En d'autres termes, "il devrait exister un, et de préférence un seul moyen évident de le faire [1]": lorsque vous souhaitez une liaison tardive, il existe déjà un moyen parfaitement évident de le réaliser (puisque toutes les le code de la fonction n'est exécuté qu'au moment de l'appel, évidemment tout ce qui est évalué il est lié tardivement); le fait que l'évaluation default-arg produise une liaison précoce vous donne un moyen évident d'obtenir une liaison précoce (un plus! -) plutôt que de donner à DEUX moyens évidents d'obtenir une liaison tardive et aucun moyen évident d'obtenir une liaison précoce (un moins! -).

[1]: "Bien que cette façon de procéder ne soit pas évidente au premier abord, sauf si vous êtes néerlandais."

Autres conseils

Le problème est le suivant.

Il est trop coûteux d'évaluer une fonction en tant qu'initialiseur à chaque fois que la fonction est appelée .

  • 0 est un littéral simple. Évaluez-le une fois, utilisez-le pour toujours.

  • int est une fonction (comme une liste) qui devrait être évaluée chaque fois que cela est nécessaire en tant qu'initialiseur.

La construction [] est littérale, comme 0 , ce qui signifie "cet objet exact".

Le problème est que certaines personnes espèrent que cela signifie list comme dans "évaluer cette fonction pour moi, s'il vous plaît, pour obtenir l'objet qui est l'initialiseur".

Il serait extrêmement contraignant d’ajouter l’instruction if nécessaire pour effectuer cette évaluation à tout moment. Il est préférable de prendre tous les arguments comme des littéraux et de ne pas effectuer d’évaluation de fonction supplémentaire dans le cadre d’une tentative d’évaluation de fonction.

De manière plus fondamentale, il est techniquement impossible d'implémenter des arguments par défaut en tant qu'évaluations de fonctions.

Considérez un instant l’horreur récursive de ce type de circularité. Supposons qu'au lieu que les valeurs par défaut soient des littéraux, nous leur permettons d'être des fonctions qui sont évaluées chaque fois que les valeurs par défaut d'un paramètre sont requises.

[Cela correspondrait au fonctionnement de collections.defaultdict .]

def aFunc( a=another_func ):
    return a*2

def another_func( b=aFunc ):
    return b*3

Quelle est la valeur de another_func () ? Pour obtenir la valeur par défaut pour b , il doit évaluer aFunc , ce qui nécessite une évaluation de another_func . Oops.

Bien sûr, dans votre situation, il est difficile à comprendre. Mais vous devez voir que l’évaluation systématique des arguments par défaut imposerait une lourde charge d’exécution au système.

Vous devez également savoir que, dans le cas de types de conteneur, ce problème peut se produire, mais vous pouvez le contourner en rendant la chose explicite:

def __init__(self, children = None):
    if children is None:
       children = []
    self.children = children

La solution de contournement, discutée ici (et très solide), est:

class Node(object):
    def __init__(self, children = None):
        self.children = [] if children is None else children

Pourquoi chercher une réponse de von Löwis, mais c'est probablement parce que la définition de la fonction crée un objet code en raison de l'architecture de Python, et il se peut qu'il n'existe aucune possibilité d'utiliser des types de référence tels que celui-ci dans les arguments par défaut.

Je pensais que c'était également contre-intuitif, jusqu'à ce que j'apprenne comment Python implémente les arguments par défaut.

Une fonction est un objet. Au moment du chargement, Python crée l'objet fonction, évalue les valeurs par défaut dans l'instruction def , les met dans un tuple et ajoute ce tuple en tant qu'attribut de la fonction func_defaults . . Ensuite, lorsqu'une fonction est appelée, si l'appel ne fournit pas de valeur, Python récupère la valeur par défaut dans func_defaults .

Par exemple:

>>> class C():
        pass

>>> def f(x=C()):
        pass

>>> f.func_defaults
(<__main__.C instance at 0x0298D4B8>,)

Tous les appels à f qui ne fournissent pas d'argument utiliseront la même instance de C , car il s'agit de la valeur par défaut.

En ce qui concerne la raison pour laquelle Python le fait de cette façon: eh bien, ce tuple pourrait contenir des fonctions qui seraient appelées à chaque fois qu’une valeur d’argument par défaut était requise. Outre le problème de performance immédiatement évident, vous commencez à vous plonger dans un univers de cas particuliers, tels que le stockage de valeurs littérales au lieu de fonctions pour les types non mutables, afin d'éviter des appels de fonction inutiles. Et bien sûr, il y a des implications en termes de performances.

Le comportement réel est vraiment simple. Et il existe une solution simple, dans le cas où vous souhaitez qu'une valeur par défaut soit produite par un appel de fonction au moment de l'exécution:

def f(x = None):
   if x == None:
      x = g()

Cela vient de l'accent mis par Python sur la syntaxe et la simplicité d'exécution. une déclaration def apparaît à un moment donné au cours de l'exécution. Lorsque l'interpréteur python atteint ce point, il évalue le code de cette ligne, puis crée un objet code à partir du corps de la fonction, qui sera exécuté ultérieurement, lorsque vous appelez la fonction.

C'est une simple séparation entre la déclaration de fonction et le corps de la fonction. La déclaration est exécutée lorsqu'elle est atteinte dans le code. Le corps est exécuté au moment de l'appel. Notez que la déclaration est exécutée à chaque fois, vous pouvez donc créer plusieurs fonctions en boucle.

funcs = []
for x in xrange(5):
    def foo(x=x, lst=[]):
        lst.append(x)
        return lst
    funcs.append(foo)
for func in funcs:
    print "1: ", func()
    print "2: ", func()

Cinq fonctions distinctes ont été créées. Une liste distincte est créée à chaque exécution de la déclaration de fonction. Sur chaque boucle via funcs , la même fonction est exécutée deux fois à chaque passage, en utilisant à chaque fois la même liste. Cela donne les résultats:

1:  [0]
2:  [0, 0]
1:  [1]
2:  [1, 1]
1:  [2]
2:  [2, 2]
1:  [3]
2:  [3, 3]
1:  [4]
2:  [4, 4]

D'autres vous ont expliqué comment utiliser param = None et attribuer une liste dans le corps si la valeur est None, ce qui correspond à un python totalement idiomatique. C'est un peu moche, mais la simplicité est puissante et la solution de contournement n'est pas trop douloureuse.

Modifié pour ajouter: pour plus d'informations à ce sujet, voir l'article de effbot ici: http: // effbot.org/zone/default-values.htm et la référence de langue, ici: http://docs.python.org/reference/compound_stmts.html#function

Les définitions de fonctions Python ne sont que du code, comme tous les autres codes; ils ne sont pas "magiques" à la manière de certaines langues. Par exemple, en Java, vous pouvez faire référence à "maintenant". à quelque chose défini "plus tard":

public static void foo() { bar(); }
public static void main(String[] args) { foo(); }
public static void bar() {}

mais en Python

def foo(): bar()
foo()   # boom! "bar" has no binding yet
def bar(): pass
foo()   # ok

Ainsi, l'argument par défaut est évalué au moment où cette ligne de code est évaluée!

Parce que s'ils l'avaient fait, quelqu'un poserait une question demandant pourquoi ce n'était pas l'inverse :-p

Supposons maintenant qu’ils en avaient. Comment mettriez-vous en œuvre le comportement actuel si nécessaire? Il est facile de créer de nouveaux objets dans une fonction, mais vous ne pouvez pas "annuler la création". eux (vous pouvez les supprimer, mais ce n'est pas pareil).

Je fournirai une opinion dissidente en ajoutant les principaux arguments des autres articles.

  

Évaluer les arguments par défaut lors de l'exécution de la fonction serait mauvais pour les performances.

Je trouve cela difficile à croire. Si les assignations d'arguments par défaut telles que foo = 'chaîne_quelque ajoutent vraiment une surcharge inacceptable, je suis sûr qu'il serait possible d'identifier les assignations aux littéraux immuables et de les pré-calculer.

  

Si vous souhaitez une affectation par défaut avec un objet mutable tel que foo = [] , utilisez simplement foo = None , suivi de foo = foo ou [] dans le corps de la fonction.

Bien que cela puisse être sans problème dans des cas individuels, en tant que modèle de conception, il n’est pas très élégant. Il ajoute du code passe-partout et masque les valeurs d'argument par défaut. Des modèles tels que foo = foo ou ... ne fonctionnent pas si foo peut être un objet comme un tableau numpy avec une valeur de vérité non définie. Et dans les situations où None est une valeur d'argument explicite pouvant être transmise intentionnellement, elle ne peut pas être utilisée comme sentinelle et cette solution de contournement devient vraiment moche.

  

Le comportement actuel est utile pour les objets mutables par défaut qui doivent être partagés lors d'appels de fonctions.

Je serais heureux de voir des preuves du contraire, mais d'après mon expérience, ce cas d'utilisation est beaucoup moins fréquent que les objets mutables qui devraient être créés à nouveau chaque fois que la fonction est appelée. Pour moi, cela semble également être un cas d'utilisation plus avancé, alors que les assignations par défaut accidentelles avec des conteneurs vides sont courantes pour les nouveaux programmeurs Python. Par conséquent, le principe de moindre étonnement suggère que les valeurs d'argument par défaut doivent être évaluées lors de l'exécution de la fonction.

De plus, il me semble qu’il existe une solution de contournement simple pour les objets mutables qui doivent être partagés entre les appels de fonctions: initialisez-les en dehors de la fonction.

Donc, je dirais que c'était une mauvaise décision de conception. Mon hypothèse est qu'il a été choisi parce que son implémentation est en réalité plus simple et qu'il a un cas d'utilisation valide (bien que limité). Malheureusement, je ne pense pas que cela changera jamais, car les principaux développeurs de Python veulent éviter de répéter l’incompatibilité avec les versions antérieures introduite par Python 3.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top