Python >> Tutoriel Python >  >> Python

Fermetures et décorateurs en Python

Ce didacticiel vous apprend deux compétences Python avancées :les fermetures et les décorateurs. Les maîtriser fera de vous un meilleur codeur aujourd'hui, alors plongeons-y !

Fermetures

Chaque fonction de Python est de première classe, car elle peut être transmise comme n'importe quel autre objet. Habituellement, lorsqu'un langage de programmation crée une fonction comme d'autres types de données, ce langage de programmation prend en charge quelque chose appelé Closures.

Une fermeture est une fonction imbriquée. Il est défini dans une fonction externe.

def outer_hello_fn():
    def hello():
        print("Hello Finxter!")
        
    hello()

Ici, nous avons une fonction externe appelée outer_ hello_ fn , il n'a pas d'arguments d'entrée. La fonction hello est une fonction imbriquée définie dans la fonction externe. Le hello la fonction est une fermeture.

Essayez-le vous-même :

Exercice  :Quel est le résultat de cet extrait de code ? Exécutez le code pour tester si vous avez raison.

Lorsque la fonction externe est appelée, le hello la fonction qu'il contient sera définie puis invoquée. Voici l'appel et la sortie de la fonction :

outer_hello_fn()

Sortie :

Hello Finxter!

bonjour a été défini dans outer_hello_fn , ce qui signifie que si vous essayez d'invoquer le hello fonction, cela ne fonctionnera pas.

hello()

Sortie :

NameError: name 'hello' is not defined

Si vous souhaitez accéder à une fonction définie dans une autre fonction, renvoyez l'objet fonction lui-même. Voici comment.

def get_hello_fn():
    def hello():
        print("Hello Finxter!")

    return hello

La fonction externe est appelée get_hello_fn . hello , est une fonction interne, ou fermeture. Au lieu d'invoquer cette fonction hello, renvoyez simplement le hello fonction à celui qui appelle get_hello_fn . Par exemple :

hello_fn = get_hello_fn()

Invoquer get_hello_fn stocke l'objet de la fonction de retour dans le hello_fn variable. Si vous explorez le contenu de ce hello_fn variable, vous verrez qu'il s'agit d'un objet fonction.

hello_fn

Sortie :

<function __main__.get_hello_fn.<locals>.hello>

Comme vous pouvez le voir dans la structure, il s'agit d'une fonction définie localement dans get_hello_fn , c'est-à-dire une fonction définie dans une autre fonction, c'est-à-dire une fermeture. Maintenant, cette fermeture peut être invoquée en utilisant la variable hello_fn.

hello_fn()

Sortie :

Hello Finxter!

Appelez hello_fn() imprimera Hello Finxter! à l'écran. Une fermeture est quelque chose de plus qu'une simple fonction interne définie dans une fonction externe. Il y a plus à cela. Voici un autre exemple :

def hello_by_name(name):
    
    def hello():
        print("Hello!", name)
        
    hello()
    
    return hello

Ici, la fonction externe s'appelle hello_by_name , qui prend en argument d'entrée le nom d'un individu. Dans cette fonction externe, il y a le hello fonction intérieure. Il imprime à l'écran Hello! , et la valeur du nom.

La variable name est un argument d'entrée de la fonction externe. Il est également accessible dans la fonction hello interne. La variable de nom ici peut être considérée comme une variable locale à la fonction externe. Les variables locales dans la fonction externe sont accessibles par des fermetures. Voici un exemple de passage d'un argument à la fonction externe :

greet_hello_fn = hello_by_name("Chris")

La fonction hello est retournée et elle est stockée dans le greet_hello_fn variables.

L'exécution de ceci imprime Hello! Chris à l'écran. C'est parce que nous avons invoqué la fermeture depuis la fonction externe. Nous avons une référence à la fermeture qui a été définie par la fonction externe.

greet_hello_fn()

Sortie :

Hello! Chris

Remarquez quelque chose d'intéressant ici. Chris est disponible dans le nom de la variable qui est local au hello_by_name fonction.

Maintenant, nous avons déjà appelé et quitté hello_by_name mais la valeur dans la variable name est toujours disponible pour notre fermeture. Et c'est un autre concept important sur les fermetures en Python. Ils conservent la référence à l'état local même après que la fonction externe qui a défini l'état local s'est exécutée et n'existe plus. Voici un autre exemple légèrement différent illustrant ce concept.

def greet_by_name(name):
    
    greeting_msg = "Hi there!"

    def greeting():
        print(greeting_msg, name)
        
    return greeting

La fonction externe, greet_by_name , prend un argument d'entrée, name. Dans la fonction externe, une variable locale appelée greeting_msg est défini qui dit, “Hi there!” . Une fermeture appelée salutation est définie dans la fonction externe. Il accède à la variable locale greeting_msg ainsi que le nom de l'argument d'entrée. Une référence à cette fermeture de message d'accueil est renvoyée depuis le greet_by_name externe fonction.

Continuons et invoquons greet_by_name et stockons l'objet fonction qu'il renvoie dans la variable greet_fn. Nous utiliserons cet objet fonction pour saluer Ray par son nom. Allez-y et appelez le greet_fn() en spécifiant des parenthèses. Et il devrait dire, bonjour ! Rayon. Observez comment la fermeture a accès non seulement au nom Ray mais aussi au message d'accueil, même après avoir exécuté et quitté la fonction externe.

greet_fn = greet_by_name("Ray")
greet_fn()

Sortie :

Hi there! Ray

Les fermetures véhiculent des informations sur l'état local. Voyons ce qui se passe lorsque la fonction greet_by_name est supprimée, vous n'avez donc plus accès à la fonction externe.

del greet_by_name

Maintenant, rappelez-vous que le nom et le message d'accueil sont tous deux des variables définies dans la fonction externe. Que leur arrive-t-il ? Maintenant, si vous essayez d'invoquer la salutation par son nom.

greet_by_name("Ray")

Sortie :

NameError: name 'greet_by_name' is not defined

Qu'en est-il du greet_fn ?

N'oubliez pas que greet_fn est une référence à notre fermeture. Cela fonctionne-t-il toujours ?

greet_fn()

Sortie :

Hi there! Ray

Non seulement cela fonctionne, mais il a toujours accès aux variables locales qui ont été définies dans la fonction externe. La fonction externe n'existe plus dans la mémoire Python, mais les variables locales sont toujours disponibles avec notre fermeture.

Décorateurs – Modification du code

Les décorateurs aident à ajouter des fonctionnalités au code existant sans avoir à modifier le code lui-même. Les décorateurs sont appelés ainsi parce qu'ils décorent le code, ils ne modifient pas le code, mais ils font en sorte que le code fasse différentes choses en utilisant la décoration. Maintenant que nous avons compris les fermetures, nous pouvons progresser étape par étape pour comprendre et utiliser les décorateurs.

def print_message():
    print("Decorators are cool!")

Voici une fonction simple qui imprime un message à l'écran.

print_message()

Sortie :

Decorators are cool!

Chaque fois que vous invoquerez cette fonction, elle imprimera toujours le même message. Je souhaite utiliser quelques caractères pour décorer le message d'origine, et je le fais à l'aide de la fonction de surbrillance.

import random

def highlight():
    
    annotations = ['-', '*', '+', ':', '^']
    annotate = random.choice(annotations)
    
    print(annotate * 50)
    
    print_message()
    
    print(annotate * 50)

La surbrillance de la fonction externe n'a pas d'arguments d'entrée. Dans la fonction de surbrillance, un choix aléatoire d'annotations est utilisé pour décorer le message d'origine. Le message sera mis en surbrillance avec un choix aléatoire entre le tiret, l'astérisque, le plus, les deux-points et le caret. La sortie aura une annotation de 50 caractères avant et après le message qui se trouve dans la fonction print_message.

Essayez-le vous-même :

Exercice  :Quel est le résultat de cet extrait de code ? Exécutez le code pour tester votre compréhension !

highlight()

Sortie :

::::::::::::::::::::::::::::::::::::::::::::::::::
Decorators are cool!
::::::::::::::::::::::::::::::::::::::::::::::::::

Voici une autre fonction avec un message différent, print_another_message.

def print_another_message():
    print("Decorators use closures.")

