W jaki pythoniczny sposób można uniknąć parametrów domyślnych, które są pustymi listami?

118

Czasami wydaje się naturalne, że domyślny parametr jest pustą listą. Jednak w takich sytuacjach Python zachowuje się w nieoczekiwany sposób .

Jeśli na przykład mam funkcję:

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

Pierwsze wywołanie ustawienia domyślnego zadziała, ale późniejsze wywołania zaktualizują istniejącą listę (z jednym „a” na każde połączenie) i wydrukują zaktualizowaną wersję.

Jaki jest więc pythonowy sposób uzyskania pożądanego zachowania (nowa lista przy każdym połączeniu)?

John Mulder
źródło
17
jeśli ktoś jest zainteresowany tym, dlaczego tak się dzieje, sprawdź effbot.org/zone/default-values.htm
Ryan Haining.
To samo dzieje się w przypadku zestawów, chociaż potrzebujesz nieco bardziej skomplikowanego przykładu, aby pojawił się jako błąd.
abeboparebop
Gdy linki umierają, pozwolę sobie wyraźnie zaznaczyć, że jest to pożądane zachowanie. Zmienne domyślne są oceniane w definicji funkcji (co ma miejsce przy jej pierwszym wywołaniu), a NIE za każdym razem, gdy funkcja jest wywoływana. W konsekwencji, jeśli zmienisz zmienny argument domyślny, każde kolejne wywołanie funkcji będzie mogło używać tylko zmutowanego obiektu.
Moritz,

Odpowiedzi:

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

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

Dokumentacja mówi, że powinieneś używać go Nonejako domyślnego i jawnie testować go w treści funkcji.

HenryR
źródło
1
Czy lepiej powiedzieć: if working_list == None: or if working_list: ??
John Mulder,
2
Jest to preferowany sposób robienia tego w Pythonie, nawet jeśli mi się to nie podoba, ponieważ jest brzydki. Powiedziałbym, że najlepszą praktyką byłoby „jeśli working_list ma wartość None”.
e-satis
21
Preferowanym sposobem w tym przykładzie jest powiedzenie: if working_list ma wartość None. Obiekt wywołujący mógł użyć pustego obiektu podobnego do listy z niestandardowym dopisaniem.
tzot
5
Mohit Ranka: uważaj, że not working_list ma wartość True, jeśli jej długość wynosi 0. Prowadzi to do niespójnego zachowania: jeśli funkcja otrzyma listę z jakimś elementem, wywołujący ją zaktualizuje, a jeśli lista jest pusta, nie zostanie dotknięty.
vincent,
1
@PatrickT Właściwe narzędzie zależy od przypadku - funkcja varargs bardzo różni się od tej, która przyjmuje (opcjonalny) argument listy. Sytuacje, w których musiałbyś wybierać między nimi, zdarzają się rzadziej niż myślisz. Varargs jest świetny, gdy zmienia się liczba argumentów, ale jest naprawiany, gdy kod jest PISANY. Jak twój przykład. Jeśli jest to zmienna uruchomieniowa lub chcesz wywołać f()listę, musisz wywołać, f(*l)która jest obrzydliwa. Gorzej, wdrożenie mate(['larch', 'finch', 'robin'], ['bumble', 'honey', 'queen'])byłoby SUCK z varargs. O wiele lepiej, jeśli tak jest def mate(birds=[], bees=[]):.
FeRD
26

Istniejące odpowiedzi dostarczyły już bezpośrednich rozwiązań, o które proszono. Jednakże, ponieważ jest to bardzo częsta pułapka dla nowych programistów Pythona, warto dodać wyjaśnienie, dlaczego python zachowuje się w ten sposób, co jest ładnie podsumowane w " The Hitchhikers Guide to Python " jako " Mutable Default Arguments ": http: // docs .python-guide.org / pl / latest / writing / gotchas /

Cytuj: „ Domyślne argumenty Pythona są oceniane raz, gdy funkcja jest zdefiniowana, a nie za każdym razem, gdy funkcja jest wywoływana (tak jak to jest w przypadku Rubiego). Oznacza to, że jeśli użyjesz zmiennego argumentu domyślnego i zmienisz go, będziesz zmutował ten obiekt również dla wszystkich przyszłych wywołań funkcji "

Przykładowy kod do implementacji:

def foo(element, to=None):
    if to is None:
        to = []
    to.append(element)
    return to
Zhenhua
źródło
13

Nie ma to znaczenia w tym przypadku, ale możesz użyć tożsamości obiektu, aby przetestować brak:

if working_list is None: working_list = []

Możesz również skorzystać z tego, jak operator boolowski lub jest zdefiniowany w Pythonie:

working_list = working_list or []

Chociaż będzie to zachowywać się nieoczekiwanie, jeśli wywołujący poda pustą listę (która liczy się jako fałsz) jako working_list i oczekuje, że funkcja zmodyfikuje podaną przez niego listę.

bendin
źródło
10

Jeśli celem funkcji jest zmodyfikowanie parametru przekazanego jako working_list, zobacz odpowiedź HenryR (= None, sprawdź brak wewnątrz).

Ale jeśli nie zamierzałeś modyfikować argumentu, po prostu użyj go jako punktu wyjścia dla listy, możesz go po prostu skopiować:

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

(lub w tym prostym przypadku tylko, print starting_list + ["a"]ale myślę, że to był tylko przykład zabawki)

Ogólnie rzecz biorąc, mutowanie argumentów to zły styl w Pythonie. Jedynymi funkcjami, od których w pełni oczekuje się, że będą mutować obiekt, są metody obiektu. Jeszcze rzadziej mutuje się opcjonalny argument - czy efekt uboczny, który występuje tylko w niektórych wywołaniach, jest naprawdę najlepszym interfejsem?

  • Jeśli robisz to z nawyku „argumentów wyjściowych” w języku C, jest to całkowicie niepotrzebne - zawsze możesz zwrócić wiele wartości jako krotkę.

  • Jeśli robisz to, aby efektywnie budować długą listę wyników bez tworzenia list pośrednich, rozważ napisanie jej jako generatora i używanie go result_list.extend(myFunc())podczas wywoływania. W ten sposób konwencje powołania pozostają bardzo czyste.

Jednym ze wzorców, w którym często przeprowadza się mutację opcjonalnego argumentu, jest ukryty argument typu „memo” w funkcjach rekurencyjnych:

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
źródło
3

Mogę być poza tematem, ale pamiętaj, że jeśli chcesz przekazać tylko zmienną liczbę argumentów, pythonowym sposobem jest przekazanie krotki *argslub słownika **kargs. Są one opcjonalne i lepsze niż składniamyFunc([1, 2, 3]) .

Jeśli chcesz przekazać krotkę:

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]

Jeśli chcesz przekazać słownik:

def myFunc(arg1, **kargs):
   print kargs
>>>myFunc(1, option1=2, option2=3)
{'option2' : 2, 'option1' : 3}
Mapad
źródło
0

Udzielono już dobrych i poprawnych odpowiedzi. Chciałem tylko podać inną składnię do pisania tego, co chcesz zrobić, co uważam za piękniejsze, gdy na przykład chcesz utworzyć klasę z domyślnymi pustymi listami:

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 []

Ten fragment wykorzystuje składnię operatora if else. Podoba mi się to zwłaszcza dlatego, że jest to zgrabny, mały, jednolinijkowy tekst bez dwukropków itp. I prawie brzmi jak normalne angielskie zdanie. :)

W twoim przypadku mógłbyś pisać

def myFunc(working_list=None):
    working_list = [] if working_list is None else working_list
    working_list.append("a")
    print working_list
drssdinblck
źródło
-3

Wziąłem klasę rozszerzenia UCSC Python for programmer

Co jest prawdą w przypadku: def Fn (data = []):

a) to dobry pomysł, aby listy danych były puste przy każdym wywołaniu.

b) jest dobrym pomysłem, aby wszystkie wywołania funkcji, które nie dostarczają żadnych argumentów w wywołaniu, otrzymają pustą listę jako dane.

c) jest rozsądnym pomysłem, o ile twoje dane są listą ciągów.

d) to zły pomysł, ponieważ domyślne [] będzie gromadzić dane, a domyślne [] będzie się zmieniać wraz z kolejnymi wywołaniami.

Odpowiedź:

d) to zły pomysł, ponieważ domyślne [] będzie gromadzić dane, a domyślne [] będzie się zmieniać wraz z kolejnymi wywołaniami.

Peter Chen
źródło