UnboundLocalError บนตัวแปรโลคัลเมื่อกำหนดใหม่หลังจากการใช้ครั้งแรก


208

รหัสต่อไปนี้ทำงานได้ตามที่คาดไว้ใน Python 2.5 และ 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()

แต่เมื่อฉันสาย uncomment (B) , ฉันได้รับUnboundLocalError: 'c' not assignedที่เส้น(A) ค่าของaและbถูกพิมพ์อย่างถูกต้อง สิ่งนี้ทำให้ฉันงงงวยอย่างสมบูรณ์ด้วยเหตุผลสองประการ:

  1. เหตุใดจึงมีข้อผิดพลาด runtime โยนที่สาย(A)เพราะเป็นคำสั่งในภายหลังบรรทัด(B) ?

  2. ทำไมตัวแปรaและbพิมพ์ได้ตามที่คาดไว้ในขณะที่cเกิดข้อผิดพลาด

คำอธิบายเดียวที่ฉันสามารถขึ้นมาเป็นที่ท้องถิ่นตัวแปรcถูกสร้างขึ้นโดยได้รับมอบหมายc+=1ซึ่งจะนำแบบอย่างมากกว่าตัวแปร "ทั่วโลก" cแม้กระทั่งก่อนที่ตัวแปรท้องถิ่นจะถูกสร้างขึ้น แน่นอนมันไม่สมเหตุสมผลที่ตัวแปรจะ "ขโมย" ขอบเขตก่อนที่มันจะมีอยู่

มีคนช่วยอธิบายพฤติกรรมนี้ได้ไหม


สิ่งนี้ตอบคำถามของคุณหรือไม่ ไม่เข้าใจว่าทำไม UnboundLocalError เกิดขึ้น (ปิด)
norok2

คำตอบ:


216

Python ปฏิบัติกับตัวแปรในฟังก์ชั่นที่แตกต่างกันขึ้นอยู่กับว่าคุณกำหนดค่าให้กับพวกเขาจากภายในหรือภายนอกฟังก์ชั่น หากมีการกำหนดตัวแปรภายในฟังก์ชั่นจะถือว่าค่าเริ่มต้นเป็นตัวแปรท้องถิ่น ดังนั้นเมื่อคุณไม่ใส่เครื่องหมายในบรรทัดคุณจะพยายามอ้างอิงตัวแปรท้องถิ่นcก่อนที่จะกำหนดค่าใด ๆ

หากคุณต้องการให้ตัวแปรcอ้างถึงส่วนกลางที่c = 3กำหนดไว้ก่อนหน้าฟังก์ชันให้ใส่

global c

เป็นบรรทัดแรกของฟังก์ชั่น

สำหรับงูหลาม 3 ตอนนี้มี

nonlocal c

คุณสามารถใช้เพื่ออ้างถึงขอบเขตการล้อมรอบฟังก์ชั่นที่ใกล้ที่สุดที่มีcตัวแปร


3
ขอบคุณ คำถามอย่างรวดเร็ว. นี่หมายความว่า Python ตัดสินใจขอบเขตของตัวแปรแต่ละตัวก่อนเรียกใช้โปรแกรมหรือไม่? ก่อนใช้งานฟังก์ชั่น?
tba

7
การตัดสินใจเกี่ยวกับขอบเขตตัวแปรนั้นกระทำโดยคอมไพเลอร์ซึ่งโดยปกติจะรันหนึ่งครั้งเมื่อคุณเริ่มโปรแกรมครั้งแรก อย่างไรก็ตามโปรดจำไว้ว่าคอมไพเลอร์อาจทำงานในภายหลังหากคุณมีคำสั่ง "eval" หรือ "exec" ในโปรแกรมของคุณ
เกร็กฮิวกิล

2
โอเคขอบคุณ. ฉันเดาว่า "ภาษาที่ตีความ" ไม่ได้แปลว่าฉันคิดอะไรมาก
tba

1
อาที่คำหลัก 'nonlocal' เป็นสิ่งที่ฉันกำลังมองหาดูเหมือนว่า Python หายไปนี้ สมมุติว่า 'น้ำตก' ผ่านแต่ละขอบเขตที่ล้อมรอบซึ่งนำเข้าตัวแปรโดยใช้คำหลักนี้หรือไม่
เบรนแดน

