UnboundLocalError pada variabel lokal ketika dipindahkan setelah digunakan pertama kali

209

Kode berikut berfungsi seperti yang diharapkan di Python 2.5 dan 3.0:

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

print(a, b, c)

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

Namun, ketika saya batalkan komentar pada baris (B) , saya mendapat UnboundLocalError: 'c' not assignedat at line (A) . Nilai-nilai adan bdicetak dengan benar. Ini membuat saya benar-benar bingung karena dua alasan:

  1. Mengapa ada kesalahan runtime yang dilemparkan pada baris (A) karena pernyataan selanjutnya pada baris (B) ?

  2. Mengapa variabel adan bdicetak seperti yang diharapkan, sementara cmenimbulkan kesalahan?

Satu-satunya penjelasan yang dapat saya kemukakan adalah bahwa variabel lokalc dibuat oleh penugasan c+=1, yang lebih diutamakan daripada variabel "global" cbahkan sebelum variabel lokal dibuat. Tentu saja, tidak masuk akal jika variabel mencuri "ruang lingkup" sebelum ada.

Bisakah seseorang tolong jelaskan perilaku ini?

tba
sumber
Apakah ini menjawab pertanyaan Anda? Tidak mengerti mengapa UnboundLocalError terjadi (penutupan)
norok2

Jawaban:

216

Python memperlakukan variabel dalam fungsi berbeda tergantung pada apakah Anda menetapkan nilai kepada mereka dari dalam atau di luar fungsi. Jika variabel ditugaskan dalam suatu fungsi, itu diperlakukan secara default sebagai variabel lokal. Oleh karena itu, ketika Anda menghapus tanda komentar pada baris Anda mencoba untuk referensi variabel lokal csebelum nilai apa pun ditugaskan untuk itu.

Jika Anda ingin variabel cmerujuk ke global yang c = 3ditugaskan sebelum fungsi, masukkan

global c

sebagai baris pertama dari fungsi.

Adapun python 3, sekarang ada

nonlocal c

yang bisa Anda gunakan untuk merujuk ke ruang lingkup fungsi melampirkan terdekat yang memiliki cvariabel.

rekursif
sumber
3
Terima kasih. Pertanyaan cepat. Apakah ini menyiratkan bahwa Python menentukan ruang lingkup masing-masing variabel sebelum menjalankan program? Sebelum menjalankan suatu fungsi?
tba
7
Keputusan lingkup variabel dibuat oleh kompiler, yang biasanya berjalan sekali ketika Anda pertama kali memulai program. Namun perlu diingat bahwa kompiler juga dapat berjalan nanti jika Anda memiliki pernyataan "eval" atau "exec" di program Anda.
Greg Hewgill
2
Oke terima kasih. Saya kira "bahasa yang ditafsirkan" tidak menyiratkan sebanyak yang saya pikirkan.
tba
1
Ah kata kunci 'nonlokal' itu persis seperti yang saya cari, sepertinya Python kehilangan ini. Agaknya 'kaskade' ini melalui masing-masing ruang lingkup penutup yang mengimpor variabel menggunakan kata kunci ini?
Brendan
6
@brainfsck: paling mudah dipahami jika Anda membuat perbedaan antara "mencari" dan "menetapkan" variabel. Pencarian turun kembali ke ruang lingkup yang lebih tinggi jika nama tidak ditemukan dalam ruang lingkup saat ini. Penugasan selalu dilakukan dalam lingkup lokal (kecuali jika Anda menggunakan globalatau nonlocaluntuk memaksa penugasan global atau non-lokal)
Steven
71

Python sedikit aneh karena menyimpan semuanya dalam kamus untuk berbagai cakupan. Aslinya a, b, c berada dalam ruang lingkup teratas dan dalam kamus paling atas itu. Fungsi memiliki kamus sendiri. Saat Anda mencapai print(a)dan print(b)pernyataan, tidak ada yang namanya dalam kamus, jadi Python mencari daftar dan menemukan mereka di kamus global.

Sekarang kita bisa c+=1, yang, tentu saja, setara dengan c=c+1. Ketika Python memindai baris itu, dikatakan "aha, ada variabel bernama c, saya akan memasukkannya ke dalam kamus lingkup lokal saya." Kemudian ketika ia pergi mencari nilai untuk c untuk c di sisi kanan penugasan, ia menemukan variabel lokal bernama c , yang belum memiliki nilai, dan melempar kesalahan.

Pernyataan yang global cdisebutkan di atas hanya memberitahu parser bahwa ia menggunakan cdari lingkup global sehingga tidak perlu yang baru.

Alasannya mengatakan ada masalah pada baris yang dilakukannya adalah karena secara efektif mencari nama-nama sebelum mencoba untuk menghasilkan kode, dan dalam beberapa hal tidak berpikir itu benar-benar melakukan garis itu. Saya berpendapat bahwa ini adalah bug kegunaan, tetapi umumnya merupakan praktik yang baik untuk hanya belajar untuk tidak menganggap pesan kompiler terlalu serius.

Jika itu suatu kenyamanan, saya mungkin menghabiskan sehari menggali dan bereksperimen dengan masalah yang sama ini sebelum saya menemukan sesuatu yang ditulis Guido tentang kamus yang Menjelaskan Segalanya.

Perbarui, lihat komentar:

Itu tidak memindai kode dua kali, tetapi memindai kode dalam dua fase, lexing dan parsing.

Pertimbangkan bagaimana penguraian baris kode ini bekerja. Lexer membaca teks sumber dan memecahnya menjadi leksem, "komponen terkecil" dari tata bahasa. Jadi ketika itu menyentuh garis

c+=1

itu memecahnya menjadi sesuatu seperti

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

Pengurai akhirnya ingin menjadikan ini menjadi pohon parse dan menjalankannya, tetapi karena ini adalah tugas, sebelum itu, ia mencari nama c dalam kamus lokal, tidak melihatnya, dan memasukkannya ke dalam kamus, menandai sebagai tidak diinisialisasi. Dalam bahasa yang dikompilasi penuh, itu hanya akan pergi ke tabel simbol dan menunggu parse, tetapi karena TIDAK AKAN memiliki kemewahan pass kedua, lexer melakukan sedikit pekerjaan ekstra untuk membuat hidup lebih mudah di kemudian hari. Hanya saja, kemudian ia melihat OPERATOR, melihat bahwa aturan mengatakan "jika Anda memiliki operator + = sisi kiri pasti telah diinisialisasi" dan mengatakan "whoops!"

Intinya di sini adalah bahwa itu belum benar-benar memulai parse dari garis . Ini semua terjadi semacam persiapan untuk parse yang sebenarnya, sehingga penghitung baris belum maju ke baris berikutnya. Jadi ketika sinyal kesalahan, ia masih berpikir itu pada baris sebelumnya.

Seperti yang saya katakan, Anda bisa berpendapat itu adalah bug kegunaan, tetapi sebenarnya hal yang cukup umum. Beberapa kompiler lebih jujur ​​tentang hal itu dan mengatakan "kesalahan pada atau di sekitar jalur XXX", tetapi yang ini tidak.

Charlie Martin
sumber
1
Oke terima kasih atas tanggapan Anda; itu membersihkan beberapa hal bagi saya tentang cakupan di python. Namun, saya masih tidak mengerti mengapa kesalahan muncul pada baris (A) daripada baris (B). Apakah Python membuat kamus lingkup variabelnya SEBELUM menjalankan program?
tba
1
Tidak, ini di level ekspresi. Saya akan menambahkan jawabannya, saya rasa saya tidak bisa memasukkan ini dalam komentar.
Charlie Martin
2
Catatan tentang detail implementasi: Dalam CPython, lingkup lokal biasanya tidak ditangani sebagai a dict, itu secara internal hanya sebuah array ( locals()akan mengisi a dictuntuk kembali, tetapi perubahan itu tidak membuat yang baru locals). Fase parse menemukan setiap penugasan ke lokal dan mengkonversi dari nama ke posisi dalam array itu, dan menggunakan posisi itu setiap kali nama direferensikan. Saat masuk ke fungsi, penduduk lokal non-argumen diinisialisasi ke placeholder, dan UnboundLocalErrorterjadi ketika variabel dibaca dan indeks terkait masih memiliki nilai placeholder.
ShadowRanger
44

