İlk kullanımdan sonra yeniden atandığında yerel değişkende UnboundLocalError


208

Aşağıdaki kod, hem Python 2.5 hem de 3.0'da beklendiği gibi çalışır:

a, b, c = (1, 2, 3)

print(a, b, c)

def test():
    print(a)
    print(b)
    print(c)    # (A)
    #c+=1       # (B)
test()

Ancak, (B) satırını açtığımda UnboundLocalError: 'c' not assigned, (A) satırına ulaşıyorum . Değerleri ave bdoğru basılmaktadır. Bu beni iki nedenden dolayı tamamen şaşırttı:

  1. (B) satırında daha sonra yapılan bir açıklama nedeniyle neden (A) satırında bir çalışma zamanı hatası var ?

  2. Neden değişkenlerdir ave bsüre beklendiği gibi baskılı, cbir hata oluşturacaktır?

Gelebileceğim tek açıklama , yerel değişken oluşturulmadan önce bile "global" değişken üzerinde emsal olan bir yerel değişkenin catama tarafından oluşturulmasıdır. Elbette, bir değişkenin kapsamı var olmadan önce "çalması" mantıklı değildir.c+=1c

Birisi bu davranışı açıklayabilir mi?


Yanıtlar:


216

Python, işlev içindeki değişkenleri, işlevin içinden veya dışından değer atamanıza bağlı olarak farklı şekilde ele alır. Bir işlev içinde bir değişken atanırsa, varsayılan olarak yerel değişken olarak kabul edilir. Bu nedenle, satırı açtığınızda, cherhangi bir değer atanmadan önce yerel değişkene başvurmaya çalışıyorsunuzdur.

Değişkenin işlevden önce atanan cglobal c = 3değerine başvurmasını istiyorsanız,

global c

fonksiyonun ilk satırı olarak.

Python 3'e gelince, şimdi var

nonlocal c

bir cdeğişkeni olan en yakın çevreleme işlevi kapsamına başvurmak için kullanabilirsiniz .


3
Teşekkürler. Hızlı soru. Bu, bir programı çalıştırmadan önce Python'un her değişkenin kapsamına karar verdiği anlamına mı geliyor? Bir işlevi çalıştırmadan önce?
tba

7
Değişken kapsam kararı, programı ilk başlattığınızda normalde bir kez çalışan derleyici tarafından verilir. Ancak, programınızda "eval" veya "exec" ifadeleri varsa, derleyicinin daha sonra da çalışabileceğini akılda tutmak gerekir.
Greg Hewgill

2
Tamam teşekkür ederim. "Yorumlanan dil" sanırım düşündüğüm kadar önemli değil.
tba

1
Ah 'yerel olmayan' anahtar kelime tam olarak aradığım şeydi, Python bunu kaçırıyordu. Muhtemelen bu 'anahtar kelimeyi' bu anahtar kelimeyi kullanarak değişkeni ithal eden her bir çevreleme kapsamından geçer.
Brendan

6
@brainfsck: "yukarıya bakmak" ile "değişken" atamak arasında bir ayrım yapıp yapmadığınızı anlamak en kolay yoldur. Adı geçerli kapsamda bulunmazsa, arama daha yüksek bir kapsama geri döner. Atama her zaman yerel kapsamda yapılır ( global veya yerel olmayan atamayı kullanmadığınız globalveya nonlocalzorlamadığınız sürece )
Steven

71

Python, çeşitli kapsamlar için her şeyi sözlükte tutması bakımından biraz garip. Orijinal a, b, c en üst kapsamdadır ve bu yüzden en üstteki sözlüktedir. Fonksiyonun kendi sözlüğü vardır. print(a)Ve print(b)ifadelerine ulaştığınızda, sözlükte bu ada göre hiçbir şey yoktur, bu nedenle Python listeye bakar ve bunları global sözlükte bulur.

Şimdi c+=1, elbette, buna eşdeğeriz c=c+1. Python bu satırı taradığında, "aha, c adında bir değişken var, bunu yerel kapsam sözlüğüme koyacağım" diyor. Ardından, ödevin sağ tarafında c için c değeri aramaya başladığında , henüz değeri olmayan c adlı yerel değişkenini bulur ve hatayı atar.

global cYukarıda belirtilen ifade , ayrıştırıcıya cglobal kapsamdan kullandığını ve yeni bir taneye ihtiyaç duymadığını bildirir .

Satırda bir sorun olduğunu söylemesinin nedeni, kod üretmeye çalışmadan önce isimleri etkili bir şekilde aramasıdır ve bu yüzden bir anlamda gerçekten bu satırı yaptığını düşünmemektedir. Bunun bir kullanılabilirlik hatası olduğunu iddia ediyorum, ancak bir derleyicinin mesajlarını çok ciddiye almamayı öğrenmek genellikle iyi bir uygulamadır .

Herhangi bir rahatlık varsa, Guido'nun Her Şeyi Açıklayan sözlükler hakkında yazdığı bir şey bulmadan önce muhtemelen aynı günü kazıp bir gün geçirdim.

Güncelleme, yorumlara bakın:

Kodu iki kez taramaz, ancak kodu iki aşamada tarar: lexing ve ayrıştırma.

Bu kod satırının ayrıştırmasının nasıl çalıştığını düşünün. Lexer kaynak metni okur ve dilbilgisinin "en küçük bileşenleri" olan lexemes'e böler. Yani çizgiye çarptığında

c+=1

onu böyle bir şeye ayırır

SYMBOL(c) OPERATOR(+=) DIGIT(1)

Ayrıştırıcı sonunda bunu bir ayrıştırma ağacına dönüştürmek ve yürütmek ister, ancak bir ödev olduğundan, daha önce yerel sözlükte c adını arar, görmez ve sözlüğe ekler, işaretler başlatılmamış olarak. Tamamen derlenmiş bir dilde, sadece sembol tablosuna gidip ayrışmayı bekleyecekti, ancak ikinci bir geçiş lüksüne sahip olamayacağı için, lexer daha sonra hayatı kolaylaştırmak için biraz ekstra iş yapıyor. Ancak, o zaman OPERATÖR'ü görür, kuralların "bir operatörünüz + = sol tarafın başlatılmış olması gerekir" der ve "whoops!"

Buradaki nokta , çizginin ayrıştırılmasına henüz başlamamış olmasıdır . Tüm bunlar gerçek ayrışmaya hazırlanıyor, bu nedenle satır sayacı bir sonraki satıra ilerlemedi. Böylece hatayı işaret ettiğinde, hala bir önceki satırda olduğunu düşünür.

Dediğim gibi, bunun bir kullanılabilirlik hatası olduğunu iddia edebilirsiniz, ama aslında oldukça yaygın bir şey. Bazı derleyiciler bu konuda daha dürüst davranırlar ve "XXX satırında veya çevresinde hata" derler.


1
Tamam, cevabınız için teşekkür ederim; Python'daki kapsamlarla ilgili bazı şeyleri temizledi. Ancak, hatanın neden (B) yerine (A) satırında ortaya çıktığını hala anlamıyorum. Python programı çalıştırmadan ÖNCE değişken kapsam sözlüğünü oluşturuyor mu?
tba

1
Hayır, ifade seviyesinde. Cevaba ekleyeceğim, bir yorumda buna sığabileceğimi sanmıyorum.
Charlie Martin

2
Uygulama ayrıntılarıyla ilgili not: CPython'da yerel kapsam genellikle a olarak ele alınmaz dict, dahili olarak yalnızca bir dizidir ( döndürülecek locals()bir a yerleştirir dict, ancak değişiklikler yeni oluşturmaz locals). Ayrıştırma aşaması, her atamayı yerel bir konuma bulur ve addan bu dizideki konuma dönüştürür ve adın her başvurulduğunda bu konumu kullanır. Bu işleve girişte, bağımsız değişken olmayan yerel ayarlar bir yer tutucusuna başlatılır ve UnboundLocalErrorbir değişken okunduğunda ve ilişkili dizini hala yer tutucu değerine sahip olduğunda oluşur.
ShadowRanger

44

Sökmeye bir göz atmak, neler olduğunu netleştirebilir:

>>> def f():
...    print a
...    print b
...    a = 1

