Accès aux variables définies à l’intérieur d’une fermeture Python

Vous souhaitez étendre une fermeture avec des fonctions qui permettent d’accéder aux variables internes et de les modifier.

Normalement, les variables internes d’une fermeture sont complètement cachées au monde extérieur. Toutefois, vous pouvez fournir l’accès en écrivant des fonctions d’accès et en les attachant à la fermeture en tant qu’attributs de fonction. Par exemple:

def exemple():
    n = 0
    # Fonction de fermeture
    def fct():
        print('n=', n)

    # Méthodes d'accès pour n
    def get_n():
        return n

    def set_n(valeur):
        nonlocal n
        n = valeur

    # Attacher comme attributs de fonction
    fct.get_n = get_n
    fct.set_n = set_n
    return fct

Voici un exemple d’utilisation de ce code:

>>> f = exemple()
>>> f()
n= 0
>>> f.set_n(10)
>>> f()
n= 10
>>> f.get_n()
10
>>>

Il y a deux caractéristiques principales qui font que ce code fonctionne. Tout d’abord, les déclarations nonlocal permettent d’écrire des fonctions qui modifient les variables internes.

Deuxièmement, les attributs de fonction permettent aux méthodes accesseurs d’être attachées à la fonction de fermeture d’une manière simple où elles fonctionnent un peu comme les méthodes d’instance (même si aucune classe n’est impliquée).

Une légère extension de ce idée peut être faite pour que les fermetures émulent les instances d’une classe. Tout ce que vous avez à faire est de copier les fonctions internes dans le dictionnaire d’une instance et de la retourner. Par exemple:

import sys
class ClosureInstance:
    def __init__(self, locals=None):
        if locals is None:
            locals = sys._getframe(1).f_locals

        #  Mettre à jour le dictionnaire d'instances avec des appelables
        self.__dict__.update((key,value) for key, value in locals.items()
                             if callable(value) )
    # Redirigez les méthodes spéciales
    def __len__(self):
        return self.__dict__['__len__']()

# Exemple d'utilisation
def Stack():
    items = []

    def push(item):
        items.append(item)

    def pop():
        return items.pop()

    def __len__():
        return len(items)

    return ClosureInstance()

Voici un exemple interactif pour montrer que ça marche vraiment:

>>> s = Stack()
>>> s
<__main__.ClosureInstance object at 0x10069ed10>
>>> s.push(10)
>>> s.push(20)
>>> s.push('Hello')
>>> len(s)
3
>>> s.pop()
'Hello'
>>> s.pop()
20
>>> s.pop()
10
>>>

Il est intéressant de noter que ce code tourne un peu plus vite qu’avec une définition de classe normale. Par exemple, vous pourriez être enclin à tester la performance par rapport à une classe comme celle-ci:

class Stack2:
    def __init__(self):
        self.items = []

    def push(self, item):
        self.items.append(item)

    def pop(self):
        return self.items.pop()

    def __len__(self):
        return len(self.items)

Si vous le faites, vous obtiendrez des résultats similaires à ceux qui suivent:

>>> from timeit import timeit
>>> # Test impliquant des fermetures
>>> s = Stack()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')
0.9874754269840196
>>> # Test impliquant une classe
>>> s = Stack2()
>>> timeit('s.push(1);s.pop()', 'from __main__ import s')
1.0707052160287276
>>>

Comme le montre l’illustration, la version de fermeture est environ 8% plus rapide. La plus grande partie de cela provient d’un accès simplifié aux variables d’instance. Les fermetures sont plus rapides parce qu’il n’y a pas de variable automatique supplémentaire.

Raymond Hettinger a imaginé une variante encore plus diabolique de cette idée. Cependant, si vous êtes enclin à faire quelque chose comme ceci dans votre code, sachez que c’est quand même un substitut assez bizarre pour une vraie classe.

Par exemple, les principales fonctionnalités telles que l’héritage, les propriétés, les descripteurs ou les méthodes de classe ne fonctionnent pas.

Vous devez également jouer quelques trucs pour obtenir des méthodes spéciales pour travailler (par exemple, voir l’implémentation de __len__() dans ClosureInstance).

Enfin, vous risquez de confondre les gens qui lisent votre code et de vous demander pourquoi il ne ressemble en rien à une définition de classe normale (bien sûr, ils se demanderont aussi pourquoi il est plus rapide).

Néanmoins, c’est un exemple intéressant de ce qui peut être fait en donnant accès aux internes d’une fermeture.

Dans l’ensemble, l’ajout de méthodes aux fermetures peut avoir plus d’utilité dans les paramètres où vous voulez faire des choses comme réinitialiser l’état interne, affleurer les tampons, effacer les caches, ou avoir une sorte de mécanisme de feedback.

LAISSER UN COMMENTAIRE

Please enter your comment!
Please enter your name here