Séchez votre code Python avec les décorateurs

Les décorateurs sont l’une des fonctionnalités les plus agréables de Python, mais pour les programmeurs débutants, ils peuvent sembler magiques. Le but de cet article est de comprendre en profondeur le mécanisme qui se cache derrière les décorateurs Python..

Voici ce que vous allez apprendre:

  • Que sont les décorateurs Python et à quoi servent-ils?
  • comment définir nos propres décorateurs
  • des exemples de décorateurs du monde réel et leur fonctionnement
  • comment écrire un meilleur code en utilisant des décorateurs

introduction

Au cas où vous n'en auriez pas encore vu un (ou peut-être que vous ne saviez pas que vous en avez eu un), les décorateurs ressemblent à ceci:

@decorator def function_to_decorate (): pass

Vous les rencontrez généralement au-dessus de la définition d'une fonction, et ils sont préfixés par @. Les décorateurs sont particulièrement efficaces pour conserver votre code SEC (Ne te répète pas), et ils le font tout en améliorant la lisibilité de votre code.

Encore flou? Ne le soyez pas, car les décorateurs ne sont que des fonctions Python. C'est vrai! Vous savez déjà comment en créer un. En fait, le principe fondamental des décorateurs est la composition des fonctions. Prenons un exemple:

def x_plus_2 (x): renvoie x + 2 print (x_plus_2 (2)) # 2 + 2 == 4 def x_squared (x): renvoie x * x print (x_squared (3)) # 3 ^ 2 == 9 # composer les deux fonctions pour x = 2 print (x_squared (x_plus_2 (2))) # # 2 + 2) ^ 2 == 16 print (x_squared (x_plus_2 (3))) # # 3 + 2) ^ 2 == 25 print (x_squared (x_plus_2 (4))) # (4 + 2) ^ 2 == 36

Et si nous voulions créer une autre fonction, x_plus_2_squared? Essayer de composer les fonctions serait vain:

x_squared (x_plus_2) # TypeError: type (s) d'opérande non pris en charge pour *: 'fonction' et 'fonction'

Vous ne pouvez pas composer des fonctions de cette manière, car les deux fonctions prennent des nombres comme arguments. Cependant, cela fonctionnera:

# Créons maintenant une composition de fonction appropriée sans appliquer réellement la fonction x_plus_2_squared = lambda x: x_squared (x_plus_2 (x)) print (x_plus_2_squared (2)) # (2 + 2) ^ 2 == 16 print (x_plus_2_squared (3)) # (3 + 2) ^ 2 == 25 imprimer (x_plus_2_squared (4)) # (4 + 2) ^ 2 == 36

Redéfinissons comment x_squared travaux. Si nous voulons x_squared pour être composable par défaut, il devrait:

  1. Accepter une fonction en tant qu'argument
  2. Renvoyer une autre fonction

Nous nommerons la version composable de x_squared simplement au carré.

def squared (func): return lambda x: func (x) * func (x) empreinte (carrée (x_plus_2) (2)) # (2 + 2) ^ 2 == 16 empreinte (carrée (x_plus_2) (3)) # (3 + 2) ^ 2 == 25 imprimer (carré (x_plus_2) (4)) # (4 + 2) ^ 2 == 36

Maintenant que nous avons défini le au carré fonctionner de manière à le rendre composable, nous pouvons l’utiliser avec n’importe quelle autre fonction. Voici quelques exemples:

def x_plus_3 (x): retourne x + 3 def x_times_2 (x): retourne x * 2 empreinte (carré (x_plus_3) (2)) # (2 + 3) ^ 2 == 25 empreinte (carré (x_times_2) (2) ) # (2 * 2) ^ 2 == 16

On peut dire ça au carré décore les fonctions x_plus_2x_plus_3, et x_times_2. Nous sommes très proches de la notation standard de décorateur. Regarde ça:

x_plus_2 = carré (x_plus_2) # Nous avons décoré x_plus_2 en caractères carrés (x_plus_2 (2)) # x_plus_2 renvoie maintenant le résultat carré décoré: (2 + 2) ^ 2 

