Puoi spiegare le chiusure (in quanto si riferiscono a Python)?


85

Ho letto molto sulle chiusure e penso di capirle, ma senza offuscare il quadro per me e per gli altri, spero che qualcuno possa spiegare le chiusure nel modo più sintetico e chiaro possibile. Sto cercando una semplice spiegazione che possa aiutarmi a capire dove e perché dovrei usarli.

Risposte:


97

Chiusura su chiusure

Gli oggetti sono dati con metodi allegati, le chiusure sono funzioni con dati allegati.

def make_counter():
    i = 0
    def counter(): # counter() is a closure
        nonlocal i
        i += 1
        return i
    return counter

c1 = make_counter()
c2 = make_counter()

print (c1(), c1(), c2(), c2())
# -> 1 2 1 2

6
Si noti che è nonlocalstato aggiunto in python 3, python 2.x non aveva chiusure di lettura / scrittura complete (cioè si poteva leggere chiuse sulle variabili, ma non modificare i loro valori)
James Porter

6
@JamesPorter: nota: puoi emulare la nonlocalparola chiave in Python 2 usando un oggetto modificabile, ad L = [0] \n def counter(): L[0] += 1; return L[0]esempio, non puoi cambiare il nome (legarlo a un altro oggetto) in questo caso ma puoi cambiare l'oggetto mutabile stesso a cui il nome fa riferimento per. L'elenco è obbligatorio perché i numeri interi non sono modificabili in Python.
jfs

1
@JFSebastian: giusto. però sembra sempre un trucco sporco e sporco :)
James Porter

46

È semplice: una funzione che fa riferimento a variabili da un ambito contenitore, potenzialmente dopo che il flusso di controllo ha lasciato tale ambito. Quest'ultimo pezzo è molto utile:

>>> def makeConstantAdder(x):
...     constant = x
...     def adder(y):
...         return y + constant
...     return adder
... 
>>> f = makeConstantAdder(12)
>>> f(3)
15
>>> g = makeConstantAdder(4)
>>> g(3)
7

Nota che 12 e 4 sono "scomparsi" rispettivamente all'interno di f e g, questa caratteristica è ciò che rende f e g chiusure appropriate.


Non è necessario fare constant = x; potresti semplicemente farlo return y + xnella funzione annidata (o ricevere l'argomento con il nome constant), e funzionerebbe perfettamente; gli argomenti vengono catturati dalla chiusura non diversamente dai locals non-argomento.
ShadowRanger

15

Mi piace questa definizione approssimativa e succinta :

Una funzione che può riferirsi ad ambienti non più attivi.

Aggiungerei

Una chiusura consente di associare variabili in una funzione senza passarle come parametri .

I decoratori che accettano parametri sono un uso comune per le chiusure. Le chiusure sono un meccanismo di implementazione comune per quel tipo di "fabbrica di funzioni". Scelgo spesso di utilizzare le chiusure nello Strategy Pattern quando la strategia viene modificata dai dati in fase di esecuzione.

In un linguaggio che consente la definizione di blocchi anonimi, ad esempio Ruby, C #, le chiusure possono essere utilizzate per implementare (quanto ammontano) nuove strutture di controllo nuove. La mancanza di blocchi anonimi è tra i limiti delle chiusure in Python .


15

Ad essere onesti, capisco perfettamente le chiusure tranne che non sono mai stato chiaro su cosa sia esattamente la cosa che è la "chiusura" e cosa c'è di così "chiusura" al riguardo. Ti consiglio di rinunciare a cercare una logica dietro la scelta del termine.

Comunque, ecco la mia spiegazione:

def foo():
   x = 3
   def bar():
      print x
   x = 5
   return bar

bar = foo()
bar()   # print 5

Un'idea chiave qui è che l'oggetto funzione restituito da foo mantiene un hook alla variabile locale "x" anche se "x" è uscito dall'ambito e dovrebbe essere disattivato. Questo hook è per la var stessa, non solo per il valore che var aveva in quel momento, quindi quando viene chiamata bar, stampa 5, non 3.

Inoltre, sii chiaro che Python 2.x ha una chiusura limitata: non è possibile modificare 'x' all'interno di 'bar' perché scrivere 'x = bla' dichiarerebbe una 'x' locale in bar, non assegnare a 'x' di pippo . Questo è un effetto collaterale dell'assegnazione di Python = dichiarazione. Per aggirare questo problema, Python 3.0 introduce la parola chiave non locale:

def foo():
   x = 3
   def bar():
      print x
   def ack():
      nonlocal x
      x = 7
   x = 5
   return (bar, ack)

bar, ack = foo()
ack()   # modify x of the call to foo
bar()   # print 7

7

Non ho mai sentito parlare di transazioni utilizzate nello stesso contesto in cui spiegano cos'è una chiusura e qui non c'è davvero alcuna semantica di transazione.

Si chiama chiusura perché "chiude" la variabile esterna (costante), cioè non è solo una funzione ma un recinto dell'ambiente in cui la funzione è stata creata.

Nell'esempio seguente, chiamare la chiusura g dopo aver modificato x cambierà anche il valore di x all'interno di g, poiché g si chiude su x:

x = 0

def f():
    def g(): 
        return x * 2
    return g


closure = f()
print(closure()) # 0
x = 2
print(closure()) # 4

Inoltre, allo stato attuale, g()calcola x * 2ma non restituisce nulla. Dovrebbe essere return x * 2. +1 tuttavia per una spiegazione della parola "chiusura".
Bruno Le Floch

3

Ecco un tipico caso d'uso per le chiusure: callback per elementi GUI (questa sarebbe un'alternativa alla sottoclasse della classe del pulsante). Ad esempio, è possibile costruire una funzione che verrà chiamata in risposta alla pressione di un pulsante e "chiudere" sulle variabili rilevanti nello scope padre necessarie per l'elaborazione del clic. In questo modo puoi collegare interfacce piuttosto complicate dalla stessa funzione di inizializzazione, costruendo tutte le dipendenze nella chiusura.


2

In Python, una chiusura è un'istanza di una funzione a cui sono associate variabili immutabilmente.

In effetti, il modello di dati lo spiega nella sua descrizione dell'attributo delle funzioni __closure__:

Nessuno o una tupla di celle che contengono associazioni per le variabili libere della funzione. Sola lettura

Per dimostrarlo:

def enclosure(foo):
    def closure(bar):
        print(foo, bar)
    return closure

closure_instance = enclosure('foo')

Chiaramente, sappiamo che ora abbiamo una funzione puntata dal nome della variabile closure_instance. Apparentemente, se lo chiamiamo con un oggetto, bardovrebbe stampare la stringa 'foo'e qualunque sia la rappresentazione della stringa bar.

Infatti la stringa 'foo' è vincolata all'istanza della funzione, e possiamo leggerla direttamente qui, accedendo cell_contentsall'attributo della prima (e unica) cella nella tupla __closure__dell'attributo:

>>> closure_instance.__closure__[0].cell_contents
'foo'

Per inciso, gli oggetti cella sono descritti nella documentazione dell'API C:

Gli oggetti "cella" vengono utilizzati per implementare le variabili a cui fanno riferimento più ambiti

E possiamo dimostrare l'utilizzo della nostra chiusura, notando che 'foo'è bloccata nella funzione e non cambia:

>>> closure_instance('bar')
foo bar
>>> closure_instance('baz')
foo baz
>>> closure_instance('quux')
foo quux

E niente può cambiarlo:

>>> closure_instance.__closure__ = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: readonly attribute

Funzioni parziali

L'esempio fornito utilizza la chiusura come funzione parziale, ma se questo è il nostro unico obiettivo, lo stesso obiettivo può essere raggiunto functools.partial

>>> from __future__ import print_function # use this if you're in Python 2.
>>> partial_function = functools.partial(print, 'foo')
>>> partial_function('bar')
foo bar
>>> partial_function('baz')
foo baz
>>> partial_function('quux')
foo quux

Ci sono anche chiusure più complicate che non si adattano all'esempio della funzione parziale e le dimostrerò ulteriormente quando il tempo lo consente.


2
# A Closure is a function object that remembers values in enclosing scopes even if they are not present in memory.

# Defining a closure

# This is an outer function.
def outer_function(message):
    # This is an inner nested function.
    def inner_function():
        print(message)
    return inner_function

# Now lets call the outer function and return value bound to name 'temp'
temp = outer_function("Hello")
# On calling temp, 'message' will be still be remembered although we had finished executing outer_function()
temp()
# Technique by which some data('message') that remembers values in enclosing scopes 
# even if they are not present in memory is called closures

# Output: Hello

I criteri che devono soddisfare le chiusure sono:

  1. Dobbiamo avere una funzione annidata.
  2. La funzione annidata deve fare riferimento al valore definito nella funzione che lo racchiude.
  3. La funzione di inclusione deve restituire la funzione nidificata.

# Example 2
def make_multiplier_of(n): # Outer function
    def multiplier(x): # Inner nested function
        return x * n
    return multiplier
# Multiplier of 3
times3 = make_multiplier_of(3)
# Multiplier of 5
times5 = make_multiplier_of(5)
print(times5(3)) # 15
print(times3(2)) #  6

1

Ecco un esempio di chiusure di Python3

def closure(x):
    def counter():
        nonlocal x
        x += 1
        return x
    return counter;

counter1 = closure(100);
counter2 = closure(200);

print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 2 " + str(counter2()))
print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 2 " + str(counter2()))

# result

i from closure 1 101
i from closure 1 102
i from closure 2 201
i from closure 1 103
i from closure 1 104
i from closure 1 105
i from closure 2 202

0

Per me le "chiusure" sono funzioni capaci di ricordare l'ambiente in cui sono state create. Questa funzionalità, ti permette di utilizzare variabili o metodi all'interno della chiusura che, altrimenti, non potresti utilizzare né perché non esistono più o perché sono fuori portata per scope. Diamo un'occhiata a questo codice in ruby:

def makefunction (x)
  def multiply (a,b)
    puts a*b
  end
  return lambda {|n| multiply(n,x)} # => returning a closure
end

func = makefunction(2) # => we capture the closure
func.call(6)    # => Result equal "12"  

funziona anche quando sia il metodo "moltiplicare" che la variabile "x" non esistono più. Tutto perché la capacità di chiusura di ricordare.


0

tutti noi abbiamo usato decoratori in pitone. Sono ottimi esempi per mostrare cosa sono le funzioni di chiusura in Python.

class Test():
    def decorator(func):
        def wrapper(*args):
            b = args[1] + 5
            return func(b)
        return wrapper

@decorator
def foo(val):
    print val + 2

obj = Test()
obj.foo(5)

qui il valore finale è 12

Qui, la funzione wrapper è in grado di accedere all'oggetto func perché wrapper è "chiusura lessicale", può accedere ai suoi attributi padre. Questo è il motivo per cui è in grado di accedere all'oggetto func.


0

Vorrei condividere il mio esempio e una spiegazione sulle chiusure. Ho fatto un esempio di Python e due figure per dimostrare gli stati dello stack.

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n’ * margin_top, a * n, 
            ' ‘ * padding, msg, ' ‘ * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)
…
f('hello')
g(‘good bye!')

L'output di questo codice sarebbe il seguente:

*****      hello      #####

      good bye!    ♥♥♥

Di seguito sono riportate due figure per mostrare le pile e la chiusura allegata all'oggetto funzione.

quando la funzione viene restituita dal produttore

quando la funzione viene chiamata in seguito

Quando la funzione viene chiamata tramite un parametro o una variabile non locale, il codice necessita di associazioni di variabili locali come margin_top, padding e a, b, n. Per garantire il funzionamento del codice funzione, dovrebbe essere accessibile lo stack frame della funzione maker che era scomparsa molto tempo fa, che è supportato nella chiusura che possiamo trovare insieme all'oggetto funzione 'message'.


-2

La migliore spiegazione che abbia mai visto di una chiusura è stata quella di spiegare il meccanismo. È andata più o meno così:

Immagina il tuo stack di programma come un albero degenerato in cui ogni nodo ha un solo figlio e il singolo nodo foglia è il contesto della procedura attualmente in esecuzione.

Ora allenta il vincolo che ogni nodo può avere un solo figlio.

Se lo fai, puoi avere un costrutto ('yield') che può ritornare da una procedura senza scartare il contesto locale (cioè non lo fa uscire dallo stack quando torni). La volta successiva che viene richiamata la procedura, l'invocazione riprende il vecchio frame dello stack (albero) e continua l'esecuzione da dove era stata interrotta.


NON è una spiegazione delle chiusure.
Jules,

Stai descrivendo continuazioni, non chiusure.
Matthew Olenik
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.