Değişken işlev bağımsız değişkeni varsayılan değerleri için iyi kullanımlar?


84

Python'da, değişken bir nesneyi bir işlevdeki bir argümanın varsayılan değeri olarak ayarlamak yaygın bir hatadır. İşte David Goodger'ın bu mükemmel yazısından bir örnek :

>>> def bad_append(new_item, a_list=[]):
        a_list.append(new_item)
        return a_list
>>> print bad_append('one')
['one']
>>> print bad_append('two')
['one', 'two']

Bunun neden olduğunun açıklaması burada .

Ve şimdi sorum için: Bu sözdizimi için iyi bir kullanım durumu var mı?

Demek istediğim, onunla karşılaşan herkes aynı hatayı yaparsa, hatayı çözerse, sorunu anlarsa ve bundan kaçınmaya çalışırsa, böyle bir sözdiziminin ne faydası var?


1
Bunun için bildiğim en iyi açıklama bağlantılı soruda: işlevler, tıpkı sınıflar gibi birinci sınıf nesnelerdir. Sınıfların değişken nitelik verileri vardır; işlevler değiştirilebilir varsayılan değerlere sahiptir.
Katriel

10
Bu davranış bir "tasarım seçimi" değildir - dilin çalışma şeklinin bir sonucudur - mümkün olduğunca az istisna dışında basit çalışma ilkelerinden başlayarak. Benim için bir noktada, "Python'da düşünmeye" başladığımda bu davranış doğal hale geldi - ve gerçekleşmezse
şaşırırdım

2
Bunu ben de merak ettim. Bu örnek web'in her yerinde, ancak bir anlam ifade etmiyor - ya geçirilen listeyi değiştirmek istiyorsun ve bir varsayılana sahip olmak bir anlam ifade etmiyor ya da yeni bir liste döndürmek istiyorsun ve hemen bir kopyasını almalısın işleve girdikten sonra. Her ikisini de yapmanın yararlı olduğu bir durumu hayal edemiyorum.
Mark Ransom


2
Yukarıda şikayet ettiğim sorunu olmayan daha gerçekçi bir örnekle karşılaştım. Varsayılan, __init__bir örnek değişkenine ayarlanan bir sınıf için işlevin argümanıdır ; bu, yapmak istemek için tamamen geçerli bir şey ve değişken bir varsayılanla her şey korkunç derecede yanlış gidiyor. stackoverflow.com/questions/43768055/…
Mark Ransom

Yanıtlar:


61

İşlev çağrıları arasında değerleri önbelleğe almak için kullanabilirsiniz:

def get_from_cache(name, cache={}):
    if name in cache: return cache[name]
    cache[name] = result = expensive_calculation()
    return result

ancak genellikle bu tür şeyler bir sınıfta daha iyi yapılır, çünkü önbelleği temizlemek için ek özelliklere sahip olabilirsiniz.


12
... ya da hatırlatıcı bir dekoratör.
Daniel Roseman

29
@functools.lru_cache(maxsize=None)
Katriel

3
@katrielalex lru_cache Python 3.2'de yenidir, bu yüzden herkes onu kullanamaz.
Duncan

2
Bilginize artık backports.functools_lru_cache pypi.python.org/pypi/backports.functools_lru_cache
Panda

1
lru_cachekarma değerlere sahipseniz kullanılamaz.
Synedraacus

14

Kanonik cevap şu sayfadır: http://effbot.org/zone/default-values.htm

Ayrıca, değiştirilebilir varsayılan argüman için 3 "iyi" kullanım durumundan da bahsedilmektedir:

  • bir geri aramada yerel değişkeni dış değişkenin geçerli değerine bağlama
  • önbellek / not alma
  • küresel adların yerel olarak yeniden bağlanması (yüksek düzeyde optimize edilmiş kod için)

12

Belki değişebilir argümanı değiştirmiyorsunuz, ancak değişebilir bir argüman bekliyorsunuz:

def foo(x, y, config={}):
    my_config = {'debug': True, 'verbose': False}
    my_config.update(config)
    return bar(x, my_config) + baz(y, my_config)

(Evet, config=()bu özel durumda kullanabileceğinizi biliyorum , ancak bunu daha az açık ve daha az genel buluyorum.)


3
Ayrıca, değiştirmediğinizden ve bu varsayılan değeri doğrudan işlevden döndürmediğinizden emin olun , aksi takdirde işlevin dışındaki bazı kodlar onu değiştirebilir ve tüm işlev çağrılarını etkiler.
Andrey Semakin

11
import random

def ten_random_numbers(rng=random):
    return [rng.random() for i in xrange(10)]

Kullanımları randomvarsayılan rasgele sayı üreteci olarak modül etkin bir değişken singleton.


7
Ancak bu da çok önemli bir kullanım durumu değil.
Evgeni Sergeev

3
Python'un "bir kez referans al" ve Python olmayan " randomişlev çağrısı başına bir kez arama" arasında davranış açısından hiçbir fark olmadığını düşünüyorum . Her ikisi de aynı nesneyi kullanır.
nyanpasu64

4

DÜZENLEME (açıklama): Değişebilir varsayılan bağımsız değişken sorunu, daha derin bir tasarım seçiminin bir belirtisidir, yani varsayılan bağımsız değişken değerlerinin işlev nesnesinde öznitelikler olarak saklanmasıdır. Bu seçimin neden yapıldığını sorabilirsiniz; her zaman olduğu gibi, bu tür soruları doğru cevaplamak zordur. Ama kesinlikle iyi kullanımları var:

Performans için optimize etme:

def foo(sin=math.sin): ...

Değişken yerine bir kapanışta nesne değerlerini yakalama.

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

7
tamsayılar ve yerleşik işlevler değiştirilebilir değildir!
12'de Monica'yı

2
@Jonathan: Kalan örnekte hala değişken bir varsayılan argüman yok, yoksa onu göremiyor muyum?
Monica'yı

2
@Jonathan: Benim açımdan bunların değiştirilebilir olması değil. Python'un varsayılan argümanları saklamak için kullandığı sistem - derleme zamanında tanımlanan fonksiyon nesnesinde - yararlı olabilir. Bu, değişken varsayılan bağımsız değişken sorununu ifade eder, çünkü her işlev çağrısında bağımsız değişkeni yeniden değerlendirmek hileyi işe yaramaz hale getirecektir.
Katriel

2
@katriealex: Tamam, ama lütfen cevabınızda argümanların yeniden değerlendirilmesi gerektiğini varsaydığınızı ve bunun neden kötü olduğunu gösterdiğinizi söyleyin. Nit-pick: varsayılan argüman değerleri derleme zamanında depolanmaz, ancak işlev tanımı ifadesi çalıştırıldığında saklanır.
12'de Monica'yı

@WolframH: doğru: P! İkisi genellikle çakışsa da.
Katriel

0

Bunun eski bir şey olduğunu biliyorum, ancak sadece bu konuya bir kullanım durumu eklemek istiyorum. TensorFlow / Keras için düzenli olarak özel işlevler ve katmanlar yazıyorum, komut dosyalarımı bir sunucuya yüklüyorum, modelleri orada eğitiyorum (özel nesnelerle) ve ardından modelleri kaydedip indiriyorum. Bu modelleri yüklemek için, tüm bu özel nesneleri içeren bir sözlük sağlamam gerekiyor.

Benimki gibi durumlarda yapabilecekleriniz, bu özel nesneleri içeren modüle bazı kodlar eklemektir:

custom_objects = {}

def custom_object(obj, storage=custom_objects):
    storage[obj.__name__] = obj
    return obj

Ardından, sözlükte olması gereken herhangi bir sınıfı / işlevi dekore edebilirim

@custom_object
def some_function(x):
    return 3*x*x + 2*x - 2

