Чому python вкладені функції не називаються закриттями?


249

Я бачив і використовував вкладені функції в Python, і вони відповідають визначенню закриття. То чому їх називають nested functionsзамість closures?

Чи вкладені функції не є закриттями, оскільки вони не використовуються зовнішнім світом?

ОНОВЛЕННЯ: Я читав про закриття, і це змусило мене думати про цю концепцію стосовно Python. Я шукав та знайшов статтю, згадану кимось у коментарі нижче, але не зміг повністю зрозуміти пояснення в цій статті, тому я задаю це питання.


8
Цікаво, що деякий googling знайшов мені це від грудня 2006 року: effbot.org/zone/closure.htm . Я не впевнений - чи нахмурилися "зовнішні дублікати" на SO?
hbw

1
PEP 227 - Статистично вкладені сфери для отримання додаткової інформації.
Чесний Абе

Відповіді:


394

Закриття відбувається, коли функція має доступ до локальної змінної із вкладеної області, яка закінчила її виконання.

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

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

Коли make_printerвикликається, на стек кладеться новий кадр із скомпільованим кодом для printerфункції як константа та значення msgяк локальний. Потім він створює та повертає функцію. Оскільки функція printerпосилається на msgзмінну, вона залишається активною після make_printerповернення функції.

Отже, якщо ваші вкладені функції цього не роблять

  1. отримати доступ до змінних, локальних для областей застосування,
  2. робити це, коли вони виконуються поза цим обсягом,

то вони не є закриттями.

Ось приклад вкладеної функції, яка не є закриттям.

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

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

Тут ми прив'язуємо значення до значення за замовчуванням параметра. Це відбувається, коли функція printerстворена, і тому ніяке посилання на значення msgзовнішнього, яке printer потребує збереження, не повинно зберігатися після make_printerповернення. msgє лише нормальною локальною змінною функції printerв цьому контексті.


2
Ви відповідаєте набагато краще, ніж у мене, ви добре зазначаєте, але якщо ми будемо робити найсуворіші визначення функціонального програмування, чи ваші приклади навіть функціонують? Минуло деякий час, і я не можу згадати, чи дозволяє суворе функціональне програмування функцій, які не повертають значення. Справа в суперечливості, якщо ви вважаєте, що повернене значення не було, але це зовсім інша тема.
mikerobi

6
@mikerobi, я не впевнений, що нам потрібно враховувати функціональне програмування, оскільки python насправді не є функціональною мовою, хоча він, безумовно, може бути використаний як такий. Але, ні, внутрішні функції не є функціями в цьому сенсі, оскільки вся їх суть полягає у створенні побічних ефектів. Неважко створити функцію, яка так само добре ілюструє точки,
aaronasterling

31
@mikerobi: Закриття чи ні блокування коду залежить від того, закриває він це середовище, а не те, як ви його називаєте. Це може бути рутина, функція, процедура, метод, блок, підпрограма, будь-що. У Ruby методи не можуть бути закритими, лише блоки можуть. У Java методи не можуть бути закритими, але класи можуть. Це не робить їх менш закритим. (Хоча той факт, що вони закриваються лише над деякими змінними, і вони не можуть їх змінити, робить їх поруч непотрібними.) Можна стверджувати, що метод - це лише процедура, закрита над self. (У JavaScript / Python це майже правда.)
Jörg W Mittag

3
@ JörgWMittag Будь ласка, визначте "закривається".
Євгеній Сергєєв

4
@EvgeniSergeev "закривається над", тобто посилається "на локальну змінну [скажімо, i] з області, що додається". посилається, тобто може перевірити (або змінити) iзначення, навіть якщо / коли ця область "закінчила своє виконання", тобто виконання програми перейшло до інших частин коду. Блок, де iвизначено, більше не є, але функція (и), що посилаються на iвсе ще, може це зробити. Це зазвичай називають "закриттям змінної i". Щоб не мати справу з конкретними змінними, це може бути реалізовано як закриття над усім середовищем, де визначена ця змінна.
Буде Несс

103

