3396 Stimmen

"Least Astonishment" und das Argument des veränderlichen Standards

Jeder, der lange genug an Python herumgebastelt hat, wurde von dem folgenden Problem gebissen (oder in Stücke gerissen):

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

Python-Neulinge würden erwarten, dass diese Funktion immer eine Liste mit nur einem Element zurückgibt: [5] . Das Ergebnis ist jedoch ganz anders und (für einen Anfänger) sehr erstaunlich:

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

Ein Manager von mir hatte einmal seine erste Begegnung mit dieser Funktion und nannte sie einen "dramatischen Designfehler" der Sprache. Ich entgegnete ihm, dass es für dieses Verhalten eine Erklärung gibt, und dass es in der Tat sehr verwirrend und unerwartet ist, wenn man die Interna nicht versteht. Allerdings konnte ich mir die folgende Frage nicht beantworten: Warum wird das Standardargument bei der Funktionsdefinition und nicht bei der Funktionsausführung gebunden? Ich bezweifle, dass das erfahrene Verhalten einen praktischen Nutzen hat (wer hat wirklich statische Variablen in C verwendet, ohne Bugs zu erzeugen?)

Editar :

Baczek gibt ein interessantes Beispiel . Zusammen mit den meisten Ihrer Kommentare und Utaal's im Besonderen habe ich weiter ausgearbeitet:

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

Ich habe den Eindruck, dass die Design-Entscheidung sich darauf bezog, wo der Bereich der Parameter platziert werden sollte: innerhalb der Funktion oder "zusammen" mit ihr?

Wenn die Bindung innerhalb der Funktion erfolgt, würde dies bedeuten, dass x ist beim Aufruf der Funktion effektiv an die angegebene Vorgabe gebunden und nicht definiert, was einen schwerwiegenden Fehler darstellen würde: Die def Zeile wäre "hybrid" in dem Sinne, dass ein Teil der Bindung (des Funktionsobjekts) bei der Definition und ein Teil (Zuweisung von Standardparametern) beim Funktionsaufruf erfolgen würde.

Das tatsächliche Verhalten ist konsistenter: alles in dieser Zeile wird ausgewertet, wenn diese Zeile ausgeführt wird, also bei der Funktionsdefinition.

81 Stimmen

9 Stimmen

Ich habe keinen Zweifel daran, dass veränderbare Argumente das Prinzip des geringsten Erstaunens für eine durchschnittliche Person verletzen, und ich habe gesehen, wie Anfänger dort hineingetreten sind und dann heldenhaft Mailinglisten durch Mailing-Tupel ersetzt haben. Nichtsdestotrotz sind veränderbare Argumente immer noch im Einklang mit Python Zen (Pep 20) und fallen unter die Klausel "obvious for Dutch" (verstanden/ausgenutzt von Hardcore-Python-Programmierern). Der empfohlene Workaround mit doc string ist der beste, aber der Widerstand gegen doc strings und jegliche (geschriebene) Doku ist heutzutage nicht mehr so ungewöhnlich. Ich persönlich würde einen Dekorator bevorzugen (sagen wir @fixed_defaults).

6 Stimmen

Mein Argument, wenn ich darauf stoße, ist: "Warum müssen Sie eine Funktion erstellen, die eine Variable zurückgibt, die optional eine Variable sein kann, die Sie an die Funktion übergeben würden? Entweder wird eine Mutable geändert oder eine neue erstellt. Warum muss man beides mit einer Funktion machen? Und warum sollte der Interpreter so umgeschrieben werden, dass man das tun kann, ohne drei Zeilen zum Code hinzuzufügen?" Weil es hier darum geht, die Art und Weise, wie der Interpreter mit Funktionsdefinitionen und Evokationen umgeht, neu zu schreiben. Das ist eine Menge Arbeit für einen kaum notwendigen Anwendungsfall.

17voto

Marcin Punkte 46457

Die Lösungen sind hier:

  1. Utilisez None als Standardwert (oder ein nonce object ), und schalten Sie dies ein, um Ihre Werte zur Laufzeit zu erstellen; oder
  2. Verwenden Sie eine lambda als Standardparameter und rufen Sie es innerhalb eines Try-Blocks auf, um den Standardwert zu erhalten (für solche Dinge ist die Lambda-Abstraktion gedacht).

Die zweite Option ist gut, weil die Benutzer der Funktion ein Callable übergeben können, das bereits vorhanden sein kann (z. B. ein type )

1 Stimmen

Damit ist die Frage nicht beantwortet.

16voto

Saish Punkte 501

Wenn wir dies tun:

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

... weisen wir das Argument a zu einer unbenannt Liste, wenn der Aufrufer den Wert von a nicht übergibt.

Um die Diskussion zu vereinfachen, sollten wir der unbenannten Liste vorübergehend einen Namen geben. Wie wäre es mit pavlo ?

def foo(a=pavlo):
   ...

Wenn der Anrufer uns nicht sagt, was er will, kann er jederzeit a ist, verwenden wir wieder pavlo .

