Biến cục bộ trong các hàm lồng nhau


105

Được rồi, chịu đựng tôi với điều này, tôi biết nó trông rất phức tạp, nhưng hãy giúp tôi hiểu chuyện gì đang xảy ra.

from functools import partial

class Cage(object):
    def __init__(self, animal):
        self.animal = animal

def gotimes(do_the_petting):
    do_the_petting()

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        cage = Cage(animal)

        def pet_function():
            print "Mary pets the " + cage.animal + "."

        yield (animal, partial(gotimes, pet_function))

funs = list(get_petters())

for name, f in funs:
    print name + ":", 
    f()

Cung cấp:

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Về cơ bản, tại sao tôi không nhận được ba con vật khác nhau? Không phải cage'đóng gói' vào phạm vi cục bộ của hàm lồng nhau? Nếu không, làm thế nào để một lệnh gọi hàm lồng nhau tìm kiếm các biến cục bộ?

Tôi biết rằng gặp phải những vấn đề kiểu này thường có nghĩa là người ta đang 'làm sai', nhưng tôi muốn hiểu điều gì sẽ xảy ra.


1
Hãy thử for animal in ['cat', 'dog', 'cow']... tôi chắc chắn rằng ai đó sẽ đi cùng và giải thích điều này - đó là một trong những gotcha của Python :)
Jon Clements

Câu trả lời:


114

Hàm lồng nhau tìm kiếm các biến từ phạm vi cha khi được thực thi, không phải khi được định nghĩa.

Phần thân hàm được biên dịch và các biến 'tự do' (không được định nghĩa trong chính hàm bằng phép gán), được xác minh, sau đó được liên kết dưới dạng các ô đóng đối với hàm, với mã sử dụng chỉ mục để tham chiếu đến mỗi ô. pet_functiondo đó có một biến tự do ( cage) sau đó được tham chiếu qua một ô đóng, chỉ số 0. Bản thân bao đóng trỏ đến biến cục bộ cagetrong get_pettershàm.

Khi bạn thực sự gọi hàm, bao đóng đó sẽ được sử dụng để xem giá trị của cagephạm vi xung quanh tại thời điểm bạn gọi hàm . Vấn đề nằm ở đây. Vào thời điểm bạn gọi các hàm của mình, get_pettershàm đã được thực hiện tính toán kết quả của nó. Các cagebiến cục bộ tại một số điểm trong quá trình thực đã được phân công từng 'cow', 'dog''cat'chuỗi, nhưng ở phần cuối của hàm, cagechứa giá trị cuối cùng 'cat'. Do đó, khi bạn gọi từng hàm trả về động, bạn sẽ nhận được giá trị 'cat'được in ra.

Công việc xung quanh là không dựa vào đóng cửa. Thay vào đó, bạn có thể sử dụng một hàm một phần , tạo một phạm vi hàm mới hoặc ràng buộc biến làm giá trị mặc định cho tham số từ khóa .

  • Ví dụ về một phần chức năng, sử dụng functools.partial():

    from functools import partial
    
    def pet_function(cage=None):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, partial(pet_function, cage=cage)))
  • Tạo một ví dụ phạm vi mới:

    def scoped_cage(cage=None):
        def pet_function():
            print "Mary pets the " + cage.animal + "."
        return pet_function
    
    yield (animal, partial(gotimes, scoped_cage(cage)))
  • Ràng buộc biến dưới dạng giá trị mặc định cho tham số từ khóa:

    def pet_function(cage=cage):
        print "Mary pets the " + cage.animal + "."
    
    yield (animal, partial(gotimes, pet_function))

Không cần xác định scoped_cagehàm trong vòng lặp, quá trình biên dịch chỉ diễn ra một lần, không phải trên mỗi lần lặp của vòng lặp.


1
Hôm nay tôi đã đập đầu vào bức tường này 3 tiếng đồng hồ để viết kịch bản cho công việc. Điểm cuối cùng của bạn là rất quan trọng, và là lý do chính tại sao tôi gặp sự cố này. Tôi có các lệnh gọi lại với các bao đóng rất nhiều trong suốt mã của mình, nhưng thử cùng một kỹ thuật trong một vòng lặp là điều khiến tôi gặp phải.
DrEsperanto

12

Sự hiểu biết của tôi là lồng được tìm kiếm trong không gian tên của hàm cha khi hàm pet_ function được tạo ra thực sự được gọi, chứ không phải trước đó.

