Vraag "Least Astonishment" en het veranderlijke standaardargument


Iedereen die lang genoeg aan Python sleutelde, is gebeten (of in stukken gescheurd) door het volgende probleem:

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

Python-nieuwelingen verwachten dat deze functie altijd een lijst retourneert met slechts één element: [5]. Het resultaat is in plaats daarvan heel anders en zeer verbazingwekkend (voor een beginneling):

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

Een manager van mij had ooit zijn eerste kennismaking met deze functie en noemde het "een dramatische ontwerpfout" van de taal. Ik antwoordde dat het gedrag een onderliggende verklaring had, en het is inderdaad heel raadselachtig en onverwacht als je de internals niet begrijpt. Ik was echter niet in staat om (tegen mezelf) de volgende vraag te beantwoorden: wat is de reden voor het binden van het standaardargument bij functiedefinitie en niet bij het uitvoeren van de functie? Ik betwijfel of het ervaren gedrag een praktisch nut heeft (wie gebruikte statische variabelen echt in C, zonder fokvirus?)

Bewerk:

Baczek maakte een interessant voorbeeld. Samen met de meeste van je opmerkingen en Utaal's in het bijzonder, heb ik verder uitgewerkt:

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

Voor mij lijkt het erop dat de ontwerpbeslissing relatief was ten opzichte van waar de reikwijdte van parameters moest worden geplaatst: binnen de functie of "samen" ermee?

Het binden in de functie zou dat betekenen x is effectief gebonden aan de opgegeven standaard wanneer de functie wordt aangeroepen, niet gedefinieerd, iets dat een diepe tekortkoming zou vertonen: de def regel zou "hybride" zijn in de zin dat een deel van de binding (van het functieobject) zou plaatsvinden bij de definitie, en een deel (toewijzing van standaardparameters) op de functieaanroepingstijd.

Het feitelijke gedrag is consistenter: alles van die regel wordt geëvalueerd wanneer die lijn wordt uitgevoerd, wat betekent dat bij functiedefinitie.


2049
2017-07-15 18:00


oorsprong


antwoorden:


Eigenlijk is dit geen ontwerpfout, en het is niet vanwege internals of prestaties.
Het komt eenvoudigweg voort uit het feit dat functies in Python eersteklas objecten zijn, en niet alleen een stuk code.

Zodra je op deze manier gaat denken, is het volkomen logisch: een functie is een object dat wordt geëvalueerd op zijn definitie; standaardparameters zijn soort "lidgegevens" en daarom kan hun status van de ene aanroep naar de andere veranderen - precies zoals bij elk ander object.

In ieder geval heeft Effbot een heel mooie uitleg over de redenen voor dit gedrag in Standaard parameterwaarden in Python.
Ik vond het heel duidelijk en ik stel voor om het te lezen voor een betere kennis van hoe functieobjecten werken.


1349
2017-07-17 21:29



Stel dat je de volgende code hebt

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

Wanneer ik de verklaring van eten zie, is het minst verwonderlijke ding om te denken dat als de eerste parameter niet wordt gegeven, deze gelijk zal zijn aan de tuple ("apples", "bananas", "loganberries")

Echter, verondersteld later in de code, doe ik iets als

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

dan als standaardparameters werden gebonden aan functie-uitvoering in plaats van functieverklaring, zou ik (op een zeer slechte manier) verbaasd zijn dat fruit was veranderd. Dit zou een meer verbluffende IMO zijn dan ontdekken dat jouw foobovenstaande functie was de lijst aan het muteren.

Het echte probleem ligt bij veranderlijke variabelen, en alle talen hebben dit probleem tot op zekere hoogte. Hier is een vraag: stel dat ik in Java de volgende code heb:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Nu gebruikt mijn kaart de waarde van de StringBuffer toets wanneer deze op de kaart is geplaatst of slaat deze de sleutel op door ernaar te verwijzen? Hoe dan ook, iemand is verbaasd; ofwel de persoon die probeerde het object uit de Map een waarde gebruiken die identiek is aan degene waarmee ze in het spel zijn geplaatst, of de persoon die hun object niet lijkt te kunnen ophalen, ook al is de sleutel die ze gebruiken letterlijk hetzelfde object dat werd gebruikt om het op de kaart te plaatsen (dit is eigenlijk waarom Python niet toestaat dat de veranderlijke ingebouwde gegevenstypen worden gebruikt als woordenboeksleutels).

Je voorbeeld is een goed geval waarin Python-nieuwkomers verrast en gebeten zullen worden. Maar ik zou zeggen dat als we dit zouden 'repareren', dat alleen maar een andere situatie zou creëren waarin ze in plaats daarvan zouden worden gebeten, en dat iemand nog minder intuïtief zou zijn. Bovendien is dit altijd het geval bij veranderlijke variabelen; je komt altijd gevallen tegen waarin iemand intuïtief het ene of het andere gedrag zou kunnen verwachten, afhankelijk van de code die ze schrijven.

Persoonlijk vind ik de huidige benadering van Python leuk: standaard functieargumenten worden geëvalueerd wanneer de functie is gedefinieerd en dat object is altijd de standaard. Ik denk dat ze speciaal kunnen zijn als je een lege lijst gebruikt, maar dat soort speciale omhulsel zou nog meer verbazing wekken, om nog maar te zwijgen van achterwaarts onverenigbaar zijn.


231
2017-07-15 18:11



AFAICS niemand heeft het relevante deel van de documentatie:

Standaardparameterwaarden worden geëvalueerd wanneer de functiedefinitie wordt uitgevoerd. Dit betekent dat de uitdrukking eenmaal wordt geëvalueerd, wanneer de functie is gedefinieerd en dat dezelfde "vooraf berekend" waarde wordt gebruikt voor elke oproep. Dit is vooral belangrijk om te begrijpen wanneer een standaardparameter een variabel object is, zoals een lijst of een woordenboek: als de functie het object wijzigt (bijvoorbeeld door een item aan een lijst toe te voegen), wordt de standaardwaarde in feite gewijzigd. Dit is over het algemeen niet wat was bedoeld. Een manier om dit te omzeilen is om None als de standaard te gebruiken en expliciet te testen in de body van de functie [...]


195
2017-07-10 14:50



Ik weet niets van de interne werking van de Python interpreter (en ik ben ook geen expert in compilers en tolken) dus geef mij niet de schuld als ik iets onhoudbaars of onmogelijk voorstel.

Vooropgesteld dat python objecten maakt zijn veranderlijk Ik denk dat hiermee rekening moet worden gehouden bij het ontwerpen van de standaard argumenten. Wanneer u een lijst maakt:

a = []

je verwacht een te krijgen nieuwe lijst waarnaar wordt verwezen door een.

Waarom zou de a = [] in

def x(a=[]):

een nieuwe lijst maken met functie-definitie en niet bij aanroep? Het is net alsof u vraagt ​​"of de gebruiker het argument dan niet levert instantiëren een nieuwe lijst en gebruik deze alsof deze door de beller is geproduceerd ". Ik denk dat dit in plaats daarvan dubbelzinnig is:

def x(a=datetime.datetime.now()):

gebruiker, wil je dat een standaard in de datetime die overeenkomt met wanneer je aan het definiëren of uitvoeren bent X? In dit geval, net als in de vorige, zal ik hetzelfde gedrag behouden alsof het standaardargument "toewijzing" de eerste instructie van de functie was (datetime.now () aanroepde functie-aanroep). Aan de andere kant, als de gebruiker de definitie-tijd-toewijzing wilde, kon hij schrijven:

b = datetime.datetime.now()
def x(a=b):

Ik weet het, ik weet het: dat is een afsluiting. Als alternatief kan Python een sleutelwoord leveren om definitie-tijdbinding te forceren:

def x(static a=b):

97
2017-07-15 23:21



Nou, de reden is simpelweg dat bindingen worden uitgevoerd wanneer code wordt uitgevoerd en de functiedefinitie wordt uitgevoerd, tja ... wanneer de functies zijn gedefinieerd.

Vergelijk dit:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

Deze code lijdt aan exact dezelfde onverwachte toevalligheid. bananen is een klasseattribuut, en daarom, wanneer je er dingen aan toevoegt, wordt het toegevoegd aan alle instanties van die klasse. De reden is precies hetzelfde.

Het is gewoon "Hoe het werkt", en het anders werken in de functie zal waarschijnlijk gecompliceerd zijn, en in de klas is dit waarschijnlijk onmogelijk, of in ieder geval vertragen object-instantiatie veel, omdat je de klassencode rond zou moeten houden en voer het uit wanneer objecten worden gemaakt.

Ja, het is onverwacht. Maar zodra het kwartje valt, past het perfect in hoe Python in het algemeen werkt. Het is zelfs een goede leerhulp, en als je eenmaal begrijpt waarom dit gebeurt, zul je veel beter python maken.

Dat zei dat het prominent aanwezig zou moeten zijn in elke goede Python-tutorial. Want zoals je al zegt, loopt iedereen vroeg of laat tegen dit probleem aan.


72
2017-07-15 18:54



Vroeger dacht ik dat het beter zou zijn om de objecten tijdens runtime te maken. Ik ben het nu minder zeker, omdat je wel een aantal nuttige functies verliest, hoewel het de moeite waard kan zijn, ongeacht of je newbie-verwarring kunt voorkomen. De nadelen hiervan zijn:

1. Prestaties

def foo(arg=something_expensive_to_compute())):
    ...

Als beltijdevaluatie wordt gebruikt, wordt de dure functie aangeroepen telkens wanneer uw functie zonder argument wordt gebruikt. U betaalt bij elke oproep een dure prijs, of u moet de waarde handmatig extern opslaan in de cache, uw naamruimte vervuilen en uitgebreidheid toevoegen.

2. Geforceerde parameters dwingen

Een nuttige truc is om parameters van een lambda te binden aan de actueel binding van een variabele wanneer de lambda is gemaakt. Bijvoorbeeld:

funcs = [ lambda i=i: i for i in range(10)]

Dit geeft een lijst met functies terug die respectievelijk 0,1,2,3 ... retourneren. Als het gedrag wordt gewijzigd, zullen ze in plaats daarvan binden i naar de beltijd waarde van i, dus je krijgt een lijst met functies die allemaal zijn geretourneerd 9.

De enige manier om dit te implementeren, is om een ​​verdere afsluiting te maken met de i-bound, dat wil zeggen:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspectie

Beschouw de code:

def foo(a='test', b=100, c=[]):
   print a,b,c

We kunnen informatie over de argumenten en de standaardinstellingen verkrijgen met behulp van de inspect module, welke

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Deze informatie is erg handig voor zaken als het genereren van documenten, metaprogrammering, decorateurs enz.

Stel nu dat het gedrag van de standaardinstellingen kan worden gewijzigd, zodat dit het equivalent is van:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

We zijn echter het vermogen tot introspecteren verloren en zien wat de standaardargumenten zijn zijn. Omdat de objecten niet zijn geconstrueerd, kunnen we ze nooit bereiken zonder de functie daadwerkelijk te gebruiken. Het beste wat we kunnen doen is de broncode opslaan en die als een string retourneren.


50
2017-07-16 10:05



