UnboundLocalError trên biến cục bộ khi được gán lại sau lần sử dụng đầu tiên


208

Đoạn mã sau hoạt động như mong đợi trong cả Python 2.5 và 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()

Tuy nhiên, khi tôi bỏ dòng (B) , tôi nhận được một UnboundLocalError: 'c' not assigneddòng (A) . Các giá trị của abđược in chính xác. Điều này khiến tôi hoàn toàn bối rối vì hai lý do:

  1. Tại sao có lỗi thời gian chạy được ném tại dòng (A) vì câu lệnh sau trên dòng (B) ?

  2. Tại sao các biến abđược in như mong đợi, trong khi cgây ra lỗi?

Lời giải thích duy nhất tôi có thể đưa ra là một địa phương biến cđược tạo ra bởi sự phân công c+=1, trong đó có tiền lệ trong biến "toàn cầu" cngay cả trước khi biến cục bộ được tạo ra. Tất nhiên, nó không có ý nghĩa đối với một biến để "đánh cắp" phạm vi trước khi nó tồn tại.

Ai đó có thể vui lòng giải thích hành vi này?

Câu trả lời:


215

Python xử lý các biến trong các hàm khác nhau tùy thuộc vào việc bạn gán giá trị cho chúng từ bên trong hay bên ngoài hàm. Nếu một biến được gán trong một hàm, nó được coi theo mặc định là biến cục bộ. Do đó, khi bạn bỏ ghi chú dòng bạn đang cố tham chiếu biến cục bộ ctrước khi bất kỳ giá trị nào được gán cho nó.

Nếu bạn muốn biến ctham chiếu đến toàn cục c = 3được gán trước hàm, hãy đặt

global c

là dòng đầu tiên của hàm.

Đối với python 3, hiện đã có

nonlocal c

mà bạn có thể sử dụng để tham khảo phạm vi hàm kèm theo gần nhất có một cbiến.


3
Cảm ơn. Câu hỏi nhanh. Có phải điều này ngụ ý rằng Python quyết định phạm vi của từng biến trước khi chạy một chương trình? Trước khi chạy một chức năng?
tba

7
Quyết định phạm vi biến được thực hiện bởi trình biên dịch, thường chạy một lần khi bạn khởi động chương trình lần đầu tiên. Tuy nhiên, điều đáng lưu ý là trình biên dịch cũng có thể chạy sau nếu bạn có câu lệnh "eval" hoặc "exec" trong chương trình của mình.
Greg Hewgill

2
OK cảm ơn bạn. Tôi đoán "ngôn ngữ được giải thích" không ngụ ý nhiều như tôi nghĩ.
tba

1
À, từ khóa 'không nhắm mục tiêu' chính xác là thứ tôi đang tìm kiếm, có vẻ như Python đã thiếu thứ này. Có lẽ đây là 'thác' qua từng phạm vi kèm theo nhập biến bằng cách sử dụng từ khóa này?
Brendan

6
@brainfsck: dễ hiểu nhất nếu bạn phân biệt giữa "tra cứu" và "gán" một biến. Tra cứu rơi trở lại phạm vi cao hơn nếu không tìm thấy tên trong phạm vi hiện tại. Việc chuyển nhượng luôn được thực hiện trong phạm vi cục bộ (trừ khi bạn sử dụng globalhoặc nonlocalđể thực hiện chuyển nhượng toàn cầu hoặc không nhắm mục tiêu)
Steven

71

Python hơi kỳ lạ ở chỗ nó giữ mọi thứ trong từ điển cho các phạm vi khác nhau. Bản gốc a, b, c nằm trong phạm vi trên cùng và do đó trong từ điển cao nhất đó. Các chức năng có từ điển riêng của mình. Khi bạn tiếp cận print(a)print(b)phát biểu, không có gì có tên đó trong từ điển, vì vậy Python tìm danh sách và tìm thấy chúng trong từ điển toàn cầu.

Bây giờ chúng ta nhận c+=1được, tất nhiên, tương đương với c=c+1. Khi Python quét dòng đó, nó báo "aha, có một biến có tên c, tôi sẽ đưa nó vào từ điển phạm vi cục bộ của mình." Sau đó, khi nó tìm kiếm một giá trị cho c cho c ở phía bên phải của bài tập, nó tìm thấy biến cục bộ có tên c , chưa có giá trị nào và do đó ném lỗi.

Câu lệnh global cđược đề cập ở trên chỉ đơn giản nói với trình phân tích cú pháp rằng nó sử dụng ctừ phạm vi toàn cầu và do đó không cần một cái mới.

Lý do nó nói rằng có một vấn đề trên dòng đó là vì nó đang tìm kiếm tên một cách hiệu quả trước khi nó cố gắng tạo mã, và vì vậy trong một số ý nghĩa không nghĩ rằng nó thực sự đang thực hiện dòng đó. Tôi cho rằng đó là một lỗi sử dụng, nhưng nói chung, đó là một cách thực hành tốt để chỉ học cách không quá coi trọng các thông điệp của nhà soạn nhạc .

Nếu cảm thấy thoải mái, có lẽ tôi đã dành một ngày để đào bới và thử nghiệm vấn đề tương tự này trước khi tôi tìm thấy thứ gì đó mà Guido đã viết về từ điển giải thích mọi thứ.

Cập nhật, xem bình luận:

Nó không quét mã hai lần, nhưng nó quét mã theo hai giai đoạn, lexing và phân tích cú pháp.

Xem xét cách phân tích của dòng mã này hoạt động. Nhà từ vựng đọc văn bản nguồn và chia nó thành các từ vựng, "thành phần nhỏ nhất" của ngữ pháp. Vì vậy, khi nó chạm dòng

c+=1

nó phá vỡ nó thành một cái gì đó như

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

Trình phân tích cú pháp cuối cùng muốn biến nó thành một cây phân tích cú pháp và thực thi nó, nhưng vì nó là một nhiệm vụ, nên trước đó, nó tìm tên c trong từ điển cục bộ, không nhìn thấy nó và chèn nó vào từ điển, đánh dấu nó như chưa được khởi tạo. Trong một ngôn ngữ được biên dịch đầy đủ, nó sẽ chỉ đi vào bảng biểu tượng và chờ phân tích cú pháp, nhưng vì nó không có sự sang trọng của lần thứ hai, nên lexer làm thêm một chút để cuộc sống dễ dàng hơn về sau. Chỉ sau đó, nó nhìn thấy HOẠT ĐỘNG, thấy rằng các quy tắc nói "nếu bạn có một toán tử + = phía bên trái phải được khởi tạo" và nói "Rất tiếc!"

Vấn đề ở đây là nó chưa thực sự bắt đầu phân tích cú pháp . Đây là tất cả các loại chuẩn bị xảy ra cho phân tích cú pháp thực tế, vì vậy bộ đếm dòng đã không chuyển sang dòng tiếp theo. Do đó, khi nó báo hiệu lỗi, nó vẫn nghĩ nó ở dòng trước.

Như tôi nói, bạn có thể cho rằng đó là một lỗi sử dụng, nhưng thực ra đây là một điều khá phổ biến. Một số trình biên dịch trung thực hơn về nó và nói "lỗi trên hoặc xung quanh dòng XXX", nhưng trình biên dịch này thì không.


1
Được rồi cảm ơn bạn đã phản hồi của bạn; nó xóa một số thứ cho tôi về phạm vi của con trăn. Tuy nhiên, tôi vẫn không hiểu tại sao lỗi được đưa ra ở dòng (A) thay vì dòng (B). Python có tạo từ điển phạm vi biến của nó TRƯỚC KHI chạy chương trình không?
tba

1
Không, đó là ở cấp độ biểu hiện. Tôi sẽ thêm vào câu trả lời, tôi không nghĩ rằng tôi có thể phù hợp với điều này trong một bình luận.
Charlie Martin

2
Lưu ý về chi tiết triển khai: Trong CPython, phạm vi cục bộ thường không được xử lý như một dict, bên trong nó chỉ là một mảng ( locals()sẽ tạo ra một dicttrả về, nhưng thay đổi thành không tạo mới locals). Giai đoạn phân tích là tìm từng phép gán cho một cục bộ và chuyển đổi từ tên này sang vị trí trong mảng đó và sử dụng vị trí đó bất cứ khi nào tên được tham chiếu. Khi vào hàm, các địa phương không đối số được khởi tạo cho một trình giữ chỗ và UnboundLocalErrors xảy ra khi một biến được đọc và chỉ mục liên quan của nó vẫn có giá trị giữ chỗ.
ShadowRanger

44

Nhìn vào sự tháo gỡ có thể làm rõ những gì đang xảy ra:

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

Như bạn có thể thấy, mã byte để truy cập a là LOAD_FASTvà cho b , LOAD_GLOBAL. Điều này là do trình biên dịch đã xác định rằng a được gán cho trong hàm và phân loại nó là một biến cục bộ. Cơ chế truy cập cho người dân địa phương về cơ bản là khác nhau đối với toàn cầu - họ được gán tĩnh một phần bù trong bảng biến của khung, có nghĩa là tra cứu là một chỉ mục nhanh, thay vì tra cứu chính tả đắt hơn như đối với toàn cầu. Do đó, Python đang đọc print adòng này là "lấy giá trị của biến cục bộ 'a' được giữ trong khe 0 và in nó" và khi phát hiện ra biến này vẫn chưa được xác định, sẽ đưa ra một ngoại lệ.


10

Python có hành vi khá thú vị khi bạn thử ngữ nghĩa biến toàn cầu truyền thống. Tôi không nhớ chi tiết, nhưng bạn có thể đọc giá trị của một biến được khai báo trong phạm vi 'toàn cầu', nhưng nếu bạn muốn sửa đổi nó, bạn phải sử dụng globaltừ khóa. Hãy thử thay đổi test()điều này:

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

Ngoài ra, lý do bạn gặp phải lỗi này là vì bạn cũng có thể khai báo một biến mới bên trong hàm đó có cùng tên với một 'toàn cầu' và nó sẽ hoàn toàn tách biệt. Trình thông dịch nghĩ rằng bạn đang cố gắng tạo một biến mới trong phạm vi này được gọi cvà sửa đổi tất cả trong một thao tác, điều này không được phép trong Python vì điều này mới ckhông được khởi tạo.


Cảm ơn phản hồi của bạn, nhưng tôi không nghĩ nó giải thích tại sao lỗi được ném ở dòng (A), trong đó tôi chỉ đang cố gắng in một biến. Chương trình không bao giờ được chuyển đến dòng (B) khi nó đang cố gắng sửa đổi một biến không được khởi tạo.
tba

1
Python sẽ đọc, phân tích và biến toàn bộ hàm thành mã byte nội bộ trước khi nó bắt đầu chạy chương trình, do đó, việc "biến c thành biến cục bộ" xảy ra bằng văn bản sau khi in giá trị không thành vấn đề.
Vatine

6

Ví dụ tốt nhất làm cho nó rõ ràng là:

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

khi gọi foo(), điều này cũng tăng lên UnboundLocalError mặc dù chúng ta sẽ không bao giờ đạt được đường dây bar=0, vì vậy biến cục bộ không bao giờ được tạo.

Điều bí ẩn nằm ở " Python là một ngôn ngữ được giải thích " và việc khai báo hàm foođược hiểu là một câu lệnh đơn (nghĩa là một câu lệnh ghép), nó chỉ diễn giải nó một cách ngu ngốc và tạo ra phạm vi địa phương và toàn cầu. Vì vậy, barđược công nhận trong phạm vi địa phương trước khi thực hiện.

Để biết thêm ví dụ như thế này Đọc bài đăng này: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

Bài đăng này cung cấp Mô tả và Phân tích đầy đủ về Phạm vi biến của Python:


5

Đây là hai liên kết có thể giúp

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-feft-with-output-parameter-call-by-reference

liên kết một mô tả lỗi UnboundLocalError. Liên kết hai có thể giúp viết lại chức năng kiểm tra của bạn. Dựa trên liên kết hai, vấn đề ban đầu có thể được viết lại thành:

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

Đây không phải là một câu trả lời trực tiếp cho câu hỏi của bạn, nhưng nó có liên quan chặt chẽ, vì nó là một vấn đề khác gây ra bởi mối quan hệ giữa nhiệm vụ gia tăng và phạm vi chức năng.

Trong hầu hết các trường hợp, bạn có xu hướng nghĩ về phép gán tăng ( a += b) chính xác tương đương với phép gán đơn giản ( a = a + b). Có thể gặp một số rắc rối với điều này mặc dù, trong một trường hợp góc. Hãy để tôi giải thích:

Cách thức chuyển nhượng đơn giản của Python có nghĩa là nếu ađược truyền vào một hàm (như func(a); lưu ý rằng Python luôn luôn là tham chiếu), thì a = a + bsẽ không sửa đổi acái được truyền vào. Thay vào đó, nó sẽ chỉ sửa đổi con trỏ cục bộ thành a.

Nhưng nếu bạn sử dụng a += b, thì đôi khi nó được triển khai như sau:

a = a + b

hoặc đôi khi (nếu phương thức tồn tại) là:

a.__iadd__(b)

Trong trường hợp đầu tiên (miễn alà không được khai báo toàn cầu), không có tác dụng phụ ngoài phạm vi cục bộ, vì việc gán cho achỉ là một cập nhật con trỏ.

Trong trường hợp thứ hai, athực sự sẽ tự sửa đổi, vì vậy tất cả các tham chiếu asẽ trỏ đến phiên bản sửa đổi. Điều này được thể hiện bằng mã sau:

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

Vì vậy, mẹo là để tránh gán tăng cho các đối số hàm (tôi cố gắng chỉ sử dụng nó cho các biến cục bộ / vòng lặp). Sử dụng bài tập đơn giản, và bạn sẽ an toàn trước hành vi mơ hồ.


2

Trình thông dịch Python sẽ đọc một hàm dưới dạng một đơn vị hoàn chỉnh. Tôi nghĩ về nó như đọc nó trong hai lần, một lần để thu thập đóng của nó (các biến cục bộ), sau đó một lần nữa để biến nó thành mã byte.

Như tôi chắc chắn rằng bạn đã biết, bất kỳ tên nào được sử dụng ở bên trái của '=' đều là một biến cục bộ. Đã hơn một lần tôi bị bắt gặp khi thay đổi quyền truy cập biến thành + = và đột nhiên nó là một biến khác.

Tôi cũng muốn chỉ ra rằng nó thực sự không liên quan gì đến phạm vi toàn cầu. Bạn nhận được hành vi tương tự với các chức năng lồng nhau.


2

c+=1gán c, python giả định các biến được gán là cục bộ, nhưng trong trường hợp này nó đã không được khai báo cục bộ.

Hoặc sử dụng globalhoặc nonlocaltừ khóa.

nonlocal chỉ hoạt động trong python 3, vì vậy nếu bạn đang sử dụng python 2 và không muốn biến toàn cầu thành biến, bạn có thể sử dụng một đối tượng có thể thay đổi:

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

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

test()

1

Cách tốt nhất để tiếp cận biến lớp là nhập trực tiếp theo tên lớp

class Employee:
    counter=0

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

0

Trong python, chúng ta có khai báo tương tự cho tất cả các loại biến cục bộ, biến lớp và biến toàn cục. Khi bạn tham chiếu biến toàn cục từ phương thức, python nghĩ rằng bạn thực sự đang tham chiếu biến từ chính phương thức chưa được xác định và do đó đưa ra lỗi. Để tham chiếu biến toàn cục, chúng ta phải sử dụng globalals () ['biếnName'].

trong trường hợp của bạn, hãy sử dụng globalals () ['a], globalals () [' b '] và globalals () [' c '] thay vì a, b và c tương ứng.


0

Vấn đề tương tự làm phiền tôi. Sử dụng nonlocalglobalcó thể giải quyết vấn đề.
Tuy nhiên, sự chú ý cần thiết cho việc sử dụng nonlocal, nó hoạt động cho các chức năng lồng nhau. Tuy nhiên, ở cấp độ mô-đun, nó không hoạt động. Xem ví dụ ở đây.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.