Maintenant, si je veux également mettre en surbrillance ce message, la fonction de surbrillance existante ne fonctionnera pas car elle a été codée en dur pour invoquer la fonction print_message. Alors, comment puis-je modifier cette fonction de surbrillance afin qu'elle soit capable de mettre en surbrillance tout message que je souhaite imprimer à l'écran ? N'oubliez pas que les fonctions sont des citoyens de première classe en Python, ce qui signifie que quelle que soit la fonction d'impression que vous avez, vous pouvez la passer comme argument d'entrée à la fonction de surbrillance. Voici une fonction de surbrillance redéfinie, make_highlighted.

def make_highlighted(func):
    
    annotations = ['-', '*', '+', ':', '^']
    annotate = random.choice(annotations)
    
    def highlight():
        print(annotate * 50)
        func()
        print(annotate * 50)            
    
    return highlight

La seule différence ici est que make_highlighted prend un argument d'entrée qui est une fonction. Cette fonction est ce qui imprimera le message à afficher. Le changement suivant est que dans la fermeture de surbrillance, l'objet fonction qui a été transmis est appelé. C'est l'objet fonction qui imprimera le message. Nous avons maintenant deux fonctions d'impression jusqu'à présent.

print_message()
print_another_message()

Et maintenant, avec l'aide de la fonction make_highlighted, tout message imprimé peut être mis en surbrillance. Par exemple :

highlight_and_print_message = make_highlighted(print_message)

highlight_and_print_message()

Sortie :

++++++++++++++++++++++++++++++++++++++++++++++++++
Decorators are cool!
++++++++++++++++++++++++++++++++++++++++++++++++++

Pour imprimer un message différent et le mettre en surbrillance, passez simplement un objet de fonction différent à la fonction make_highlighted.

highlight_and_print_another_message = make_highlighted(print_another_message)

highlight_and_print_another_message()

Sortie :

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Decorators use closures.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Il est clair que la fonction make_highlighted est très générique, vous pouvez l'utiliser pour mettre en surbrillance n'importe quel message que vous souhaitez afficher à l'écran. La fonction make_highlighted est un décorateur.

Pourquoi est-ce décorateur ? Eh bien, il prend un objet fonctionnel, le décore et le modifie. Dans cet exemple, il met en surbrillance la fonction avec des caractères aléatoires. Les décorateurs sont un modèle de conception standard, et en Python, vous pouvez utiliser les décorateurs plus facilement. Au lieu de passer un objet de fonction à make_highlighted, d'accéder à la fermeture, puis d'invoquer la fermeture, vous pouvez simplement décorer n'importe quelle fonction en utilisant @ et en plaçant le décorateur avant la fonction à décorer.

@make_highlighted
def print_a_third_message():
    print("This is how decorators are used")

L'utilisation du décorateur @make_highlighted passera automatiquement la fonction print_a_third_message comme entrée à make_highlighted et mettra en surbrillance le message.

print_a_third_message()

Sortie :

^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is how decorators are used
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Vous pouvez maintenant utiliser le décorateur pour mettre en surbrillance n'importe quel message.

@make_highlighted
def print_any_message():
    print("This message is highlighted!")

Et maintenant, si vous appelez print_any_message, vous constaterez que le résultat affiché à l'écran est mis en surbrillance.

print_any_message()

Sortie :

++++++++++++++++++++++++++++++++++++++++++++++++++
This message is highlighted!
++++++++++++++++++++++++++++++++++++++++++++++++++

Décorateurs - Personnalisation

Voyons un autre exemple de décorateur qui fera du travail. Il effectuera une vérification des erreurs pour nous.

Voici deux fonctions qui seront l'entrée de notre décorateur

def square_area(length):
    
    return length**2

def square_perimeter(length):
    
    return 4 * length

Nous supposons que la valeur du rayon transmis est positive et correcte.

square_area(5)

Sortie :

25

Et si j'invoque le square_area et passe en -1 ?

square_area(-1)

Sortie :

-4

L'entrée -1 n'a pas de sens en tant que valeur pour la longueur. La fonction aurait dû renvoyer une erreur ou nous dire d'une manière ou d'une autre que les valeurs négatives de longueur ne sont pas valides. Maintenant, si vous deviez effectuer une vérification des erreurs pour chacune de ces fonctions, nous devrions le faire individuellement. Nous devrions avoir une instruction if dans la fonction de zone ainsi que dans la fonction de périmètre. Au lieu de cela, écrivons un décorateur qui effectuera cette vérification d'erreur pour nous. Le décorateur safe_calculate prend un argument d'entrée qui est un objet de fonction.

