8.1 Hiểu sai quy tắc phạm vi trong Python
Phạm vi trong Python dựa trên cái gọi là quy tắc LEGB, đây là viết tắt của:
-
Local
(tên, được gán bằng bất kỳ cách nào trong hàm (def
hoặclambda
), và không được khai báo là toàn cục trong hàm này); Enclosing
(tên trong phạm vi cục bộ của bất kỳ hàm bao gồm tĩnh (def
hoặclambda
), từ trong ra ngoài);Global
(tên, được gán ở cấp độ trên cùng của tệp mô-đun, hoặc thông qua việc thực thi lệnhglobal
trongdef
bên trong tệp);-
Built-in
(tên, được gán trước trong mô-đun các tên tích hợp sẵn:open
,range
,SyntaxError
, và những thứ khác).
Có vẻ đơn giản đúng không?
Tuy nhiên có một vài tinh tế về cách điều này hoạt động trong Python, điều này dẫn chúng ta đến vấn đề lập trình phức tạp trong Python. Hãy xem xét ví dụ sau:
x = 10
def foo():
x += 1
print(x)
foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment
Vấn đề là gì?
Lỗi trên xảy ra vì, khi bạn gán giá trị cho biến trong phạm vi
, Python
tự động coi nó là cục bộ
trong phạm vi này và ẩn bất kỳ biến
nào có tên tương tự trong bất kỳ
phạm vi bên trên nào.
Vậy nhiều người ngạc nhiên khi họ nhận được UnboundLocalError
trong mã đã hoạt động trước đó khi mã
này được sửa đổi bằng cách thêm một phép gán ở đâu đó trong thân của hàm.
Đặc điểm này đặc biệt làm rối loạn các nhà phát triển khi sử dụng danh sách. Hãy xem xét ví dụ sau:
lst = [1, 2, 3]
def foo1():
lst.append(5) # Cái này hoạt động bình thường...
foo1()
print(lst)
[1, 2, 3, 5]
lst = [1, 2, 3]
def foo2():
lst += [5] # ... còn cái này thì bị lỗi!
foo2()
Traceback (most recent call last):
File "", line 1, in
File "", line 2, in foo
UnboundLocalError: local variable 'lst' referenced before assignment
Tại sao foo2
bị lỗi, trong khi foo1
hoạt động bình thường?
Câu trả lời tương tự như trong ví dụ trước, nhưng theo quan điểm phổ biến, tình huống này tinh tế hơn. foo1
không áp dụng phép gán cho lst
, nhưng foo2
có. Nhớ rằng lst += [5]
thực sự chỉ là
cách viết tắt cho lst = lst + [5]
, chúng ta thấy rằng chúng ta đang cố gắng gán giá trị cho lst
(do đó Python giả định
rằng nó trong phạm vi cục bộ). Tuy nhiên giá trị chúng ta muốn gán cho lst
dựa trên
chính lst
(một lần nữa, bây giờ giả định rằng nó trong phạm vi cục bộ), và nó chưa được
xác định. Vậy nên chúng ta gặp lỗi.
8.2 Thay đổi danh sách khi đang lặp qua nó
Vấn đề trong mẩu mã sau đây nên khá rõ ràng:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
for i in range(len(numbers)):
if odd(numbers[i]):
del numbers[i] # BAD: Xóa phần tử khỏi danh sách khi đang lặp qua nó
Traceback (most recent call last):
File "", line 2, in
IndexError: list index out of range
Xóa phần tử khỏi danh sách hoặc mảng khi đang lặp qua nó là một vấn đề trong Python, điều này thì bất kỳ nhà phát triển phần mềm có kinh nghiệm nào cũng biết. Nhưng, dù ví dụ trên có thể khá rõ ràng, ngay cả những nhà phát triển có kinh nghiệm cũng có thể mắc phải vấn đề này trong mã phức tạp hơn nhiều.
May mắn thay, Python bao gồm một số mô hình lập trình thanh lịch mà khi dùng đúng cách có thể dẫn đến việc đơn giản hóa và tối ưu hóa mã đáng kể. Kết quả tích cực thêm vào đó là trong mã đơn giản hơn, khả năng mắc phải lỗi tình cờ xóa phần tử khỏi danh sách khi đang lặp qua nó là ít hơn nhiều.
Một trong những mô hình đó là list comprehensions. Ngoài ra, việc hiểu cách hoạt động của list comprehensions đặc biệt hữu ích để tránh vấn đề cụ thể này, như được chỉ ra trong hiện thực thay thế của mã trên, hoạt động một cách tuyệt vời:
odd = lambda x: bool(x % 2)
numbers = [n for n in range(10)]
numbers[:] = [n for n in numbers if not odd(n)] # chỉ chọn các phần tử mới
print(numbers)
# [0, 2, 4, 6, 8]
Quan trọng!
Không có gán đối tượng danh sách mới ở đây. Sử dụng
numbers[:]
là việc gán nhóm giá trị mới cho tất cả các phần tử trong danh sách.
8.3 Không hiểu cách Python liên kết biến trong closures
Xem ví dụ sau:
def create_multipliers():
return [lambda x: i * x for i in range(5)] # Trả về danh sách các hàm!
for multiplier in create_multipliers():
print(multiplier(2))
Bạn có thể mong đợi đầu ra sau:
0
2
4
6
8
Nhưng thực tế bạn sẽ nhận được cái này:
8
8
8
8
8
Ngạc nhiên chưa!
Điều này xảy ra do hàm gán muộn trong Python, có nghĩa là, các giá trị của biến dùng trong closures được tìm kiếm trong thời gian gọi hàm nội bộ.
Vậy nên trong mã trên, bất cứ khi nào gọi một trong các hàm trả về, giá trị
i
được tìm kiếm trong phạm vi xung quanh trong thời gian gọi (và lúc đó vòng lặp đã kết thúc, vì vậy i
đã được
gán giá trị cuối cùng — giá trị 4).
Giải pháp cho vấn đề phổ biến này với Python sẽ như sau:
def create_multipliers():
return [lambda x, i = i : i * x for i in range(5)]
for multiplier in create_multipliers():
print(multiplier(2))
# 0
# 2
# 4
# 6
# 8
Voilà! Chúng ta sử dụng các đối số mặc định để sinh các hàm ẩn danh để đạt được hành vi mong muốn. Một số người gọi đây là một giải pháp thanh lịch. Một số thì thấy nó là tinh tế. Một số thì ghét những thứ kiểu này. Nhưng nếu bạn là một nhà phát triển Python, điều này là quan trọng để hiểu dù sao đi nữa.
GO TO FULL VERSION