0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Khám Phá Quy Tắc LEGB trong Python và Biến Chiếu Bóng

Đăng vào 7 tháng trước

• 6 phút đọc

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

  1. Phạm Vi Biến và Bóng Của Chúng
  2. Phần 1: Ánh Sáng Từ Phạm Vi Địa Phương
  3. Phần 2: Phòng Bao Phủ
  4. Phần 3: Sân Khấu Toàn Cục
  5. Phần 4: Vũ Trụ Xây Dựng
  6. Phần 5: Khi Bóng Lừa Dối - Gán Biến
  7. Phần 6: Ảo Tưởng Của Tính Địa Phương
  8. Phần 7: Khai Báo Ý Định - globalnonlocal
  9. Tắt Đèn
  10. 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 Copy
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 Copy
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 Copy
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, FalseNone. 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:

  1. L ocal (hàm hiện tại)
  2. E nclosing function locals (từ hàm bao phủ bên trong đến bên ngoài)
  3. G lobal (cấp độ cao nhất của mô-đun)
  4. 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 Copy
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:

  1. shadow = 10 thiết lập một shadow toàn cục.
  2. Bên trong outer_lamp(), shadow = 20 tạo một biến mới, địa phương trong phạm vi của outer_lamp. Biến shadow này hoàn toàn tách biệt với shadow toàn cục. Nó không sửa đổi shadow toàn cục.
  3. Bên trong inner_lamp(), khi print(shadow) được gọi, Python tìm kiếm shadow theo quy tắc LEGB. Nó tìm thấy shadow = 20 trong phạm vi bao phủ (outer_lamp), và đó là shadow mà 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 Copy
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:

Copy
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"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 - globalnonlocal

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 Copy
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 Copy
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ý.

pythonkoans.substack.com

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào