Tại sao các hàm python lồng nhau được gọi là bao đóng?


249

Tôi đã thấy và sử dụng các hàm lồng nhau trong Python và chúng khớp với định nghĩa của bao đóng. Vậy tại sao chúng được gọi nested functionsthay vì closures?

Các hàm lồng nhau không được đóng bởi vì chúng không được thế giới bên ngoài sử dụng?

CẬP NHẬT: Tôi đã đọc về việc đóng cửa và nó khiến tôi suy nghĩ về khái niệm này đối với Python. Tôi đã tìm kiếm và tìm thấy bài báo được đề cập bởi một người nào đó trong một bình luận bên dưới, nhưng tôi không thể hoàn toàn hiểu được lời giải thích trong bài viết đó, vì vậy đó là lý do tại sao tôi hỏi câu hỏi này.


8
Thật thú vị, một số googling đã tìm thấy tôi điều này, ngày 12 tháng 12 năm 2006: effbot.org/zone/clenses.htmlm . Tôi không chắc chắn là "các bản sao bên ngoài" cau mày trên SO?
hbw

1
PEP 227 - Phạm vi lồng nhau tĩnh để biết thêm thông tin.
thật Abe

Câu trả lời:


394

Việc đóng cửa xảy ra khi một hàm có quyền truy cập vào một biến cục bộ từ một phạm vi kèm theo đã hoàn thành việc thực hiện của nó.

def make_printer(msg):
    def printer():
        print msg
    return printer

printer = make_printer('Foo!')
printer()

Khi make_printerđược gọi, một khung mới được đặt vào ngăn xếp với mã được biên dịch cho printerhàm dưới dạng hằng số và giá trị msglà cục bộ. Sau đó nó tạo và trả về hàm. Bởi vì hàm printertham chiếu msgbiến, nó được giữ nguyên saumake_printer hàm đã trở lại.

Vì vậy, nếu các hàm lồng nhau của bạn không

  1. truy cập các biến cục bộ để bao quanh phạm vi,
  2. làm như vậy khi chúng được thực thi ngoài phạm vi đó,

sau đó họ không đóng cửa.

Đây là một ví dụ về hàm lồng nhau không phải là hàm đóng.

def make_printer(msg):
    def printer(msg=msg):
        print msg
    return printer

printer = make_printer("Foo!")
printer()  #Output: Foo!

Ở đây, chúng tôi đang ràng buộc giá trị với giá trị mặc định của một tham số. Điều này xảy ra khi hàm printerđược tạo và do đó không có tham chiếu đến giá trị msgbên ngoài printer cần được duy trì sau khi make_printertrả về. msgchỉ là một biến cục bộ bình thường của hàm printertrong ngữ cảnh này.


2
Bạn trả lời là tốt hơn nhiều so với của tôi, bạn đưa ra một quan điểm tốt, nhưng nếu chúng ta sẽ đi theo các định nghĩa lập trình chức năng nghiêm ngặt nhất, các ví dụ của bạn thậm chí có chức năng không? Đã được một thời gian và tôi không thể nhớ nếu lập trình chức năng nghiêm ngặt cho phép các hàm không trả về giá trị. Vấn đề là phải tranh luận, nếu bạn coi giá trị trả về là Không, nhưng đó là một chủ đề hoàn toàn khác.
mikerobi

6
@mikerobi, tôi không chắc rằng chúng ta cần tính đến lập trình chức năng vì python không thực sự là ngôn ngữ chức năng mặc dù chắc chắn nó có thể được sử dụng như vậy. Nhưng, không, các chức năng bên trong không phải là chức năng theo nghĩa đó vì toàn bộ quan điểm của chúng là tạo ra các tác dụng phụ. Thật dễ dàng để tạo ra một chức năng minh họa các điểm cũng như vậy,
aaronasterling

31
@mikerobi: Có hay không một đốm mã là một bao đóng tùy thuộc vào việc nó có đóng trên môi trường của nó hay không, không phải là những gì bạn gọi nó. Nó có thể là một thói quen, chức năng, thủ tục, phương thức, khối, chương trình con, bất cứ điều gì. Trong Ruby, các phương thức không thể được đóng, chỉ các khối có thể. Trong Java, các phương thức không thể được đóng, nhưng các lớp thì có thể. Điều đó không làm cho họ ít hơn một đóng cửa. (Mặc dù thực tế là chúng chỉ đóng trên một số biến và chúng không thể sửa đổi chúng, khiến chúng bên cạnh vô dụng.) Bạn có thể lập luận rằng một phương thức chỉ là một thủ tục được đóng lại self. (Trong JavaScript / Python gần như đúng.)
Jörg W Mittag

3
@ JörgWMittag Vui lòng xác định "đóng cửa".
Evgeni Sergeev

4
@EvgeniSergeev "đóng lại" tức là "tham chiếu đến một biến cục bộ [nói, i] từ một phạm vi kèm theo". tham chiếu, tức là có thể kiểm tra (hoặc thay đổi) igiá trị, ngay cả khi / khi phạm vi đó "đã hoàn thành việc thực thi", tức là việc thực hiện chương trình đã đi ra các phần khác của mã. Khối iđược xác định là không còn nữa, nhưng các hàm tham chiếu đến ivẫn có thể làm như vậy. Điều này thường được mô tả là "đóng trên biến i". Để không đối phó với các biến cụ thể, nó có thể được thực hiện dưới dạng đóng trên toàn bộ khung môi trường nơi biến đó được xác định.
Will Ness

103

Câu hỏi đã được trả lời bởi aaronasterling

Tuy nhiên, ai đó có thể quan tâm đến cách các biến được lưu trữ dưới mui xe.

Trước khi đến với đoạn trích:

Đóng cửa là các chức năng kế thừa các biến từ môi trường kèm theo của chúng. Khi bạn chuyển một hàm gọi lại dưới dạng đối số cho một hàm khác sẽ thực hiện I / O, hàm gọi lại này sẽ được gọi sau đó và hàm này sẽ - gần như kỳ diệu - ghi nhớ bối cảnh mà nó được khai báo, cùng với tất cả các biến có sẵn trong bối cảnh đó.

  • Nếu một hàm không sử dụng các biến miễn phí thì nó không tạo thành một bao đóng.

  • Nếu có một cấp độ bên trong khác sử dụng các biến miễn phí - tất cả các cấp độ trước đó sẽ lưu môi trường từ vựng (ví dụ ở cuối)

  • thuộc tính hàm func_closuretrong python <3.X hoặc __closure__trong python> 3.X lưu các biến miễn phí.

  • Mọi hàm trong python đều có thuộc tính đóng này, nhưng nó không lưu bất kỳ nội dung nào nếu không có biến miễn phí.

ví dụ: của các thuộc tính đóng nhưng không có nội dung bên trong vì không có biến miễn phí.

>>> def foo():
...     def fii():
...         pass
...     return fii
...
>>> f = foo()
>>> f.func_closure
>>> 'func_closure' in dir(f)
True
>>>

Lưu ý: BIỂU TƯỢNG MIỄN PHÍ LÀ PHẢI TẠO MỘT ĐÓNG

Tôi sẽ giải thích bằng cách sử dụng đoạn mã giống như trên:

>>> def make_printer(msg):
...     def printer():
...         print msg
...     return printer
...
>>> printer = make_printer('Foo!')
>>> printer()  #Output: Foo!

Và tất cả các hàm Python đều có thuộc tính bao đóng, vì vậy hãy kiểm tra các biến kèm theo được liên kết với hàm đóng.

Đây là thuộc tính func_closurecho hàmprinter

