Yerel değişkenler hakkında

Merhabalar,

Konu
İç içe iki fonksiyon ve bir tane paylaşılamayan değişken.

Olay

def disari():
    # Başrol
    nl_var = 2
    
    def iceri():
        # Havadan sudan şimdilik
        if 8 < -2:
            print("pek oluru yok gibiydi ama hayirlisi")
            peki

        # Ne var ne yok
        if nl_var < 0:
            print("negatif diyelim")
        else:
            print("pozitif olabilir")   
    iceri() 
disari()

Bu kodu (ve yalnız bu kodu) çalıştırıp pozitif olabilir çıktısı aldım, ne güzel.

Şimdi küçük bir değişiklik:

def disari():
    # Başrol
    nl_var = 2
    
    def iceri():
        # İçerisi biraz değişiyor
        if 8 < -2:
            print("pek oluru yok gibiydi ama hayirlisi")
            peki
            nl_var = 7   # <--- yeni geldi

        # Ne var ne yok
        if nl_var < 0:
            print("negatif diyelim")
        else:
            print("pozitif olabilir")   
    iceri() 
disari()

Bu kodu çalıştırdım ve o da ne:

Traceback (most recent call last):
  File ".\fr.py", line 18, in <module>
    disari()
  File ".\fr.py", line 17, in disari
    iceri()
  File ".\fr.py", line 13, in iceri
    if nl_var < 0:
UnboundLocalError: local variable 'nl_var' referenced before assignment

Ee, sorun ne?
Yahu “referanslık” bi’ durum olmadı ki, neden böyle bir hata mesajıyla karşı karşıyayız? Biraz önce de bakıyordum işaretine sorun yoktu da, şimdi ne oldu? O eklenen satırın etkisi nedir?


sys.version_info(major=3, minor=7, micro=3, releaselevel='final', serial=0)
Windows 10
İki script de ".\fr.py"den çalıştı
1 Beğeni

İlginçmiş.

def dışarı():
    var = 2
    
    def içeri():
        if False:
            var = 1
            
        print(locals())
        print(var)
        
    içeri()

dışarı()
def dışarı():
    var = 2
    
    def içeri():            
        print(locals())
        print(var)
        
    içeri()

dışarı()

Bu ilk kodun derlenmiş hali:

  2           0 LOAD_CONST               0 (<code object dışarı at 0x000001F1DA147A80, file "<dis>", line 2>)
              2 LOAD_CONST               1 ('dışarı')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (dışarı)

 14           8 LOAD_NAME                0 (dışarı)
             10 CALL_FUNCTION            0
             12 POP_TOP
             14 LOAD_CONST               2 (None)
             16 RETURN_VALUE

Disassembly of <code object dışarı at 0x000001F1DA147A80, file "<dis>", line 2>:
  3           0 LOAD_CONST               1 (2)
              2 STORE_FAST               0 (var)

  5           4 LOAD_CONST               2 (<code object içeri at 0x000001F1DA1479D0, file "<dis>", line 5>)
              6 LOAD_CONST               3 ('dışarı.<locals>.içeri')
              8 MAKE_FUNCTION            0
             10 STORE_FAST               1 (içeri)

 12          12 LOAD_FAST                1 (içeri)
             14 CALL_FUNCTION            0
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Disassembly of <code object içeri at 0x000001F1DA1479D0, file "<dis>", line 5>:
  9           0 LOAD_GLOBAL              0 (print)
              2 LOAD_GLOBAL              1 (locals)
              4 CALL_FUNCTION            0
              6 CALL_FUNCTION            1
              8 POP_TOP

 10          10 LOAD_GLOBAL              0 (print)
             12 LOAD_FAST                0 (var)
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Bu da ikincinin:

  2           0 LOAD_CONST               0 (<code object dışarı at 0x0000027244D47A80, file "<dis>", line 2>)
              2 LOAD_CONST               1 ('dışarı')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (dışarı)

 11           8 LOAD_NAME                0 (dışarı)
             10 CALL_FUNCTION            0
             12 POP_TOP
             14 LOAD_CONST               2 (None)
             16 RETURN_VALUE

