Giới Thiệu
Trong Python, việc quản lý phạm vi biến không phải lúc nào cũng đơn giản. Biến trong Python có thể "chiếu bóng" qua các hàm lồng nhau, làm cho việc hiểu rõ quy tắc LEGB (Local, Enclosing, Global, Built-in) trở nên cần thiết. Bài viết này sẽ giúp bạn hiểu rõ cách hoạt động của quy tắc LEGB, các khái niệm liên quan đến closures và lý do tại sao biến đôi khi có thể hành xử như những cái bóng.
Mục Lục
- Phạm Vi Biến và Bóng Của Chúng
- Phần 1: Ánh Sáng Từ Phạm Vi Địa Phương
- Phần 2: Phòng Bao Phủ
- Phần 3: Sân Khấu Toàn Cục
- Phần 4: Vũ Trụ Xây Dựng
- Phần 5: Khi Bóng Lừa Dối - Gán Biến
- Phần 6: Ảo Tưởng Của Tính Địa Phương
- Phần 7: Khai Báo Ý Định -
globalvànonlocal - Tắt Đèn
- Câu Hỏi Thường Gặp (FAQ)
Phạm Vi Biến và Bóng Của Chúng
Trong Python, các biến không phải lúc nào cũng ở vị trí mà bạn nghĩ chúng có thể. Khi một hàm được thực thi, nó không chỉ mang theo mã của chính nó mà còn mang theo "bóng" của những tên mà nó đã thấy, tên thuộc về các phạm vi bên ngoài.
Để hiểu điều này, chúng ta cần tìm hiểu cách Python giải quyết tên.
Phần 1: Ánh Sáng Từ Phạm Vi Địa Phương
Hình thức đơn giản nhất của phạm vi là phạm vi địa phương. Khi bạn định nghĩa một biến bên trong một hàm, nó chỉ tồn tại trong quá trình thực thi của hàm đó.
python
def greet():
message = "Xin chào, thế giới!"
print(message)
greet()
print(message) # Điều này sẽ gây lỗi: NameError
Ở đây, message chỉ tồn tại trong hàm greet. Nó giống như một chiếc đèn chỉ sáng trong một căn phòng nhỏ; ánh sáng của nó không vượt ra ngoài ngưỡng cửa.
Phần 2: Phòng Bao Phủ
Bây giờ, hãy mở một cánh cửa khác. Python có khái niệm gọi là phạm vi bao phủ. Điều này áp dụng cho các hàm lồng nhau. Nếu một biến không được tìm thấy trong phạm vi địa phương ngay lập tức, Python sẽ tìm kiếm ra bên ngoài đến bất kỳ hàm bao phủ nào.
Hãy xem ví dụ sau:
python
def outer_function():
outer_message = "Từ phòng bên ngoài."
def inner_function():
print(outer_message)
inner_function()
outer_function()
Khi inner_function được gọi, nó đầu tiên tìm kiếm outer_message trong phạm vi của chính nó. Nó không tìm thấy. Vì vậy, nó tìm trong phạm vi của outer_function, nơi outer_message tồn tại. Điều này hoạt động. Hàm bên trong có thể thấy các biến của hàm bao phủ của nó, giống như thấy ánh sáng của một chiếc đèn trong một căn phòng bên cạnh qua một cánh cửa mở.
Phần 3: Sân Khấu Toàn Cục
Ngoài các phạm vi địa phương và bao phủ, còn có phạm vi toàn cục. Các biến được định nghĩa ở cấp độ cao nhất của một kịch bản hoặc mô-đun là toàn cục. Chúng có thể được truy cập từ bất kỳ đâu trong mô-đun đó.
python
global_message = "Từ thế giới rộng lớn."
def display_global():
print(global_message)
display_global()
Ở đây, display_global có thể truy cập global_message vì nó nằm trong phạm vi toàn cục. Điều này giống như ánh sáng của mặt trời, có thể nhìn thấy từ mọi phòng.
Phần 4: Vũ Trụ Xây Dựng
Cuối cùng, có phạm vi xây dựng. Phạm vi này chứa tất cả các tên mà Python đã định nghĩa trước, chẳng hạn như print, len, str, True, False và None. Những tên này luôn có sẵn.
Thứ tự mà Python tìm kiếm tên được gọi là quy tắc LEGB:
- L ocal (hàm hiện tại)
- E nclosing function locals (từ hàm bao phủ bên trong đến bên ngoài)
- G lobal (cấp độ cao nhất của mô-đun)
- B uilt-in (các tên đã được định nghĩa trước trong Python)
Python sẽ dừng lại ở nơi đầu tiên nó tìm thấy tên.
Phần 5: Khi Bóng Lừa Dối - Gán Biến
Sự phức tạp thực sự phát sinh khi bạn gán giá trị cho một biến trong một phạm vi sâu hơn. Khi Python gặp một câu lệnh gán, nó giả định bạn có ý định tạo hoặc sửa đổi một biến trong phạm vi hiện tại, trừ khi được chỉ định rõ ràng.
Xem lại ví dụ sau:
python
shadow = 10 # Bóng toàn cục
def outer_lamp():
shadow = 20 # 'shadow' này chỉ tồn tại trong outer_lamp()
def inner_lamp():
print(shadow) # 'shadow' này tham chiếu đến bóng của outer_lamp()
return inner_lamp
lamp = outer_lamp()
lamp() # Output: 20
Dưới đây là phần quan trọng:
shadow = 10thiết lập mộtshadowtoàn cục.- Bên trong
outer_lamp(),shadow = 20tạo một biến mới, địa phương trong phạm vi củaouter_lamp. Biếnshadownày hoàn toàn tách biệt vớishadowtoàn cục. Nó không sửa đổishadowtoàn cục. - Bên trong
inner_lamp(), khiprint(shadow)được gọi, Python tìm kiếmshadowtheo quy tắc LEGB. Nó tìm thấyshadow = 20trong phạm vi bao phủ (outer_lamp), và đó làshadowmà nó sử dụng.
Gọi lamp(), thực thi inner_lamp(), do đó in ra 20. Biến shadow toàn cục (vẫn là 10) không bị ảnh hưởng, và biến shadow trong outer_lamp() chiếu bóng riêng của nó, độc lập với đèn toàn cục.
Phần 6: Ảo Tưởng Của Tính Địa Phương
Bây giờ, một khía cạnh tinh tế:
python
shadow = "toàn cục"
def inner():
print(shadow)
shadow = "địa phương"
inner()
Điều gì xảy ra ở đây?
Python sẽ ném ra một lỗi:
UnboundLocalError: không thể truy cập biến địa phương 'shadow' trước khi gán
Tại sao?
Bởi vì Python thấy câu lệnh gán shadow = "địa phương" và giả định rằng shadow phải là địa phương trong inner. Nó không tìm kiếm bên ngoài nữa. Vì vậy, khi bạn cố gắng đọc shadow trước khi gán giá trị cho nó, Python bị nhầm lẫn. Có một shadow địa phương, nhưng nó chưa có giá trị.
Trong Python, bất kỳ gán nào cho một biến bên trong hàm đều làm cho biến đó trở thành địa phương đối với hàm đó, trừ khi được khai báo rõ ràng khác đi.
Phần 7: Khai Báo Ý Định - global và nonlocal
Nếu chúng ta muốn sử dụng hoặc sửa đổi một biến từ phạm vi bên ngoài, chúng ta phải khai báo ý định của mình.
Sử dụng global:
python
shadow = 5
def modify():
global shadow
shadow = 10
modify()
print(shadow) # 10
Ở đây, global shadow cho Python biết: “Tôi muốn nói đến shadow từ cấp độ cao nhất của mô-đun.”
Khi làm việc với các hàm lồng nhau, và sử dụng nonlocal:
python
def outer():
shadow = 5
def inner():
nonlocal shadow
shadow = 10
inner()
print(shadow)
outer() # 10
Không có nonlocal, việc gán sẽ tạo ra một shadow mới bên trong inner.
Với nó, Python hiểu: sử dụng biến từ hàm bao phủ.
Tắt Đèn
Chiếc đèn thứ hai của Bậc Thầy đã chiếu sáng sự thật: mỗi chiếc đèn đều chiếu bóng riêng của nó. Tương tự, trong Python, mỗi phạm vi có thể định nghĩa các biến của riêng nó, tạo ra những "chiếc đèn" khác nhau của các biến. Nếu không có chỉ dẫn rõ ràng (như nonlocal hoặc global), một câu lệnh gán luôn mặc định vào phạm vi hiện tại, bảo vệ các biến ở cấp độ cao hơn khỏi việc bị sửa đổi không mong muốn.
Hiểu rõ phạm vi không chỉ giúp tránh lỗi mà còn giúp thiết kế mã rõ ràng, dự đoán được và dễ bảo trì. Điều này liên quan đến việc biết nơi biến của bạn thực sự nằm và cách ánh sáng của chúng lan tỏa, hoặc không lan tỏa, vào mã xung quanh.
Nếu bạn thấy bài viết này hữu ích, hãy cân nhắc đăng ký hoặc chia sẻ với bạn bè của bạn:
Python Koans | Vivis Dev | Substack
Các bài học Python được gói trong những câu koan. Những câu đố nhỏ, sự thật sâu sắc. Không phải là chuỗi hướng dẫn thông thường của bạn. Nhấp để đọc Python Koans, của Vivis Dev, một ấn phẩm Substack với hàng trăm người đăng ký.