Quelle est la manière pythonique d'éviter les paramètres par défaut qui sont des listes vides?

117

Parfois, il semble naturel d'avoir un paramètre par défaut qui est une liste vide. Pourtant, Python donne un comportement inattendu dans ces situations .

Si par exemple, j'ai une fonction:

def my_func(working_list = []):
    working_list.append("a")
    print(working_list)

La première fois qu'elle est appelée, la valeur par défaut fonctionnera, mais les appels suivants mettront à jour la liste existante (avec un "a" à chaque appel) et imprimera la version mise à jour.

Alors, quelle est la manière pythonique d'obtenir le comportement que je désire (une nouvelle liste à chaque appel)?

John Mulder
la source
17
si quelqu'un est intéressé par pourquoi cela se produit, consultez effbot.org/zone/default-values.htm
Ryan Haining
Le même comportement se produit pour les ensembles, bien que vous ayez besoin d'un exemple un peu plus compliqué pour qu'il apparaisse comme un bogue.
abeboparebop
Au fur et à mesure que les liens meurent, permettez-moi de souligner explicitement qu'il s'agit d'un comportement souhaité. Les variables par défaut sont évaluées lors de la définition de la fonction (ce qui se produit la première fois qu'elle est appelée), et NON à chaque fois que la fonction est appelée. Par conséquent, si vous modifiez un argument par défaut mutable, tout appel de fonction ultérieur ne peut utiliser que l'objet muté.
Moritz

Réponses:

150
def my_func(working_list=None):
    if working_list is None: 
        working_list = []

    working_list.append("a")
    print(working_list)

Les documents indiquent que vous devez utiliser Nonepar défaut et le tester explicitement dans le corps de la fonction.

HenryR
la source
1
Vaut-il mieux dire: if working_list == None: ou si working_list: ??
John Mulder
2
C'est la manière préférée de le faire en python, même si je ne l'aime pas car c'est moche. Je dirais que la meilleure pratique serait "si working_list est None".
e-satis
21
La méthode préférée dans cet exemple est de dire: si working_list vaut None. L'appelant peut avoir utilisé un objet de type liste vide avec un ajout personnalisé.
tzot
5
Mohit Ranka: attention que not working_list vaut True si sa longueur est 0. Cela conduit à un comportement incohérent: si la fonction reçoit une liste avec un élément en elle, son appelant verra sa liste mise à jour, et si la liste est vide, elle ne sera pas touché.
vincent
1
@PatrickT Le bon outil dépend du cas - une fonction varargs est très différente de celle qui prend un argument de liste (facultatif). Les situations où vous auriez à choisir entre elles se présentent moins souvent que vous ne le pensez. Varargs est excellent lorsque le nombre d'arguments change, mais est corrigé lorsque le code est ÉCRIT. Comme votre exemple. Si c'est une variable d'exécution, ou si vous voulez appeler f()une liste, vous devez appeler f(*l)ce qui est grossier. Pire encore, la mise en œuvre mate(['larch', 'finch', 'robin'], ['bumble', 'honey', 'queen'])serait SUCK w / varargs. Beaucoup mieux si c'est def mate(birds=[], bees=[]):.
FeRD
26

Les réponses existantes ont déjà fourni les solutions directes demandées. Cependant, comme il s'agit d'un piège très courant pour les nouveaux programmeurs Python, il vaut la peine d'ajouter l'explication pourquoi python se comporte de cette façon, ce qui est bien résumé dans " le Guide des auto-stoppeurs de Python " comme " Arguments mutables par défaut ": http: // docs .python-guide.org / fr / dernier / écriture / gotchas /

Quote: " Les arguments par défaut de Python sont évalués une fois lorsque la fonction est définie, pas à chaque fois que la fonction est appelée (comme c'est le cas, par exemple, Ruby). Cela signifie que si vous utilisez un argument par défaut mutable et que vous le muter, vous aurez et aurez muté cet objet pour tous les futurs appels à la fonction également "

Exemple de code pour l'implémenter:

def foo(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to
Zhenhua
la source
13

Ce n'est pas important dans ce cas, mais vous pouvez utiliser l'identité d'objet pour tester None:

if working_list is None: working_list = []

Vous pouvez également profiter de la façon dont l'opérateur booléen ou est défini en python:

working_list = working_list or []

Bien que cela se comporte de manière inattendue si l'appelant vous donne une liste vide (qui compte comme fausse) comme working_list et s'attend à ce que votre fonction modifie la liste qu'il lui a donnée.

bendin
la source
10

Si l'intention de la fonction est de modifier le paramètre passé en tant que working_list, consultez la réponse de HenryR (= Aucun, vérifiez Aucun à l'intérieur).

