En savoir plus sur les fermetures (Closures)

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Dans ce laboratoire (lab), vous allez en apprendre davantage sur les fermetures (closures) en Python. Les fermetures sont un concept de programmation puissant qui permet aux fonctions de mémoriser et d'accéder aux variables de leur portée englobante, même après que la fonction externe a terminé son exécution.

Vous comprendrez également les fermetures en tant que structure de données, les explorerez en tant que générateur de code et découvrirez comment implémenter la vérification de type (type - checking) avec les fermetures. Ce laboratoire vous aidera à découvrir certains aspects plus inhabituels et puissants des fermetures Python.

Les fermetures (Closures) en tant que structure de données

En Python, les fermetures offrent un moyen puissant d'encapsuler des données. L'encapsulation consiste à garder les données privées et à contrôler l'accès à celles-ci. Avec les fermetures, vous pouvez créer des fonctions qui gèrent et modifient des données privées sans avoir à utiliser des classes ou des variables globales. Les variables globales peuvent être accédées et modifiées depuis n'importe quel endroit de votre code, ce qui peut entraîner un comportement inattendu. Les classes, en revanche, nécessitent une structure plus complexe. Les fermetures offrent une alternative plus simple pour l'encapsulation de données.

Créons un fichier appelé counter.py pour illustrer ce concept :

  1. Ouvrez l'WebIDE et créez un nouveau fichier nommé counter.py dans le répertoire /home/labex/project. C'est là que nous allons écrire le code qui définit notre compteur basé sur une fermeture.

  2. Ajoutez le code suivant au fichier :

def counter(value):
    """
    Create a counter with increment and decrement functions.

    Args:
        value: Initial value of the counter

    Returns:
        Two functions: one to increment the counter, one to decrement it
    """
    def incr():
        nonlocal value
        value += 1
        return value

    def decr():
        nonlocal value
        value -= 1
        return value

    return incr, decr

Dans ce code, nous définissons une fonction appelée counter(). Cette fonction prend une valeur initiale value en argument. À l'intérieur de la fonction counter(), nous définissons deux fonctions internes : incr() et decr(). Ces fonctions internes ont accès au même variable value. Le mot-clé nonlocal est utilisé pour indiquer à Python que nous voulons modifier la variable value de la portée englobante (la fonction counter()). Sans le mot-clé nonlocal, Python créerait une nouvelle variable locale à l'intérieur des fonctions internes au lieu de modifier la variable value de la portée externe.

  1. Maintenant, créons un fichier de test pour voir cela en action. Créez un nouveau fichier nommé test_counter.py avec le contenu suivant :
from counter import counter

## Create a counter starting at 0
up, down = counter(0)

## Increment the counter several times
print("Incrementing the counter:")
print(up())  ## Should print 1
print(up())  ## Should print 2
print(up())  ## Should print 3

## Decrement the counter
print("\nDecrementing the counter:")
print(down())  ## Should print 2
print(down())  ## Should print 1

Dans ce fichier de test, nous importons d'abord la fonction counter() depuis le fichier counter.py. Ensuite, nous créons un compteur démarrant à 0 en appelant counter(0) et en déballant les fonctions retournées dans up et down. Nous appelons ensuite la fonction up() plusieurs fois pour incrémenter le compteur et afficher les résultats. Après cela, nous appelons la fonction down() pour décrémenter le compteur et afficher les résultats.

  1. Exécutez le fichier de test en exécutant la commande suivante dans le terminal :
python3 test_counter.py

Vous devriez voir la sortie suivante :

Incrementing the counter:
1
2
3

Decrementing the counter:
2
1

Remarquez qu'il n'y a pas de définition de classe ici. Les fonctions up() et down() manipulent une valeur partagée qui n'est ni une variable globale ni un attribut d'instance. Cette valeur est stockée dans la fermeture, ce qui la rend accessible uniquement aux fonctions retournées par counter().

Voici un exemple de comment les fermetures peuvent être utilisées comme une structure de données. La variable enfermée value est maintenue entre les appels de fonction, et elle est privée pour les fonctions qui y accèdent. Cela signifie qu'aucune autre partie de votre code ne peut directement accéder ou modifier cette variable value, offrant ainsi un niveau de protection des données.

Les fermetures (Closures) en tant que générateur de code

Dans cette étape, nous allons apprendre comment les fermetures peuvent être utilisées pour générer du code de manière dynamique. Plus précisément, nous allons construire un système de vérification de type (type-checking) pour les attributs de classe en utilisant les fermetures.

Tout d'abord, comprenons ce qu'est une fermeture. Une fermeture est un objet fonction qui se souvient des valeurs de la portée englobante même si elles ne sont plus présentes en mémoire. En Python, les fermetures sont créées lorsqu'une fonction imbriquée fait référence à une valeur de sa fonction englobante.

Maintenant, commençons à implémenter notre système de vérification de type.

  1. Créez un nouveau fichier nommé typedproperty.py dans le répertoire /home/labex/project avec le code suivant :
## typedproperty.py

def typedproperty(name, expected_type):
    """
    Create a property with type checking.

    Args:
        name: The name of the property
        expected_type: The expected type of the property value

    Returns:
        A property object that performs type checking
    """
    private_name = '_' + name

    @property
    def value(self):
        return getattr(self, private_name)

    @value.setter
    def value(self, val):
        if not isinstance(val, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, val)

    return value

Dans ce code, la fonction typedproperty est une fermeture. Elle prend deux arguments : name et expected_type. Le décorateur @property est utilisé pour créer une méthode getter pour la propriété, qui récupère la valeur de l'attribut privé. Le décorateur @value.setter crée une méthode setter qui vérifie si la valeur à définir est du type attendu. Sinon, elle lève une erreur TypeError.

  1. Maintenant, créons une classe qui utilise ces propriétés typées. Créez un fichier nommé stock.py avec le code suivant :
from typedproperty import typedproperty

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Dans la classe Stock, nous utilisons la fonction typedproperty pour créer des attributs avec vérification de type pour name, shares et price. Lorsque nous créons une instance de la classe Stock, la vérification de type sera appliquée automatiquement.

  1. Créons un fichier de test pour voir cela en action. Créez un fichier nommé test_stock.py avec le code suivant :
from stock import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.shares = "hundred"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

Dans ce fichier de test, nous créons d'abord un objet Stock avec les types corrects. Ensuite, nous essayons de définir l'attribut shares avec une chaîne de caractères, ce qui devrait lever une erreur TypeError car le type attendu est un entier.

  1. Exécutez le fichier de test :
python3 test_stock.py

Vous devriez voir une sortie similaire à :

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'int'>

Cette sortie montre que la vérification de type fonctionne correctement.

  1. Maintenant, améliorons le fichier typedproperty.py en ajoutant des fonctions pratiques pour les types courants. Ajoutez le code suivant à la fin du fichier :
def String(name):
    """Create a string property with type checking."""
    return typedproperty(name, str)

def Integer(name):
    """Create an integer property with type checking."""
    return typedproperty(name, int)

def Float(name):
    """Create a float property with type checking."""
    return typedproperty(name, float)

Ces fonctions sont simplement des enveloppes autour de la fonction typedproperty, ce qui facilite la création de propriétés pour les types courants.

  1. Créez un nouveau fichier nommé stock_enhanced.py qui utilise ces fonctions pratiques :
from typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    name = String('name')
    shares = Integer('shares')
    price = Float('price')

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Cette classe Stock utilise les fonctions pratiques pour créer des attributs avec vérification de type, ce qui rend le code plus lisible.

  1. Créez un fichier de test test_stock_enhanced.py pour tester la version améliorée :
from stock_enhanced import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try to set an attribute with the wrong type
try:
    s.price = "490.1"  ## This should raise a TypeError
    print("Type check failed")
except TypeError as e:
    print(f"Type check succeeded: {e}")

Ce fichier de test est similaire au précédent, mais il teste la classe Stock améliorée.

  1. Exécutez le test :
python3 test_stock_enhanced.py

Vous devriez voir une sortie similaire à :

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Type check succeeded: Expected <class 'float'>

Dans cette étape, nous avons démontré comment les fermetures peuvent être utilisées pour générer du code. La fonction typedproperty crée des objets propriétés qui effectuent une vérification de type, et les fonctions String, Integer et Float créent des propriétés spécialisées pour les types courants.

Élimination des noms de propriétés avec les descripteurs (Descriptors)

Dans l'étape précédente, lors de la création de propriétés typées, nous devions explicitement indiquer les noms des propriétés. Cela est redondant car les noms des propriétés sont déjà spécifiés dans la définition de la classe. Dans cette étape, nous allons utiliser des descripteurs pour éliminer ce redondance.

Un descripteur en Python est un objet spécial qui contrôle la manière dont l'accès aux attributs fonctionne. Lorsque vous implémentez la méthode __set_name__ dans un descripteur, il peut automatiquement récupérer le nom de l'attribut à partir de la définition de la classe.