def safe_calculate(func):
    
    def calculate(length):
        if length <= 0:
            raise ValueError("Length cannot be negative or zero")
        
        return func(length)
    
    return calculate

C'est l'objet fonction qui effectuera le calcul. Dans la fonction externe safe_calculate, la fonction interne appelée calculate est la fermeture. calculate prend un argument d'entrée, la longueur. Il vérifie si la longueur est inférieure ou égale à 0. Si oui, il génère une erreur. Et la façon dont il génère une erreur consiste simplement à appeler une augmentation ValueError, "La longueur ne peut pas être négative ou nulle". Une fois cette erreur déclenchée, Python arrêtera l'exécution. Mais si length est positif, il invoquera func et passera length comme argument d'entrée. Le safe_calculate est notre décorateur, qui prend en entrée un objet fonction et renvoie une fermeture qui effectuera le calcul sécurisé.

square_area_safe = safe_calculate(square_area)

Testons-le d'abord :

square_area_safe(5)

C'est sûr et j'obtiens le résultat ici sur l'écran.

25

L'invoquer avec un nombre négatif générera une erreur

square_area_safe(-1)

Sortie :

ValueError: Length cannot be negative or zero

Décorons également la fonction de périmètre avec le safe_calculate.

square_perimeter_safe = safe_calculate(square_perimeter)

square_perimeter(10)

Sortie :

40

Mais si vous deviez appeler square_perimeter_safe avec une valeur négative pour la longueur du puits, c'est une ValueError.

square_perimeter_safe(-10)

Sortie :

ValueError: Length cannot be negative or zero

Maintenant que vous avez un décorateur, vous devriez décorer vos fonctions plutôt que d'utiliser la méthode que nous avons utilisée jusqu'à présent.

@safe_calculate
def square_area(length):
    return length**2

@safe_calculate
def square_perimeter(length):
    return 4 * length

Maintenant, la prochaine fois que square_area ou square_perimeter est appelé, le contrôle de sécurité sera effectué.

square_perimeter(3)

Sortie :

12

Si vous essayez de calculer le périmètre pour une valeur négative de la longueur, vous obtiendrez une ValueError. La fonction safe_calculate que nous avons configurée précédemment a une limitation, et vous verrez ce qu'elle est dans un futur exemple.

square_perimeter(-3)

Sortie :

ValueError: Length cannot be negative or zero

Que se passe-t-il lorsque vous avez plusieurs entrées ? Voici une fonction qui calcule l'aire d'un rectangle.

@safe_calculate
def rectangle_area(length, width):
    return length * width

Dans notre fonction safe_calculate, nous avions invoqué l'objet func qui effectue le calcul avec un seul argument d'entrée, avec juste la longueur variable. Cela va poser un problème lorsque nous utilisons le décorateur safe_calculate pour la fonction rectangle_area.

Une fois que j'ai décoré cette fonction, je vais l'invoquer avec 4, 5.

rectangle_area(4, 5)

Sortie :

TypeError: calculate() takes 1 positional argument but 2 were given

Le problème vient de la façon dont nous avions défini la fermeture dans la fonction safe_calculate.

La fermeture calculate ne prend qu'un seul argument d'entrée. Si une fonction a plusieurs arguments d'entrée, alors safe_calculate ne peut pas être utilisé. Une fonction safe_calculate_all redéfinie est illustrée ci-dessous :

def safe_calculate_all(func):
    
    def calculate(*args):
        
        for arg in args:
            if arg <= 0:
                raise ValueError("Argument cannot be negative or zero")
        
        return func(*args)
    
    return calculate. 

Il prend un argument d'entrée qui est l'objet fonction qui doit être décoré. Le principal changement concerne les arguments d'entrée qui sont transmis à la fermeture calculate. La fonction calculate prend maintenant des arguments de longueur variable, *args. La fonction itère sur tous les arguments qui ont été passés et vérifie si l'argument est inférieur ou égal à 0. Si l'un des arguments est inférieur ou égal à 0, une ValueError sera levée. N'oubliez pas que *args décompressera les arguments d'origine afin que les éléments du tuple soient transmis individuellement à l'objet fonction, func. Vous pouvez maintenant utiliser ce décorateur safe_calculate_all avec des fonctions qui ont n'importe quel nombre d'arguments.