>>> 'func_closure' in dir(printer)
True
>>> printer.func_closure
(<cell at 0x108154c90: str object at 0x108151de0>,)
>>>

Các closurethuộc tính trả về một tuple của các đối tượng tế bào có chứa thông tin chi tiết của các biến được định nghĩa trong phạm vi kèm theo.

Phần tử đầu tiên trong func_clenses có thể là Không hoặc một tuple các ô có chứa các ràng buộc cho các biến miễn phí của hàm và nó chỉ đọc.

>>> dir(printer.func_closure[0])
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__',
 '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
 '__setattr__',  '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>>

Ở đây trong đầu ra ở trên bạn có thể thấy cell_contents, hãy xem những gì nó lưu trữ:

>>> printer.func_closure[0].cell_contents
'Foo!'    
>>> type(printer.func_closure[0].cell_contents)
<type 'str'>
>>>

Vì vậy, khi chúng ta gọi hàm printer(), nó truy cập giá trị được lưu trữ bên trong cell_contents. Đây là cách chúng tôi có đầu ra là 'Foo!'

Một lần nữa tôi sẽ giải thích bằng đoạn trích trên với một số thay đổi:

 >>> def make_printer(msg):
 ...     def printer():
 ...         pass
 ...     return printer
 ...
 >>> printer = make_printer('Foo!')
 >>> printer.func_closure
 >>>

Trong đoạn trích trên, tôi không in thông điệp bên trong chức năng của máy in, vì vậy nó không tạo ra bất kỳ biến miễn phí nào. Vì không có biến miễn phí, sẽ không có nội dung bên trong bao đóng. Đó chính xác là những gì chúng ta thấy ở trên.

Bây giờ tôi sẽ giải thích một đoạn khác để xóa mọi thứ Free Variablevới Closure:

>>> def outer(x):
...     def intermediate(y):
...         free = 'free'
...         def inner(z):
...             return '%s %s %s %s' %  (x, y, free, z)
...         return inner
...     return intermediate
...
>>> outer('I')('am')('variable')
'I am free variable'
>>>
>>> inter = outer('I')
>>> inter.func_closure
(<cell at 0x10c989130: str object at 0x10c831b98>,)
>>> inter.func_closure[0].cell_contents
'I'
>>> inn = inter('am')

Vì vậy, chúng ta thấy rằng một thuộc func_closuretính là một bộ các ô đóng , chúng ta có thể giới thiệu chúng và nội dung của chúng một cách rõ ràng - một ô có thuộc tính "cell_contents"

>>> inn.func_closure
(<cell at 0x10c9807c0: str object at 0x10c9b0990>, 
 <cell at 0x10c980f68: str object at   0x10c9eaf30>, 
 <cell at 0x10c989130: str object at 0x10c831b98>)
>>> for i in inn.func_closure:
...     print i.cell_contents
...
free
am 
I
>>>

Ở đây khi chúng tôi gọi inn, nó sẽ tham chiếu tất cả các biến miễn phí lưu để chúng tôi nhận đượcI am free variable

>>> inn('variable')
'I am free variable'
>>>

9
Trong Python 3, func_closurebây giờ được gọi __closure__, tương tự như các func_*thuộc tính khác .
lvc

3
Cũng __closure_có sẵn trong Python 2.6+ để tương thích với Python 3.
Pierre

Đóng cửa đề cập đến bản ghi lưu trữ các biến đóng, được gắn vào đối tượng hàm. Đó không phải là chức năng của chính nó. Trong Python, nó là __closure__đối tượng đóng cửa.
Martijn Pieters

Cảm ơn @MartijnPieters đã làm rõ.
James Sapam

71

Python có hỗ trợ yếu để đóng. Để xem những gì tôi muốn nói, hãy lấy ví dụ sau đây về bộ đếm bằng cách sử dụng bao đóng với JavaScript:

function initCounter(){
    var x = 0;
    function counter  () {
        x += 1;
        console.log(x);
    };
    return counter;
}

count = initCounter();

count(); //Prints 1
count(); //Prints 2
count(); //Prints 3

Việc đóng cửa khá thanh lịch vì nó cung cấp cho các chức năng được viết như thế này khả năng có "bộ nhớ trong". Kể từ Python 2.7, điều này là không thể. Nếu bạn cố gắng

def initCounter():
    x = 0;
    def counter ():
        x += 1 ##Error, x not defined
        print x
    return counter

count = initCounter();

count(); ##Error
count();
count();

Bạn sẽ nhận được một lỗi nói rằng x không được xác định. Nhưng làm thế nào có thể nếu nó đã được hiển thị bởi những người khác mà bạn có thể in nó? Điều này là do cách Python quản lý phạm vi biến hàm. Mặc dù hàm bên trong có thể đọc các biến của hàm ngoài, nhưng nó không thể ghi chúng.

Đây là một sự xấu hổ thực sự. Nhưng chỉ với việc đóng chỉ đọc, ít nhất bạn có thể thực hiện mô hình trang trí hàm mà Python cung cấp đường cú pháp.

Cập nhật

Như đã chỉ ra, có nhiều cách để đối phó với giới hạn phạm vi của python và tôi sẽ tiết lộ một số.

1. Sử dụng globaltừ khóa (nói chung không được khuyến nghị).

2. Trong Python 3.x, sử dụng nonlocaltừ khóa (được đề xuất bởi @unutbu và @leewz)

3. Xác định một lớp có thể sửa đổi đơn giảnObject

class Object(object):
    pass

và tạo một Object scopebên trong initCounterđể lưu trữ các biến

def initCounter ():
    scope = Object()
    scope.x = 0
    def counter():
        scope.x += 1
        print scope.x

    return counter

scopethực sự chỉ là một tài liệu tham khảo, các hành động được thực hiện với các trường của nó không thực sự scopetự sửa đổi , do đó không có lỗi phát sinh.

4. Một cách khác, như @unutbu đã chỉ ra, sẽ là định nghĩa mỗi biến là một mảng (x = [0] ) và sửa đổi phần tử đầu tiên của nó ( x[0] += 1). Một lần nữa không có lỗi phát sinh vì xbản thân nó không được sửa đổi.

5. Theo đề xuất của @raxacoricofallapatorius, bạn có thể tạo xmột tài sản củacounter

def initCounter ():

    def counter():
        counter.x += 1
        print counter.x

    counter.x = 0
    return counter

27
Có nhiều cách xung quanh điều này. Trong Python2, bạn có thể thực hiện x = [0]trong phạm vi bên ngoài và sử dụng x[0] += 1trong phạm vi bên trong. Trong Python3, bạn có thể giữ mã của mình như cũ và sử dụng từ khóa không nhắm mục tiêu .
unutbu

"Mặc dù hàm bên trong có thể đọc các biến của hàm ngoài, nhưng nó không thể ghi chúng." - Điều này không chính xác theo nhận xét của unutbu. Vấn đề là khi Python gặp một cái gì đó như x = ..., x được hiểu là một biến cục bộ, điều này tất nhiên chưa được xác định tại thời điểm đó. OTOH, nếu x là một đối tượng có thể thay đổi với phương thức có thể thay đổi, nó có thể được sửa đổi tốt, ví dụ: nếu x là một đối tượng hỗ trợ phương thức inc () tự biến đổi, x.inc () sẽ hoạt động mà không gặp trở ngại.
Thanh DK

@ThanhDK Điều đó không có nghĩa là bạn không thể ghi vào biến? Khi bạn sử dụng gọi một phương thức từ một đối tượng có thể thay đổi, bạn chỉ bảo nó tự sửa đổi, bạn không thực sự sửa đổi biến (chỉ đơn thuần giữ một tham chiếu đến đối tượng). Nói cách khác, tham chiếu mà biến xchỉ ra vẫn giữ nguyên chính xác ngay cả khi bạn gọi inc()hoặc bất cứ điều gì, và bạn đã không ghi vào biến một cách hiệu quả.
dùng193130

4
Có một lựa chọn khác, tốt hơn nhiều so với # 2, imv, về việc tạo xmột tài sản củacounter .
orome

9
Python 3 có nonlocaltừ khóa, giống như globalnhưng đối với các biến của hàm ngoài. Điều này sẽ cho phép một chức năng bên trong để đặt lại tên từ (các) chức năng bên ngoài của nó. Tôi nghĩ rằng "liên kết với tên" chính xác hơn "sửa đổi biến".
leewz

16

Python 2 không có các bao đóng - nó có các cách giải quyết giống như các bao đóng.

Có rất nhiều ví dụ trong các câu trả lời đã được đưa ra - sao chép các biến vào hàm bên trong, sửa đổi một đối tượng trên hàm bên trong, v.v.

Trong Python 3, hỗ trợ rõ ràng hơn - và cô đọng:

def closure():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

Sử dụng:

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

Các nonlocaltừ khóa liên kết với các chức năng bên trong để biến bên ngoài đề cập một cách rõ ràng, có hiệu lực bao quanh nó. Do đó rõ ràng hơn một 'đóng cửa'.


1
Thú vị, để tham khảo: docs.python.org/3/reference/ ,. Tôi không biết tại sao không dễ để tìm thêm thông tin về việc đóng cửa (và cách bạn có thể mong đợi họ hành xử, đến từ JS) trong tài liệu python3?
dùng3773048

9

Tôi đã có một tình huống mà tôi cần một không gian tên riêng biệt nhưng liên tục. Tôi đã sử dụng các lớp học. Tôi không khác. Tên tách biệt nhưng liên tục là đóng cửa.

>>> class f2:
...     def __init__(self):
...         self.a = 0
...     def __call__(self, arg):
...         self.a += arg
...         return(self.a)
...
>>> f=f2()
>>> f(2)
2
>>> f(2)
4
>>> f(4)
8
>>> f(8)
16

# **OR**
>>> f=f2() # **re-initialize**
>>> f(f(f(f(2)))) # **nested**
16

# handy in list comprehensions to accumulate values
>>> [f(i) for f in [f2()] for i in [2,2,4,8]][-1] 
16

6
def nested1(num1): 
    print "nested1 has",num1
    def nested2(num2):
        print "nested2 has",num2,"and it can reach to",num1
        return num1+num2    #num1 referenced for reading here
    return nested2

Cung cấp:

In [17]: my_func=nested1(8)
nested1 has 8

In [21]: my_func(5)
nested2 has 5 and it can reach to 8
Out[21]: 13

Đây là một ví dụ về việc đóng cửa là gì và làm thế nào nó có thể được sử dụng.


0

Tôi muốn đưa ra một so sánh đơn giản khác giữa ví dụ python và JS, nếu điều này giúp mọi thứ rõ ràng hơn.

JS:

function make () {
  var cl = 1;
  function gett () {
    console.log(cl);
  }
  function sett (val) {
    cl = val;
  }
  return [gett, sett]
}

và thực hiện:

a = make(); g = a[0]; s = a[1];
s(2); g(); // 2
s(3); g(); // 3

Con trăn

def make (): 
  cl = 1
  def gett ():
    print(cl);
  def sett (val):
    cl = val
  return gett, sett

và thực hiện:

g, s = make()
g() #1
s(2); g() #1
s(3); g() #1

Lý do: Như nhiều người khác đã nói ở trên, trong python, nếu có một phép gán trong phạm vi bên trong cho một biến có cùng tên, một tham chiếu mới trong phạm vi bên trong được tạo. Không như vậy với JS, trừ khi bạn tuyên bố rõ ràng với vartừ khóa.

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.