Dahası, özel kayıp işlevlerimi özel Keras katmanlarımdan farklı bir sözlükte saklamak istediğimi varsayalım. Functools.partial kullanmak yeni bir dekoratöre kolay erişim sağlıyor

import functools
import tf

custom_losses = {}
custom_loss = functools.partial(custom_object, storage=custom_losses)

@custom_loss
def my_loss(y, y_pred):
    return tf.reduce_mean(tf.square(y - y_pred))

-1

Değişken varsayılan argüman değerlerinin iyi kullanımları sorusuna yanıt olarak, aşağıdaki örneği sunuyorum:

Değiştirilebilir bir varsayılan, kendi oluşturduğunuz, kullanımı kolay, içe aktarılabilir komutları programlamak için yararlı olabilir. Değişebilir varsayılan yöntem, bir işlevde ilk çağrıda (bir sınıfa çok benzer), ancak globallere başvurmak zorunda kalmadan, bir sarmalayıcı kullanmak zorunda kalmadan ve bir içe aktarılan sınıf nesnesi. Kendi yolunda zarif, umarım kabul edersiniz.

Şu iki örneği düşünün:

def dittle(cache = []):

    from time import sleep # Not needed except as an example.

    # dittle's internal cache list has this format: cache[string, counter]
    # Any argument passed to dittle() that violates this format is invalid.
    # (The string is pure storage, but the counter is used by dittle.)

     # -- Error Trap --
    if type(cache) != list or cache !=[] and (len(cache) == 2 and type(cache[1]) != int):
        print(" User called dittle("+repr(cache)+").\n >> Warning: dittle() takes no arguments, so this call is ignored.\n")
        return

    # -- Initialize Function. (Executes on first call only.) --
    if not cache:
        print("\n cache =",cache)
        print(" Initializing private mutable static cache. Runs only on First Call!")
        cache.append("Hello World!")
        cache.append(0)
        print(" cache =",cache,end="\n\n")
    # -- Normal Operation --
    cache[1]+=1 # Static cycle count.
    outstr = " dittle() called "+str(cache[1])+" times."
    if cache[1] == 1:outstr=outstr.replace("s.",".")
    print(outstr)
    print(" Internal cache held string = '"+cache[0]+"'")
    print()
    if cache[1] == 3:
        print(" Let's rest for a moment.")
        sleep(2.0) # Since we imported it, we might as well use it.
        print(" Wheew! Ready to continue.\n")
        sleep(1.0)
    elif cache[1] == 4:
        cache[0] = "It's Good to be Alive!" # Let's change the private message.

# =================== MAIN ======================        
if __name__ == "__main__":

    for cnt in range(2):dittle() # Calls can be loop-driven, but they need not be.

    print(" Attempting to pass an list to dittle()")
    dittle([" BAD","Data"])
    
    print(" Attempting to pass a non-list to dittle()")
    dittle("hi")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the private mutable value from the outside.")
    # Even an insider's attempt to feed a valid format will be accepted
    # for the one call only, and is then is discarded when it goes out
    # of scope. It fails to interrupt normal operation.
    dittle([" I am a Grieffer!\n (Notice this change will not stick!)",-7]) 
    
    print(" Calling dittle() normally once again.")
    dittle()
    dittle()

Bu kodu çalıştırırsanız, dittle () işlevinin ilk çağrıda içselleştirildiğini, ancak ek çağrılarda olmadığını göreceksiniz, çağrılar arasında dahili statik depolama için özel bir statik önbellek (değiştirilebilir varsayılan) kullanır, kaçırma girişimlerini reddeder. statik depolama, kötü amaçlı girdilere karşı dayanıklıdır ve dinamik koşullara göre hareket edebilir (burada işlevin kaç kez çağrıldığına göre).

Değişken varsayılanları kullanmanın anahtarı, değişkeni bellekte yeniden atayacak hiçbir şey yapmamak, değişkeni her zaman yerinde değiştirmektir.

Bu tekniğin potansiyel gücünü ve kullanışlılığını gerçekten görmek için, bu ilk programı "DITTLE.py" adı altında mevcut dizininize kaydedin ve ardından sonraki programı çalıştırın. Yeni dittle () komutumuzu hatırlamak için herhangi bir adım gerektirmeden veya çemberleri atlamak için programlamadan alır ve kullanır.

İşte ikinci örneğimiz. Bunu yeni bir program olarak derleyin ve çalıştırın.

from DITTLE import dittle

print("\n We have emulated a new python command with 'dittle()'.\n")
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

Şimdi bu olabildiğince kaygan ve temiz değil mi? Bu değiştirilebilir varsayılanlar gerçekten kullanışlı olabilir.

========================

Cevabımı bir süre düşündükten sonra, değiştirilebilir varsayılan yöntemi kullanmakla aynı şeyi başarmanın normal yolu arasındaki farkı net bir şekilde yaptığımdan emin değilim.

Normal yol, bir Class nesnesini saran (ve bir global kullanan) içe aktarılabilir bir işlev kullanmaktır. Karşılaştırma için, burada değiştirilebilir varsayılan yöntemle aynı şeyleri yapmaya çalışan Sınıf tabanlı bir yöntem.

from time import sleep

class dittle_class():

    def __init__(self):
        
        self.b = 0
        self.a = " Hello World!"
        
        print("\n Initializing Class Object. Executes on First Call only.")
        print(" self.a = '"+str(self.a),"', self.b =",self.b,end="\n\n")
    
    def report(self):
        self.b  = self.b + 1
        
        if self.b == 1:
            print(" Dittle() called",self.b,"time.")
        else:
            print(" Dittle() called",self.b,"times.")
        
        if self.b == 5:
            self.a = " It's Great to be alive!"
        
        print(" Internal String =",self.a,end="\n\n")
            
        if self.b ==3:
            print(" Let's rest for a moment.")
            sleep(2.0) # Since we imported it, we might as well use it.
            print(" Wheew! Ready to continue.\n")
            sleep(1.0)

cl= dittle_class()

def dittle():
    global cl
    
    if type(cl.a) != str and type(cl.b) != int:
        print(" Class exists but does not have valid format.")
        
    cl.report()

# =================== MAIN ====================== 
if __name__ == "__main__":
    print(" We have emulated a python command with our own 'dittle()' command.\n")
    for cnt in range(2):dittle() # Call can be loop-driver, but they need not be.
    
    print(" Attempting to pass arguments to dittle()")
    try: # The user must catch the fatal error. The mutable default user did not. 
        dittle(["BAD","Data"])
    except:
        print(" This caused a fatal error that can't be caught in the function.\n")
    
    print(" Calling dittle() normally..")
    dittle()
    
    print(" Attempting to set the Class variable from the outside.")
    cl.a = " I'm a griefer. My damage sticks."
    cl.b = -7
    
    dittle()
    dittle()

Bu Sınıf tabanlı programı mevcut dizininize DITTLE.py olarak kaydedin ve ardından aşağıdaki kodu çalıştırın (önceki ile aynıdır)

from DITTLE import dittle
# Nothing to declare, nothing to instantize, nothing to remember.

dittle()
dittle()
dittle()
dittle()
dittle()

İki yöntemi karşılaştırarak, bir işlevde değiştirilebilir bir varsayılan kullanmanın avantajları daha net olmalıdır. Değiştirilebilir varsayılan yöntemin globale ihtiyacı yoktur, dahili değişkenleri doğrudan ayarlanamaz. Değişken yöntem, tek bir döngü için bilgili bir geçerliliği olan argümanı kabul edip omuzlarını silkerken, Sınıf yöntemi kalıcı olarak değiştirildi çünkü dahili değişkeni doğrudan dışarıya maruz kaldı. Hangi yöntemin programlanması daha kolaydır? Bunun, yöntemlerle rahatlığınıza ve hedeflerinizin karmaşıklığına bağlı olduğunu düşünüyorum.

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.