Tạo các hàm trong một vòng lặp


102

Tôi đang cố gắng tạo các hàm bên trong vòng lặp:

functions = []

for i in range(3):
    def f():
        return i

    # alternatively: f = lambda: i

    functions.append(f)

Vấn đề là tất cả các chức năng cuối cùng đều giống nhau. Thay vì trả về 0, 1 và 2, cả ba hàm đều trả về 2:

print([f() for f in functions])
# expected output: [0, 1, 2]
# actual output:   [2, 2, 2]

Tại sao điều này lại xảy ra và tôi nên làm gì để có được 3 hàm khác nhau xuất ra lần lượt là 0, 1 và 2?


4
như một lời nhắc nhở cho bản thân mình: docs.python-guide.org/en/latest/writing/gotchas/...
Chuntao Lu

Câu trả lời:


167

Bạn đang gặp sự cố với ràng buộc muộn - mỗi hàm tìm kiếm icàng muộn càng tốt (do đó, khi được gọi sau khi kết thúc vòng lặp, isẽ được đặt thành 2).

Dễ dàng sửa chữa bằng cách buộc ràng buộc sớm: thay đổi def f():thành def f(i=i):như thế này:

def f(i=i):
    return i

Giá trị mặc định (bên phải itrong i=ilà một giá trị mặc định cho tên tham số i, đó là trái tay itrong i=i) đều nhìn lên defthời gian, không phải ở callthời gian, vì vậy về cơ bản họ là một cách để tìm cách cụ thể cho đầu ràng buộc.

Nếu bạn lo lắng về fviệc nhận được thêm một đối số (và do đó có khả năng bị gọi nhầm), có một cách phức tạp hơn liên quan đến việc sử dụng bao đóng làm "nhà máy chức năng":

def make_f(i):
    def f():
        return i
    return f

và trong vòng lặp của bạn sử dụng f = make_f(i)thay cho defcâu lệnh.


7
làm thế nào để bạn biết làm thế nào để sửa chữa những điều này?
alwbtc

3
@alwbtc đó chủ yếu chỉ là trải nghiệm, hầu hết mọi người đều đã phải đối mặt với những điều này tại một số thời điểm.
ruohola

Bạn có thể giải thích tại sao nó hoạt động được không? (Bạn cứu tôi khi gọi lại được tạo trong vòng lặp, các đối số luôn là phần cuối cùng của vòng lặp, vì vậy cảm ơn bạn!)
Vincent Bénet

20

Giải thích

Vấn đề ở đây là giá trị của ikhông được lưu khi hàm fđược tạo. Thay vào đó, ftìm kiếm giá trị của ithời điểm nó được gọi .

Nếu bạn nghĩ về nó, hành vi này có ý nghĩa hoàn hảo. Trên thực tế, đó là cách hợp lý duy nhất mà các chức năng có thể hoạt động. Hãy tưởng tượng bạn có một hàm truy cập một biến toàn cục, như sau:

global_var = 'foo'

def my_function():
    print(global_var)

global_var = 'bar'
my_function()

Khi bạn đọc mã này, tất nhiên - bạn sẽ mong đợi nó in "bar", không phải "foo", vì giá trị của global_varđã thay đổi sau khi hàm được khai báo. Điều tương tự cũng đang xảy ra trong mã của riêng bạn: Vào thời điểm bạn gọi f, giá trị của iđã thay đổi và được đặt thành2 .

Giải pháp

Thực tế có nhiều cách để giải quyết vấn đề này. Dưới đây là một số tùy chọn:

  • Buộc ràng buộc sớm của ibằng cách sử dụng nó làm đối số mặc định

    Không giống như các biến đóng (như i), các đối số mặc định được đánh giá ngay lập tức khi hàm được định nghĩa:

    for i in range(3):
        def f(i=i):  # <- right here is the important bit
            return i
    
        functions.append(f)

    Để cung cấp một chút thông tin chi tiết về cách / tại sao điều này hoạt động: Các đối số mặc định của một hàm được lưu trữ dưới dạng một thuộc tính của hàm; do đó giá trị hiện tại của iđược chụp nhanh và lưu lại.

    >>> i = 0
    >>> def f(i=i):
    ...     pass
    >>> f.__defaults__  # this is where the current value of i is stored
    (0,)
    >>> # assigning a new value to i has no effect on the function's default arguments
    >>> i = 5
    >>> f.__defaults__
    (0,)
  • Sử dụng một nhà máy chức năng để nắm bắt giá trị hiện tại của imột lần đóng

    Gốc của vấn đề của bạn ilà một biến số có thể thay đổi. Chúng ta có thể giải quyết vấn đề này bằng cách tạo một biến khác được đảm bảo sẽ không bao giờ thay đổi - và cách dễ nhất để làm điều này là đóng :

    def f_factory(i):
        def f():
            return i  # i is now a *local* variable of f_factory and can't ever change
        return f
    
    for i in range(3):           
        f = f_factory(i)
        functions.append(f)
  • Sử dụng functools.partialđể ràng buộc giá trị hiện tại của iđểf

    functools.partialcho phép bạn đính kèm các đối số vào một hàm hiện có. Theo một cách nào đó, nó cũng là một loại nhà máy chức năng.

    import functools
    
    def f(i):
        return i
    
    for i in range(3):    
        f_with_i = functools.partial(f, i)  # important: use a different variable than "f"
        functions.append(f_with_i)

Lưu ý: Các giải pháp này chỉ hoạt động nếu bạn gán giá trị mới cho biến. Nếu bạn sửa đổi đối tượng được lưu trữ trong biến, bạn sẽ gặp lại vấn đề tương tự:

>>> i = []  # instead of an int, i is now a *mutable* object
>>> def f(i=i):
...     print('i =', i)
...
>>> i.append(5)  # instead of *assigning* a new value to i, we're *mutating* it
>>> f()
i = [5]

Lưu ý rằng cách ivẫn thay đổi mặc dù chúng tôi đã biến nó thành một đối số mặc định! Nếu mã của bạn thay đổi i , thì bạn phải liên kết một bản sao của ihàm của mình, như sau:

  • def f(i=i.copy()):
  • f = f_factory(i.copy())
  • f_with_i = functools.partial(f, i.copy())
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.