На питання вже відповів ааронастерлінг

Однак когось може зацікавити, як змінні зберігаються під кришкою.

Перш ніж прийти до фрагменту:

Закриття - це функції, які успадковують змінні зі свого оточуючого середовища. Якщо ви передаєте функцію зворотного виклику функції як аргумент іншій функції, яка виконуватиме введення-виведення, ця функція зворотного виклику буде викликана пізніше, і ця функція - майже магічно - запам’ятає контекст, в якому вона була оголошена разом із усіма доступними змінними в цьому контексті.

  • Якщо функція не використовує вільних змінних, вона не утворює закриття.

  • Якщо є інший внутрішній рівень, який використовує вільні змінні - всі попередні рівні зберігають лексичне середовище (приклад наприкінці)

  • атрибути функції func_closureв python <3.X або __closure__в python> 3.X зберігають вільні змінні.

  • Кожна функція python має такі атрибути закриття, але вона не зберігає вміст, якщо немає вільних змінних.

приклад: атрибути закриття, але немає вмісту всередині, оскільки немає вільної змінної.

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

Примітка: БЕЗКОШТОВНА ВІДМІННА ПОВИННА СТВОРИТИ ЗАКРИТИЙ

Я поясню, використовуючи той же фрагмент, що і вище:

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

І всі функції Python мають атрибут закриття, тому давайте вивчимо змінні, що вкладаються, пов'язані з функцією закриття.

Ось атрибут func_closureфункціїprinter

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

closureАтрибут повертає кортеж об'єктів клітин , які містять інформацію про змінних , визначених в рамках вміщають.

Перший елемент func_closure, який може бути None або кортеж комірок, що містять прив'язки для вільних змінних функції, і він є лише для читання.

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

Тут у наведеному вище висновку ви побачите cell_contents, давайте подивимось, що він зберігає:

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

Отже, коли ми викликали функцію printer(), вона отримує доступ до значення, що зберігається всередині cell_contents. Ось так ми отримали вихід "Foo!"

Ще раз поясню, використовуючи вищевказаний фрагмент, з деякими змінами:

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

У наведеному вище фрагменті я не друкую msg у функції принтера, тому він не створює жодної вільної змінної. Оскільки немає вільної змінної, всередині закриття не буде вмісту. Саме це ми бачимо вище.

Тепер я поясню ще один фрагмент, щоб очистити все за Free Variableдопомогою 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')

Отже, ми бачимо, що func_closureвластивість - це набір осередків закриття , ми можемо чітко посилатися на них та їх вміст - комірка має властивість "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
>>>

Тут, коли ми зателефонували inn, він посилатиметься на всі збережені безкоштовні змінні, щоб ми отрималиI am free variable

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

9
У Python 3 func_closureтепер називається __closure__аналогічно різним func_*атрибутам.
lvc

3
Також __closure_доступний в Python 2.6+ для сумісності з Python 3.
П'єр

Закриття посилається на запис, який зберігає змінні, що закриваються, прикріплені до об'єкта функції. Це не сама функція. У Python це __closure__закриття об'єкта.
Martijn Pieters

Дякуємо @MartijnPieters за уточнення.
Джеймс Сапам

71

Python має слабку підтримку закриття. Щоб побачити, що я маю на увазі, візьміть наступний приклад лічильника за допомогою закриття з 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

Закриття є досить елегантним, оскільки надає таким функціям можливість мати "внутрішню пам'ять". Що стосується Python 2.7, це неможливо. Якщо ви спробуєте

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

count = initCounter();

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

Ви отримаєте помилку про те, що x не визначено. Але як це може бути, якщо іншим показали, що ви можете його надрукувати? Це пояснюється тим, як Python управляє зміною області функцій. Хоча внутрішня функція може читати змінні зовнішньої функції, вона не може їх записувати .

Це справді ганьба. Але лише закривши лише для читання, ви можете принаймні реалізувати схему декоратора функцій, для якої Python пропонує синтаксичний цукор.

Оновлення