Commençons par créer un nouveau fichier.

  1. Créez un nouveau fichier nommé improved_typedproperty.py avec le code suivant :
## improved_typedproperty.py

class TypedProperty:
    """
    A descriptor that performs type checking.

    This descriptor automatically captures the attribute name from the class definition.
    """
    def __init__(self, expected_type):
        self.expected_type = expected_type
        self.name = None

    def __set_name__(self, owner, name):
        ## This method is called when the descriptor is assigned to a class attribute
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(f'Expected {self.expected_type}')
        instance.__dict__[self.name] = value

## Convenience functions
def String():
    """Create a string property with type checking."""
    return TypedProperty(str)

def Integer():
    """Create an integer property with type checking."""
    return TypedProperty(int)

def Float():
    """Create a float property with type checking."""
    return TypedProperty(float)

Ce code définit une classe de descripteur appelée TypedProperty qui vérifie le type des valeurs assignées aux attributs. La méthode __set_name__ est appelée automatiquement lorsque le descripteur est assigné à un attribut de classe. Cela permet au descripteur de capturer le nom de l'attribut sans que nous ayons à le spécifier manuellement.

Ensuite, nous allons créer une classe qui utilise ces propriétés typées améliorées.

  1. Créez un nouveau fichier nommé stock_improved.py qui utilise les propriétés typées améliorées :
from improved_typedproperty import String, Integer, Float

class Stock:
    """A class representing a stock with type-checked attributes."""

    ## No need to specify property names anymore
    name = String()
    shares = Integer()
    price = Float()

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Remarquez que nous n'avons plus besoin de spécifier les noms des propriétés lors de la création des propriétés typées. Le descripteur récupérera automatiquement le nom de l'attribut à partir de la définition de la classe.

Maintenant, testons notre classe améliorée.

  1. Créez un fichier de test test_stock_improved.py pour tester la version améliorée :
from stock_improved import Stock

## Create a stock with correct types
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}")
print(f"Stock shares: {s.shares}")
print(f"Stock price: {s.price}")

## Try setting attributes with wrong types
try:
    s.name = 123  ## Should raise TypeError
    print("Name type check failed")
except TypeError as e:
    print(f"Name type check succeeded: {e}")

try:
    s.shares = "hundred"  ## Should raise TypeError
    print("Shares type check failed")
except TypeError as e:
    print(f"Shares type check succeeded: {e}")

try:
    s.price = "490.1"  ## Should raise TypeError
    print("Price type check failed")
except TypeError as e:
    print(f"Price type check succeeded: {e}")

Enfin, nous allons exécuter le test pour voir si tout fonctionne comme prévu.

  1. Exécutez le test :
python3 test_stock_improved.py

Vous devriez voir une sortie similaire à :

Stock name: GOOG
Stock shares: 100
Stock price: 490.1
Name type check succeeded: Expected <class 'str'>
Shares type check succeeded: Expected <class 'int'>
Price type check succeeded: Expected <class 'float'>

Dans cette étape, nous avons amélioré notre système de vérification de type en utilisant des descripteurs et la méthode __set_name__. Cela élimine la spécification redondante des noms de propriétés, rendant le code plus court et moins sujet aux erreurs.

La méthode __set_name__ est une fonctionnalité très utile des descripteurs. Elle leur permet de recueillir automatiquement des informations sur la manière dont ils sont utilisés dans une définition de classe. Cela peut être utilisé pour créer des API plus faciles à comprendre et à utiliser.

Résumé

Dans ce laboratoire (lab), vous avez appris des aspects avancés des fermetures (closures) en Python. Tout d'abord, vous avez exploré l'utilisation des fermetures comme une structure de données, qui peut encapsuler des données et permettre aux fonctions de maintenir un état entre les appels sans avoir recours à des classes ou des variables globales. Ensuite, vous avez vu comment les fermetures peuvent agir comme un générateur de code, générant des objets propriétés avec vérification de type pour une approche plus fonctionnelle de la validation des attributs.

Vous avez également découvert comment utiliser le protocole de descripteur (descriptor protocol) et la méthode __set_name__ pour créer des attributs de vérification de type élégants qui capturent automatiquement leurs noms à partir des définitions de classe. Ces techniques montrent la puissance et la flexibilité des fermetures, vous permettant d'implémenter des comportements complexes de manière concise. Comprendre les fermetures et les descripteurs vous offre plus d'outils pour créer un code Python maintenable et robuste.