Mais si vous n'avez pas l'intention de muter l'argument, utilisez-le simplement comme point de départ d'une liste, vous pouvez simplement le copier:

def myFunc(starting_list = []):
    starting_list = list(starting_list)
    starting_list.append("a")
    print starting_list

(ou dans ce cas simple, print starting_list + ["a"]mais je suppose que ce n'était qu'un exemple de jouet)

En général, muter vos arguments est un mauvais style en Python. Les seules fonctions dont on attend totalement la mutation d'un objet sont les méthodes de l'objet. Il est encore plus rare de faire muter un argument optionnel - un effet secondaire qui ne se produit que dans certains appels est-il vraiment la meilleure interface?

  • Si vous le faites à partir de l'habitude C des "arguments de sortie", c'est complètement inutile - vous pouvez toujours renvoyer plusieurs valeurs sous forme de tuple.

  • Si vous faites cela pour créer efficacement une longue liste de résultats sans créer de listes intermédiaires, envisagez de l'écrire en tant que générateur et de l'utiliser result_list.extend(myFunc())lorsque vous l'appelez. De cette façon, vos conventions d'appel restent très propres.

Un modèle où la mutation d'un argument optionnel est fréquemment effectuée est un arg "mémo" caché dans les fonctions récursives:

def depth_first_walk_graph(graph, node, _visited=None):
    if _visited is None:
        _visited = set()  # create memo once in top-level call

    if node in _visited:
        return
    _visited.add(node)
    for neighbour in graph[node]:
        depth_first_walk_graph(graph, neighbour, _visited)
Beni Cherniavsky-Paskin
la source
3

Je suis peut-être hors sujet, mais rappelez-vous que si vous voulez juste passer un nombre variable d'arguments, la manière pythonique est de passer un tuple *argsou un dictionnaire **kargs. Ceux-ci sont facultatifs et sont meilleurs que la syntaxe myFunc([1, 2, 3]).

Si vous voulez passer un tuple:

def myFunc(arg1, *args):
  print args
  w = []
  w += args
  print w
>>>myFunc(1, 2, 3, 4, 5, 6, 7)
(2, 3, 4, 5, 6, 7)
[2, 3, 4, 5, 6, 7]

Si vous voulez passer un dictionnaire:

def myFunc(arg1, **kargs):
   print kargs
>>>myFunc(1, option1=2, option2=3)
{'option2' : 2, 'option1' : 3}
Mapad
la source
0

Des réponses bonnes et correctes ont déjà été fournies. Je voulais juste donner une autre syntaxe pour écrire ce que vous voulez faire, ce que je trouve plus beau lorsque vous voulez par exemple créer une classe avec des listes vides par défaut:

class Node(object):
    def __init__(self, _id, val, parents=None, children=None):
        self.id = _id
        self.val = val
        self.parents = parents if parents is not None else []
        self.children = children if children is not None else []

Cet extrait de code utilise la syntaxe de l'opérateur if else. Je l'aime particulièrement parce que c'est un joli petit one-liner sans deux-points, etc. impliqué et il se lit presque comme une phrase anglaise normale. :)

Dans votre cas, vous pourriez écrire

def myFunc(working_list=None):
    working_list = [] if working_list is None else working_list
    working_list.append("a")
    print working_list
drssdinblck
la source
-3

J'ai suivi la classe d'extension UCSC Python for programmer

Ce qui est vrai pour: def Fn (data = []):

a) est une bonne idée pour que vos listes de données commencent vides à chaque appel.

b) est une bonne idée pour que tous les appels à la fonction qui ne fournissent aucun argument lors de l'appel obtiennent la liste vide sous forme de données.

c) est une idée raisonnable tant que vos données sont une liste de chaînes.

d) est une mauvaise idée car la valeur par défaut [] accumulera des données et la valeur par défaut [] changera avec les appels suivants.

Répondre:

d) est une mauvaise idée car la valeur par défaut [] accumulera des données et la valeur par défaut [] changera avec les appels suivants.

Peter Chen
la source