CodeGym /Khóa học Java /Python SELF VI /Lỗi chuẩn, phần 2

Lỗi chuẩn, phần 2

Python SELF VI
Mức độ , Bài học
Có sẵn

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ặc lambda), 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ặc lambda), 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ệnh global trong def 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.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION