"En Az Şaşkınlık" ve Değişken Temerrüt Argümanı
Python ile yeterince uzun süre uğraşan herkes aşağıdaki sorun nedeniyle ı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]
. Bunun yerine sonuç çok farklı ve çok şaşırtıcı (acemi için):
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
Bir yöneticim bu özellikle ilk kez karşılaşmış ve bunu dilin "dramatik bir tasarım kusuru" olarak adlandırmıştı. Davranışın altında yatan bir açıklaması olduğunu söyledim ve içindekileri anlamamanız gerçekten çok şaşırtıcı ve beklenmedik bir durum. Ancak, şu soruyu yanıtlayamadım (kendi kendime): Varsayılan argümanı işlevin yürütülmesinde değil, işlev tanımında bağlamanın nedeni nedir? Tecrübeli davranışın pratik bir kullanımı olduğundan şüpheliyim (C de statik değişkenleri gerçekten hatalar olmadan kim kullandı?)
Düzenle :
Baczek ilginç bir örnek verdi. Yorumlarınızın çoğu ve özellikle Utaal ile birlikte, daha fazla ayrıntıya girdim:
>>> 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ı parametrelerin kapsamının nereye konulacağına bağlı gibi görünüyor: işlevin içinde mi yoksa onunla birlikte mi?
İşlevin içinde bağlama yapmak, x
işlev çağrıldığında, tanımlanmadığında, belirtilen varsayılana etkin bir şekilde bağlı olduğu anlamına gelir , derin bir kusur sunacak bir şey: def
satır, bağlamanın parçası anlamında "hibrit" olacaktır ( fonksiyon nesnesi) tanımda ve kısımda (varsayılan parametrelerin atanması) fonksiyon çağırma zamanında gerçekleşir.
Gerçek davranış daha tutarlıdır: bu satırın her şeyi, o satır çalıştırıldığında değerlendirilir, yani fonksiyon tanımında.
Yanıtlar
Aslında, bu bir tasarım hatası değildir ve bunun nedeni dahili unsurlar veya performans değildir.
Basitçe, Python'daki işlevlerin birinci sınıf nesneler olması ve yalnızca bir kod parçası olmaması gerçeğinden gelir.
Bu şekilde düşünmeye başladığınız anda, o zaman tamamen mantıklı geliyor: Bir işlev, tanımına göre değerlendirilen bir nesnedir; varsayılan parametreler "üye verileridir" ve bu nedenle durumları bir çağrıdan diğerine değişebilir - tıpkı diğer herhangi bir nesnede olduğu gibi.
Her durumda, Effbot'un Python'daki Varsayılan Parametre Değerlerinde bu davranışın nedenleri hakkında çok güzel bir açıklaması vardır .
Bunu çok net buldum ve işlev nesnelerinin nasıl çalıştığına dair daha iyi bir bilgi için okumayı gerçekten öneririm.
Aşağıdaki koda sahip olduğunuzu varsayalım
fruits = ("apples", "bananas", "loganberries")
def eat(food=fruits):
...
Eat bildirimini gördüğümde, en az şaşırtıcı olan şey, ilk parametre verilmediği takdirde, tuple'a eşit olacağını düşünmektir. ("apples", "bananas", "loganberries")
Ancak, kodun ilerleyen bölümlerinde şöyle bir şey yapıyorum:
def some_random_function():
global fruits
fruits = ("blueberries", "mangos")
o zaman varsayılan parametreler işlev bildirimi yerine işlev yürütmede bağlı olsaydı, meyvelerin değiştiğini keşfetmek (çok kötü bir şekilde) beni şaşırtabilirdi. Bu, foo
yukarıdaki işlevinizin listeyi değiştirdiğini keşfetmekten daha şaşırtıcı bir IMO olurdu .
Gerçek sorun değişken değişkenlerde yatmaktadır ve tüm dillerde bir ölçüde bu sorun vardır. İşte bir soru: Java'da aşağıdaki koda sahip olduğumu varsayalım:
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 StringBuffer
yerleştirildiğinde anahtarın değerini kullanıyor mu yoksa anahtarı referans olarak saklıyor mu? Her iki durumda da biri şaşırır; ya nesneyi Map
içine koyduğu değerle aynı değeri kullanarak çıkarmaya çalışan kişi ya da kullandığı anahtar kelimenin tam anlamıyla aynı nesne olmasına rağmen nesnesini geri getiremeyen kişi haritaya koymak için kullanıldı (aslında bu yüzden Python, değiştirilebilir yerleşik veri türlerinin sözlük anahtarı olarak kullanılmasına izin vermez).
Örneğiniz, Python'a yeni gelenlerin şaşırıp ısırılacağı iyi bir örnektir. Ama bunu "düzeltirsek", bunun sadece ısırılacağı farklı bir durum yaratacağını ve bunun daha da sezgisel olacağını iddia ediyorum. Ayrıca, değişebilir değişkenlerle uğraşırken durum her zaman böyledir; Her zaman birisinin hangi koda bağlı olarak sezgisel olarak bir veya zıt davranış bekleyebileceği durumlarla karşılaşırsınız.
Ben şahsen Python'un şu anki yaklaşımını seviyorum: varsayılan fonksiyon argümanları, fonksiyon tanımlandığında ve bu nesne her zaman varsayılan olduğunda değerlendirilir. Sanırım boş bir liste kullanarak özel durum oluşturabilirlerdi, ancak bu tür bir özel durum, geriye dönük uyumsuz olduğundan bahsetmiyorum bile daha fazla şaşkınlığa neden olur.
Dokümantasyonun ilgili kısmı :
Varsayılan parametre değerleri, fonksiyon tanımı yürütüldüğünde soldan sağa doğru değerlendirilir. Bu, işlev tanımlandığında ifadenin bir kez değerlendirildiği ve her çağrı için aynı "önceden hesaplanmış" değerin kullanıldığı anlamına gelir. Bu, özellikle bir varsayılan parametrenin bir liste veya sözlük gibi değiştirilebilir bir nesne olduğunda anlamak için önemlidir: işlev nesneyi değiştirirse (örneğin bir listeye bir öğe ekleyerek), varsayılan değer aslında değiştirilir. Bu genellikle amaçlanan şey değildir. Bunu aşmanın bir yolu
None
, varsayılan olarak kullanmak ve bunu 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
Python yorumlayıcısının iç işleyişi hakkında hiçbir şey bilmiyorum (ve ben de derleyiciler ve yorumlayıcılar konusunda uzman değilim), bu yüzden mantıksız veya imkansız bir şey önerirsem beni suçlamayın.
Python nesnelerinin değiştirilebilir olması koşuluyla , varsayılan argümanlar malzemelerini tasarlarken bunun dikkate alınması gerektiğini düşünüyorum. Bir liste oluşturduğunuzda:
a = []
tarafından referans verilen yeni bir liste almayı bekliyorsunuz a
.
Niye olsun a=[]
içinde
def x(a=[]):
Çağrıda değil işlev tanımında yeni bir liste başlatılsın mı? 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ı, a
tanımlarken veya yürütürken karşılık gelen tarih saatini varsayılan olarak ayarlamak istiyor x
musunuz? Bu durumda, öncekinde olduğu gibi, varsayılan argüman "atama" işlevin ilk talimatıymış gibi aynı davranışı koruyacağım ( datetime.now()
işlev çağrısında çağrılır). Öte yandan, kullanıcı tanım-zaman haritalamasını isterse şunları yazabilirdi:
b = datetime.datetime.now()
def x(a=b):
Biliyorum, biliyorum: bu bir kapanış. Alternatif olarak Python, tanım zamanı bağlamayı zorlamak için bir anahtar sözcük sağlayabilir:
def x(static a=b):
Nedeni oldukça basittir ki, kod çalıştırıldığında bağlama yapılır ve fonksiyon tanımı çalıştırılır, yani ... fonksiyonlar tanımlandığında.
Şunu 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 özelliğidir ve bu nedenle, ona bir şeyler eklediğinizde, o sınıfın tüm örneklerine eklenir. Sebep tamamen aynı.
Sadece "Nasıl Çalışır?" ve nesneler oluşturulduğunda çalıştırın.
Evet, beklenmedik. Ancak kuruş düştüğünde, Python'un genel olarak çalışma şekline mükemmel bir şekilde uyuyor. Aslında, bu iyi bir öğretim yardımıdır ve bunun neden olduğunu anladığınızda, python'u çok daha iyi sallayacaksınız.
Bununla birlikte, herhangi bir Python eğitiminde belirgin bir şekilde yer alması gerektiğini söyledi. Çünkü sizin de bahsettiğiniz gibi, er ya da geç herkes bu sorunla karşılaşır.
Neden iç gözlem yapmıyorsun?
Ben ediyorum gerçekten Python tarafından sunulan anlayışlı iç gözlem gerçekleştirildi (konuşup hiç kimsenin şaşırttı 2
ve 3
callables üzerine uygulanır).
Aşağıdaki func
gibi tanımlanan basit küçük bir işlev verildiğinde :
>>> def func(a = []):
... a.append(5)
Python onunla karşılaştığında, yapacağı ilk şey code
, bu işlev için bir nesne oluşturmak amacıyla onu derlemek olacaktır . Bu derleme adımı yapılırken, Python *[]
değerini değerlendirir ve ardından varsayılan argümanları ( burada boş bir liste ) işlev nesnesinin kendisinde saklar . En önemli cevapta belirtildiği gibi: liste a
artık işlevin bir üyesi olarak kabul edilebilir func
.
Öyleyse, listenin fonksiyon nesnesi 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 de aynı şey geçerli (kullanım __defaults__
veya func_defaults
Python 2; evet, aynı şey için iki isim).
Yürütme Öncesi İşlev:
>>> def func(a = []):
... a.append(5)
...
Python bu tanımı yürüttükten sonra, belirtilen tüm varsayılan parametreleri ( a = []
burada) alacak ve bunları __defaults__işlev nesnesinin özniteliğinde sıkıştıracaktır (ilgili bölüm: Çağrılabilirler):
>>> func.__defaults__
([],)
Tamam, __defaults__
beklendiği gibi tek giriş olarak boş bir liste .
Yürütme Sonrası İşlev:
Şimdi bu işlevi çalıştıralım:
>>> func()
Şimdi bunlara __defaults__
tekrar bakalım :
>>> func.__defaults__
([5],)
Şaşırdınız mı? Nesnenin içindeki değer değişir! İşleve yapılan ardışık çağrılar artık bu gömülü list
nesneye eklenecektir :
>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)
İşte burada, bu 'kusurun' olmasının nedeni , varsayılan bağımsız değişkenlerin işlev nesnesinin bir parçası olmasıdır. Burada garip bir şey yok, hepsi biraz şaşırtıcı.
Bununla mücadele None
etmenin ortak çözümü , varsayılan olarak kullanmak ve ardından 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 çalıştırıldığı için, herhangi bir argüman iletilmediyse her zaman yeni bir boş liste alırsınız a
.
İçindeki listenin __defaults__
işlevde kullanılanla aynı olduğunu daha fazla doğrulamak func
için işlevinizi işlev gövdesi içinde kullanılan id
listeyi döndürmek için değiştirebilirsiniz a
. Ardından, listeye karşılaştırmak __defaults__
(pozisyon [0]
içinde __defaults__
) ve bunlar gerçekten aynı liste örneğine atıfta nasıl göreceksiniz:
>>> def func(a = []):
... a.append(5)
... return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True
Hepsi iç gözlemin gücüyle!
* 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 çalıştırmayı 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, işlevin oluşturulması ve isme bağlanması işleminden önce çağrılır bar
.
Ç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 kaybedersiniz, ancak yeni başlayanların kafa karışıklığını önlemek için buna değebilir. Bunu yapmanın dezavantajları şunlardır:
1. Performans
def foo(arg=something_expensive_to_compute())):
...
Çağrı zamanı 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. Ya her aramada pahalı bir fiyat ödersiniz ya da değeri harici olarak manuel olarak önbelleğe almanız gerekir, ad alanınızı kirletir ve ayrıntılar eklersiniz.
2. Bağlı parametreleri zorlama
Kullanışlı bir numara, bir lambda'nın 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 i
için çağrı zamanlı tüm döndüğünü fonksiyonların bir listesini alacağı, böylece i değerine 9
.
Aksi takdirde bunu uygulamanın tek yolu, i bağıyla daha fazla kapanış oluşturmaktır, yani:
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
inspect
Modülü kullanarak argümanlar ve varsayılanlar hakkında bilgi alabiliriz.
>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))
Bu bilgiler belge oluşturma, meta programlama, dekoratörler vb. Şeyler için çok kullanışlıdır.
Şimdi, varsayılanların davranışının değiştirilebileceğini varsayalım, bu şuna eşdeğerdir:
_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ı asla ele geçiremeyiz. Yapabileceğimiz en iyi şey, kaynak kodunu saklamak ve bunu bir dize olarak döndürmektir.
Python savunmasında 5 puan
Basitlik : Davranış şu anlamda basittir: Çoğu insan bu tuzağa birkaç kez değil, yalnızca bir kez düşer.
Tutarlılık : Python her zaman isimleri değil nesneleri iletir. Varsayılan parametre, tabii ki, işlev başlığının bir parçasıdır (işlev gövdesi değil). Bu nedenle, işlev çağrısı zamanında değil, modül yükleme zamanında (ve yuvalanmadıkça yalnızca modül yükleme zamanında) değerlendirilmelidir.
Yararlılık : Frederik Lundh'un "Python'daki Varsayılan Parametre Değerleri" açıklamasında belirttiği gibi, mevcut davranış, gelişmiş programlama için oldukça yararlı olabilir. (Tutarlı kullanın.)
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ı tipini bile kullanır. RTFM: Ayrıntılı kılavuzu okuyun.
Meta öğrenme : Tuzağa düşmek aslında çok yardımcı bir andır (en azından derinlemesine düşünen bir öğreniciyseniz), çünkü daha sonra yukarıdaki "Tutarlılık" konusunu daha iyi anlayacaksınız ve bu size Python hakkında çok şey öğretecek.
Bu davranış şu şekilde açıklanabilir:
- işlev (sınıf vb.) bildirimi yalnızca bir kez yürütülür ve tüm varsayılan değer nesnelerini oluşturur
- her şey referansla aktarılır
Yani:
def x(a=0, b=[], c=[], d=0):
a = a + 1
b = b + [1]
c.append(1)
print a, b, c
a
değişmez - her atama çağrısı yeni int nesnesi oluşturur - yeni nesne yazdırılırb
değişmez - yeni dizi varsayılan değerden oluşturulur ve yazdırılırc
değişiklikler - işlem aynı nesne üzerinde gerçekleştirilir - ve yazdırılır
Sorduğun şey bunun nedeni:
def func(a=[], b = 2):
pass
dahili olarak buna eşdeğer değildir:
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 func (Yok, Yok) özelliğinin açıkça çağrılması durumu dışında.
Başka bir deyişle, varsayılan parametreleri değerlendirmek yerine, neden her birini saklamayalım ve işlev çağrıldığında bunları değerlendirmeyelim?
Bir cevap muhtemelen tam oradadır - varsayılan parametrelere sahip her işlevi etkin bir şekilde bir kapanışa dönüştürecektir. Hepsi yorumlayıcıda gizlenmiş olsa ve tam anlamıyla kapanmamış olsa bile, verilerin bir yerde saklanması gerekir. Daha yavaş olur ve daha fazla hafıza kullanır.
1) "Değişken Temerrüt Argümanı" olarak adlandırılan problem, genel olarak şunu gösteren özel bir örnektir:
"Bu probleme sahip tüm fonksiyonlar , gerçek parametrede benzer yan etki probleminden de muzdariptir ,"
Bu, fonksiyonel programlama kurallarına aykırıdır genellikle önlenemez ve her ikisi birlikte düzeltilmelidir.
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
Kesinlikle güvenli bir çözüm, önce copy
veya deepcopy
girdi nesnesine ve sonra kopya ile ne yaparsa yapsın.
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 tarafından copy.copy(any_object)
veya daha kapsamlı bir şekilde kopyalanabilir copy.deepcopy()
(ikincisi, değiştirilebilir nesne değiştirilebilir nesnelerden oluşuyorsa yararlıdır). Bazı nesneler temelde "dosya" nesnesi gibi yan etkilere dayanır ve kopyalanarak 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 herhangi bir genel özniteliğine kaydedilmemelidir . ( Örneğin özel özniteliklerinin bu sınıfın veya alt sınıfların dışından değiştirilmemesi gerektiğini varsayarsak, yani _var1
bir özel özniteliktir)
Sonuç:
Girdi parametreleri nesneleri yerinde değiştirilmemeli (değiştirilmemeli) veya işlev tarafından döndürülen bir nesneye bağlanmamalıdır. (Eğer yan etkiler olmadan programlamayı tercih edersek ki bu şiddetle tavsiye edilir. "Yan etki" ile ilgili Wiki'ye bakın (İlk iki paragraf bu bağlamda önemlidir.)
2)
Yalnızca gerçek parametre üzerindeki yan etki gerekliyse, ancak varsayılan parametre üzerinde istenmeyen bir etki söz konusu ise, o zaman yararlı çözüm def ...(var1=None):
if var1 is None:
var1 = []
Diğer'dir ..
3) Bazı durumlarda, varsayılan parametrelerin değiştirilebilir davranışı yararlıdır .
Bunun aslında varsayılan değerlerle hiçbir ilgisi yoktur, bunun dışında, değişken varsayılan değerlere sahip işlevler yazdığınızda genellikle beklenmeyen 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ğerler yok, ancak tamamen aynı sorunu yaşıyorsunuz.
Sorun olduğunu foo
edilir değiştirerek arayan bu beklemediğini zaman, arayanın gelen geçirilen bir değişken değişken. İşleve şöyle bir şey çağrılsaydı, böyle bir kod iyi olurdu append_5
; daha sonra arayan, ilettikleri değeri değiştirmek için işlevi çağırır ve davranış beklenir. Ancak böyle bir işlevin varsayılan bir argüman alması pek olası değildir ve muhtemelen listeyi döndürmez (çünkü arayan kişi bu listeye zaten bir referanstır; az önce gönderdiği).
foo
Varsayılan bir bağımsız değişkene sahip orijinaliniz , a
açıkça aktarılıp aktarılmadığını veya varsayılan değeri alıp almadığını değiştirmemelidir . Bağlamdan / addan / belgeden bağımsız değişkenlerin değiştirilmesinin beklendiği açık olmadığı sürece kodunuz değişebilir bağımsız değişkenleri bırakmalıdır. Değişken değerleri argümanlar olarak yerel geçiciler olarak kullanmak, Python'da olsak da olmasak da ve varsayılan argümanlar olup olmadığına bakılmaksızın son derece kötü bir fikirdir.
Bir şeyi hesaplama sırasında yerel bir geçiciyi yıkıcı bir şekilde manipüle etmeniz gerekiyorsa ve manipülasyonunuzu bir argüman değerinden başlatmanız gerekiyorsa, bir kopya oluşturmanız gerekir.
Zaten meşgul bir konu, ancak burada okuduğumdan, 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
Bu bir performans optimizasyonu. Bu işlevselliğin bir sonucu olarak, 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 işlemi (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
Tecrübeli davranışın pratik bir kullanımı olduğundan şüpheliyim (C de statik değişkenleri gerçekten hatalar olmadan kim kullandı?)
Gördüğünüz gibi, orada olan değişmez varsayılan argümanlar kullanırken bir performans yararı. Bu, sık olarak adlandırılan bir işlevse veya varsayılan bağımsız değişkenin oluşturulması uzun sürüyorsa bir fark yaratabilir. Ayrıca, Python'un C olmadığını aklınızda bulundurun. C'de hemen hemen bedava sabitler vardır. Python'da bu avantaja sahip değilsiniz.
Python: Değişken Varsayılan Argüman
Varsayılan argümanlar, fonksiyon bir fonksiyon nesnesine derlendiğinde değerlendirilir. İşlev tarafından birden çok kez kullanıldığında, bunlar aynı nesnedir ve kalırlar.
Değiştirilebilir olduklarında, mutasyona uğradıklarında (örneğin, ona bir eleman ekleyerek), ardışık çağrılarda mutasyona uğramış kalırlar.
Her seferinde aynı nesne oldukları için mutasyona uğramış kalırlar.
Eşdeğer kod:
İşlev nesnesi derlendiğinde ve başlatı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 tam olarak buna eşdeğerdir:
_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österi - her başvuruda bulunduklarında aynı nesne olduklarını doğrulayabilirsiniz.
- listenin, fonksiyon bir fonksiyon nesnesine derlemeyi bitirmeden önce oluşturulduğunu görmek,
- listeye her başvurulduğunda id'nin aynı olduğunu gözlemleyerek,
- onu kullanan işlev 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ılıyor 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 Az Ş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 bir hale gelir.
Yeni Python kullanıcılarına olağan talimat:
Ancak bu nedenle, yeni kullanıcılara her zamanki talimat, bunun yerine varsayılan argümanlarını 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 bir sentinel nesne olarak Yok tekli kullanır. Eğer bir argüman almazsak, aslında []
varsayılan olarak yeni bir boş liste kullanmak isteriz .
Kontrol akışıyla ilgili eğitim bölümünde söylendiği gibi:
Varsayılanın sonraki aramalar arasında paylaşılmasını istemiyorsanız, bunun yerine işlevi şu şekilde yazabilirsiniz:
def f(a, L=None): if L is None: L = [] L.append(a) return L
En kısa cevap muhtemelen "tanım yürütmedir" olacaktır, bu nedenle tüm argüman kesin bir anlam ifade etmiyor. Daha uydurma bir örnek olarak, şundan alıntı yapabilirsiniz:
def a(): return []
def b(x=a()):
print x
Öntanımlı argüman ifadelerini ifadenin çalıştırma anında çalıştırılmamasının def
kolay olmadığını veya mantıklı olmadığını veya her ikisini birden göstermenin yeterli olacağını umuyoruz .
Yine de, varsayılan kurucuları kullanmaya çalıştığınızda bunun bir aldatmaca olduğuna katılıyorum.
Aşağıdakileri dikkate alırsanız bu davranış şaşırtıcı değildir:
- Atama girişimleri üzerine salt okunur sınıf özniteliklerinin davranışı ve
- Fonksiyonlar nesnelerdir (kabul edilen cevapta iyi açıklanmıştır).
(2) ' nin rolü bu ileti dizisinde kapsamlı bir şekilde ele alınmıştır. (1) muhtemelen şaşkınlık yaratan faktördür, çünkü bu davranış diğer dillerden geldiğinde "sezgisel" değildir.
(1) Python derslerinde sınıflarla ilgili olarak açıklanmıştır . Salt okunur bir sınıf özniteliğine bir değer atama girişiminde:
... en içteki 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 yaratır ve aynı adlandırılmış dış değişkeni değiştirmeden bırakır ).
Orijinal örneğe dönüp yukarıdaki noktaları göz önünde bulundurun:
def foo(a=[]):
a.append(5)
return a
Burada foo
bir nesne ve a
bir özniteliği foo
(mevcut foo.func_defs[0]
). Yana a
bir listedir, a
değişken ve böylece bir okuma-yazma özelliğidir foo
. İşlev başlatıldığında imza tarafından belirtildiği gibi boş listeye başlatılır ve işlev nesnesi var olduğu sürece okuma ve yazma için kullanılabilir.
foo
Bir varsayılanı geçersiz kılmadan çağrı yapmak o 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
. Nesnenin bir parçası olan ve kodun yürütülmesi arasında devam eden değişiklik a
değişiklikleri .foo.func_defs[0]
foo
foo
Şimdi, bunu diğer dillerin varsayılan bağımsız değişken davranışını taklit etme konusundaki dokümantasyondaki örnekle karşılaştırın , öyle ki işlev imza varsayılanları işlev her çalıştırıldığında kullanılır:
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
foo
fonksiyonu nesne örneği,foo.func_defs[0]
ayarlanırNone
değişken olmayan bir obje. - İşlev varsayılanlarla çalıştırıldığında (
L
işlev çağrısında herhangi bir parametre belirtilmeden ),foo.func_defs[0]
(None
) yerel kapsamda olarak kullanılabilirL
. - Bundan sonra
L = []
, atama başarılı olamazfoo.func_defs[0]
, çünkü bu öznitelik salt okunurdur. - Per (1) 'e göre , yerel kapsamda ayrıca adlandırılan yeni bir yerel değişken
L
yaratılır ve işlev çağrısının geri kalanı için kullanılır.foo.func_defs[0]
bu nedenle gelecekteki çağrılarda değişmeden kalırfoo
.
Yok kullanarak basit bir çö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]
Bir işleve varsayılan bir liste değeri geçirmek için alternatif bir yapı göstereceğim (sözlüklerle eşit derecede iyi çalışır).
Başkalarının kapsamlı bir şekilde yorumladığı gibi, liste parametresi, çalıştırıldığı zamanın aksine tanımlandığında işleve bağlıdır. Listeler ve sözlükler değiştirilebilir olduğundan, bu parametreye yapılacak herhangi bir değişiklik, bu işleve yönelik diğer çağrıları etkileyecektir. Sonuç olarak, işleve sonraki çağrılar, işleve yapılan diğer çağrılarla değiştirilmiş olabilecek bu paylaşılan listeyi alacaktır. Daha da kötüsü, iki parametre bu işlevin paylaşılan parametresini aynı anda kullanıyor ve diğerinin yaptığı değişikliklerden habersiz.
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
Şunları kullanarak tek ve aynı nesne olduklarını doğrulayabilirsiniz id
:
>>> id(a)
5347866528
>>> id(b)
5347866528
Brett Slatkin'in "Etkili Python: Daha İyi Python Yazmanın 59 Spesifik Yolu" na göre, Madde 20: None
Dinamik varsayılan argümanları belirtmek için Kullanım ve Dokümanlar (s. 48)
Python'da istenen sonucu elde etmenin kuralı, varsayılan bir değer sağlamak
None
ve docstring'deki gerçek davranışı belgelemektir.
Bu uygulama, işleve yapılan her çağrının ya varsayılan listeyi almasını ya da 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]
Programcının varsayılan liste parametresinin paylaşılmasını amaçladığı 'Yanlış Yöntem' için meşru kullanım durumları olabilir, ancak bu, kuraldan daha büyük olasılıkla istisnadır.
Buradaki çözümler:
None
Varsayılan değeriniz (veya nonceobject
) olarak kullanın ve çalışma zamanında değerlerinizi oluşturmak için bunu açın; veyalambda
Varsayılan parametreniz olarak a kullanın ve varsayılan değeri elde etmek için bunu bir try bloğu içinde çağırın (bu, lambda soyutlamasının gerektirdiği türden bir şeydir).
İkinci seçenek güzeldir çünkü işlevin kullanıcıları, zaten var olan bir çağrılabilir (örneğin a type
)
Nesneyi değiştirerek (ve dolayısıyla kapsamla bağ kurarak) bunu aşabilirsiniz:
def foo(a=[]):
a = list(a)
a.append(5)
return a
Çirkin, ama işe yarıyor.
Bunu yaptığımızda:
def foo(a=[]):
...
... biz argüman atamak a
bir karşı isimsiz Arayan değerini geçmez ise, listede.
Bu tartışmada işleri daha basit hale getirmek için, adsız listeye geçici olarak bir ad verelim. Nasıl olur pavlo
?
def foo(a=pavlo):
...
Herhangi bir zamanda, arayan bize ne a
olduğunu söylemezse , yeniden kullanırız pavlo
.
Eğer pavlo
değiştirilebilir (değiştirilebilir) ise ve foo
onu değiştirmeyi bitirirse, bir dahaki sefere fark ettiğimiz bir efekt foo
belirtilmeden çağrılır a
.
İşte gördüğünüz şey bu (Unutmayın, pavlo
[] olarak başlatılmıştır):
>>> foo()
[5]
Şimdi, pavlo
[5].
foo()
Tekrar arama tekrar değiştirir pavlo
:
>>> foo()
[5, 5]
Belirtme a
çağıran foo()
sağlar pavlo
dokunulmaz.
>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]
Yani, pavlo
hala [5, 5]
.
>>> foo()
[5, 5, 5]
Bazen bu davranışı aşağıdaki modele alternatif olarak kullanıyorum:
singleton = None
def use_singleton():
global singleton
if singleton is None:
singleton = _make_singleton()
return singleton.use_me()
Eğer singleton
sadece 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 hafızaya alma için dikteler veya listeler oluşturmak için kullandım.
Bu modelin iyi bilindiğini düşünmediğim için, gelecekteki yanlış anlaşılmalara karşı korunmak için kısa bir yorum yapıyorum.
Doğru olabilir:
- Birisi her dili / kitaplık özelliğini kullanıyor ve
- Buradaki davranışı değiştirmek yanlış olur, ancak
Yukarıdaki her iki özelliğe sahip olmak ve yine de başka bir noktaya değinmek tamamen tutarlıdır:
- Bu kafa karıştırıcı bir özelliktir ve Python'da talihsizdir.
Diğer cevaplar, ya da en azından bazıları ya 1. ve 2. noktaları yapar ama 3. noktayı koymaz ya da 3. noktayı yapar ve 1. ve 2. noktaları önemsiz gösterir. Ancak üçü de doğru.
Burada ortadaki atları değiştirmenin önemli bir kırılma gerektireceği ve Python'u Stefano'nun açılış pasajını sezgisel olarak işleyecek şekilde değiştirerek daha fazla sorun yaratılabileceği doğru olabilir. Ve Python'un iç kısımlarını iyi bilen birinin bir mayın tarlasının sonuçlarını 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 bu kadar yakın herhangi bir yerde en az şaşkınlık ilkesini ihlal eder . Onu kökünden söküp atmanın akıllıca olup olmayacağı gerçek bir sorundur. Bu bir tasarım hatasıdır. Davranışın izini sürmeye çalışarak dili çok daha iyi anlıyorsanı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. Ancak bu Pythonic değil: Python'u bu davranış karşısında sebat edecek kadar önemseyen insanlar dile ilgi duyan insanlardır çünkü Python diğer dillerden çok daha az sürpriz yaşar. Dabbler ve meraklılar, bir şeyi çalıştırmanın ne kadar kısa sürdüğünü görünce şaşırdıklarında Pythonist olurlar - bir tasarım nedeniyle değil - yani gizli mantık bulmacası - Python'a ilgi duyan programcıların sezgilerine aykırıdır. çünkü Sadece Çalışıyor .
Bu bir tasarım hatası değil . Buna takılıp kalan herkes yanlış bir şey yapıyor.
Bu problemle nerede karşılaşabileceğinizi gördüğüm 3 durum var:
- Fonksiyonun bir yan etkisi olarak argümanı değiştirmeyi planlıyorsunuz. Bu durumda varsayılan bir argümana sahip olmak hiçbir zaman mantıklı değildir. Bunun tek istisnası, bağımsız değişken listesini örneğin işlev özniteliklerine sahip olmak için kötüye kullandığınız zamandır
cache={}
ve işlevi gerçek bir bağımsız değişkenle çağırmanız beklenmez. - Sen değiştirilmemiş argüman bırakmak niyetinde, ancak yanlışlıkla vermedi değiştirmek. Bu bir hata, düzeltin.
- İşlevin içinde kullanım için bağımsız değişkeni değiştirmeyi planlıyorsunuz, ancak değişikliğin işlevin dışında görüntülenmesini beklemiyorsunuz. Bu durumda , varsayılan olsun ya da olmasın, argümanın bir kopyasını almanız gerekir ! Python, değer bazında bir çağrı dili değildir, bu yüzden sizin için bir kopya oluşturmaz, bu konuda açık olmanız gerekir.
Sorudaki örnek kategori 1 veya 3'e girebilir. Hem geçirilen listeyi değiştirmesi hem de onu döndürmesi tuhaftır; birini veya diğerini seçmelisiniz.
Bu "hata" bana çok fazla mesai saati verdi! Ama bunun potansiyel bir kullanımını görmeye başlıyorum (ama yine de yürütme zamanında olmasını isterdim)
Size yararlı bir örnek olarak gördüğüm şeyi 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
Sadece işlevi şu şekilde değiştirin:
def notastonishinganymore(a = []):
'''The name is just a joke :)'''
a = a[:]
a.append(5)
return a
Sanırım bu sorunun cevabı, python'un verileri parametreye nasıl geçirdiğinde (değere göre veya referansa göre), değişkenliğe veya python'un "def" ifadesini nasıl işlediğine değil.
Kısa bir giriş. Birincisi, python'da iki tür veri türü vardır, biri sayılar gibi basit temel veri türü ve diğer veri türü nesnelerdir. İkincisi, verileri parametrelere aktarırken, python değere göre temel veri türünü geçirir, yani değerin yerel bir değişkene yerel bir kopyasını oluşturur, ancak nesneyi referansla, yani nesneye işaretçilerle iletir.
Yukarıdaki iki noktayı kabul ederek python koduna ne olduğunu açıklayalım. Bu sadece nesneler için referans olarak geçme nedeniyledir, ancak değişken / değişmez ile hiçbir ilgisi yoktur veya muhtemelen "def" ifadesinin tanımlandığında yalnızca bir kez çalıştırılması gerçeğidir.
[] bir nesnedir, bu yüzden python [] 'nin referansını iletir a
, yani a
sadece bellekte bir nesne olarak bulunan []' a bir göstericidir. Ancak [] 'nın birçok referansı olan tek bir kopyası var. İlk foo () için, [] listesi ekleme yöntemi ile 1 olarak değiştirilir . Ancak liste nesnesinin yalnızca bir kopyası olduğuna ve bu nesnenin artık 1 olduğuna dikkat edin . İ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, referansla geçmenin etkisidir! Foo (3) 'ün sonucu da aynı şekilde kolaylıkla türetilebilir.
Cevabımı daha fazla doğrulamak için, iki ek koda bir göz atalım.
====== Hayır. 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 nesnedir, öyledir None
(ilki değişebilirken ikincisi değişmezdir. Ancak değişebilirliğin soruyla hiçbir ilgisi yoktur). Hiçbiri uzayda bir yerde değil ama orada olduğunu biliyoruz ve orada Yok'un yalnızca bir kopyası var. Bu nedenle, foo her çağrıldığında, öğeler Yok olarak değerlendirilir (yalnızca bir kez değerlendirilen bazı yanıtların aksine), açık olması için Yok'un referansı (veya adresi). Daha sonra foo'da öğe [] olarak değiştirilir, yani farklı bir adresi olan başka bir nesneyi işaret eder.
====== Hayır.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) 'in çağrılması, öğelerin 11111111 adresli bir liste nesnesine [] işaret etmesini sağlar. Listenin içeriği devam dizisindeki foo işlevinde 1 olarak değiştirilir , ancak adres değişmez, yine de 11111111 Sonra foo (2, []) geliyor. Foo (2, []) içindeki [], foo (1) çağrılırken [] varsayılan parametresi ile aynı içeriğe sahip olsa da, adresleri farklıdır! Parametreyi açıkça sağladığımız için items
, bu yeninin adresini []
, diyelim ki 2222222'yi almalı ve bazı değişiklikler yaptıktan sonra onu iade etmeliyiz. Şimdi foo (3) çalıştırılır. yalnızca x
sağlandığından, öğelerin varsayılan değerini yeniden alması gerekir. Varsayılan değer nedir? Foo işlevi tanımlanırken ayarlanır: 11111111'de bulunan liste nesnesi. Dolayısıyla, öğeler, bir öğesi 1'e sahip 11111111 adresi olarak değerlendirilir. 2222222'de bulunan liste ayrıca bir öğe 2 içerir, ancak öğelerle işaret edilmemiştir. Daha. Sonuç olarak, 3'ün bir eki items
[1,3] yapacaktır .
Yukarıdaki açıklamalardan, kabul edilen cevapta önerilen effbot web sayfasının bu soruya uygun bir cevap vermediğini görebiliyoruz. Dahası, effbot web sayfasındaki bir noktanın yanlış olduğunu düşünüyorum. UI.Button ile ilgili kodun 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ı bir değeri görüntüleyecek farklı bir geri arama işlevi tutabilir i
. Bunu göstermek için bir örnek verebilirim:
x=[]
for i in range(10):
def callback():
print(i)
x.append(callback)
Eğer çalıştırırsak x[7]()
beklendiği gibi 7'yi elde ederiz ve x[9]()
9'un başka bir değerini veririz i
.
TLDR: Define-time varsayılanları tutarlıdır ve kesinlikle daha açıklayıcıdır.
Bir işlevin tanımlanması iki kapsamı etkiler: işlevi içeren tanımlayıcı kapsam ve işlevin içerdiği yürütme kapsamı . Kapsamlara haritayı nasıl blokladığı oldukça açık olsa da, soru nereye def <name>(<args=defaults>):
ait olduğudur:
... # defining scope
def name(parameter=default): # ???
... # execution scope
def name
Bölüm olmalıdır tanımlayan kapsamında değerlendirmek - istediğimiz name
sonuçta orada kullanılabilir olması. İşlevi yalnızca kendi içinde değerlendirmek, onu erişilemez kılacaktır.
Yana parameter
sürekli bir isimdir, biz aynı anda "değerlendirmek" olabilir def name
. Bu aynı zamanda işlevi name(parameter=...):
çıplak yerine bilinen bir imzayla üretme avantajına da sahiptir name(...):
.
Şimdi, ne zaman değerlendirilmeli default
?
Tutarlılık zaten "tanımda" diyor: diğer her şey def <name>(<args=defaults>):
de en iyi tanımda değerlendirilir. Bazı kısımlarını ertelemek şaşırtıcı bir seçim olacaktır.
İki seçenek de eşdeğer değildir: default
Tanım zamanında değerlendirilirse, yine de yürütme süresini etkileyebilir. Eğer default
yürütme sırasında değerlendirilir, bu olamaz tanım süresini etkiler. "Tanımda" seçeneğinin seçilmesi, her iki durumun da ifade edilmesine izin verirken, "yürütme sırasında" seçeneği 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
...
Diğer yanıtların her biri, bunun neden gerçekten hoş ve istenen bir davranış olduğunu ya da neden buna ihtiyaç duymamanız gerektiğini açıklıyor. Benimki, dili kendi iradesine göre bükme hakkını kullanmak isteyenler için, tersi değil.
Bu davranışı, varsayılan değerinde bırakılan her konumsal argüman için aynı örneği yeniden kullanmak yerine varsayılan değeri kopyalayacak bir dekoratörle "düzelteceğiz".
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 çok argüman alan işlevler için düzgündür. 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 değiştirgeleri kullanmaya çalışırsanız yukarıdaki çözümün bozulacağını unutmamak önemlidir, örneğin:
foo(a=[4])
Dekoratör buna izin verecek şekilde ayarlanabilir, ancak bunu okuyucu için bir alıştırma olarak bırakıyoruz;)