C'est tout! x_plus_2 est une fonction proprement décorée en Python. Voici où le @ la notation se met en place:

def x_plus_2 (x): return x + 2 x_plus_2 = carré (x_plus_2) # ^ Ceci est complètement équivalent à: @squared def x_plus_2 (x): return x + 2

En fait, le @ la notation est une forme de sucre syntaxique. Essayons ça:

@squared def x_times_3 (x): renvoie 3 * x print (x_times_3 (2)) # (3 * 2) ^ 2 = 36. # Cela peut paraître un peu déroutant, mais en le décorant avec un carré, x_times_3 est devenu en fait ( 3 * x) * (3 * x) @squared def x_minus_1 (x): retour x - 1 empreinte (x_minus_1 (3)) # (3 - 1) ^ 2 = 4

Si au carré est le premier décorateur que vous avez écrit, donnez-vous une grande tape dans le dos. Vous avez saisi l'un des concepts les plus complexes de Python. En cours de route, vous avez appris une autre caractéristique fondamentale des langages de programmation fonctionnels: composition fonctionnelle.

Construisez votre propre décorateur

Un décorateur est une fonction qui prend une fonction en tant qu'argument et renvoie une autre fonction. Cela étant dit, le modèle générique permettant de définir un décorateur est le suivant:

def decorator (function_to_decorate): #… return déjà décoré_fonction

Si vous ne le saviez pas, vous pouvez définir des fonctions dans les fonctions. Dans la plupart des cas, le fonction_décorée sera défini à l'intérieur décorateur.

def decorator (function_to_decorate): def decorated_function (* arguments, ** kwargs): #… Puisque nous décorons 'function_to_decorate', nous devrions l'utiliser quelque part à l'intérieur d'ici.

Regardons un exemple plus pratique:

importer pytz depuis date-heure import date-heure def to_utc (function_to_decorate): def colored_function (): # Récupère le résultat de function_to_decorate et transforme le résultat au format UTC. "" Cela peut provenir d'une base de données ou d'une API "" "tz = pytz.timezone ('US / Pacific') retourne tz.localize (date / heure (2017, 8, 2, 12, 30, 0, 0)) @ to_utc def package_delivery_time (): "" "Cela peut provenir d'une base de données ou d'une API" "" tz = pytz.timezone ('US / Eastern') renvoie tz.localize (date / heure (2017, 8, 2, 12, 30). , 0, 0)) # Quelle coïncidence, même fuseau horaire différent! print ("PICKUP:", package_pickup_time ()) # '2017-08-02 19: 30: 00 + 00: 00' print ("DELIVERY:", package_delivery_time ()) # '2017-08-02 16:30: 00 + 00: 00 '

Sucré! Maintenant, vous pouvez être sûr que tout ce qui se trouve dans votre application est normalisé pour le fuseau horaire UTC..

Un exemple pratique

Un autre cas d'utilisation très populaire et classique pour les décorateurs est la mise en cache du résultat d'une fonction:

import time def mis en cache (function_to_decorate): _cache =  # Où nous conservons les résultats def created_function (* args): start_time = time.time () print ('_ cache:', _cache) si l'argument n'est pas dans _cache: _cache [args ] = function_to_decorate (* args) # Effectue le calcul et le stocke dans le cache print ('Heure de calcul:% ss'% round (time.time () - start_time, 2)) return _cache [args] return colored_function @cached def complex_computation (x, y): print ('Processing…') time.sleep (2) renvoie x + y print (complex_computation (1, 2)) # 3, exécution de l'opération coûteuse print (complex_computation (1, 2)) # 3. , SAUTER en exécutant l'opération onéreuse print (complex_computation (4, 5)) # 9, Exécuter en onéreuse opération d'impression (complex_computation (4, 5)) # 9, SAUTER en effectuant une opération onéreuse print (complex_computation (1, 2)) # 3 , SKIP effectuer l'opération coûteuse