6
@brainfsck: มันง่ายที่สุดที่จะเข้าใจถ้าคุณสร้างความแตกต่างระหว่าง "ค้นหา" และ "กำหนด" ตัวแปร การค้นหาจะกลับไปสู่ขอบเขตที่สูงกว่าหากไม่พบชื่อในขอบเขตปัจจุบัน การมอบหมายจะทำในขอบเขตท้องถิ่นเสมอ (เว้นแต่ว่าคุณจะใช้globalหรือnonlocalเพื่อบังคับใช้การมอบหมายทั่วโลกหรือไม่ใช่ท้องถิ่น)
สตีเวน

71

Python ค่อนข้างแปลกที่มันเก็บทุกอย่างไว้ในพจนานุกรมสำหรับขอบเขตต่างๆ ต้นฉบับ a, b, c อยู่ในขอบเขตบนสุดและอื่น ๆ ในพจนานุกรมบนสุดนั้น ฟังก์ชั่นมีพจนานุกรมของตัวเอง เมื่อคุณมาถึงprint(a)และprint(b)ข้อความสั่งไม่มีชื่อใดในพจนานุกรมดังนั้น Python ค้นหารายการและค้นหาในพจนานุกรมทั่วโลก

ตอนนี้เราได้รับซึ่งเป็นของหลักสูตรที่เทียบเท่ากับc+=1 c=c+1เมื่อ Python สแกนบรรทัดนั้นจะมีข้อความว่า "aha มีตัวแปรชื่อ c ฉันจะใส่มันลงในพจนานุกรมขอบเขตท้องถิ่นของฉัน" จากนั้นเมื่อมันไปหาค่าสำหรับ c สำหรับ c ทางด้านขวามือของการมอบหมายมันจะพบตัวแปรท้องถิ่นชื่อ cซึ่งยังไม่มีค่าและยังโยนข้อผิดพลาด

คำสั่งglobal cดังกล่าวข้างต้นเพียงบอก parser ว่ามันใช้cจากขอบเขตทั่วโลกและดังนั้นจึงไม่จำเป็นต้องมีใหม่

เหตุผลที่บอกว่ามีปัญหาในบรรทัดเนื่องจากเป็นอย่างมีประสิทธิภาพค้นหาชื่อก่อนที่จะพยายามสร้างรหัสและในบางกรณีไม่คิดว่ามันกำลังทำสายนั้น ฉันยืนยันว่าเป็นข้อผิดพลาดในการใช้งาน แต่โดยทั่วไปแล้วเป็นวิธีปฏิบัติที่ดีเพียงแค่เรียนรู้ที่จะไม่ใช้ข้อความของคอมไพเลอร์อย่างจริงจังเกินไป

ถ้ามันสะดวกสบายฉันอาจใช้เวลาหนึ่งวันขุดและทดลองกับปัญหาเดียวกันนี้ก่อนที่ฉันจะพบบางสิ่งที่กุยโด้เขียนเกี่ยวกับพจนานุกรมที่อธิบายทุกอย่าง

อัพเดทดูความคิดเห็น:

มันไม่สแกนโค้ดสองครั้ง แต่สแกนรหัสในสองขั้นตอนคือการอ่านและแยก

พิจารณาว่าการแยกวิเคราะห์รหัสบรรทัดนี้ทำงานอย่างไร lexer อ่านข้อความต้นฉบับและแบ่งออกเป็น lexemes "ส่วนประกอบที่เล็กที่สุด" ของไวยากรณ์ ดังนั้นเมื่อมันกระทบกับเส้น

c+=1

มันแบ่งมันออกเป็นสิ่งที่ชอบ

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

ตัวแยกวิเคราะห์ในที่สุดต้องการทำให้สิ่งนี้เป็นทรีแยกวิเคราะห์และดำเนินการ แต่เนื่องจากเป็นการมอบหมายก่อนที่มันจะค้นหาชื่อ c ในพจนานุกรมท้องถิ่นไม่เห็นและแทรกในพจนานุกรมการทำเครื่องหมาย มันไม่ได้กำหนดค่าเริ่มต้น ในภาษาที่รวบรวมอย่างสมบูรณ์มันจะเข้าไปในตารางสัญลักษณ์และรอการแยกวิเคราะห์ แต่เนื่องจากมันไม่มีความหรูหราในรอบที่สอง lexer จึงทำงานพิเศษเล็กน้อยเพื่อทำให้ชีวิตง่ายขึ้นในภายหลัง เท่านั้นจากนั้นจะเห็นผู้ดำเนินการเห็นว่ากฎบอกว่า "ถ้าคุณมีผู้ดำเนินการ + = ด้านซ้ายมือจะต้องได้รับการเริ่มต้น" และพูดว่า "โอ้โห!"