>>> import dis
>>> dis.dis(f)

  2           0 LOAD_FAST                0 (a)
              3 PRINT_ITEM
              4 PRINT_NEWLINE

  3           5 LOAD_GLOBAL              0 (b)
              8 PRINT_ITEM
              9 PRINT_NEWLINE

  4          10 LOAD_CONST               1 (1)
             13 STORE_FAST               0 (a)
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE

Gördüğünüz gibi, a LOAD_FASTve b, için erişim için bayt kodu LOAD_GLOBAL. Bunun nedeni, derleyicinin a işlevine atandığını tanımlaması ve yerel değişken olarak sınıflandırmasıdır. Yerliler için erişim mekanizması temel olarak küreseller için farklıdır - çerçevenin değişkenleri tablosunda statik olarak bir ofset atanır, yani arama, küreseller için olduğu gibi daha pahalı dikte aramasından ziyade hızlı bir endekstir. Bu nedenle, Python print asatırı "0 yuvasında tutulan 'a' yerel değişkeninin değerini al ve yazdır" şeklinde okuyor ve bu değişkenin hala başlatılmadığını tespit ettiğinde bir istisna ortaya çıkarıyor.


10

Geleneksel global değişken semantiği denediğinizde Python'un oldukça ilginç bir davranışı vardır. Ayrıntıları hatırlamıyorum, ancak 'küresel' kapsamda bildirilen bir değişkenin değerini iyi okuyabilirsiniz, ancak değiştirmek istiyorsanız, globalanahtar kelimeyi kullanmanız gerekir . Bunu değiştirmeyi deneyin test():

def test():
    global c
    print(a)
    print(b)
    print(c)    # (A)
    c+=1        # (B)

Ayrıca, bu hatayı almanızın nedeni, bu işlevin içinde 'global' ile aynı ada sahip yeni bir değişken bildirebileceğiniz ve tamamen ayrı olacağıdır. Tercüman, bu kapsamda yeni bir değişken oluşturmaya çalıştığınızı düşünüyor cve hepsini tek bir işlemde değiştiriyor c.


Yanıtınız için teşekkürler, ancak hatanın neden sadece bir değişken yazdırmaya çalıştığım (A) satırına atıldığını açıklamıyorum. Program asla başlatılmamış bir değişkeni değiştirmeye çalıştığı satır (B) 'ye ulaşmaz.
tba

1
Python, programı çalıştırmaya başlamadan önce tüm işlevi okuyacak, ayrıştıracak ve dahili bayt koduna çevirecektir, bu nedenle "c değerini yerel değişkene çevirin" değerin yazdırılmasından sonra metinsel olarak gerçekleşir, önemli değildir.
Vatine

6

Bunu netleştiren en iyi örnek şudur:

bar = 42
def foo():
    print bar
    if False:
        bar = 0

çağrılırken foo(), bu da yükseltir UnboundLocalError biz hattına ulaşmak asla rağmen bar=0bu kadar mantıklı yerel değişken oluşturulur asla.

Gizem " Python Yorumlanmış Bir Dildir " te yatar ve fonksiyonun bildirimi footek bir ifade (yani bileşik bir ifade) olarak yorumlanır, sadece onu aptalca yorumlar ve yerel ve küresel kapsamlar yaratır. Böylece bar, uygulamadan önce yerel kapsamda tanınır.

Bunun gibi daha fazla örnek için Bu yazıyı okuyun: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Bu yazı, değişkenlerin Python kapsamının tam bir tanımını ve analizini sağlar:


5

İşte size yardımcı olabilecek iki bağlantı

1: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value

2: docs.python.org/3.1/faq/programming.html?highlight=nonlocal#how-do-i-write-a-function-with-output-parameters-call-by-reference

Birinci bağlantı UnboundLocalError hatasını açıklar. İkinci bağlantı, test fonksiyonunuzu yeniden yazmanıza yardımcı olabilir. İkinci bağlantıya dayanarak, orijinal sorun şu şekilde yeniden yazılabilir:

>>> a, b, c = (1, 2, 3)
>>> print (a, b, c)
(1, 2, 3)
>>> def test (a, b, c):
...     print (a)
...     print (b)
...     print (c)
...     c += 1
...     return a, b, c
...
>>> a, b, c = test (a, b, c)
1
2
3
>>> print (a, b ,c)
(1, 2, 4)