Si vous regardez le code de manière superficielle, vous pourriez vous objecter. Le décorateur n'est pas réutilisable! Si on décore une autre fonction (disons autre_complexe_computation) et appelez-le avec les mêmes paramètres, nous obtiendrons les résultats mis en cache à partir du fonction complex_computation. Cela n'arrivera pas. Le décorateur est réutilisable, et voici pourquoi:

@cached def another_complex_computation (x, y): print ('Processing…') time.sleep (2) renvoie x * y print (another_complex_computation (1, 2)) # 2, exécution de l'opération coûteuse print (another_complex_computation (1, 2 )) # 2, PASSER l'opération coûteuse print (another_complex_computation (1, 2)) # 2, PASSER l'opération couteuse

le mis en cache fonction est appelée une fois pour chaque fonction qu’elle décore, donc une autre _cache variable est instanciée à chaque fois et vit dans ce contexte. Testons ceci:

print (complex_computation (10, 20)) # -> 30 print (another_complex_computation (10, 20)) # -> 200

Décorateurs à l'état sauvage

Comme vous l'avez peut-être remarqué, le décorateur que nous venons de coder est très utile. C'est tellement utile qu'une version plus complexe et robuste existe déjà dans la norme functools module. Il s'appelle lru_cache. LRU est l'abréviation de Moins récemment utilisé, une stratégie de cache. 

from functools import lru_cache @lru_cache () def complex_computation (x, y): print ('Traitement en cours') time.sleep (2) renvoie x + y print (complex_computation (1, 2)) # 3… print (3) (complex_computation ( 1, 2)) # 3 print (complex_computation (2, 3)) # Traitement en cours… 5 print (complex_computation (1, 2)) # 3 print (complex_computation (2, 3)) # 5

L'une de mes utilisations préférées des décorateurs est le framework Web Flask. Il est tellement intéressant que cet extrait de code soit la première chose que vous voyez sur le site Web de Flask. Voici l'extrait de code:

depuis une importation de flacon Flask app = Flask (__ name__) @ app.route ("/") def hello (): renvoie "Hello World!" si __name__ == "__main__": app.run ()

le app.route décorateur assigne la fonction Bonjour en tant que gestionnaire de demandes pour l'itinéraire "/". La simplicité est incroyable. 

Une autre utilisation intéressante de décorateurs est à l'intérieur de Django. Habituellement, les applications Web ont deux types de pages: 

  1. les pages que vous pouvez visualiser sans être authentifiées (page d'accueil, page d'arrivée, article de blog, connexion, enregistrement)
  2. pages à authentifier pour pouvoir les visualiser (paramètres de profil, boîte de réception, tableau de bord)

Si vous essayez d'afficher une page de ce dernier type, vous serez généralement redirigé vers une page de connexion. Voici comment implémenter cela dans Django:

depuis django.http, importez HttpResponse depuis django.contrib.auth.decorators, importez login_required # Pages publiques def home (demande): renvoyer HttpResponse ("Accueil") def landing (demande): retourne HttpResponse ("Atterrissage") # Pages authentifiées @login_required (login_url = '/ login') def tableau de bord (demande): renvoyer HttpResponse ("Tableau de bord") @login_required (login_url = '/ login') def profile_settings (demande): renvoie HttpResponse ("Paramètres de profil")

Observez comment les vues privées sont marquées avec Connexion requise. En parcourant le code, le lecteur sait très bien quelles pages nécessitent la connexion de l'utilisateur et quelles pages ne le sont pas..

Conclusions

J'espère que vous avez eu du plaisir à apprendre sur les décorateurs car ils représentent une fonctionnalité très soignée de Python. Voici quelques points à retenir:

  • Utiliser et concevoir correctement les décorateurs peut rendre votre code meilleur, plus propre et plus beau.
  • Utiliser des décorateurs peut vous aider à SÉCHER votre code en déplaçant un code identique de fonctions internes à des décorateurs..
  • Plus vous utiliserez de décorateurs, plus vous découvrirez de façons plus complexes de les utiliser..

N'oubliez pas de vérifier ce que nous avons à vendre et à étudier sur le marché Envato, et n'hésitez pas à poser des questions et à fournir vos précieux commentaires en utilisant le flux ci-dessous..

!