จุดนี่คือมันยังไม่ได้เริ่มแยกวิเคราะห์ของบรรทัดยังไม่ได้เริ่มต้นจริงๆแยกของบรรทัดเลยนี่คือสิ่งที่เกิดขึ้นในการเตรียมการกับการแยกวิเคราะห์จริงดังนั้นตัวนับบรรทัดจึงไม่ก้าวหน้าไปสู่บรรทัดถัดไป ดังนั้นเมื่อมันส่งสัญญาณข้อผิดพลาดมันก็ยังคิดว่ามันอยู่ในบรรทัดก่อนหน้า

อย่างที่ฉันบอกคุณอาจโต้แย้งว่ามันเป็นข้อผิดพลาดในการใช้งาน แต่จริงๆแล้วมันเป็นเรื่องธรรมดา คอมไพเลอร์บางคนมีความซื่อสัตย์ต่อมันมากกว่าและพูดว่า "เกิดข้อผิดพลาดหรือใกล้เคียงกับบรรทัด XXX" แต่อันนี้ไม่มี


1
โอเคขอบคุณสำหรับการตอบกลับของคุณ มันล้างบางสิ่งบางอย่างสำหรับฉันเกี่ยวกับขอบเขตในหลาม อย่างไรก็ตามฉันยังไม่เข้าใจว่าทำไมข้อผิดพลาดเกิดขึ้นที่บรรทัด (A) แทนที่จะเป็นบรรทัด (B) Python สร้างพจนานุกรมขอบเขตตัวแปรไว้ก่อนที่จะรันโปรแกรมหรือไม่?
tba

1
ไม่มันอยู่ในระดับนิพจน์ ฉันจะเพิ่มคำตอบฉันไม่คิดว่าฉันสามารถใส่ความคิดเห็นนี้ได้
Charlie Martin

2
หมายเหตุเกี่ยวกับรายละเอียดการติดตั้งใช้งาน: ใน CPython ขอบเขตโลคัลมักจะไม่ได้รับการจัดการเป็นdictภายในเป็นเพียงอาร์เรย์ ( locals()จะเติมค่า a dictเพื่อส่งคืน แต่การเปลี่ยนแปลงจะไม่สร้างใหม่locals) เฟสการแยกวิเคราะห์กำลังค้นหาแต่ละการมอบหมายให้เป็นโลคัลและการแปลงจากชื่อเป็นตำแหน่งในอาร์เรย์นั้นและใช้ตำแหน่งนั้นเมื่อใดก็ตามที่ชื่อถูกอ้างอิง เมื่อเข้าสู่ฟังก์ชั่นจะไม่มีการกำหนดค่าเริ่มต้นให้กับตัวยึดตำแหน่งและUnboundLocalErrorจะเกิดขึ้นเมื่อมีการอ่านตัวแปรและดัชนีที่เกี่ยวข้องยังคงมีค่าตัวยึดตำแหน่ง
ShadowRanger

44

ดูที่การถอดแยกชิ้นส่วนอาจอธิบายสิ่งที่เกิดขึ้น:

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

ในขณะที่คุณสามารถดู bytecode สำหรับการเข้าถึงเป็นLOAD_FASTและ LOAD_GLOBALB, นี่เป็นเพราะคอมไพเลอร์ได้ระบุว่าได้รับมอบหมายให้ภายในฟังก์ชั่นและจัดเป็นตัวแปรท้องถิ่น กลไกการเข้าถึงสำหรับคนในท้องถิ่นนั้นแตกต่างกันอย่างมากสำหรับกลม - พวกเขาได้รับมอบหมายแบบคงที่ชดเชยในตารางตัวแปรของกรอบความหมายการค้นหาเป็นดัชนีอย่างรวดเร็วมากกว่าการค้นหา dict แพงกว่าสำหรับ globals ด้วยเหตุนี้ Python จึงอ่านprint aบรรทัดว่า "รับค่าของตัวแปรโลคอล 'a' ที่ถืออยู่ในสล็อต 0 และพิมพ์" และเมื่อตรวจพบว่าตัวแปรนี้ยังคงไม่ได้กำหนดค่าเริ่มต้นยกข้อยกเว้น


10