@safe_calculate_all
def rectangle_area(length, width):
    return length * width
rectangle_area(10, 3)

Sortie :

30

Essayons d'invoquer la même fonction, mais cette fois l'un des arguments est négatif. La largeur est négative et cela me donne une ValueError, grâce à notre décorateur safe_calculate_all.

rectangle_area(10, -3)

Lorsque vous invoquez cette fonction, elle vérifie tous les arguments.

ValueError: Argument cannot be negative or zero

Peu importe quel argument est négatif, vous obtenez toujours la ValueError. Ici la longueur est négative :

rectangle_area(-10, 3)

Sortie :

ValueError: Argument cannot be negative or zero

Décorateurs d'enchaînement

Vous pouvez décorer une fonction à l'aide de plusieurs décorateurs. Et ces décorateurs seront enchaînés.

Voici deux décorateurs, l'un imprime des astérisques et l'autre des signes plus

def asterisk_highlight(func):
    
    def highlight():
        print("*" * 50)

        func()

        print("*" * 50)            
    
    return highlight

def plus_highlight(func):
    
    def highlight():
        print("+" * 50)

        func()

        print("+" * 50)            
    
    return highlight

Le print_message_one est décoré de l'asterisk_highlight.

@asterisk_highlight
def print_message_one():
    print("Decorators are cool!") 
print_message_one()

Sortie :

**************************************************
Decorators are cool!
**************************************************

Définissons maintenant une autre fonction d'impression, mais cette fois nous allons la décorer à l'aide de deux décorateurs, le plus_highlight et l'asterisk_highlight.

@plus_highlight
@asterisk_highlight
def print_message_one():
    print("Decorators are cool!")

Ce que vous voyez ici est un exemple d'enchaînement de décorateurs. Mais comment sont-ils enchaînés ? Quelle décoration vient en premier, l'astérisque_highlight ou le plus_highlight ? Le décorateur le plus proche de la définition de la fonction est celui qui est exécuté en premier, puis le décorateur le plus éloigné de la définition de la fonction. Cela signifie que le message sera d'abord mis en surbrillance avec l'astérisque, puis le plus.

print_message_one()

Sortie :

++++++++++++++++++++++++++++++++++++++++++++++++++
**************************************************
Decorators are cool!
**************************************************
++++++++++++++++++++++++++++++++++++++++++++++++++

Si vous modifiez l'ordre des décorateurs, l'ordre des décorations changera également.

@asterisk_highlight
@plus_highlight
def print_message_one():
    print("Decorators are cool!") 

Vous aurez la même fonction print_message_one, mais le décorateur le plus proche de la définition de la fonction est le plus_highlight puis l'asterisk_highlight.

print_message_one()

Sortie :

**************************************************
++++++++++++++++++++++++++++++++++++++++++++++++++
Decorators are cool!
++++++++++++++++++++++++++++++++++++++++++++++++++
**************************************************

Utilisation des kwargs dans les décorateurs

Dans cet exemple, nous utilisons kwargs pour afficher différents messages pour un décorateur qui chronomètre l'exécution d'une fonction

def timeit(func):
        def timed(*args, **kw):
            if 'start_timeit_desc' in kw:
                print(kw.get('start_timeit_desc'))
            ts = time.time()
            result = func(*args, **kw)
            te = time.time()
            if 'end_timeit_desc' in kw:
                print('Running time for {} is {} ms'.format(kw.get('end_timeit_desc'), (te - ts) * 1000))
            return result
        return timed 

Le décorateur timeit est utilisé pour la fonction de test. Trois paramètres sont passés au test de fonction :a, b et **kwargs. Les paramètres a et b sont manipulés dans le décorateur avec *args comme nous l'avons vu précédemment. Le paramètre **kwargs est utilisé pour transmettre les descriptions de la fonction. Ces paramètres sont start_timeit_desc et end_timeit_desc. Ces deux paramètres sont vérifiés à l'intérieur de la fermeture temporisée et afficheront les messages qu'ils contiennent.

@timeit
def test(a,b, **kwargs):
    return a * b


result = test(10,20, start_timeit_desc = "Start of test(10,20)...", end_timeit_desc = "End of test(10,20)")
print("result of test(10,20) = " + str(result))
Output:
Start of test(10,20)...
Running time for End of test(10,20) is 0.0 ms
result of test(10,20) = 200