Pourquoi les fonctions imbriquées python ne sont-elles pas appelées fermetures?

249

J'ai vu et utilisé des fonctions imbriquées en Python, et elles correspondent à la définition d'une fermeture. Alors pourquoi sont-ils appelés nested functionsau lieu de closures?

Les fonctions imbriquées ne sont-elles pas des fermetures car elles ne sont pas utilisées par le monde extérieur?

MISE À JOUR: Je lisais sur les fermetures et cela m'a fait penser à ce concept en ce qui concerne Python. J'ai cherché et trouvé l'article mentionné par quelqu'un dans un commentaire ci-dessous, mais je ne pouvais pas comprendre complètement l'explication de cet article, c'est pourquoi je pose cette question.

Srikar Appalaraju
la source
8
Fait intéressant, certains googleurs m'ont trouvé ceci, daté de décembre 2006: effbot.org/zone/closure.htm . Je ne suis pas sûr - les "doublons externes" sont-ils mal vus sur SO?
hbw
1
PEP 227 - Étendues imbriquées statiquement pour plus d'informations.
Honest Abe

Réponses:

394

Une fermeture se produit lorsqu'une fonction a accès à une variable locale à partir d'une étendue englobante qui a terminé son exécution.

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

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

Quand make_printerest appelé, un nouveau cadre est placé sur la pile avec le code compilé pour la printerfonction en tant que constante et la valeur de en msgtant que local. Il crée et renvoie ensuite la fonction. Étant donné que la fonction fait printerréférence à la msgvariable, elle est maintenue en vie après le make_printerretour de la fonction.

Donc, si vos fonctions imbriquées ne le font pas

  1. accéder aux variables locales aux étendues englobantes,
  2. faites-le quand ils sont exécutés en dehors de cette portée,

alors ce ne sont pas des fermetures.

Voici un exemple de fonction imbriquée qui n'est pas une fermeture.

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

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

Ici, nous lions la valeur à la valeur par défaut d'un paramètre. Cela se produit lorsque la fonction printerest créée et donc aucune référence à la valeur de msgexterne à ne printer doit être conservée après les make_printerretours. msgest juste une variable locale normale de la fonction printerdans ce contexte.

aaronasterling
la source
2
Votre réponse est bien meilleure que la mienne, vous faites valoir un bon argument, mais si nous allons suivre les définitions de programmation fonctionnelle les plus strictes, vos exemples sont-ils même des fonctions? Cela fait un moment, et je ne me souviens pas si une programmation fonctionnelle stricte permet des fonctions qui ne renvoient pas de valeurs. Le point est théorique, si vous considérez la valeur de retour comme étant None, mais c'est un tout autre sujet.
mikerobi
6
@mikerobi, je ne suis pas sûr que nous devions prendre en compte la programmation fonctionnelle car python n'est pas vraiment un langage fonctionnel bien qu'il puisse certainement être utilisé comme tel. Mais non, les fonctions internes ne sont pas des fonctions dans ce sens puisque leur but est de créer des effets secondaires. Il est facile de créer une fonction qui illustre aussi bien les points,
aaronasterling
31
@mikerobi: Le fait qu'une blob de code soit ou non une fermeture dépend de la fermeture ou non de son environnement, et non de ce que vous appelez. Cela peut être une routine, une fonction, une procédure, une méthode, un bloc, un sous-programme, peu importe. Dans Ruby, les méthodes ne peuvent pas être des fermetures, seuls les blocs le peuvent. En Java, les méthodes ne peuvent pas être des fermetures, mais les classes le peuvent. Cela n'en fait pas moins une fermeture. (Bien que le fait qu'ils ne se ferment que sur certaines variables, et qu'ils ne peuvent pas les modifier, les rend presque inutiles.) On pourrait dire qu'une méthode n'est qu'une procédure fermée self. (En JavaScript / Python, c'est presque vrai.)
Jörg W Mittag
3
@ JörgWMittag Veuillez définir "se referme".
Evgeni Sergeev
4
@EvgeniSergeev "se referme" c'est-à-dire se réfère "à une variable locale [disons, i] d'une portée englobante". fait référence, c.-à-d. peut inspecter (ou changer) ila valeur de, même si / quand cette portée "a terminé son exécution", c.-à-d. que l'exécution d'un programme est passée à d'autres parties du code. Le bloc où iest défini n'est plus, mais les fonctions auxquelles il est fait référence ipeuvent toujours le faire. Ceci est communément décrit comme «fermer la variable i». Pour ne pas traiter les variables spécifiques, il peut être implémenté comme se fermant sur tout le cadre d'environnement où cette variable est définie.
Will Ness
103

La question a déjà été répondue par aaronasterling

Cependant, quelqu'un pourrait être intéressé par la façon dont les variables sont stockées sous le capot.

Avant de venir à l'extrait:

Les fermetures sont des fonctions qui héritent des variables de leur environnement englobant. Lorsque vous passez un rappel de fonction comme argument à une autre fonction qui fera des E / S, cette fonction de rappel sera invoquée plus tard, et cette fonction se souviendra - presque comme par magie - du contexte dans lequel elle a été déclarée, ainsi que de toutes les variables disponibles dans ce contexte.

  • Si une fonction n'utilise pas de variables libres, elle ne forme pas de fermeture.

  • S'il existe un autre niveau interne qui utilise des variables libres - tous les niveaux précédents sauvegardent l'environnement lexical (exemple à la fin)

  • les attributs de fonction func_closureen python <3.X ou __closure__en python> 3.X enregistrent les variables libres.

  • Chaque fonction en python a ces attributs de fermeture, mais elle n'enregistre aucun contenu s'il n'y a pas de variables libres.

exemple: d'attributs de fermeture mais pas de contenu à l'intérieur car il n'y a pas de variable libre.

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

NB: UNE VARIABLE GRATUITE EST OBLIGATOIRE POUR CRÉER UNE FERMETURE.

Je vais vous expliquer en utilisant le même extrait que ci-dessus:

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

Et toutes les fonctions Python ont un attribut de fermeture, examinons donc les variables englobantes associées à une fonction de fermeture.

Voici l'attribut func_closurede la fonctionprinter

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

L' closureattribut renvoie un tuple d'objets de cellule qui contiennent des détails des variables définies dans la portée englobante.

Le premier élément de func_closure qui pourrait être None ou un tuple de cellules contenant des liaisons pour les variables libres de la fonction et il est en lecture seule.

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

Ici, dans la sortie ci-dessus, vous pouvez voir cell_contents, voyons ce qu'il stocke:

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

Ainsi, lorsque nous avons appelé la fonction printer(), elle accède à la valeur stockée à l'intérieur du cell_contents. C'est ainsi que nous avons obtenu la sortie en tant que 'Foo!'

Je vais à nouveau expliquer l'utilisation de l'extrait ci-dessus avec quelques modifications:

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

Dans l'extrait ci-dessus, je n'imprime pas le msg à l'intérieur de la fonction imprimante, donc il ne crée aucune variable libre. Comme il n'y a pas de variable libre, il n'y aura pas de contenu à l'intérieur de la fermeture. C'est exactement ce que nous voyons ci-dessus.

Je vais maintenant expliquer un autre extrait différent pour tout effacer Free Variableavec 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')

Donc, nous voyons qu'une func_closurepropriété est un tuple de cellules de fermeture , nous pouvons les référencer explicitement et leur contenu - une cellule a la propriété "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
>>>

Ici, lorsque nous avons appelé inn, il fera référence à toutes les variables de sauvegarde gratuites afin que nous obtenionsI am free variable

>>> inn('variable')
'I am free variable'
>>>
James Sapam
la source
9
En Python 3, func_closureest maintenant appelé __closure__, de la même manière que les divers autres func_*attributs.
lvc
3
Est également __closure_disponible en Python 2.6+ pour la compatibilité avec Python 3.
Pierre
La fermeture fait référence à l'enregistrement qui stocke les variables fermées, attachées à l'objet fonction. Ce n'est pas la fonction elle-même. En Python, c'est l' __closure__objet qui est la fermeture.
Martijn Pieters
Merci @MartijnPieters pour votre clarification.
James Sapam
71

Python a un faible support pour la fermeture. Pour voir ce que je veux dire, prenons l'exemple suivant d'un compteur utilisant la fermeture avec 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

La fermeture est assez élégante car elle donne aux fonctions écrites comme celle-ci la possibilité d'avoir une "mémoire interne". Depuis Python 2.7, cela n'est pas possible. Si tu essayes

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

count = initCounter();

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

Vous obtiendrez une erreur indiquant que x n'est pas défini. Mais comment est-ce possible s'il a été démontré par d'autres que vous pouvez l'imprimer? Cela est dû à la façon dont Python gère la portée variable des fonctions. Alors que la fonction intérieure peut lire les variables de la fonction externe, elle ne peut pas les écrire .

C'est vraiment dommage. Mais avec une fermeture en lecture seule, vous pouvez au moins implémenter le modèle de décorateur de fonctions pour lequel Python offre du sucre syntaxique.

Mettre à jour

Comme cela a été souligné, il existe des moyens de gérer les limitations de portée de python et je vais en exposer quelques-unes.

1. Utilisez leglobal mot clé (en général non recommandé).

2. Dans Python 3.x, utilisez le nonlocalmot - clé (suggéré par @unutbu et @leewz)

3. Définissez une classe modifiable simpleObject

class Object(object):
    pass

et créer un Object scopeintérieur initCounterpour stocker les variables

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

    return counter

Comme il ne scopes'agit que d'une référence, les actions prises avec ses champs ne se modifient pas vraiment scope, donc aucune erreur ne se produit.

4. Une manière alternative, comme l'a souligné @unutbu, serait de définir chaque variable comme un tableau ( x = [0]) et de modifier son premier élément ( x[0] += 1). Encore une fois, aucune erreur ne se produit car xelle-même n'est pas modifiée.

5. Comme suggéré par @raxacoricofallapatorius, vous pouvez faire xune propriété decounter

def initCounter ():

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

    counter.x = 0
    return counter
Cristian Garcia
la source
27
Il existe des façons de le faire. En Python2, vous pouvez créer x = [0]dans la portée externe et utiliser x[0] += 1dans la portée interne. En Python3, vous pouvez conserver votre code tel quel et utiliser le mot-clé non local .
unutbu
"Bien que la fonction interne puisse lire les variables de la fonction externe, elle ne peut pas les écrire." - C'est inexact selon le commentaire d'unutbu. Le problème est que lorsque Python rencontre quelque chose comme x = ..., x est interprété comme une variable locale, qui bien sûr n'est pas encore définie à ce stade. OTOH, si x est un objet mutable avec une méthode mutable, il peut être très bien modifié, par exemple si x est un objet qui prend en charge la méthode inc () qui se mute, x.inc () fonctionnera sans accroc.
Thanh DK
@ThanhDK Cela ne signifie-t-il pas que vous ne pouvez pas écrire dans la variable? Lorsque vous utilisez appeler une méthode à partir d'un objet mutable, vous lui dites simplement de se modifier, vous ne modifiez pas réellement la variable (qui contient simplement une référence à l'objet). En d'autres termes, la référence vers laquelle xpointe la variable reste exactement la même, même si vous appelez inc()ou quoi que ce soit, et vous n'avez pas effectivement écrit dans la variable.
user193130
4
Il y a une autre option, strictement meilleure que # 2, imv, de faire xune propriété decounter .
orome
9
Python 3 a le nonlocalmot - clé, qui est comme globalmais pour les variables d'une fonction externe. Cela permettra à une fonction interne de relier un nom à ses fonctions externes. Je pense que "lier au nom" est plus précis que "modifier la variable".
leewz
16

Python 2 n'avait pas de fermetures - il y avait des solutions de contournement qui ressemblaient à fermetures.

Il y a beaucoup d'exemples dans les réponses déjà données - copie de variables dans la fonction interne, modification d'un objet sur la fonction interne, etc.

Dans Python 3, le support est plus explicite - et succinct:

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

Usage:

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

Le nonlocalmot-clé lie la fonction interne à la variable externe explicitement mentionnée, l'enfermant en effet. D'où plus explicitement une «fermeture».

Lee Benson
la source
1
Intéressant, pour référence: docs.python.org/3/reference/… . Je ne sais pas pourquoi il n'est pas facile de trouver plus d'informations sur les fermetures (et comment vous pouvez vous attendre à ce qu'elles se comportent, venant de JS) dans la documentation de python3?
user3773048
9

J'ai eu une situation où j'avais besoin d'un espace de nom séparé mais persistant. J'ai utilisé des cours. Je n'en ai pas autrement. Les noms séparés mais persistants sont des fermetures.

>>> 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
fp_mora
la source
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

Donne:

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

Ceci est un exemple de ce qu'est une fermeture et comment elle peut être utilisée.

Krcn U
la source
0

J'aimerais offrir une autre comparaison simple entre python et l'exemple JS, si cela aide à rendre les choses plus claires.

JS:

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

et exécuter:

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

Python:

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

et exécuter:

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

Motif: comme beaucoup d'autres l'ont dit plus haut, en python, s'il existe une affectation dans la portée interne à une variable du même nom, une nouvelle référence dans la portée interne est créée. Ce n'est pas le cas avec JS, sauf si vous en déclarez explicitement un avec le varmot - clé.

forumulateur
la source