Disassembly of <code object dışarı at 0x0000027244D47A80, file "<dis>", line 2>:
  3           0 LOAD_CONST               1 (2)
              2 STORE_DEREF              0 (var)

  5           4 LOAD_CLOSURE             0 (var)
              6 BUILD_TUPLE              1
              8 LOAD_CONST               2 (<code object içeri at 0x0000027244D479D0, file "<dis>", line 5>)
             10 LOAD_CONST               3 ('dışarı.<locals>.içeri')
             12 MAKE_FUNCTION            8 (closure)
             14 STORE_FAST               0 (içeri)

  9          16 LOAD_FAST                0 (içeri)
             18 CALL_FUNCTION            0
             20 POP_TOP
             22 LOAD_CONST               0 (None)
             24 RETURN_VALUE

Disassembly of <code object içeri at 0x0000027244D479D0, file "<dis>", line 5>:
  6           0 LOAD_GLOBAL              0 (print)
              2 LOAD_GLOBAL              1 (locals)
              4 CALL_FUNCTION            0
              6 CALL_FUNCTION            1
              8 POP_TOP

  7          10 LOAD_GLOBAL              0 (print)
             12 LOAD_DEREF               0 (var)
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Bir de şunu inceledim:

def dışarı():
    var = 2

    def içeri():
        if False:
            var = 1
            
        print(locals())
        print(var)
        
    return içeri

code = dışarı.__code__
clousure = dışarı().__code__

def dışarı2():
    var = 2

    def içeri():            
        print(locals())
        print(var)
        
    return içeri

code2 = dışarı2.__code__
clousure2 = dışarı2().__code__
>>> clousure.co_varnames
('var',)
>>> clousure2.co_varnames
()
>>> code.co_varnames
('var', 'içeri')
>>> code2.co_varnames
('içeri',)

Hatayı veren dışarı fonksiyonunun içindeki içeri fonksiyonu syntax olarak bir assigment içerdiği için Python var değişkenini içeri fonksiyonunda arıyor, dışarı değil. Bir bug olabilir bence.

1 Beğeni

if False yazdığımız için teşekkür ediyorum çünkü if 8 < -2 ile beraber şimdi şöylesi 3 durum var:

Durum 1, klasik mevzu

def one():
    x = 2
    def two():
        x
    two()

Durum 2, “if 8 < -2” ile

def one():
    x = 2
    def two():
        if 8 < -2:
            x = 7
        x
    two()

Durum 3, “if False” ile

def one():
    x = 2
    def two():
        if False:
            x = 7
        x
    two()

Her durum için

from dis import dis
dis(one)
one()

kodlarını uyguluyoruz.

Durum 1’de herhangi bir hata yok, bytecode’da LOAD/STORE DEREF ve LOAD CLOSURE görüyoruz:

  2           0 LOAD_CONST               1 (2)
              2 STORE_DEREF              0 (x)    # <----

  3           4 LOAD_CLOSURE             0 (x)    # <----
              6 BUILD_TUPLE              1
              8 LOAD_CONST               2 (<code object two at 0x0000023829ABC810, file ".\fr.py", line 3>)
             10 LOAD_CONST               3 ('one.<locals>.two')
             12 MAKE_FUNCTION            8
             14 STORE_FAST               0 (two)

  5          16 LOAD_FAST                0 (two)
             18 CALL_FUNCTION            0
             20 POP_TOP
             22 LOAD_CONST               0 (None)
             24 RETURN_VALUE

Disassembly of <code object two at 0x0000023829ABC810, file ".\fr.py", line 3>:
  4           0 LOAD_DEREF               0 (x)    # <----
              2 POP_TOP
              4 LOAD_CONST               0 (None)
              6 RETURN_VALUE

Durum 2 ve 3’te hata alıyoruz: UnboundLocalError: local variable 'x' referenced before assignment, x'in tek başına salındığı satırı işaret ediyor. Bytecode’larında ilginç bir olay var:

Durum 2: (if 8 < -2)

  2           0 LOAD_CONST               1 (2)
              2 STORE_FAST               0 (x)

  3           4 LOAD_CONST               2 (<code object two at 0x0000013CEBA8C810, file ".\fr.py", line 3>)
              6 LOAD_CONST               3 ('one.<locals>.two')
              8 MAKE_FUNCTION            0
             10 STORE_FAST               1 (two)

  7          12 LOAD_FAST                1 (two)
             14 CALL_FUNCTION            0
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Disassembly of <code object two at 0x0000013CEBA8C810, file ".\fr.py", line 3>:
  4           0 LOAD_CONST               1 (8)    # Buralara dikkat
              2 LOAD_CONST               2 (-2)   #
              4 COMPARE_OP               0 (<)    #
              6 POP_JUMP_IF_FALSE       12        #
 
  5           8 LOAD_CONST               3 (7)    #
             10 STORE_FAST               0 (x)    # ^^^^^^^^^^^^^

  6     >>   12 LOAD_FAST                0 (x)    
             14 POP_TOP
             16 LOAD_CONST               0 (None)
             18 RETURN_VALUE

Durum 3: (if False)

  2           0 LOAD_CONST               1 (2)
              2 STORE_FAST               0 (x)

  3           4 LOAD_CONST               2 (<code object two at 0x000001A3C669C810, file ".\fr.py", line 3>)
              6 LOAD_CONST               3 ('one.<locals>.two')
              8 MAKE_FUNCTION            0
             10 STORE_FAST               1 (two)

  7          12 LOAD_FAST                1 (two)
             14 CALL_FUNCTION            0
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Disassembly of <code object two at 0x000001A3C669C810, file ".\fr.py", line 3>:
  6           0 LOAD_FAST                0 (x)   # Az önceki bytecode'lar yok burada
              2 POP_TOP
              4 LOAD_CONST               0 (None)
              6 RETURN_VALUE

Durum 2 ve 3’te DEREF'ten eser yok. İlginç olan şu: False ile baktığımızda, Python durumun imkansızlığını fark edip o kısım için bir bytecode üretmiyor. 8 < -2 için ise if bloğu ve de içerisindeki assignment için bytecode’ları görüyoruz.

Diyelim ki if'in içerisindeki assignment DEREF üretilmesini baskılıyor FAST üretiliyor. E peki False yazdığımızda tabiri caizse aklını kullanıp orayı göz ardı ediyor da, neden DEREF/CLOSURE'a dönmüyor?

Ben zaten Python’un if False’ı optimize ettiğini bildiğim için öyle denedim, if 8 < -2 ile aynı sonucu almamız x değişkeninin nasıl load edileceğinin syntax’a göre belirlendiğini gösteriyor. Sıkıntı da burada aslında, dinamik olarak yüklenmesini bekliyorduk ama CPython yazdığımız koda göre hareket ediyor.

Burası daha çok optimizasyon ile alakalı. Dediğiniz gibi if False’ı direkt silecek kadar zeki olan CPython if 8 < -2’ye dokunmuyor. Aslında bu biraz çelişkili, const değerler arasındaki aritmetik işlemlerde optimizasyon var çünkü:

>>> from dis import dis
>>> dis("8 < -2")
  1           0 LOAD_CONST               0 (8)
              2 LOAD_CONST               1 (-2)
              4 COMPARE_OP               0 (<)
              6 RETURN_VALUE
>>> dis("8 + -2")
  1           0 LOAD_CONST               0 (6)
              2 RETURN_VALUE

Ama bunun aldığımız hatayla çok alakası yok, sadece x’in nasıl yükleneceğinin bu optimizasyonlardan önceki durum dikkate alınarak kararlaştırıldığını söyleyebiliriz.


Bu arada sorunun çözümü basit:

def one():
    x = 2
    def two():
        nonlocal x
        if 8 < -2:
            x = 7
        x
    two()

1 Beğeni

Meğerse sorunun closure’la nested function’la felan pek bi alakası yokmuş. Şurada dediğine göre

“Python doesn’t have variable declarations, so it has to figure out the scope of variables itself. It does so by a simple rule: If there is an assignment to a variable inside a function, that variable is considered local”

Yani Python orayı parse ederken if’in içindeki assignment’i görüp (her ne kadar bytecode üretmese de) x’i local olarak bind ediyor ve alt satırdaki x , if False çalışamadığından, “assign edilmeden refere edildi” konumuna düşüyor.

Dolayısıyla nested fonksiyon olmasıyla bir alakası yokmuş, şu da aynı olaya tekabül ediyor:

x = 5
def fonk():
    if False:
        x = 3
    x

@EkremDincel’e yardımları için teşekkürü bir borç biliriz.

1 Beğeni