Melihat pembongkaran dapat mengklarifikasi apa yang terjadi:

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

Seperti yang Anda lihat, bytecode untuk mengakses a adalah LOAD_FAST, dan untuk b LOAD_GLOBAL,. Ini karena kompiler telah mengidentifikasi bahwa ditugaskan ke dalam fungsi, dan mengklasifikasikannya sebagai variabel lokal. Mekanisme akses untuk penduduk lokal pada dasarnya berbeda untuk global - mereka secara statis ditugaskan offset dalam tabel variabel frame, yang berarti pencarian adalah indeks cepat, daripada pencarian dict lebih mahal untuk global. Karena itu, Python membaca print abaris sebagai "dapatkan nilai variabel lokal 'a' yang disimpan di slot 0, dan cetaklah", dan ketika mendeteksi bahwa variabel ini masih belum diinisialisasi, menimbulkan pengecualian.

Brian
sumber
10

Python memiliki perilaku yang agak menarik ketika Anda mencoba semantik variabel global tradisional. Saya tidak ingat detailnya, tetapi Anda dapat membaca nilai variabel yang dideklarasikan dalam lingkup 'global' baik-baik saja, tetapi jika Anda ingin memodifikasinya, Anda harus menggunakan globalkata kunci. Coba ubah test()ke ini:

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

Selain itu, alasan Anda mendapatkan kesalahan ini adalah karena Anda juga dapat mendeklarasikan variabel baru di dalam fungsi itu dengan nama yang sama dengan yang 'global', dan itu akan benar-benar terpisah. Penerjemah mengira Anda mencoba membuat variabel baru dalam lingkup ini yang disebut cdan memodifikasi semuanya dalam satu operasi, yang tidak diizinkan dalam Python karena ini baru ctidak diinisialisasi.

Luwak
sumber
Terima kasih atas tanggapan Anda, tetapi saya tidak berpikir itu menjelaskan mengapa kesalahan dilemparkan pada baris (A), di mana saya hanya mencoba mencetak variabel. Program tidak pernah sampai ke garis (B) di mana ia mencoba untuk memodifikasi variabel yang tidak diinisialisasi.
tba
1
Python akan membaca, mem-parsing, dan mengubah seluruh fungsi menjadi bytecode internal sebelum mulai menjalankan program, sehingga fakta bahwa "ubah c menjadi variabel lokal" terjadi secara tekstual setelah pencetakan nilai tidak, seolah-olah, masalah.
Vatine
6

Contoh terbaik yang membuatnya jelas adalah:

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

saat memanggil foo(), ini juga memunculkan UnboundLocalError meskipun kita tidak akan pernah mencapai garis bar=0, jadi secara logis variabel lokal tidak boleh dibuat.

Misteri terletak pada " Python is an Interpreted Language " dan deklarasi fungsi fooditafsirkan sebagai pernyataan tunggal (yaitu pernyataan majemuk), itu hanya menafsirkannya dengan bodoh dan menciptakan lingkup lokal dan global. Jadi bardiakui dalam lingkup lokal sebelum eksekusi.

Untuk lebih banyak contoh seperti ini Baca posting ini: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Posting ini memberikan Deskripsi dan Analisis Lengkap tentang Pelingkupan Python variabel:

Sahil kalra
sumber
5

Berikut adalah dua tautan yang dapat membantu

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

tautan satu menjelaskan kesalahan UnboundLocalError. Tautan dua dapat membantu menulis ulang fungsi pengujian Anda. Berdasarkan tautan dua, masalah asli dapat ditulis ulang sebagai:

>>> 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)
mcdon
sumber
4

Ini bukan jawaban langsung untuk pertanyaan Anda, tetapi terkait erat, karena ini adalah masalah lain yang disebabkan oleh hubungan antara penugasan yang diperbesar dan lingkup fungsi.

Dalam kebanyakan kasus, Anda cenderung menganggap penugasan yang diperbesar ( a += b) sama persis dengan penugasan sederhana ( a = a + b). Dimungkinkan untuk mendapatkan masalah dengan ini, dalam satu kasus sudut. Biarkan saya jelaskan:

Cara penugasan sederhana Python bekerja berarti bahwa jika adilewatkan ke fungsi (seperti func(a); perhatikan bahwa Python selalu lulus-oleh-referensi), maka a = a + btidak akan mengubah ayang dilewatkan. Sebaliknya, itu hanya akan mengubah pointer lokal ke a.

Tetapi jika Anda menggunakan a += b, maka kadang-kadang diimplementasikan sebagai:

a = a + b

atau kadang-kadang (jika metode itu ada) sebagai:

a.__iadd__(b)

Dalam kasus pertama (selama atidak dinyatakan global), tidak ada efek samping di luar cakupan lokal, karena penugasan ahanyalah pembaruan pointer.

Dalam kasus kedua, asebenarnya akan memodifikasi sendiri, sehingga semua referensi aakan menunjuk ke versi yang dimodifikasi. Ini ditunjukkan oleh kode berikut:

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

Jadi triknya adalah untuk menghindari penugasan augmented pada argumen fungsi (saya mencoba hanya menggunakannya untuk variabel lokal / loop). Gunakan tugas yang sederhana, dan Anda akan aman dari perilaku yang ambigu.

alsuren
sumber
2

Penerjemah Python akan membaca fungsi sebagai unit yang lengkap. Saya menganggapnya sebagai membacanya dalam dua lintasan, sekali untuk mengumpulkan penutupannya (variabel lokal), kemudian mengubahnya menjadi byte-code.

Karena saya yakin Anda sudah mengetahui, nama apa pun yang digunakan di sebelah kiri '=' secara implisit adalah variabel lokal. Lebih dari sekali saya tertangkap dengan mengubah akses variabel ke + = dan tiba-tiba menjadi variabel yang berbeda.

Saya juga ingin menunjukkan bahwa itu tidak benar-benar ada hubungannya dengan ruang lingkup global secara khusus. Anda mendapatkan perilaku yang sama dengan fungsi bersarang.

James Hopkin
sumber
2

c+=1wakilnya c, python mengasumsikan ditugaskan variabel lokal, tetapi dalam kasus ini belum dinyatakan secara lokal.

Baik menggunakan kata kunci globalatau nonlocal.

nonlocal hanya berfungsi di python 3, jadi jika Anda menggunakan python 2 dan tidak ingin membuat variabel Anda global, Anda bisa menggunakan objek yang bisa diubah:

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

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

test()
Colegram
sumber
1

Cara terbaik untuk mencapai variabel kelas adalah akses langsung dengan nama kelas

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
Harun ERGUL
sumber
0

Dalam python kita memiliki deklarasi yang sama untuk semua jenis variabel lokal, variabel kelas dan variabel global. ketika Anda merujuk variabel global dari metode, python berpikir bahwa Anda sebenarnya merujuk variabel dari metode itu sendiri yang belum didefinisikan dan karenanya melempar kesalahan. Untuk merujuk variabel global kita harus menggunakan global () ['variabelName'].

dalam kasus Anda gunakan global () ['a], global () [' b '] dan global () [' c '] alih-alih a, b dan c.

Santosh Kadam
sumber
0

Masalah yang sama menggangguku. Menggunakan nonlocaldan globaldapat memecahkan masalah.
Namun, perhatian yang diperlukan untuk penggunaan nonlocal, itu berfungsi untuk fungsi bersarang. Namun, di tingkat modul, itu tidak berfungsi. Lihat contoh di sini.

Qinsheng Zhang
sumber