Як було зазначено, є способи вирішити обмеження сфери застосування пітона, і я розкрию деякі.

1. Використовуйтеglobal ключове слово (взагалі не рекомендується).

2. У Python 3.x використовуйте nonlocalключове слово (запропоновано @unutbu та @leewz)

3. Визначте простий клас, що змінюєтьсяObject

class Object(object):
    pass

і створити Object scopeвсередині initCounterдля зберігання змінних

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

    return counter

Оскільки scopeце справді лише посилання, дії, здійснені з його полями, насправді не змінюють scopeсебе, тому помилки не виникає.

4. Альтернативним способом, як зазначав @unutbu, було б визначити кожну змінну як масив ( x = [0]) та змінити її перший елемент ( x[0] += 1). Знову не виникає помилок, оскільки xсама по собі не змінюється.

5. Як запропонував @raxacoricofallapatorius, ви можете зробити xвласністьcounter

def initCounter ():

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

    counter.x = 0
    return counter

27
Існують способи навколо цього. У Python2 ви можете робити x = [0]у зовнішній області та використовувати x[0] += 1у внутрішній області. У Python3 ви можете зберегти свій код таким, який він є, і використовувати нелокальне ключове слово .
unutbu

"Хоча внутрішня функція може читати змінні зовнішньої функції, вона не може їх записати." - Це неточно згідно коментаря unutbu. Проблема полягає в тому, що коли Python стикається з чимось на зразок x = ..., x інтерпретується як локальна змінна, що, звичайно, ще не визначено на той момент. OTOH, якщо x є об'єктом, що змінюється, із мутаційним методом, його можна просто змінити, наприклад, якщо x є об'єктом, який підтримує метод inc (), який сам мутує, x.inc () буде працювати без сучка.
Thanh DK

@ThanhDK Чи це не означає, що ви не можете писати до змінної? Коли ви використовуєте метод виклику з об'єкта, що змінюється , ви просто говорите йому, щоб змінити його, ви фактично не змінюєте змінну (яка просто містить посилання на об'єкт). Іншими словами, посилання, на яку xвказує змінна, залишається абсолютно однаковою, навіть якщо ви дзвоните inc()або що завгодно, і ви фактично не писали в змінну.
користувач193130

4
Там ще один варіант, суворо краще , ніж # 2, IMV, з рішень xвластивостіcounter .
orome

9
У Python 3 є nonlocalключове слово, яке схоже, globalале для змінних зовнішньої функції. Це дозволить внутрішній функції відновлювати ім'я від його зовнішньої функції. Я думаю, що "прив’язати до імені" є більш точним, ніж "змінити змінну".
leewz

16

У Python 2 не було закрит - у нього були обхідні шляхи, що нагадували закриття.

У наведених відповідях є чимало прикладів - копіювання змінних до внутрішньої функції, зміна об'єкта на внутрішній функції тощо.

У Python 3 підтримка є більш чіткою та короткою:

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

Використання:

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

nonlocalКлючове слово пов'язує внутрішню функцію до зовнішньої змінної явно згаданий, по суті , уклавши його. Отже, явніше «закриття».


1
Цікаво, для довідки: docs.python.org/3/reference/… . Я не знаю, чому в документації на python3 не так просто знайти більше інформації про закриття (і як ви можете розраховувати, що вони поводяться від JS)?
користувач3773048

9

У мене була ситуація, коли мені потрібен був окремий, але стійкий простір імен. Я використовував заняття. Я не інакше. Відокремлені, але стійкі назви - це закінчення.

>>> 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

Дає:

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

Це приклад того, що таке закриття та як воно може бути використане.


0

Я хотів би запропонувати ще одне просте порівняння між прикладом python та JS, якщо це допоможе зробити речі зрозумілішими.

JS:

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

та виконує:

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

Пітон:

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

та виконує:

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

Причина: Як і багато інших, сказаних вище, у python, якщо є внутрішня область присвоєння змінній з тим самим іменем, створюється нова посилання у внутрішній області. Не так у JS, якщо ви прямо не заявляєте його за допомогою varключового слова.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.