5 punten ter verdediging van Python

  1. Eenvoud: Het gedrag is eenvoudig in de volgende zin: De meeste mensen vallen slechts eenmaal in deze val, niet meerdere keren.

  2. Consistentie: Python altijd passeert objecten, geen namen. De standaardparameter is duidelijk onderdeel van de functie heading (niet het hoofdgedeelte van de functie). Het zou daarom moeten worden beoordeeld bij module laadtijd (en alleen bij module laadtijd, tenzij genest), niet op functie beltijd.

  3. Bruikbaarheid: Zoals Frederik Lundh in zijn toelichting opmerkt van "Standaard parameterwaarden in Python", de huidig ​​gedrag kan erg handig zijn voor geavanceerde programmering. (Gebruik spaarzaam.)

  4. Voldoende documentatie: In de meest elementaire Python-documentatie, de tutorial is het probleem luid aangekondigd als een "Belangrijke waarschuwing" in de eerste subsectie van Sectie "Meer over het definiëren van functies". De waarschuwing gebruikt zelfs vette letters, die zelden wordt toegepast buiten de rubrieken. RTFM: lees de fijne handleiding.

  5. Meta-learning: Val in de val is eigenlijk een heel nuttig moment (tenminste als je een reflectieve leerling bent), omdat je het later beter begrijpt "Consistentie" hierboven en die zal leer je veel over Python.


47
2018-03-30 11:18



Waarom doe je geen introspect?

Im werkelijk verbaasd dat niemand de inzichtelijke introspectie uitgevoerd door Python (2 en 3 toepassen) op callables.

Gegeven een eenvoudige kleine functie func gedefinieerd als:

>>> def func(a = []):
...    a.append(5)

Wanneer Python het tegenkomt, zal het eerst compileren om een ​​a te maken code object voor deze functie. Hoewel deze compilatiestap is voltooid, Python evalueert* en dan winkel de standaardargumenten (een lege lijst [] hier) in het functieobject zelf. Zoals het topantwoord zei: de lijst a kan nu als een worden beschouwd lid van de functie func.

Dus, laten we wat introspectie doen, een voor en na om te onderzoeken hoe de lijst wordt uitgebreid binnen het functieobject. ik gebruik Python 3.x hiervoor geldt voor Python 2 hetzelfde (gebruik __defaults__ of func_defaults in Python 2; ja, twee namen voor hetzelfde).

Functie vóór uitvoering:

>>> def func(a = []):
...     a.append(5)
...     