Python มีพฤติกรรมที่น่าสนใจเมื่อคุณลองใช้ความหมายของตัวแปรทั่วโลก ฉันจำรายละเอียดไม่ได้ แต่คุณสามารถอ่านค่าของตัวแปรที่ประกาศในขอบเขต 'ส่วนกลาง' ได้ดี แต่ถ้าคุณต้องการแก้ไขคุณต้องใช้globalคำหลัก ลองเปลี่ยนtest()เป็น:

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

นอกจากนี้สาเหตุที่คุณได้รับข้อผิดพลาดนี้ก็เพราะคุณสามารถประกาศตัวแปรใหม่ภายในฟังก์ชันนั้นด้วยชื่อเดียวกับ 'global' และมันจะแยกจากกันโดยสิ้นเชิง ล่ามคิดว่าคุณกำลังพยายามสร้างตัวแปรใหม่ในขอบเขตนี้ที่เรียกว่าcและแก้ไขทั้งหมดในการดำเนินการครั้งเดียวซึ่งไม่ได้รับอนุญาตใน Python เพราะสิ่งนี้cไม่ได้เริ่มต้นใหม่


ขอบคุณสำหรับการตอบกลับของคุณ แต่ฉันไม่คิดว่ามันจะอธิบายได้ว่าทำไมข้อผิดพลาดเกิดขึ้นที่บรรทัด (A) ซึ่งฉันแค่พยายามพิมพ์ตัวแปร โปรแกรมไม่เคยได้รับการ line (B) ที่มันพยายามที่จะปรับเปลี่ยนตัวแปรที่ยังไม่เริ่มต้น
tba

1
Python จะอ่านแยกวิเคราะห์และเปลี่ยนฟังก์ชั่นทั้งหมดให้เป็นไบต์ภายในก่อนที่จะเริ่มใช้งานโปรแกรมดังนั้นความจริงที่ว่า "เปลี่ยน c เป็นตัวแปรท้องถิ่น" เกิดขึ้นแบบข้อความหลังจากการพิมพ์ค่าไม่เป็นอย่างที่เป็นอยู่
Vatine

6

ตัวอย่างที่ดีที่สุดที่ทำให้ชัดเจนคือ:

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

เมื่อมีการโทรfoo()สิ่งนี้จะเพิ่มขึ้น UnboundLocalErrorแม้ว่าเราจะไม่ไปถึงสายbar=0ดังนั้นไม่ควรสร้างตัวแปรโลจิคัลโลจิคัล

ความลึกลับอยู่ใน " Python เป็นภาษาตีความ " และการประกาศฟังก์ชั่นfooถูกตีความว่าเป็นคำสั่งเดียว (เช่นคำสั่งผสม) มันเพียงตีความมันเป็นใบ้และสร้างขอบเขตท้องถิ่นและทั่วโลก ดังนั้นbarได้รับการยอมรับในขอบเขตท้องถิ่นก่อนที่จะดำเนินการ

สำหรับตัวอย่างเพิ่มเติมเช่นนี้อ่านโพสต์นี้: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

โพสต์นี้ให้คำอธิบายที่สมบูรณ์และการวิเคราะห์ของ Python Scoping ของตัวแปร:


5

นี่คือลิงก์สองลิงก์ที่อาจช่วยได้

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

ลิงก์หนึ่งอธิบายถึงข้อผิดพลาด UnboundLocalError ลิงค์สองสามารถช่วยด้วยการเขียนฟังก์ชั่นการทดสอบของคุณอีกครั้ง จากลิงก์สองปัญหาดั้งเดิมอาจถูกเขียนใหม่เป็น:

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

นี่ไม่ใช่คำตอบที่ตรงกับคำถามของคุณ แต่มีความสัมพันธ์กันอย่างใกล้ชิดเนื่องจากเป็น gotcha อีกอันที่เกิดจากความสัมพันธ์ระหว่างขอบเขตการมอบหมายเพิ่มเติมและขอบเขตหน้าที่

ในกรณีส่วนใหญ่คุณมักจะคิดว่าการมอบหมายเพิ่มเติม ( a += b) เท่ากับการมอบหมายอย่างง่าย ( a = a + b) เป็นไปได้ที่จะมีปัญหากับเรื่องนี้แม้ว่าในมุมหนึ่ง ให้ฉันอธิบาย:

วิธีการทำงานอย่างง่ายของ Python นั้นหมายความว่าหากaส่งผ่านไปยังฟังก์ชั่น (เช่นfunc(a)โปรดทราบว่า Python อยู่เสมอโดยผ่านการอ้างอิง) จากนั้นa = a + bจะไม่แก้ไขสิ่งaที่ส่งผ่านเข้ามา แต่มันจะแก้ไขตัวชี้aเฉพาะที่