4

Bu, sorunuza doğrudan bir cevap değildir, ancak artırılmış atama ve işlev kapsamları arasındaki ilişkinin neden olduğu başka bir sorun olduğu için yakından ilişkilidir.

Çoğu durumda, artırılmış atamayı ( a += b) basit atamaya ( a = a + b) tam olarak eşdeğer olarak görme eğilimindesiniz . Bununla birlikte, bir köşe durumunda bu konuda biraz sorun yaşamak mümkündür. Açıklamama izin ver:

Python'un basit atamasının çalışma şekli a, bir işleve geçirilirse (örneğin func(a); Python'un her zaman referansla geçtiğini unutmayın), o a = a + bzaman ailetileni değiştirmeyeceği anlamına gelir. Bunun yerine, yalnızca yerel işaretçiyi değiştirecektir a.

Ancak kullanırsanız a += b, bazen şu şekilde uygulanır:

a = a + b

veya bazen (yöntem varsa):

a.__iadd__(b)

İlk durumda ( aglobal olarak bildirilmediği sürece ), atamanın ayalnızca bir işaretçi güncellemesi olması nedeniyle yerel kapsam dışında hiçbir yan etkisi yoktur .

İkinci durumda, aaslında kendini değiştirir, bu nedenle tüm referanslar adeğiştirilmiş sürüme işaret eder. Bu, aşağıdaki kodla gösterilmiştir:

def copy_on_write(a):
      a = a + a
def inplace_add(a):
      a += a
a = [1]
copy_on_write(a)
print a # [1]
inplace_add(a)
print a # [1, 1]
b = 1
copy_on_write(b)
print b # [1]
inplace_add(b)
print b # 1

Yani hile fonksiyon argümanlarında artırılmış atama önlemek için (Ben sadece yerel / döngü değişkenleri için kullanmaya çalışıyorum). Basit atama kullanın ve belirsiz davranışlardan koruyacaksınız.


2

Python yorumlayıcısı bir işlevi eksiksiz bir birim olarak okuyacaktır. Bunu iki geçişte okumak, bir kez kapanışını (yerel değişkenler) toplamak, sonra tekrar bayt koduna dönüştürmek olarak düşünüyorum.

Eminim zaten farkındasınızdır, '=' ifadesinin solunda kullanılan herhangi bir isim dolaylı olarak yerel bir değişkendir. Birden fazla kez bir + = değişken erişimini değiştirerek yakalandım ve aniden farklı bir değişken.

Ayrıca, özellikle küresel kapsamla ilgili bir şey olmadığını belirtmek istedim. Yuvalanmış işlevlerle aynı davranışı elde edersiniz.


2

c+=1atar c, python atanan değişkenlerin yerel olduğunu varsayar, ancak bu durumda yerel olarak bildirilmez.

globalVeya nonlocalanahtar sözcüklerini kullanın .

nonlocal yalnızca python 3'te çalışır, bu nedenle python 2 kullanıyorsanız ve değişkeninizi global yapmak istemiyorsanız, değiştirilebilir bir nesne kullanabilirsiniz:

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()

1

Sınıf değişkenine ulaşmanın en iyi yolu doğrudan sınıf adına göre erişim sağlamaktır

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1

0

Python'da, yerel, sınıf değişkeni ve global değişkenlerin her türü için benzer bir beyanımız vardır. yöntemden genel değişkene başvurduğunuzda, python aslında henüz tanımlanmamış olan yöntemin kendisinden değişkene atıfta bulunduğunuzu düşünür ve bu nedenle hata verir. Global değişkeni ifade etmek için globals () ['variableName'] kullanmalıyız.

sizin durumunuzda sırasıyla a, b ve c yerine globals () ['a], globals () [' b '] ve globals () [' c '] kullanın.


0

Aynı sorun beni rahatsız ediyor. Kullanarak nonlocalve globalsorunu çözebilir.
Bununla birlikte, kullanımı için gereken dikkat nonlocal, iç içe işlevler için çalışır. Ancak, bir modül düzeyinde, çalışmaz. Buradaki örneklere bakın .

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.