Nadat Python deze definitie heeft uitgevoerd, worden alle opgegeven standaardparameters gebruikt (a = [] hier en proppen ze in de __defaults__ attribuut voor het functieobject (relevante sectie: Callables):

>>> func.__defaults__
([],)

O.k, dus een lege lijst als de enige invoer in __defaults__, zoals verwacht.

Functie na uitvoering:

Laten we nu deze functie uitvoeren:

>>> func()

Laten we die nu eens bekijken __defaults__ nog een keer:

>>> func.__defaults__
([5],)

Verbaasd? De waarde in het object verandert! Opeenvolgende oproepen naar de functie zullen nu eenvoudig worden toegevoegd aan die ingesloten list voorwerp:

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

Dus daar heb je het, de reden waarom dit 'fout' gebeurt, is omdat standaardargumenten deel uitmaken van het functieobject. Er is hier niets vreemds aan de hand, het is allemaal een beetje verrassend.

De gebruikelijke oplossing om dit te bestrijden is normaal None als de standaard en initialiseer dan in de functie body:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

Omdat de functie body telkens opnieuw wordt uitgevoerd, krijgt u altijd een nieuwe lege lijst als er geen argument is aangenomen a.


Om verder te verifiëren dat de lijst in __defaults__ is hetzelfde als dat gebruikt in de functie func je kunt gewoon je functie veranderen om de id van de lijst a gebruikt binnen de functie body. Vergelijk het dan met de lijst in __defaults__ (positie [0] in __defaults__) en je zult zien hoe deze inderdaad verwijzen naar dezelfde lijstinstantie:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Allemaal met de kracht van introspectie!


* Om te controleren of Python de standaardargumenten evalueert tijdens het compileren van de functie, probeert u het volgende:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

zoals je zult opmerken, input() wordt opgeroepen vóór het proces van het bouwen van de functie en het binden aan de naam bar is gemaakt.


42
2017-12-09 07:13



Dit gedrag is eenvoudig verklaard door:

  1. functie (klasse enz.) wordt slechts een keer uitgevoerd, waardoor alle standaardwaardeobjecten worden gemaakt
  2. alles wordt doorgegeven door verwijzing

Zo:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a verandert niet - elke toewijzing aanroep maakt nieuw int-object - nieuw object wordt afgedrukt
  2. b verandert niet - nieuwe array wordt opgebouwd uit standaardwaarde en afgedrukt
  3. c wijzigingen - bewerking wordt uitgevoerd op hetzelfde object - en het wordt afgedrukt

40
2017-07-15 19:15



Wat u vraagt ​​is waarom dit:

def func(a=[], b = 2):
    pass

is hier niet intern equivalent aan:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

behalve in het geval van expliciet het aanroepen van func (None, None), die we zullen negeren.

Met andere woorden, in plaats van het evalueren van standaardparameters, waarom slaat u ze niet allemaal op en evalueert u ze wanneer de functie wordt aangeroepen?

Eén antwoord is waarschijnlijk daar - het zou elke functie met standaardparameters effectief in een afsluiting veranderen. Zelfs als alles verborgen is in de tolk en niet een volledige afsluiting, moeten de gegevens ergens worden opgeslagen. Het zou langzamer zijn en meer geheugen gebruiken.


30
2017-07-15 20:18



1) Het zogenaamde probleem van "veranderbaar standaardargument" is in het algemeen een speciaal voorbeeld dat aantoont dat:
"Alle functies met dit probleem lijden ook aan een vergelijkbaar neveneffectprobleem met de eigenlijke parameter,"
Dat is in strijd met de regels van functioneel programmeren, meestal onaanvaardbaar en moet samen worden opgelost.

Voorbeeld:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Oplossing: een kopiëren
Een absoluut veilige oplossing is om copy of deepcopy eerst het invoerobject en vervolgens alles te doen met de kopie.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Veel ingebouwde veranderbare typen hebben een kopieermethode some_dict.copy() of some_set.copy()of kan gemakkelijk worden gekopieerd zoals somelist[:] of list(some_list). Elk object kan ook worden gekopieerd door copy.copy(any_object) of meer grondig door copy.deepcopy() (dit laatste is handig als het veranderbare object is samengesteld uit veranderbare objecten). Sommige objecten zijn fundamenteel gebaseerd op bijwerkingen zoals een "bestand" -object en kunnen niet op betekenisvolle wijze worden gereproduceerd door kopiëren. kopiëren

Voorbeeld probleem voor een soortgelijke SO-vraag

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

Het zou in geen van de twee bewaard moeten worden openbaar kenmerk van een exemplaar dat door deze functie wordt geretourneerd. (In de veronderstelling dat privaat attributen van een instantie mogen niet per definitie van buiten deze klasse of subklassen worden gewijzigd. d.w.z. _var1 is een privéattribuut)

Conclusie:
Invoerparameters moeten niet op hun plaats worden gewijzigd (gemuteerd) en mogen ook niet worden gebonden aan een object dat door de functie wordt geretourneerd. (Als we liever zonder bijwerkingen programmeren, wordt dit sterk aanbevolen Wiki over "bijwerking" (De eerste twee alinea's zijn relevant in deze context.) .)

2)
Alleen als de bijwerking van de actuele parameter vereist is maar ongewenst voor de standaardparameter, is de bruikbare oplossing def ...(var1=None):  if var1 is None:  var1 = []  Meer..

3) In sommige gevallen is dit het veranderbare gedrag van standaardparameters nuttig.


29
2017-11-22 18:09