แต่ถ้าคุณใช้a += bบางครั้งมันจะถูกนำไปใช้เป็น:

a = a + b

หรือบางครั้ง (ถ้ามีวิธีการ) เป็น:

a.__iadd__(b)

ในกรณีแรก (ตราบใดที่aไม่ได้ประกาศเป็นโกลบอล) จะไม่มีผลข้างเคียงใด ๆ ที่อยู่นอกขอบเขตท้องถิ่นเนื่องจากการมอบหมายให้aเป็นเพียงการอัปเดตตัวชี้

ในกรณีที่สองaจะแก้ไขตัวเองจริงดังนั้นการอ้างอิงทั้งหมดaจะชี้ไปที่เวอร์ชันที่แก้ไข นี่แสดงให้เห็นโดยรหัสต่อไปนี้:

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

ดังนั้นเคล็ดลับคือการหลีกเลี่ยงการมอบหมายเพิ่มเติมในข้อโต้แย้งฟังก์ชั่น (ฉันพยายามที่จะใช้มันสำหรับตัวแปรโลคัล / วง) ใช้การมอบหมายอย่างง่ายและคุณจะปลอดภัยจากพฤติกรรมที่ไม่ชัดเจน


2

Python interpreter จะอ่านฟังก์ชั่นเป็นหน่วยที่สมบูรณ์ ฉันคิดว่ามันเป็นการอ่านในสองรอบครั้งหนึ่งเพื่อรวบรวมการปิด (ตัวแปรท้องถิ่น) จากนั้นอีกครั้งเพื่อเปลี่ยนเป็นรหัสไบต์

เนื่องจากฉันแน่ใจว่าคุณได้รับทราบแล้วชื่อใด ๆ ที่ใช้ทางด้านซ้ายของ '=' จะเป็นตัวแปรท้องถิ่นโดยปริยาย มากกว่าหนึ่งครั้งที่ฉันถูกจับได้โดยเปลี่ยนการเข้าถึงตัวแปรเป็น + = และมันก็เป็นตัวแปรที่แตกต่างกันในทันใด

ฉันอยากจะชี้ให้เห็นว่ามันไม่ได้เกี่ยวอะไรกับขอบเขตระดับโลกโดยเฉพาะ คุณได้รับพฤติกรรมเดียวกันกับฟังก์ชั่นที่ซ้อนกัน


2

c+=1 ได้รับมอบหมาย cหลามถือว่าตัวแปรที่ได้รับมอบหมายมีในท้องถิ่น แต่ในกรณีนี้มันยังไม่ได้รับการประกาศในประเทศ

อาจใช้globalหรือnonlocalคำหลัก

nonlocal ใช้งานได้ใน python 3 เท่านั้นดังนั้นหากคุณใช้ python 2 และไม่ต้องการทำให้ตัวแปรเป็นแบบโกลบอลคุณสามารถใช้อ็อบเจกต์ที่ผันแปรได้:

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

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

test()

1

วิธีที่ดีที่สุดในการเข้าถึงตัวแปรคลาสคือการใช้ชื่อคลาสโดยตรง

class Employee:
    counter=0

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

0

ในไพ ธ อนเรามีการประกาศที่คล้ายกันสำหรับตัวแปรท้องถิ่นทุกประเภทตัวแปรคลาสและตัวแปรทั่วโลก เมื่อคุณอ้างถึงตัวแปรทั่วโลกจากเมธอดไพ ธ อนคิดว่าคุณกำลังอ้างถึงตัวแปรจากเมธอดของตัวเองซึ่งยังไม่ได้กำหนดและทำให้เกิดข้อผิดพลาด ในการอ้างอิงตัวแปรทั่วโลกเราต้องใช้ globals () ['variableName']

ในกรณีของคุณใช้ globals () ['a], globals () [' b '] และ globals () [' c '] แทน a, b และ c ตามลำดับ


0

ปัญหาเดียวกันทำให้ฉันรำคาญใจ การใช้nonlocalและglobalสามารถแก้ปัญหาได้
อย่างไรก็ตามความสนใจที่จำเป็นสำหรับการใช้งานnonlocalมันจะทำงานสำหรับฟังก์ชั่นที่ซ้อนกัน อย่างไรก็ตามในระดับโมดูลมันไม่ทำงาน ดูตัวอย่างได้ที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.