Vì vậy, khi bạn làm

funs = list(get_petters())

Bạn tạo 3 hàm sẽ tìm lồng được tạo cuối cùng.

Nếu bạn thay thế vòng lặp cuối cùng của mình bằng:

for name, f in get_petters():
    print name + ":", 
    f()

Bạn thực sự sẽ nhận được:

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

6

Điều này bắt nguồn từ những điều sau

for i in range(2): 
    pass

print(i)  # prints 1

sau khi lặp giá trị của iđược lưu trữ một cách lười biếng làm giá trị cuối cùng của nó.

Như một trình tạo, hàm sẽ hoạt động (tức là in lần lượt từng giá trị), nhưng khi chuyển đổi thành một danh sách, nó chạy trên trình tạo , do đó tất cả các lệnh gọi đến cage( cage.animal) đều trả về mèo.


0

Hãy đơn giản hóa câu hỏi. Định nghĩa:

def get_petters():
    for animal in ['cow', 'dog', 'cat']:
        def pet_function():
            return "Mary pets the " + animal + "."

        yield (animal, pet_function)

Sau đó, giống như trong câu hỏi, chúng tôi nhận được:

>>> for name, f in list(get_petters()):
...     print(name + ":", f())

cow: Mary pets the cat.
dog: Mary pets the cat.
cat: Mary pets the cat.

Nhưng nếu chúng ta tránh tạo ra một list()đầu tiên:

>>> for name, f in get_petters():
...     print(name + ":", f())

cow: Mary pets the cow.
dog: Mary pets the dog.
cat: Mary pets the cat.

Chuyện gì vậy? Tại sao sự khác biệt tinh tế này lại thay đổi hoàn toàn kết quả của chúng ta?


Nếu chúng ta nhìn vào list(get_petters()), rõ ràng là từ các địa chỉ bộ nhớ thay đổi, chúng ta thực sự mang lại ba chức năng khác nhau:

>>> list(get_petters())

[('cow', <function get_petters.<locals>.pet_function at 0x7ff2b988d790>),
 ('dog', <function get_petters.<locals>.pet_function at 0x7ff2c18f51f0>),
 ('cat', <function get_petters.<locals>.pet_function at 0x7ff2c14a9f70>)]

Tuy nhiên, hãy xem cellcác hàm này được liên kết với:

>>> for _, f in list(get_petters()):
...     print(f(), f.__closure__)

Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)
Mary pets the cat. (<cell at 0x7ff2c112a9d0: str object at 0x7ff2c3f437f0>,)

>>> for _, f in get_petters():
...     print(f(), f.__closure__)

Mary pets the cow. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a95670>,)
Mary pets the dog. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c1a952f0>,)
Mary pets the cat. (<cell at 0x7ff2b86b5d00: str object at 0x7ff2c3f437f0>,)

Đối với cả hai vòng lặp, cellđối tượng được giữ nguyên trong suốt các lần lặp. Tuy nhiên, như mong đợi, strnó tham chiếu cụ thể khác nhau trong vòng lặp thứ hai. Đối celltượng tham chiếu đến animal, được tạo ra khi get_petters()được gọi. Tuy nhiên, animalthay đổi strđối tượng mà nó đề cập đến khi chạy hàm máy phát điện .

Trong vòng lặp đầu tiên, trong mỗi lần lặp, chúng tôi tạo tất cả các fs, nhưng chúng tôi chỉ gọi chúng sau khi trình tạo get_petters()hoàn toàn hết và một listtrong các hàm đã được tạo.

Trong vòng lặp thứ hai, trong mỗi lần lặp, chúng tôi tạm dừng trình get_petters()tạo và gọi fsau mỗi lần tạm dừng. Do đó, chúng ta kết thúc việc truy xuất giá trị của animaltại thời điểm mà hàm trình tạo bị tạm dừng.

Như @Claudiu đưa ra câu trả lời cho một câu hỏi tương tự :

Ba hàm riêng biệt được tạo ra, nhưng mỗi hàm đều có sự đóng lại của môi trường mà chúng được định nghĩa - trong trường hợp này là môi trường toàn cục (hoặc môi trường của hàm bên ngoài nếu vòng lặp được đặt bên trong một hàm khác). Tuy nhiên, đây chính xác là vấn đề - trong môi trường này, đã animalbị đột biến, và các lần đóng đều đề cập đến như nhau animal.

[Ghi chú của người biên tập: iđã được đổi thành animal.]

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.