“En Küçük Şaşkınlık” ve Değişken Varsayılan Argüman


2594

Python ile yeterince uzun süre uğraşan herkes aşağıdaki sorunla ısırıldı (veya parçalara ayrıldı):

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

Python acemi bu işlev her zaman yalnızca bir eleman içeren bir liste dönmek için beklenir: [5]. Sonuç bunun yerine çok farklı ve çok şaşırtıcı (bir acemi için):

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

Bir müdürüm bir zamanlar bu özellikle ilk karşılaşmasını yaptı ve ona dilin "dramatik bir tasarım hatası" adını verdi. Davranışın altında yatan bir açıklama olduğunu ve iç kısımları anlamadığınızda gerçekten çok şaşırtıcı ve beklenmedik olduğunu yanıtladı. Ancak, aşağıdaki soruya (kendi kendime) cevap veremedim: varsayılan bağımsız değişkeni işlev tanımında değil, işlev yürütmesinde bağlama nedeniniz nedir? Deneyimli davranışın pratik bir kullanımı olduğundan şüpheleniyorum (C'de üreme hataları olmadan gerçekten statik değişkenleri kullanan?)

Düzenle :

Baczek ilginç bir örnek verdi. Yorumlarınızın ve özellikle Utaal'ların çoğuyla birlikte daha ayrıntılı bir şekilde açıkladım:

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

Bana göre, tasarım kararının parametrelerin kapsamını nereye koyacağı göreceli gibi görünüyor: işlevin içinde mi, yoksa "birlikte" mi?

İşlevin içindeki xbağlantının yapılması, işlev tanımlanmadığında, derin bir kusur gösterecek bir şey olduğunda belirtilen varsayılan değere etkin bir şekilde bağlı olduğu anlamına gelir : defçizgi, bağlamanın ( işlev nesnesi) tanımda ve bölüm (varsayılan parametrelerin atanması) işlev çağırma zamanında olur.

Gerçek davranış daha tutarlıdır: bu satır yürütüldüğünde her satır, yani işlev tanımında değerlendirilir.



4
Değişken argümanların ortalama bir insan için en az şaşkınlık ilkesini ihlal ettiğinden şüphe duymuyorum ve yeni başlayanların oraya adım attığını, daha sonra kahramanca posta listelerini posta tuples'iyle değiştirdiğini gördüm. Yine de değiştirilebilir argümanlar hala Python Zen (Pep 20) ile uyumludur ve "Hollandaca için bariz" (sert çekirdekli python programcıları tarafından anlaşılmış / sömürülmüş) maddesine girmektedir. Doc dizesi ile önerilen geçici çözüm en iyisidir, ancak doc dizelerine ve (yazılı) dokümanlara karşı direnç günümüzde pek yaygın değildir. Şahsen, bir dekoratörü tercih ederim (@fixed_defaults diyelim).
Serge

5
Karşılaştığımda benim argümanım: "Neden isteğe bağlı olarak değişebilir bir değişken olabilecek bir işlev döndüren bir işlev oluşturmanız gerekiyor? Peki yorumlayıcı, kodunuza üç satır eklemeden bunu yapabilmeniz için neden yeniden yazılmalı? " Çünkü burada tercümanın işlev tanımlarını ve çağrışımları ele alma şeklini yeniden yazmaktan bahsediyoruz. Bu zar zor gerekli bir kullanım durumu için yapılacak çok şey.
Alan Leuthard

12
Msgstr "Python acemileri bu işlevin her zaman yalnızca bir öğe içeren bir liste döndürmesini bekler: [5]" Ben bir Python acemi değilim ve ben bunu beklemezdim, çünkü belli ki foo([1])dönecektir [1, 5], değil [5]. Söylemek istediğiniz şey, bir aceminin parametresiz olarak adlandırılan işlevin her zaman geri döneceğini beklemesi [5].
symplectomorphic

2
Bu soru "Bu [yanlış yol] neden böyle uygulandı?" O sormuyor "doğru yolu nedir?" , [ arg = None kullanımı neden Python'un değiştirilebilir varsayılan bağımsız değişken sorununu düzeltir? ] * ( stackoverflow.com/questions/10676729/… ). Yeni kullanıcılar neredeyse her zaman öncekiyle daha az ilgilenir ve ikincisiyle daha çok ilgilenir, bu yüzden bazen alıntı yapmak için çok yararlı bir bağlantı / dupe.
smci

Yanıtlar:


1612

Aslında, bu bir tasarım hatası değildir ve içsel veya performans nedeniyle değildir.
Basitçe Python'daki işlevlerin sadece bir kod parçası değil, birinci sınıf nesneler olması gerçeğinden kaynaklanmaktadır.

Bu şekilde düşünmeye başlar başlamaz, o zaman tamamen mantıklıdır: bir işlev tanımında değerlendirilen bir nesnedir; varsayılan parametreler bir çeşit "üye verisi" dir ve bu nedenle durumları bir çağrıdan diğerine değişebilir - tıpkı diğer nesnelerde olduğu gibi.

Her durumda, Effbot, Python'daki Varsayılan Parametre Değerleri'nde bu davranışın nedenleri hakkında çok güzel bir açıklamaya sahiptir .
Çok net buldum ve gerçekten fonksiyon nesnelerinin nasıl çalıştığı hakkında daha iyi bilgi için okumayı öneririm.


80
Yukarıdaki yanıtı okuyan herkese, bağlı Effbot makalesini okumak için zaman ayırmanızı şiddetle tavsiye ederim. Diğer tüm faydalı bilgilerin yanı sıra, bu dil özelliğinin sonuç önbellekleme / notlama için nasıl kullanılabileceğine dair kısmı bilmek çok kullanışlıdır!
Cam Jackson

85
Birinci sınıf bir nesne olsa bile, her varsayılan değerin kodunun nesne ile birlikte depolandığı ve işlev her çağrıldığında yeniden değerlendirildiği bir tasarım düşünülebilir . Bunun daha iyi olacağını söylemiyorum, sadece birinci sınıf nesneler olan fonksiyonlar onu tamamen engellemiyor.
gerrit

312
Maalesef "Python'daki en büyük WTF" olarak kabul edilen her şey kesinlikle bir tasarım hatasıdır . Bu, bir noktada herkes için bir hata kaynağıdır , çünkü hiç kimse bu davranışı ilk başta beklemez - bu, başlangıç ​​olarak bu şekilde tasarlanmamış olması gerektiği anlamına gelir. Hangi çemberleri atlamak zorunda olduklarını umursamıyorum , varsayılan argümanların statik olmaması için Python'u tasarlamış olmalılar .
BlueRaja - Danny Pflughoeft

192
Tasarım hatası olsun ya da olmasın, cevabınız, fonksiyonların birinci sınıf nesneler olduğu ve bu durumun böyle olmadığı göz önüne alındığında, bu davranışın bir şekilde gerekli, doğal ve açık olduğu anlamına geliyor. Python'un kapanışları var. Varsayılan bağımsız değişkeni işlevin ilk satırındaki bir atamayla değiştirirseniz, her çağrının ifadesini değerlendirir (büyük olasılıkla kapalı bir kapsamda bildirilen adları kullanarak). İşlev her seferinde aynı şekilde çağrıldığında varsayılan argümanların değerlendirilmesinin mümkün ya da makul olmamasının bir nedeni yoktur.
Mark Amery

24
Tasarım doğrudan takip etmiyor functions are objects. Paradigmanızda, teklif, işlevlerin varsayılan değerlerini özellikler yerine özellikler olarak uygulamak olacaktır.
bukzor

273

Aşağıdaki kodun olduğunu varsayalım

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

def eat(food=fruits):
    ...

Yemek deklarasyonunu gördüğümde, en az şaşırtıcı olan şey, ilk parametre verilmezse, tupa eşit olacağını düşünmektir. ("apples", "bananas", "loganberries")

Ancak, daha sonra kodda varsayalım, ben gibi bir şey yapmak

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

varsayılan parametreler işlev bildirimi yerine işlev yürütmede bağlı olsaydı, meyvelerin değiştiğini keşfetmek beni çok şaşırttı (çok kötü bir şekilde). Bu, fooyukarıdaki işlevinizin listeyi değiştirdiğini keşfetmekten daha şaşırtıcı IMO olacaktır .

Asıl sorun değişken değişkenlerde yatmaktadır ve tüm diller bir dereceye kadar bu probleme sahiptir. İşte bir soru: Java varsayalım aşağıdaki kod var:

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?

Şimdi, haritam haritaya StringBufferyerleştirildiğinde anahtarın değerini kullanıyor mu veya anahtarı referans olarak saklıyor mu? Her iki durumda da, biri şaşırır; nesneyi, Mapiçine koydukları değere özdeş bir değer kullanarak çıkarmaya çalışan kişi veya kullandıkları anahtar tam anlamıyla aynı nesne olmasına rağmen nesnelerini geri getiremeyen kişi haritaya yerleştirmek için kullanıldı (aslında Python'un değiştirilebilir dahili veri türlerinin sözlük anahtarı olarak kullanılmasına izin vermemesinin nedeni budur).

Örneğin Python'lu yeni gelenlerin şaşkına döneceği ve ısırılacağı bir örnek. Ama bunu "düzeltirsek", bunun yerine sadece ısırılmaları gereken farklı bir durum yaratacağını ve bunun daha az sezgisel olacağını savunabilirim. Dahası, değişken değişkenlerle uğraşırken her zaman böyle olur; her zaman birisinin hangi koda yazdıklarına bağlı olarak sezgisel olarak bir ya da tersi davranış bekleyebileceği durumlarla karşılaşırsınız.

Şahsen Python'un şu anki yaklaşımını seviyorum: fonksiyon tanımlandığında ve bu nesne her zaman varsayılan olduğunda varsayılan fonksiyon argümanları değerlendirilir. Sanırım boş bir liste kullanarak özel durum yapabilirlerdi, ancak bu tür özel bir kasa daha da şaşkınlığa neden olacaktı, geriye dönük uyumsuzluktan bahsetmiyorum bile.


30
Bence bu bir tartışma konusu. Küresel bir değişken üzerinde hareket ediyorsunuz. Kodunuzda global değişkeninizi içeren herhangi bir değerlendirme şimdi (doğru bir şekilde) ("yaban mersini", "mango") anlamına gelecektir. varsayılan parametre diğer tüm durumlar gibi olabilir.
Stefano Borini

47
Aslında, ilk örneğinize katıldığımı sanmıyorum. İlk etapta böyle bir başlatıcı değiştirme fikrini sevmediğimden emin değilim, ama eğer öyleyse, tam olarak tanımladığınız gibi davranmasını beklerdim - varsayılan değeri olarak değiştirmek ("blueberries", "mangos").
Ben Blank

12
Varsayılan parametre olan herhangi bir başka vaka gibi. Beklenmedik olan, parametrenin yerel bir değişken değil, global bir değişken olmasıdır. Bu da kodun çağrı değil fonksiyon tanımında yürütülmesi nedenidir. Bunu elde ettiğinizde ve aynı şey sınıflar için de geçerli olduğunda, tamamen açıktır.
Lennart Regebro

17
Örneği parlak yerine yanıltıcı buluyorum. Eğer some_random_function()ekler için fruitso hesaba atama, davranışı eat() olacaktır değiştirin. Mevcut harika tasarım için çok fazla. Başka bir yerde başvurulan bir varsayılan bağımsız değişken kullanır ve sonra işlevin dışından başvuruyu değiştirirseniz, sorun istiyorsunuz. Gerçek WTF, insanlar yeni bir varsayılan bağımsız değişken (bir liste hazır bilgisi veya bir kurucuya çağrı) tanımladığında ve yine de biraz olsun.
alexis

13
Sadece açık bir şekilde beyan ettiniz globalve yeniden atadınız - eatbundan sonra farklı çalışırsa kesinlikle şaşırtıcı bir şey yok .
user3467349

241

Belgelerin ilgili kısmı :

Varsayılan parametre değerleri, işlev tanımı yürütüldüğünde soldan sağa doğru değerlendirilir. Bu, ifadenin işlev tanımlandığında bir kez değerlendirildiği ve her çağrı için aynı “önceden hesaplanmış” değerin kullanıldığı anlamına gelir. Bu, varsayılan bir parametrenin liste veya sözlük gibi değiştirilebilir bir nesne olduğunu anlamak için özellikle önemlidir: işlev nesneyi değiştirirse (örn. Listeye bir öğe ekleyerek), varsayılan değer değiştirilir. Genellikle amaçlanan bu değildir. Bunun bir yolu None, varsayılan olarak kullanmak ve işlevin gövdesinde açıkça test etmektir, örneğin:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin

180
"Bu genellikle amaçlanan değildir" ve "bunun bir yolu" ifadesi, bir tasarım hatasını belgeliyormuş gibi kokuyor.
bukzor

4
@Matthew: Biliyorum, ama tuzağa düşürmeye değmez. Genellikle stil kılavuzlarını ve tiftikleri koşulsuz olarak değiştirilebilir varsayılan değerleri bu nedenle yanlış olarak görürsünüz. Aynı şeyi yapmanın açık yolu, bir niteliği işleve ( function.data = []) veya daha iyisine bir nesne yapmaktır.
bukzor

6
@bukzor: Tuzaklar not edilmeli ve belgelenmelidir, bu yüzden bu soru iyidir ve çok fazla oy almıştır. Aynı zamanda, tuzakların mutlaka kaldırılması gerekmez. Kaç Python acemi, listeyi değiştiren bir işleve bir liste geçirdi ve değişikliklerin orijinal değişkende göründüğünü görünce şok oldular? Yine de, bunları nasıl kullanacağınızı anladığınızda, değiştirilebilir nesne türleri harikadır. Ben sadece bu özel tuzak hakkında görüş kaynar sanırım.
Matthew

33
"Bu genellikle amaçlanan şey değildir" ifadesi, programcının gerçekte olmasını istediği şey değil, "Python'un yapması gereken şey değil" anlamına gelir.
holdenweb

4
@holdenweb Wow, partiye çok geç kaldım. Bağlam göz önüne alındığında, bukzor tamamen haklıdır: dilin işlevin tanımını yürütmesi gerektiğine karar verdiklerinde "amaçlanmayan" davranışları / sonuçları belgelemektedirler. Tasarım tercihlerinin istenmeyen bir sonucu olduğu için bir tasarım hatası. Bir tasarım hatası olmasaydı, "bunun için bir yol" sunmaya bile gerek olmazdı.
code_dredd

118

Python tercüman iç işleri hakkında hiçbir şey bilmiyorum (ve derleyiciler ve tercümanlar konusunda da uzman değilim), bu yüzden anlamsız veya imkansız bir şey önerirsem beni suçlamayın.

Python nesnelerinin değiştirilebilir olması koşuluyla, bu varsayılan argümanlar şeyler tasarlanırken dikkate alınması gerektiğini düşünüyorum. Bir listeyi başlattığınızda:

a = []

tarafından başvurulan yeni bir liste almayı bekliyorsunuz a.

Niye olsun a=[]içinde

def x(a=[]):

invokasyonda değil, fonksiyon tanımında yeni bir liste mi oluşturuyorsunuz? Bu "Kullanıcı daha sonra argüman sağlamaz soruyorsun kuruyor gibi hissedersiniz örneğini yeni bir liste ve arayan tarafından üretildi sanki kullanmak". Bunun yerine belirsiz olduğunu düşünüyorum:

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

kullanıcı, atanımlarken veya yürütürken karşılık gelen tarih saatini varsayılan olarak ayarlamak istiyor xmusunuz? Bu durumda, öncekinde olduğu gibi, varsayılan işlev "atama" işlevinin ilk komutu ( datetime.now()işlev çağırma olarak adlandırılır) sankiyle aynı davranacağım. Öte yandan, kullanıcı tanım-zaman eşlemesini isterse şunları yazabilir:

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

Biliyorum, biliyorum: bu bir kapanış. Alternatif olarak Python, tanım-zaman bağlayıcısını zorlamak için bir anahtar kelime sağlayabilir:

def x(static a=b):

11
Şunları yapabilirsiniz: def x (a = Yok): Ve sonra, a Yok ise, a = datetime.datetime.now ()
Anon

20
Bunun için teşekkür ederim. Bunun neden beni sonlandırdığına gerçekten parmağımı koyamadım. En az tüy ve karmaşa ile güzelce yaptın. Birisi C ++ 'da programlama yapan sistemlerden geliyor ve bazen safça dil özelliklerini "çeviriyor", bu sahte arkadaşım da tıpkı sınıf nitelikleri gibi başımın yumuşaklığına girdi. İşlerin neden bu şekilde olduğunu anlıyorum, ama ne olumlu olursa olsun, yardım edemem ama sevmem. En azından deneyimlerime aykırı, muhtemelen (umarım) asla unutmayacağım ...
AndreasT

5
@Python'u yeterince uzun süre kullandıktan sonra, Python'un şeyleri sınıf nitelikleri olarak yorumlamasının ne kadar mantıklı olduğunu görmeye başlarsınız - bunun nedeni yalnızca C ++ (ve Java gibi dillerin belirli tuhaflıkları ve sınırlamaları ve C # ...) class {}bloğun içeriğinin örneklere ait olarak yorumlanması mantıklıdır :) Ama sınıflar birinci sınıf nesneler olduğunda, tabii ki doğal olan içeriklerin (hafızadaki) içeriklerini yansıtmasıdır. (kodda).
Karl Knechtel

6
Normatif yapı, kitabımda tuhaf veya sınırlama yok. Bunun beceriksiz ve çirkin olabileceğini biliyorum, ama buna bir şeyin "tanımı" diyebilirsiniz. Dinamik diller bana biraz anarşistler gibi geliyor: Tabii ki herkes özgür, ama birinin çöpü boşaltmasını ve yolu açmasını sağlamak için bir yapıya ihtiyacınız var. Sanırım ben yaşlıyım ... :)
AndreasT

4
İşlev tanımı modül yükleme zamanında yürütülür. İşlev gövdesi işlev çağrısı zamanında yürütülür. Varsayılan argüman, işlev gövdesinin değil, işlev tanımının bir parçasıdır. (İç içe geçmiş işlevler için daha karmaşık hale gelir.)
Lutz Prechelt

84

Nedeni, kodlar yürütüldüğünde ve işlev tanımlaması yürütüldüğünde, işlevler tanımlandığında yapıldığında oldukça basittir.

Bunu karşılaştırın:

class BananaBunch:
    bananas = []

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

Bu kod, aynı beklenmedik olaydan muzdariptir. muz bir sınıf niteliğidir ve bu nedenle ona bir şeyler eklediğinizde o sınıfın tüm örneklerine eklenir. Sebebi tamamen aynı.

Sadece "Nasıl Çalışır" ve işlev durumunda farklı çalışmasını sağlamak muhtemelen karmaşık olabilir ve sınıf durumunda muhtemelen imkansız olabilir veya en azından sınıf kodunu tutmanız gerektiği için nesne örneklemesini çok yavaşlatır ve nesneler oluşturulduğunda bunu yürütün.

Evet, beklenmedik. Ama kuruş düştüğünde, Python'un genel olarak nasıl çalıştığına mükemmel uyum sağlar. Aslında, bu iyi bir öğretme yardımcısıdır ve bunun neden olduğunu anladıktan sonra, python'u çok daha iyi hale getireceksiniz.

Bu iyi bir Python öğretici belirgin bir özellik olması gerektiğini söyledi. Çünkü bahsettiğiniz gibi, herkes er ya da geç bu problemle karşılaşır.


Bir sınıfın her örneği için farklı olan bir sınıf niteliğini nasıl tanımlarsınız?
Kieveli

19
Her örnek için farklıysa, bir sınıf niteliği değildir. Sınıf nitelikleri, SINIF üzerindeki niteliklerdir. Dolayısıyla adı. Bu nedenle, tüm örnekler için aynıdır.
Lennart Regebro

1
Sınıfta, sınıfın her örneği için farklı olan bir özniteliği nasıl tanımlarsınız? (Python'un adlandırma konvansiyonlarına aşina olmayan bir kişinin bir sınıfın normal üye değişkenleri hakkında soru sorabileceğini belirleyemeyenler için yeniden tanımlanmıştır).
Kieveli

@Kievieli: Bir sınıfın normal üye değişkenlerinden bahsediyorsunuz. :-) Örnek özniteliklerini herhangi bir yöntemde self.attribute = değeri diyerek tanımlarsınız. Örneğin __init __ ().
Lennart Regebro

@Kieveli: İki cevap: yapamazsınız, çünkü sınıf düzeyinde tanımladığınız herhangi bir şey bir sınıf özniteliği olacaktır ve bu özniteliğe erişen herhangi bir örnek aynı sınıf özniteliğine erişecektir; propertyaslında normal nitelikler gibi davranan ancak nitelik yerine sınıf yerine örneği kaydedebilen ( self.attribute = valueLennart'ın dediği gibi kullanarak) s - kullanarak / türünü / türünü kullanabilirsiniz .
Ethan Furman

66

Neden içgözlem yapmıyorsunuz?

Ben ediyorum gerçekten Python tarafından sunulan anlayışlı iç gözlem gerçekleştirildi (konuşup hiç kimsenin şaşırttı 2ve 3callables üzerine uygulanır).

Basit bir küçük fonksiyon verildiğinde func:

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

Python onunla karşılaştığında, yaptığı ilk şey codebu işlev için bir nesne oluşturmak amacıyla derlemektir . Bu derleme adımı tamamlanırken, Python *[] değerini değerlendirir ve sonra varsayılan bağımsız değişkenleri ( burada boş bir liste ) işlev nesnesinin içinde saklar . En üstteki yanıtta belirtildiği gibi: liste aartık işlevin bir üyesi olarak kabul edilebilir func.

Öyleyse, listenin işlev nesnesinin içinde nasıl genişletildiğini incelemek için önce ve sonra biraz içgözlem yapalım . Bunun için kullanıyorum Python 3.x, Python 2 için aynı geçerlidir ( __defaults__veya func_defaultsPython 2'de kullanın ; evet, aynı şey için iki isim).

Yürütmeden Önce İşlev:

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

Python bu tanımı yürüttükten sonra, belirtilen ( a = []burada) varsayılan parametreleri alır ve bunları __defaults__işlev nesnesinin özniteliğinde sıkıştırır (ilgili bölüm: Callables):

>>> func.__defaults__
([],)

Tamam, __defaults__beklendiği gibi tek bir giriş olarak boş bir liste .

Yürütme Sonrası İşlev:

Şimdi bu işlevi yerine getirelim:

>>> func()

Şimdi bunları __defaults__tekrar görelim :

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

Şaşkına? Nesnenin içindeki değer değişir! Bu işleve ardışık çağrılar artık bu katıştırılmış listnesneye eklenecektir :

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

İşte burada, bu 'kusurun' olmasının nedeni , varsayılan argümanların fonksiyon nesnesinin bir parçası olmasıdır. Burada garip bir şey yok, hepsi sadece biraz şaşırtıcı.

Bununla mücadele için ortak çözüm None, varsayılan olarak kullanmak ve daha sonra işlev gövdesinde başlatmaktır:

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

İşlev gövdesi her seferinde yeniden yürütüldüğünden, hiçbir argüman iletilmezse her zaman yeni ve boş bir liste alırsınız a.


İçindeki listenin __defaults__işlevde kullanılanla aynı olduğunu doğrulamak için , işlev gövdesinde kullanılan listeyi funcdöndürmek için işlevinizi değiştirebilirsiniz . Ardından, listeye karşılaştırmak (pozisyon içinde ) ve bunlar gerçekten aynı liste örneğine atıfta nasıl göreceksiniz:ida__defaults__[0]__defaults__

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

Tüm içgözlem gücü ile!


* Python'un işlevin derlenmesi sırasında varsayılan bağımsız değişkenleri değerlendirdiğini doğrulamak için aşağıdakileri gerçekleştirmeyi deneyin:

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

fark input()edeceğiniz gibi, fonksiyonun oluşturulması ve isme bağlanması işlemi yapılmadan önce çağrılır bar.


1
id(...)Son doğrulama için gerekli mi yoksa isoperatör aynı soruyu cevaplar mı?
das-g

1
@ das-g iyi isolurdu, id(val)çünkü daha sezgisel olabileceğini düşünüyorum.
Dimitris Fasarakis Hilliard

NoneVarsayılan olarak kullanmak , __defaults__içgözlemin yararlılığını ciddi şekilde sınırlar , bu yüzden bunun işini olduğu gibi savunmanın iyi çalıştığını düşünmüyorum __defaults__. Tembel değerlendirme, işlev varsayılanlarını her iki taraftan da yararlı tutmak için daha fazlasını yapar.
Brilliand

58

Çalışma zamanında nesneleri oluşturmanın daha iyi bir yaklaşım olacağını düşünürdüm. Şimdi daha az eminim, çünkü bazı yararlı özellikleri kaybediyorsunuz, ancak acemi karışıklığını önlemek ne olursa olsun buna değebilir. Bunu yapmanın dezavantajları şunlardır:

1. Performans

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

Çağrı süresi değerlendirmesi kullanılıyorsa, işleviniz bağımsız değişken olmadan her kullanıldığında pahalı işlev çağrılır. Her çağrı için pahalı bir ücret ödersiniz veya değeri harici olarak önbelleğe almanız, ad alanınızı kirletmeniz ve ayrıntı eklemeniz gerekir.

2. Bağlı parametreleri zorlama

Yararlı bir hile, lambda parametrelerini , lambda oluşturulduğunda bir değişkenin mevcut bağlanmasına bağlamaktır. Örneğin:

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

Bu, sırasıyla 0,1,2,3 ... döndüren işlevlerin bir listesini döndürür. Davranış değiştirilirse, bunun yerine bağlayacak iiçin çağrı zamanlı tüm döndüğünü fonksiyonların bir listesini alacağı, böylece i değerine 9.

Aksi takdirde bunu uygulamak için tek yolu i bağlı, yani başka bir kapatma oluşturmak olacaktır:

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

3. İçgözlem

Kodu düşünün:

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

Biz argümanları kullanarak varsayılan hakkında bilgi alabilirsiniz inspectmodülü, hangi

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

Bu bilgiler belge oluşturma, metaprogramlama, dekoratörler vb. Şeyler için çok yararlıdır.

Şimdi, varsayılanların davranışının aşağıdakilere eşdeğer olacak şekilde değiştirilebileceğini varsayın:

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

Ancak biz kendi duygularını ve varsayılan argümanlar ne olduğunu görme olanağı kaybettim vardır . Nesneler inşa edilmediğinden, aslında işlevi çağırmadan onları tutamayız. Yapabileceğimiz en iyi şey, kaynak kodunu saklamak ve bunu bir dize olarak döndürmektir.


1
her biri için bir değer yerine varsayılan bağımsız değişken oluşturma işlevi varsa da içgözlem elde edebilirsiniz. inceleme modülü sadece bu işlevi çağırır.
yairchu

@SilentGhost: Davranışı yeniden oluşturmak için değiştirilip değiştirilmediğinden bahsediyorum - bir kez oluşturmak geçerli davranıştır ve neden değiştirilebilir varsayılan sorunun var olduğunu.
Brian

1
@yairchu: Bu, yapının güvenli olduğunu varsayar (yani yan etkisi yoktur). Args olmamalıdır introspecting yapmak şey, ama rasgele kod değerlendirmek iyi bir etkiye sahip yeterli olabilir.
Brian

1
Farklı bir dil tasarımı çoğu zaman sadece farklı şeyler yazmak anlamına gelir. İlk örneğiniz kolayca şöyle yazılabilir: _expensive = expensive (); Özellikle eğer def foo (arg = _expensive), yapamaz o reevaluated istiyorum.
Glenn Maynard

@Glenn - "değişkeni harici olarak önbelleğe al" ile bahsettiğim şey bu - biraz daha ayrıntılı ve ad alanınızda ekstra değişkenlerle karşılaşıyorsunuz.
Brian

55

Python savunmasında 5 puan

  1. Basitlik : Davranış şu anlamda basittir: Çoğu insan bu tuzağa birkaç kez değil, sadece bir kez düşer.

  2. Tutarlılık : Python her zaman adları değil nesneleri geçer. Varsayılan parametre, açıkça, fonksiyon başlığının bir parçasıdır (fonksiyon gövdesi değil). Bu nedenle, işlev çağrısı zamanında değil, modül yükleme zamanında (ve yalnızca iç içe geçmedikçe modül yükleme zamanında) değerlendirilmelidir.

  3. Yararlılık : Frederik Lundh'un "Python'daki Varsayılan Parametre Değerleri" açıklamasında belirttiği gibi, mevcut davranış ileri programlama için oldukça yararlı olabilir. (Tutumlu kullanın.)

  4. Yeterli belgeler : En temel Python belgelerinde, eğitimde, konu yüksek sesle bir olarak açıklandı "Önemli uyarı" in ilk Bölümün alt bölümünde "Fonksiyon Tanımları Üzerine Daha" . Uyarı, başlıkların dışında nadiren uygulanan kalın yazı bile kullanıyor. RTFM: Hassas kılavuzu okuyun.

  5. Meta-öğrenme : Tuzağa düşmek aslında çok yararlı bir andır (en azından yansıtıcı bir öğreniciyseniz), çünkü daha sonra yukarıdaki "Tutarlılık" noktasını daha iyi anlayacaksınız ve bu size Python hakkında çok şey öğretecektir.


18
Bu davranışın üretim kodumu bozduğunu bulmak bir yıl sürdü, bu tasarım kusuruna tesadüfen çarpıncaya kadar tam bir özellik kaldırıldı. Django kullanıyorum. Hazırlama ortamının çok fazla isteği olmadığından, bu hatanın KG üzerinde hiçbir etkisi olmadı. Canlı yayına başladığımızda ve birçok eşzamanlı istek aldığımızda - bazı yardımcı program işlevleri birbirlerinin parametrelerinin üzerine yazmaya başladı! Güvenlik delikleri, böcekler ve ne değil.
oriadam

7
@oriadam, suç yok, ama daha önce buna girmeden Python'u nasıl öğrendiğinizi merak ediyorum. Ben şimdi sadece Python öğreniyorum ve bu olası tuzak varsayılan Python öğretici varsayılan argümanların ilk sözü yanında belirtilir. (Bu cevabın 4. maddesinde belirtildiği gibi.) Ahlakın, üretim yazılımı oluşturmak için kullandığınız dilin resmi belgelerini okumak için - oldukça anlayışsız - olduğunu düşünüyorum .
Wildcard

Ayrıca, yaptığım işlev çağrısına ek olarak karmaşıklığı bilinmeyen bir işlev çağrıldığında da (benim için) şaşırtıcı olurdu.
Vatikan

52

Bu davranışı kolay açıklayan:

  1. işlev (sınıf vb.) bildirimi yalnızca bir kez yürütülür ve tüm varsayılan değer nesneleri oluşturulur
  2. her şey referansla geçti

Yani:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a değişmez - her atama çağrısı yeni int nesnesi oluşturur - yeni nesne yazdırılır
  2. b değişmez - yeni dizi varsayılan değerden oluşturulur ve yazdırılır
  3. c değişiklikler - işlem aynı nesne üzerinde gerçekleştirilir - ve yazdırılır

(Aslında, ekleme kötü bir örnektir, ancak tam sayıların değişmez olması benim ana
noktamdır

B [] olarak ayarlandığında, b .__ add __ ([1]) [1] değerini döndürdüğünü ancak listelerin değişebilse bile b'yi [] bıraktığını görmek için kontrol ettikten sonra benim üzümseme fark etti. Benim hatam.
Anon

@ANon: var __iadd__, ama int ile çalışmıyor. Elbette. :-)
Veky

35

Sorduğunuz şey bunun neden:

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

dahili olarak buna eşdeğer değil:

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()

göz ardı edeceğimiz açıkça arama işlevi (Yok, Yok) dışında.

Başka bir deyişle, varsayılan parametreleri değerlendirmek yerine, neden her birini saklamıyor ve işlev çağrıldığında bunları değerlendirmiyorsunuz?

Bir cevap muhtemelen oradadır - varsayılan parametrelerle her işlevi etkili bir şekilde kapatmaya dönüştürür. Her şey yorumlayıcıda gizlenmiş olsa ve tam bir kapanış olmasa bile, veriler bir yerde saklanmalıdır. Daha yavaş olur ve daha fazla bellek kullanır.


6
Bir kapatma olması gerekmeyecek - bunu düşünmenin daha iyi bir yolu, bayt kodu varsayılanları ilk kod satırını yapmaktır - sonuçta bedeni o noktada derliyorsunuz - kod arasında gerçek bir fark yok vücuttaki argümanlarda ve kodda.
Brian

10
Doğru, ama yine de Python'u yavaşlatacaktı ve sınıf tanımları için aynısını yapmadığınız sürece aslında oldukça şaşırtıcı olurdu, bu da her örnekleme yaptığınızda tüm sınıf tanımını yeniden çalıştırmak zorunda kalacağınız için aptalca yavaşlatacaktır. sınıf. Belirtildiği gibi, düzeltme problemden daha şaşırtıcı olacaktır.
Lennart Regebro

Lennart ile aynı fikirde. Guido'nun dediği gibi, her dil özelliği veya standart kütüphane için bunu kullanan biri var.
Jason Baker

6
Şimdi değiştirmek delilik olurdu - sadece neden böyle olduğunu araştırıyoruz. Başlangıçta varsayılan değerlendirmeyi yapmak için geç olsaydı, şaşırtıcı olmazdı. Böyle bir çekirdeğin bir ayrıştırma farkının, bir bütün olarak dil üzerinde kapsamlı ve muhtemelen çok belirsiz etkileri olacağı kesinlikle doğrudur.
Glenn Maynard

35

1) "Değişken Varsayılan Argüman" problemi genel olarak şunu gösteren özel bir örnektir:
"Bu problemle ilgili tüm fonksiyonlar , gerçek parametrede benzer yan etki probleminden muzdariptir. "
Bu, fonksiyonel programlama kurallarına aykırıdır, genellikle arzu edilmez ve her ikisi birlikte sabitlenmelidir.

Misal:

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]

Çözüm : bir kopya Öncelikle giriş nesnesine veya giriş nesnesine ve ardından kopyayla ne varsa yapmak
güvenli bir çözümdür .copydeepcopy

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

Birçok yerleşik değiştirilebilir tipler gibi bir kopya yöntemine sahip some_dict.copy()ya some_set.copy()ya da benzeri kolay kopyalanabilir somelist[:]veya list(some_list). Her nesne aynı zamanda copy.copy(any_object)veya daha ayrıntılı bir şekilde kopyalanabilir copy.deepcopy()(sonuncusu değişebilir nesne değiştirilebilir nesnelerden oluşuyorsa yararlıdır). Bazı nesneler temel olarak "file" nesnesi gibi yan etkilere dayanır ve kopya ile anlamlı bir şekilde çoğaltılamaz.kopyalama

Benzer bir SO sorusu için örnek problem

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]

Bu işlev tarafından döndürülen bir örneğin hiçbir genel özniteliğine kaydedilmemelidir . ( Özel olduğunu varsayarak, Örneğin özniteliklerinin kural tarafından bu sınıfın veya alt sınıfların dışından değiştirilmemesi gerektiği yani _var1özel bir özniteliktir)

Sonuç:
Girdi parametreleri nesneleri yerinde değiştirilmemeli (mutasyona uğratılmamalı) veya işlev tarafından döndürülen bir nesneye bağlanmamalıdır. (Eğer şiddetle tavsiye edilen yan etkiler olmadan programlamayı tercih edersek, "yan etki" hakkında Wiki'ye bakınız. (İlk iki paragraf bu bağlamda ilişkilidir.).)

2)
Sadece gerçek parametre üzerindeki yan etki gerekliyse ancak varsayılan parametre üzerinde istenmeyen ise, yararlı çözümdef ...(var1=None): if var1 is None: var1 = [] Diğer ..

3) Bazı durumlarda varsayılan parametrelerin değişken davranışı yararlıdır .


5
Umarım Python'un işlevsel bir programlama dili olmadığını bilirsiniz .
Veky

6
Evet, Python bazı işlevsel özelliklere sahip çok paragigm bir dildir. ("Sadece bir çekiçiniz olduğu için her problemin çivi gibi görünmesini sağlayın.") Birçoğu Python'un en iyi uygulamalarında. Python ilginç bir NASIL Fonksiyonel Programlamaya sahiptir Diğer özellikler burada belirtilmeyen kapaklar ve körüklerdir.
hynekcer

1
Ayrıca, bu geç aşamada, Python'un atama semantiklerinin gerektiğinde veri kopyalamasını önlemek için açıkça tasarlandığını da ekleyeceğim, böylece kopyaların (ve özellikle derin kopyaların) oluşturulması hem çalışma süresini hem de bellek kullanımını olumsuz etkileyecektir. Bu nedenle sadece gerektiğinde kullanılmalıdırlar, ancak yeni gelenler genellikle ne zaman anlamada zorlanırlar.
holdenweb

1
@holdenweb katılıyorum. Geçici bir kopya, orijinal mutable verileri potansiyel olarak değiştiren yabancı bir işlevden korumanın en genel ve bazen mümkün olan tek yoludur. Neyse ki verileri makul olmayan bir şekilde değiştiren bir işlev bir hata olarak kabul edilir ve bu nedenle nadirdir.
hynekcer

Bu yanıta katılıyorum. Ve def f( a = None )gerçekten başka bir şey ifade ettiğinizde yapının neden önerildiğini anlamıyorum . Kopyalama tamam, çünkü argümanları değiştirmemelisiniz. Ve bunu yaptığınızda if a is None: a = [1, 2, 3], listeyi yine de kopyalarsınız.
koddo

30

Bunun aslında varsayılan değerlerle bir ilgisi yoktur, bunun dışında değişken varsayılan değerlere sahip fonksiyonlar yazdığınızda genellikle beklenmedik bir davranış olarak ortaya çıkar.

>>> def foo(a):
    a.append(5)
    print a

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

Bu kodda görünürde varsayılan değer yok, ancak aynı sorunu alıyorsunuz.

Sorun olduğunu fooedilir değiştirerek arayan bu beklemediğini zaman, arayanın gelen geçirilen bir değişken değişken. Eğer işlev böyle bir şey olarak adlandırılırsa böyle bir kod iyi olurdu append_5; arayan kişi, ilettiği değeri değiştirmek için işlevi çağırır ve davranış beklenir. Ancak böyle bir fonksiyonun varsayılan bir argüman alması pek olası değildir ve muhtemelen listeyi döndürmez (arayanın zaten bu listeye bir referansı olduğu için, geçtiği işlev).

fooVarsayılan argümanı olan orijinaliniz değiştirilmemelidira açıkça geçirilmiş veya varsayılan değeri alıp almadığını . Bağlam / ad / belgelerden, bağımsız değişkenlerin değiştirilmesi gerektiği açıksa, kodunuz değiştirilebilir değişkenleri yalnız bırakmalıdır. Python'da olsak da olmasak da, varsayılan argümanların var olup olmadığı yerel geçişler olarak argümanlar olarak iletilen değiştirilebilir değerleri kullanmak son derece kötü bir fikirdir.

Bir şeyi hesaplarken yerel geçici bir öğeyi yıkıcı bir şekilde manipüle etmeniz gerekiyorsa ve manipülasyonunuzu bağımsız değişken değerinden başlatmanız gerekiyorsa, bir kopya oluşturmanız gerekir.


7
Her ne kadar ilgili olsa da, bu ( "yerinde" appenddeğiştirmek beklediğiniz gibi) farklı bir davranış olduğunu düşünüyorum a. Bir O varsayılan değişken her çağrıda-örneği yeniden değildir , en azından benim için ... "beklenmedik" biraz. :)
Andy Hayden

2
@AndyHayden işlevin bağımsız değişkeni değiştirmesi bekleniyorsa neden varsayılana sahip olmak mantıklı olur?
Mark Ransom

@MarkRansom düşünebildiğim tek örnek cache={}. Ancak, ben ne zaman bu "az şaşkınlık" çıkageldi şüpheli yok sen argüman mutasyona aradığınız fonksiyondurlar (ya istiyorum).
Andy Hayden

1
@AndyHayden Bu cevabı genişleterek kendi cevabımı burada bıraktım. Ne düşündüğü söyle. Bütünlüğünü örnek olarak ekleyebilirim cache={}.
Mark Ransom

1
@AndyHayden benim cevabın noktası Kazara bir tartışma varsayılan değerini mutasyona uğratarak şaşkına eğer, o zaman varsayılan zaman kod yanlışlıkla arayanın değerini mutasyona olmasıdır başka hata, olması değildi kullandı. Ve Nonearg ise gerçek varsayılanı kullanmanın ve atamanın None bu sorunu çözmediğini unutmayın (bu nedenle bir anti-desen olarak görüyorum). Varsayılan değerleri olsun ya da olmasın bağımsız değişken değerlerini değiştirerek diğer hatayı düzeltirseniz, bu "şaşırtıcı" davranışı asla fark etmez veya önemsemezsiniz.
Ben

27

Zaten meşgul bir konu, ancak burada okuduğum kadarıyla, aşağıdakiler dahili olarak nasıl çalıştığını anlamama yardımcı oldu:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232

2
aslında bu a = a + [1]aşırı yeni gelenler için biraz kafa karıştırıcı olabilir çünkü aşırı yükler a... onu değiştirmeyi b = a + [1] ; print id(b)ve bir satır eklemeyi düşünün a.append(2). Bu +, iki listede her zaman yeni bir liste (atanmış b) oluşturduğunu daha açık hale getirecek , ancak değiştirilmiş bir liste ahala aynı olabilir id(a).
Jörn Hees

25

Bu bir performans optimizasyonu. Bu işlevsellik sonucunda, bu iki işlev çağrısından hangisinin daha hızlı olduğunu düşünüyorsunuz?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Sana bir ipucu vereceğim. İşte sökme (bkz. Http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Deneyimli davranışın pratik bir kullanımı olduğundan şüpheleniyorum (C'de üreme hataları olmadan gerçekten statik değişkenleri kullanan?)

Gördüğünüz gibi, orada olan değişmez varsayılan argümanlar kullanırken bir performans yararı. Sık kullanılan bir işlevse veya varsayılan bağımsız değişkenin oluşturulması uzun sürüyorsa, bu bir fark yaratabilir. Ayrıca, Python'un C olmadığını aklınızda bulundurun. C'de oldukça fazla sabit olan sabitleriniz var. Python'da bu avantajınız yok.


24

Python: Değişebilir Varsayılan Bağımsız Değişken

Varsayılan bağımsız değişkenler, işlev bir işlev nesnesine derlendiğinde değerlendirilir. İşlev tarafından kullanıldığında, bu işlev tarafından birden çok kez, aynı nesnedir ve kalır.

Değişebildiklerinde, mutasyona uğratıldıklarında (örneğin, ona bir eleman ekleyerek) ardışık çağrılarda mutasyona uğramış olarak kalırlar.

Mutasyona uğradılar çünkü her seferinde aynı nesne.

Eşdeğer kod:

İşlev nesnesi derlendiğinde ve somutlaştırıldığında liste işleve bağlı olduğundan, bu:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

neredeyse tamamen buna eşittir:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

gösteri

İşte bir gösterim - her referans gösterildiklerinde aynı nesne olduklarını doğrulayabilirsiniz

  • listenin bir işlev nesnesine derleme işlemi bitmeden oluşturulduğunu görmek,
  • listeye her başvuruda kimliğin aynı olduğunu gözlemleyerek,
  • onu kullanan işleve ikinci kez çağrıldığında listenin değiştiğini gözlemleyerek,
  • çıktının kaynaktan yazdırıldığı sırayı gözlemleyerek (sizin için uygun şekilde numaralandırdım):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

ve şununla çalıştır python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

Bu "En Küçük Şaşkınlık" ilkesini ihlal ediyor mu?

Bu yürütme sırası, yeni Python kullanıcıları için sıklıkla kafa karıştırıcıdır. Python yürütme modelini anlarsanız, oldukça beklenen hale gelir.

Yeni Python kullanıcıları için olağan talimat:

Ancak bu nedenle yeni kullanıcılara yönelik genel talimat, bunun yerine varsayılan bağımsız değişkenlerini oluşturmaktır:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

Bu, işleve varsayılandan başka bir argüman alıp almadığımızı söylemek için Yok singletonunu sentinel nesnesi olarak kullanır. Herhangi bir argüman almazsak, aslında yeni bir boş liste kullanmak isteriz,[] varsayılan olarak .

Kontrol akışıyla ilgili öğretici bölümün dediği gibi:

Varsayılanın sonraki aramalar arasında paylaşılmasını istemiyorsanız, bunun yerine işlevi şöyle yazabilirsiniz:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

24

En kısa cevap muhtemelen "tanım yürütme" olacaktır, bu nedenle tüm argüman katı bir anlam ifade etmez. Daha tartışmalı bir örnek olarak, bunu gösterebilirsiniz:

def a(): return []

def b(x=a()):
    print x

Umarım, varsayılan argüman ifadelerinin, def kolay olmadığını veya anlamlı olmadığını veya her ikisini birden .

Yine de, varsayılan kurucuları kullanmaya çalıştığınızda bunun bir sorun olduğunu kabul ediyorum.


20

Yok'u kullanarak basit bir geçici çözüm

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]

19

Aşağıdakileri dikkate alırsanız bu davranış şaşırtıcı değildir:

  1. Ödev okumalarında salt okunur sınıf niteliklerinin davranışı ve
  2. Fonksiyonlar nesnedir (kabul edilen cevapta iyi açıklanmıştır).

(2) ' nin rolü bu iş parçacığında kapsamlı bir şekilde ele alınmıştır. (1) diğer dillerden geldiğinde bu davranış "sezgisel" olmadığından büyük olasılıkla şaşkınlığa neden olan faktördür.

(1) sınıflarla ilgili Python dersinde açıklanmıştır . Salt okunur bir sınıf özniteliğine değer atama girişiminde:

... en iç kapsamın dışında bulunan tüm değişkenler salt okunurdur ( böyle bir değişkene yazma girişimi, en içteki kapsamda yeni bir yerel değişken oluşturur ve özdeş olarak adlandırılan dış değişkeni değiştirmez ).

Orijinal örneğe tekrar bakın ve yukarıdaki noktaları göz önünde bulundurun:

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

Burada foobir amacı ve abir niteliğidir foo(adresinde foo.func_defs[0]). Yana abir listedir, adeğişken ve böylece bir okuma-yazma özelliğidirfoo . İşlev başlatıldığında imza tarafından belirtilen boş listeye başlatılır ve işlev nesnesi var olduğu sürece okuma ve yazma için kullanılabilir.

fooBir varsayılanı geçersiz kılmadan arama yapmak, varsayılanın değerini kullanır foo.func_defs. Bu durumda, işlev nesnesinin kod kapsamı içinde foo.func_defs[0]kullanılır a. Değiştirilmesi, adeğişim foo.func_defs[0]bir parçası olan, fookod çalıştırılmasına arasında nesne ve devam edersefoo .

Şimdi, bunu, her işlev yürütüldüğünde işlev imzası varsayılanlarının kullanıldığı şekilde , diğer dillerin varsayılan bağımsız değişken davranışını taklit etme ile ilgili belgelerdeki örnekle karşılaştırın :

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Alarak (1) ve (2) bu istenilen davranışı gerçekleştirir neden dikkate bir görebilir:

  • Zaman foofonksiyonu nesne örneği, foo.func_defs[0]ayarlanır Nonedeğişken olmayan bir obje.
  • İşlev varsayılanlarla ( Lişlev çağrısında için hiçbir parametre belirtilmemişse) yürütüldüğünde , foo.func_defs[0]( None) yerel kapsamda olarak kullanılabilir L.
  • Üzerine L = [], atama başarılı olamaz foo.func_defs[0]o özellik salt okunur çünkü.
  • Başına (1) , ayrıca adında yeni bir yerel değişken Lyerel kapsamında oluşturulan ve işlev çağrısı kalanı için kullandı. foo.func_defs[0]böylece gelecekteki çağrıları için değişmeden kalır foo.

19

Bir fonksiyona varsayılan liste değeri geçirmek için alternatif bir yapı göstereceğim (sözlüklerle eşit derecede iyi çalışır).

Diğerleri kapsamlı bir şekilde yorumladığı gibi, list parametresi, yürütüldüğünün aksine olarak tanımlandığında işleve bağlıdır. Listeler ve sözlükler değiştirilebilir olduğundan, bu parametrede yapılacak herhangi bir değişiklik bu işleve yapılan diğer çağrıları etkiler. Sonuç olarak, işleve yapılan sonraki çağrılar, işleve yapılan diğer çağrılar tarafından değiştirilmiş olabilecek bu paylaşılan listeyi alır. Daha da kötüsü, iki parametre bu işlevin paylaşılan parametresini aynı anda diğerinin yaptığı değişikliklerden habersiz kullanıyor.

Yanlış Yöntem (muhtemelen ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Bunları kullanarak bir ve aynı nesne olduklarını doğrulayabilirsiniz id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Brett Slatkin'in "Etkili Python: 59 Daha İyi Python Yazmanın Özel Yolları", Madde 20: NoneDinamik varsayılan argümanları belirtmek için Use ve Docstrings kullanın (s. 48)

Python'da istenen sonucu elde etmenin kuralı, doktrindeki varsayılan Nonedavranışın varsayılan değerini sağlamak ve belgelendirmektir.

Bu uygulama, işleve yapılan her çağrının varsayılan listeyi almasını veya işleve iletilen listeyi almasını sağlar.

Tercih Edilen Yöntem :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

Programlayıcının varsayılan liste parametresinin paylaşılmasını istediği 'Yanlış Yöntem' için geçerli kullanım örnekleri olabilir, ancak bu kuraldan ziyade istisnadır.


17

Buradaki çözümler:

  1. NoneVarsayılan değeriniz (veya bir nonce object) olarak kullanın ve çalışma zamanında değerlerinizi oluşturmak için bunu açın; veya
  2. lambdaVarsayılan parametreniz olarak a kullanın ve varsayılan değeri almak için bir try bloğu içinde çağırın (bu lambda soyutlamanın neden olduğu bir şeydir).

İkinci seçenek güzel çünkü fonksiyonun kullanıcıları zaten mevcut olan bir çağrılabilir cihazdan geçebilirler (örneğin type)


16

Bunu yaptığımızda:

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

... biz argüman atamak abir karşı adsız Arayan değerini geçmez ise, listede.

Bu tartışma için işleri kolaylaştırmak için adsız listeye geçici olarak bir ad verelim. Nasıl pavlo?

def foo(a=pavlo):
   ...

Herhangi bir zamanda, arayan bize ne aolduğunu söylemezse , tekrar kullanırız pavlo.

Eğer pavlodeğişken (değiştirilebilir), ve foobunu değiştirerek kadar uçları, bir efekt biz dahaki sefere fark foobelirtmeden denir a.

Yani gördüğünüz şey budur (Unutmayın, pavlo[] olarak ilklendirilir):

 >>> foo()
 [5]

Şimdi, pavlo[5].

foo()Tekrar aramak tekrar değişir pavlo:

>>> foo()
[5, 5]

Belirtme açağıran foo()sağlar pavlodokunulmaz.

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

Yani, pavlohala öyle [5, 5].

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

16

Bazen bu davranıştan aşağıdaki kalıba alternatif olarak yararlanırım:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Eğer singletonsadece tarafından kullanılan use_singleton, bir yedek olarak aşağıdaki deseni gibi:

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

Bunu harici kaynaklara erişen istemci sınıflarını örneklemek ve ayrıca not almak için dikte veya liste oluşturmak için kullandım.

Bu modelin iyi bilindiğini düşünmediğim için gelecekteki yanlış anlamalara karşı kısa bir yorum yapıyorum.


2
Not için bir dekoratör eklemeyi ve not önbelleğini işlev nesnesinin kendisine koymayı tercih ederim.
Stefano Borini

Bu örnek, gösterdiğiniz daha karmaşık desenin yerine geçmez, çünkü _make_singletonvarsayılan argüman örneğinde def zamanında, ancak global örnekte çağrı zamanında çağrı yaparsınız . Gerçek bir ikame, varsayılan argüman değeri için bir çeşit değiştirilebilir kutu kullanır, ancak argümanın eklenmesi alternatif değerleri iletme fırsatı verir.
Yann Vernier

15

Nesneyi (ve dolayısıyla bağı kapsamla) değiştirerek bunu tamamlayabilirsiniz:

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

Çirkin, ama işe yarıyor.


3
Bu, işlevin beklediği bağımsız değişken türlerini belgelemek için otomatik belge oluşturma yazılımı kullandığınız durumlarda güzel bir çözümdür. A = Yok koymak ve sonra a öğesi Yok ise [] olarak ayarlamak, okuyucunun bir bakışta ne beklendiğini anlamasına yardımcı olmaz.
Michael Scott Cuthbert

Harika fikir: bu ismin yeniden hatırlanması, asla değiştirilemeyeceğini garanti eder. Gerçekten beğendim.
holdenweb

Tam olarak bunu yapmanın yolu budur. Python parametrenin bir kopyasını oluşturmaz, bu yüzden kopyayı açıkça yapmak size bağlıdır. Bir kopyasına sahip olduktan sonra, beklenmedik yan etkiler olmadan istediğiniz gibi değiştirmek sizin elinizde.
Mark Ransom

13

Doğru olabilir:

  1. Birisi her dil / kütüphane özelliğini kullanıyor ve
  2. Davranışı burada değiştirmek tavsiye edilmez, ancak

yukarıdaki özelliklerin her ikisine de sahip olmak ve yine de başka bir noktaya değinmek tamamen tutarlıdır:

  1. Kafa karıştırıcı bir özelliktir ve Python'da talihsiz bir durumdur.

Diğer cevaplar, ya da en azından bazıları ya 1 ve 2 puan verir, 3 değil ya da puan 3 ve önemsiz puan 1 ve 2 yapar. Ancak her üçü de doğrudur.

Burada ortadaki atları değiştirmenin önemli bir kırılma isteyeceği ve Python'un Stefano'nun açılış pasajını sezgisel olarak ele almasıyla yaratılan daha fazla sorun olabileceği doğru olabilir. Ve Python içlerini iyi bilen birisinin bir mayın alanını sonuçları açıklayabileceği doğru olabilir. Ancak,

Mevcut davranış Pythonic değildir ve Python başarılıdır, çünkü dil hakkında çok az şey her yerde en az şaşkınlık ilkesini ihlal eder yakınlardakibu çok kötü. Onu kökünden sökmek akıllıca olsun ya da olmasın gerçek bir sorundur. Bir tasarım hatasıdır. Davranışı izlemeye çalışarak dili çok daha iyi anlarsanız, C ++ 'nın tüm bunları ve daha fazlasını yaptığını söyleyebilirim; örneğin, ince işaretçi hatalarında gezinerek çok şey öğrenirsiniz. Ama bu Pythonic değil: Python'u bu davranış karşısında ısrar edecek kadar önemseyen insanlar, dile çekilen insanlardır, çünkü Python'un diğer dilden çok daha az sürprizleri vardır. Dabblerslar ve meraklılar, bir şeyleri çalıştırmak için ne kadar az zaman aldıklarına şaşkınlık duyduklarında - Python'a çekilen programcıların sezgilerini kesen - yani, gizli mantık bulmacası - Pythonistas olurlar. çünkü Sadece Çalışıyor .


6
-1 Savunabilir bir perspektif olmasına rağmen, bu bir cevap değil ve ben buna katılmıyorum. Çok fazla özel istisna kendi köşe vakalarını başlatır.
Marcin

3
O halde, Python'da fonksiyonun her çağrıldığında [] varsayılan bir argümanının [] kalmasının daha mantıklı olduğunu söylemek "inanılmaz derecede cahil" mi?
Christos Hayward

3
Ve varsayılan olmayan bir argümanı None olarak ayarlayan talihsiz bir deyim olarak düşünmek ve sonra argüman == None: argument = []? Bu deyimi talihsiz olarak düşünmek, insanların saf bir yeni gelenin beklediği şeyi istemesi gibi cahil midir, f (argüman = []) atarsanız, argüman otomatik olarak [] değerine varsayılan olarak geçer mi?
Christos Hayward

3
Ancak Python'da, dilin ruhunun bir parçası, çok fazla derin dalış yapmak zorunda olmamanızdır; array.sort () işlevi, sıralama, büyük O ve sabitler hakkında ne kadar az şey anladığınızdan bağımsız olarak çalışır ve çalışır. Sayısız örneklerden birini vermek için dizi sıralama mekanizmasındaki Python'un güzelliği, içsellere derin bir dalış yapmanıza gerek olmadığıdır. Ve farklı bir şekilde söylemek gerekirse, Python'un güzelliği, Just Works'ün bir şey elde etmek için uygulamaya derin bir dalış yapması gerekmiyor. Ve bir geçici çözüm var (... if argümanı == Yok: argüman = []), FAIL.
Christos Hayward

3
Bağımsız bir ifade olarak x=[], "boş liste nesnesi oluşturma ve 'x' adını buna bağlama" anlamına gelir. Böylece, def f(x=[])boş bir liste de oluşturulur. Her zaman x'e bağlı değildir, bunun yerine varsayılan vekile bağlanır. Daha sonra f () çağrıldığında, varsayılan değer çıkarılır ve x'e bağlanır. Sincaplanmış boş listenin kendisi olduğu için, aynı liste, içine bir şey sıkışmış olsun ya da olmasın, x'e bağlanacak tek şeydir. Aksi nasıl olabilir?
Jerry B

10

Bu bir tasarım hatası değil . Bunun üstesinden gelen herkes yanlış bir şey yapıyor.

Bu sorunla karşılaşabileceğiniz 3 durum var:

  1. Argümanı işlevin bir yan etkisi olarak değiştirmeyi planlıyorsunuz. Bu durumda , varsayılan bir argümana sahip olmak asla mantıklı değildir. Tek istisna, argüman listesini işlev özniteliklerine sahip olmak için kötüye kullandığınızda, ör.cache={} ve işlevi gerçek bir argümanla çağırmanız beklenmez.
  2. Sen değiştirilmemiş argüman bırakmak niyetinde, ancak yanlışlıkla yaptım değiştirmek. Bu bir hata, düzelt.
  3. İşlev içinde kullanmak için bağımsız değişkeni değiştirmeyi düşünüyorsunuz, ancak değişikliğin işlev dışında görüntülenebilir olmasını beklemiyordunuz. Bu durumda , varsayılan olsun ya da olmasın, argümanın bir kopyasını oluşturmanız gerekir ! Python, değere göre çağrı yapan bir dil değildir, bu nedenle sizin için bir kopya oluşturmaz, bu konuda açık olmanız gerekir.

Sorudaki örnek kategori 1 veya 3'e girebilir. Hem iletilen listeyi değiştirmesi hem de döndürmesi tuhaftır; birini ya da diğerini seçmelisiniz.


"Yanlış bir şey yapmak" tanıdır. Bununla birlikte, zamanlar olduğunu düşünüyorum = Yok desen yararlıdır, ancak genellikle bu durumda bir mutable geçtiğinde değiştirmek istemezsiniz (2). cache={}Desen gerçekten muhtemelen istediğiniz gerçek kodundaki bir görüşme-tek çözüm olduğunu @lru_cache!
Andy Hayden

9

Bu "hata" bana fazla mesai saatleri verdi! Ama potansiyel bir kullanımını görmeye başlıyorum (ama yine de yürütme zamanında olmasını isterdim, yine de)

Yararlı bir örnek olarak gördüğümü sana vereceğim.

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

aşağıdakileri yazdırır

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way

8

Sadece işlevi şu şekilde değiştirin:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a

7

Ben bu sorunun cevabının değişebilirlik ya da python "def" deyimi işlemek için nasıl python parametre (değer veya başvuru tarafından geçmek) veri geçiş yatıyor olduğunu düşünüyorum.

Kısa bir giriş. İlk olarak, python'da iki tür veri türü vardır, biri sayılar gibi basit temel veri türüdür ve başka bir veri türü nesnelerdir. İkincisi, verileri parametrelere iletirken, python temel veri türünü değere göre geçirir, yani değerin yerel bir değişkenine yerel bir kopyasını oluşturur, ancak nesneyi referans olarak, yani nesneyi işaret ederek geçirir.

Yukarıdaki iki noktayı kabul ederek, python koduna ne olduğunu açıklayalım. Bunun nedeni yalnızca nesneler için referans olarak geçilmesidir, ancak değişebilir / değişmez ile ilgisi yoktur veya tartışmasız olarak "def" ifadesinin tanımlandığında yalnızca bir kez yürütüldüğü gerçeği.

[] bir nesnedir, bu nedenle python [] 'e yapılan referansı a, yani ahafızada bir nesne olarak yer alan []' ye bir göstericidir. Bununla birlikte, [] 'in yalnızca bir kopyası vardır, ancak bununla ilgili birçok referans vardır. İlk foo () için, ekleme yöntemi ile [] listesi 1 olarak değiştirilir . Ancak liste nesnesinin yalnızca bir kopyası olduğunu ve bu nesnenin artık 1 haline geldiğini unutmayın . İkinci foo () çalıştırılırken, effbot web sayfasının söylediği (öğeler artık değerlendirilmez) yanlıştır. aşimdi nesnenin içeriği 1 olmasına rağmen, liste nesnesi olarak değerlendirilir . Bu referans ile geçmenin etkisidir! Foo'nun (3) sonucu aynı şekilde kolayca elde edilebilir.

Cevabımı daha da doğrulamak için iki ek koda bakalım.

====== No. 2 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]bir nesne, yani None(birincisi değişmezken değişebilir. Ama değişebilirliğin soru ile ilgisi yoktur). Hiçbiri uzayda bir yerde değil ama orada olduğunu biliyoruz ve Yok'un sadece bir kopyası var. Bu nedenle, foo her çağrıldığında, öğeler (yalnızca bir kez değerlendirildiği bazı cevabın aksine) Yok, açık olmak gerekirse, Yok'un referansı (veya adresi) olarak değerlendirilir. Ardından foo'da öğe [] olarak değiştirilir, yani farklı bir adresi olan başka bir nesneyi gösterir.

====== No. 3 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,3]

Foo (1) öğesinin çağrılması, öğeleri 11111111 adresli bir liste nesnesine [] işaret eder. Listenin içeriği , devam filmindeki foo işlevinde 1 olarak değiştirilir , ancak adres değiştirilmez, yine de 11111111 Sonra foo (2, []) geliyor. Foo (2, []) içindeki [], foo (1) çağrılırken varsayılan parametre [] ile aynı içeriğe sahip olsa da, adresleri farklıdır! Parametreyi açıkça sağladığımız için items, bu yeni[] örneğin 2222222 almalı ve bir değişiklik yaptıktan sonra geri vermeliyiz. Şimdi foo (3) yürütülür. sadece berixsağlandığında, öğelerin varsayılan değerini tekrar alması gerekir. Varsayılan değer nedir? Foo işlevi tanımlanırken ayarlanır: 11111111'de bulunan liste nesnesi. Böylece öğeler, bir elemanı 1 olan 11111111 adresi olarak değerlendirilir. 2222222'de bulunan liste aynı zamanda bir eleman 2 içerir, ancak herhangi bir öğe tarafından işaretlenmez Daha. Sonuç olarak, 3 eki items[1,3] yapacaktır .

Yukarıdaki açıklamalardan, kabul edilen cevapta önerilen efbot web sayfasının bu soruya ilgili bir cevap veremediğini görebiliriz. Dahası, effbot web sayfasındaki bir noktanın yanlış olduğunu düşünüyorum. Ben UI.Button ile ilgili kod doğru olduğunu düşünüyorum:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Her düğme, farklı değerini görüntüleyecek farklı bir geri arama işlevine sahip olabilir i. Bunu göstermek için bir örnek verebilirim:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Eğer yürütürsek x[7](), beklendiği gibi 7 alırız ve x[9]()9, başka bir değer verir i.


5
Son noktan yanlış. Deneyin ve göreceksiniz x[7]()olduğunu 9.
Duncan

2
"python temel veri türünü değere göre geçirir, yani değerin yerel bir değişkene yerel kopyasını çıkar" tamamen yanlıştır. Birisinin Python'u çok iyi tanıyabileceği, ancak temellerin bu kadar korkunç yanlış anlaşılmasından şüpheleniyorum. :-(
Veky

6

TLDR: Tanımlama zamanı varsayılanları tutarlı ve kesinlikle daha etkileyici.


Bir işlevin tanımlanması iki kapsamı etkiler: işlevi içeren tanımlama kapsamı ve işlevin içerdiği yürütme kapsamı . Haritaların kapsamlarla nasıl bir araya getirildiği oldukça açık olsa da, soru nereye def <name>(<args=defaults>):aittir:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

def nameBölüm olmalıdır tanımlayan kapsamında değerlendirmek - istediğimiz namesonuçta orada kullanılabilir olması. İşlevin sadece kendi içinde değerlendirilmesi, ona erişilemez hale gelir.

Yana parametersürekli bir isimdir, biz aynı anda "değerlendirmek" olabilir def name. Bu, işlevi name(parameter=...):, çıplak yerine bilinen bir imzayla üretme avantajına da sahiptir name(...):.

Şimdi ne zaman değerlendirilir default?

Tutarlılık zaten "tanımda" diyor: diğer her şey def <name>(<args=defaults>):en iyi tanımda değerlendirilir. Parçalarını ertelemek şaşırtıcı bir seçim olacaktır.

İki seçenek de aynı değildir: defaultTanım zamanında değerlendirilirse, yürütme süresini yine de etkileyebilir. Eğer defaultyürütme sırasında değerlendirilir, bu olamaz tanım süresini etkiler. "Tanımda" seçimi, her iki vakanın da ifade edilmesine izin verirken, "yürütmede" seçimi yalnızca birini ifade edebilir:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...

"Tutarlılık zaten" tanımda "diyor: geri kalan her şey def <name>(<args=defaults>):en iyi tanım olarak değerlendirilir." Sonucun öncülden geldiğini sanmıyorum. İki şeyin aynı hatta olması, aynı kapsamda değerlendirilmesi gerektiği anlamına gelmez. defaultsatırın geri kalanından farklı bir şeydir: bir ifade. Bir ifadeyi değerlendirmek, bir işlevi tanımlamaktan çok farklı bir süreçtir.
LarsH

@LarsH Fonksiyon tanımları vardır edilir Python değerlendirilmiştir. Bunun bir deyim ( def) veya expression ( lambda) öğesinden gelip gelmediği bir işlev oluşturmanın, özellikle de imzasının değerlendirilmesi anlamına geldiği anlamına gelmez. Ve varsayılanlar bir işlevin imzasının bir parçasıdır. Yani ortalama varsayılan gelmez sahip derhal değerlendirilecek - tipi ipuçları örneğin olmayabilir. Ama kesinlikle yapmamak için iyi bir neden olmadıkları takdirde kesinlikle önermeliler.
MisterMiyagi

Tamam, bir işlev yaratmak, bir anlamda değerlendirme anlamına gelir, ancak açıkça, içindeki her ifadenin, tanımlandığı zaman değerlendirildiği anlamında değildir. Çoğu değil. Benim için imzanın özellikle tanımlandığı zaman fonksiyon gövdesinin "değerlendirildiğinden" (uygun bir temsile ayrıştırıldığından) daha fazla "değerlendirildiği" açık değildir; oysa işlev gövdesindeki ifadeler tam anlamıyla açıkça değerlendirilmez. Bu açıdan tutarlılık, imzadaki ifadelerin de "tam olarak" değerlendirilmemesi gerektiğini söyler.
LarsH

Yanlış olduğunuzu kastetmiyorum, sadece sonucunuzun yalnızca tutarlılıktan kaynaklanmadığı.
LarsH

@LarsH Varsayılanlar ne vücudun bir parçası ne de tutarlılığın tek kriter olduğunu iddia etmiyorum. Cevabı nasıl netleştireceğinize dair bir öneride bulunabilir misiniz?
MisterMiyagi

3

Diğer her cevap, bunun neden gerçekten hoş ve istenen bir davranış olduğunu veya neden buna ihtiyaç duymamanız gerektiğini açıklar. Benimki, dili, kendi istekleri doğrultusunda bükme haklarını kullanmak isteyen inatçı olanlar içindir, tersi değil.

Bu davranışı, varsayılan değerinde kalan her konum bağımsız değişkeni için aynı örneği yeniden kullanmak yerine varsayılan değeri kopyalayacak bir dekoratörle "düzeltiriz".

import inspect
from copy import copy

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(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Şimdi bu dekoratörü kullanarak işlevimizi yeniden tanımlayalım:

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

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

Bu, özellikle birden fazla argüman alan işlevler için temizdir. Karşılaştırmak:

# 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

ile

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

Anahtar kelime argümanlarını kullanmaya çalıştığınızda yukarıdaki çözümün bozulduğunu unutmamak önemlidir:

foo(a=[4])

Dekoratör buna izin verecek şekilde ayarlanabilir, ancak bunu okuyucu için bir egzersiz olarak bırakıyoruz;)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.