Si pavlo ist veränderbar (modifizierbar), und foo verändert, ein Effekt, den wir beim nächsten Mal bemerken foo aufgerufen wird, ohne die Angabe von a .

Dies ist also das, was Sie sehen (Zur Erinnerung, pavlo wird auf [] initialisiert):

 >>> foo()
 [5]

Jetzt, pavlo ist [5].

Aufruf von foo() modifiziert wiederum pavlo wieder:

>>> foo()
[5, 5]

Angabe von a beim Aufruf von foo() gewährleistet pavlo wird nicht berührt.

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

Así que, pavlo ist noch [5, 5] .

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

16voto

bgreen-litl Punkte 414

Ich nutze dieses Verhalten manchmal als Alternative zum folgenden Muster:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Si singleton wird nur verwendet von use_singleton Ich mag das folgende Muster als Ersatz:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

Ich habe dies für die Instanziierung von Client-Klassen verwendet, die auf externe Ressourcen zugreifen, und auch für die Erstellung von Dicts oder Listen für die Memoisierung.

Da ich nicht glaube, dass dieses Muster allgemein bekannt ist, füge ich einen kurzen Kommentar ein, um zukünftigen Missverständnissen vorzubeugen.

2 Stimmen

Ich ziehe es vor, einen Dekorator für die Memoisierung hinzuzufügen und den Memoisierungs-Cache auf das Funktionsobjekt selbst zu legen.

0 Stimmen

Dieses Beispiel ersetzt nicht das komplexere Muster, das Sie zeigen, denn Sie rufen _make_singleton im Beispiel mit den Standardargumenten zur Def-Zeit, im globalen Beispiel jedoch zur Call-Zeit. Eine echte Substitution würde eine Art veränderbare Box für den Standardargumentwert verwenden, aber das Hinzufügen des Arguments bietet die Möglichkeit, alternative Werte zu übergeben.

16voto

joedborg Punkte 16307

Sie können dies umgehen, indem Sie das Objekt (und damit die Verknüpfung mit dem Bereich) ersetzen:

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

Hässlich, aber es funktioniert.

3 Stimmen

Dies ist eine gute Lösung für Fälle, in denen Sie eine Software zur automatischen Dokumentationserstellung verwenden, um die von der Funktion erwarteten Argumenttypen zu dokumentieren. Die Angabe von a=None und das anschließende Setzen von a auf [], wenn a None ist, hilft dem Leser nicht, auf einen Blick zu verstehen, was erwartet wird.

0 Stimmen

Coole Idee: Das erneute Binden des Namens garantiert, dass er nicht geändert werden kann. Das gefällt mir sehr.

0 Stimmen

Das ist genau der richtige Weg. Python erstellt keine Kopie des Parameters, so dass es an Ihnen liegt, die Kopie explizit zu erstellen. Sobald Sie eine Kopie haben, können Sie sie nach Belieben ändern, ohne dass es zu unerwarteten Nebenwirkungen kommt.

15voto

Przemek D Punkte 584

In jeder anderen Antwort wird erklärt, warum das eigentlich ein nettes und erwünschtes Verhalten ist, oder warum man das sowieso nicht brauchen sollte. Meine ist für die Sturköpfe, die von ihrem Recht Gebrauch machen wollen, die Sprache nach ihrem Willen zu biegen, und nicht umgekehrt.

Wir werden dieses Verhalten mit einem Dekorator "beheben", der den Standardwert kopiert, anstatt dieselbe Instanz für jedes Positionsargument, das auf seinem Standardwert belassen wird, wieder zu verwenden.

import inspect
from copy import deepcopy  # copy would fail on deep arguments like nested dicts

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(deepcopy(arg))
        return function(*new_args, **kw)
    return wrapper

Definieren wir nun unsere Funktion mit diesem Dekorator neu:

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

foo() # '[5]'
foo() # '[5]' -- as desired

Dies ist besonders praktisch für Funktionen, die mehrere Argumente annehmen. Vergleichen Sie:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

mit

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Es ist wichtig zu beachten, dass die obige Lösung nicht funktioniert, wenn Sie versuchen, das Schlüsselwort args zu verwenden, etwa so:

foo(a=[4])

Der Dekorator könnte angepasst werden, um dies zu berücksichtigen, aber wir überlassen dies als Übung für den Leser ;)

1 Stimmen

Das funktioniert auch nicht, wenn das Standardargument tief ist, wie {"grandparent": {"parent": {"child": "value"}}} . Nur das oberste Wörterbuch wird als Wert kopiert, die anderen Wörterbücher werden als Verweis kopiert. Dieses Problem tritt auf, weil Sie copy 代わりに deepcopy

1 Stimmen

@Flimm Ich finde Ihre Formulierung "das geht kaputt" ziemlich unfair, denn sie scheint zu suggerieren, dass das gesamte Konzept irgendwie fehlerhaft ist, während es sich in Wirklichkeit nur um ein kleines Detail der Implementierung handelt. Aber trotzdem danke für den Kommentar, ich werde meine Antwort überarbeiten und verbessern.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X