Functools - La puissance des fonctions d'ordre supérieur en Python

Nœud source: 1865357

Cet article a été publié dans le cadre du Blogathon sur la science des données

Introduction

La bibliothèque standard Python contient de nombreux modules géniaux pour vous aider à garder votre code plus propre et plus simple et functools est certainement l'un des

Cache haute performance

Commençons par certaines des fonctions les plus simples mais les plus puissantes du module functools. Commençons par les fonctions de mise en cache (ainsi que les décorateurs) - lru_cache,cache ainsi que cached_property. Le premier d'entre eux - lru_cache fournit un cache des derniers résultats de l'exécution des fonctions, ou en d'autres termes, se souvient du résultat de leur travail :

from functools import lru_cache
import requests @lru_cache(maxsize=32)
def get_with_cache(url): try: r = requests.get(url) return r.text except: return "Not Found" for url in ["https://google.com/", "https://reddit.com/", "https://google.com/", "https://google.com/"]: get_with_cache(url) print(get_with_cache.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)
print(get_with_cache.cache_parameters())
# {'maxsize': 32, 'typed': False}

Dans cet exemple, nous faisons des requêtes GET et mettons en cache leurs résultats (jusqu'à 32 résultats) à l'aide d'un décorateur @lru_cache. Pour voir si la mise en cache fonctionne réellement, vous pouvez vérifier les informations du cache de fonction à l'aide d'une méthode cache_infoqui affiche le nombre d'accès au cache et d'accès. Un décorateur fournit également des méthodes clear_cacheainsi que cache_parameterspour l'annulation des résultats mis en cache et des paramètres de test, respectivement.

Si vous avez besoin d'une mise en cache plus granulaire, vous pouvez inclure un argument facultatif typed=true, qui vous permet de mettre en cache différents types d'arguments séparément.

Un autre décorateur pour la mise en cache dans functools est une fonction appelée simplement cache. C'est un simple emballage lru_cachequi omet l'argument max_size, le diminue et ne supprime pas les anciennes valeurs.

Un autre décorateur que vous pouvez utiliser pour la mise en cache est cached_property. Comme son nom l'indique, il est utilisé pour mettre en cache les résultats des attributs de classe. Ce mécanisme est très utile si vous avez une propriété coûteuse à calculer, mais qui reste la même.

from functools import cached_property class Page: @cached_property def render(self, value): # Do something with supplied value... # Long computation that renders HTML page... return html

Cet exemple simple montre. Comme cela peut être utilisé, la propriété mise en cache, par exemple, pour mettre en cache une page HTML rendue qui doit être montrée à l'utilisateur encore et encore. La même chose peut être faite pour certaines requêtes de base de données ou de longs calculs mathématiques.

Une autre beauté cached_propertyest qu'il ne s'exécute que lors de la recherche, il nous permet donc de modifier la valeur de l'attribut. Après avoir modifié l'attribut, la valeur précédemment mise en cache ne changera pas, à la place, la nouvelle valeur sera calculée et mise en cache. Vous pouvez également vider le cache et tout ce que vous avez à faire est de supprimer l'attribut.

Je veux terminer cette section avec une mise en garde à propos de tous les décorateurs ci-dessus - ne les utilisez pas si votre fonction a des effets secondaires ou si elle crée des objets modifiables à chaque fois qu'elle est appelée car ce ne sont clairement pas les fonctions que vous voulez mettre en cache .

Comparaison et commande

Vous savez probablement déjà que vous pouvez implémenter des opérateurs de comparaison en Python tels que <, >=or ==, avec lt, gtor eq. Cependant, il peut être assez frustrant de réaliser chacun des eq, lt, le, gtor ge. Heureusement, functools il y a un décorateur @total_orderingqui peuvent nous y aider, car tout ce que nous devons mettre en œuvre, c'est eql'une des méthodes restantes, et le reste du décorateur sera généré automatiquement.

from functools import total_ordering @total_ordering
class Number: def __init__(self, value): self.value = value def __lt__(self, other): return self.value Number(3))
# True
print(Number(1) = Number(15))
# True
print(Number(10) <= Number(2))
# False

De cette façon, nous pouvons implémenter toutes les opérations de comparaison étendues, même si nous n'avons que eq et hand lt. L'avantage le plus évident est la commodité, c'est-à-dire que vous n'avez pas à écrire toutes ces méthodes magiques supplémentaires, mais il est probablement plus important de réduire la quantité de code et sa meilleure lisibilité.

Surcharge

On nous a probablement tous appris qu'il n'y a pas de surcharge en Python, mais il existe en fait un moyen simple de l'implémenter en utilisant deux fonctions de functools, à savoir la répartition unique et/ou la méthode de répartition unique. Ces fonctions nous aident à implémenter ce que nous appellerions un algorithme de répartition multiple qui permet aux langages de programmation à typage dynamique tels que Python de faire la distinction entre les types lors de l'exécution.

Partiel

Nous travaillons tous avec diverses bibliothèques ou frameworks externes, dont beaucoup fournissent des fonctions et des interfaces nous obligeant à passer des rappels, comme pour les opérations asynchrones ou l'écoute d'événements. Ce n'est pas nouveau, mais que se passe-t-il si nous devons également transmettre des arguments avec le rappel. C'est là qu'interviennent les functools pratiques.partial. Peut être utilisé partialpour geler certains (ou tous) des arguments d'une fonction en créant un nouvel objet avec une signature de fonction simplifiée. Confus? Voyons quelques exemples pratiques :

def output_result(result, log=None): if log is not None: log.debug(f"Result is: {result}") def concat(a, b): return a + b import logging
from multiprocessing import Pool
from functools import partial logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("default") p = Pool()
p.apply_async(concat, ("Hello ", "World"), callback=partial(output_result, log=logger))
p.close()
p.join()

Le code ci-dessus montre comment vous pouvez l'utiliser partialpasser une fonction ( output_result) avec un argument ( log=logger) comme rappel. Dans ce cas, nous utiliserons multiprocessing.apply_async, qui calcule de manière asynchrone le résultat de la fonction ( concat) et renvoie le résultat du rappel. Cependant, apply_asyncil passera toujours le résultat comme premier argument, et si nous voulons inclure des arguments supplémentaires, comme c'est le cas avec log=logger, nous devons utiliser partial.

Nous avons considéré un cas d'utilisation assez avancé, et un exemple plus simple serait la création habituelle d'une fonction qui écrit en stderr au lieu de stdout :

import sys
from functools import partial print_stderr = partial(print, file=sys.stderr)
print_stderr("This goes to standard error output")

Avec cette astuce simple, nous avons créé une nouvelle fonction appelable qui passera toujours file=sys.stderrcomme argument nommé à la sortie, ce qui nous permet de simplifier notre code et de ne pas avoir à spécifier la valeur de l'argument nommé à chaque fois.

Et un dernier bon exemple. On peut utiliser partialen conjonction avec une fonction peu connue iterpour créer un itérateur en passant un objet appelable et sentinelin iter, qui peut être appliqué comme ceci :

from functools import partial RECORD_SIZE = 64 # Read binary file...
with open("file.data", "rb") as file: records = iter(partial(file.read, RECORD_SIZE), b'') for r in records: # Do something with the record...

Habituellement, lors de la lecture d'un fichier, nous voulons itérer sur des lignes, mais dans le cas de données binaires, nous pouvons avoir besoin d'itérer sur des enregistrements d'une taille fixe. Vous pouvez le faire en créant un objet appelable à l'aide de partialqui lit le bloc de données spécifié et le transmet iterpour créer un itérateur. Cet itérateur appelle ensuite la fonction read jusqu'à ce qu'il atteigne la fin du fichier, en ne prenant toujours que la taille de bloc spécifiée ( RECORD_SIZE). Enfin, lorsque la fin du fichier est atteinte, la valeur sentinel(b ") est retourné et l'itération s'arrête.

Décorateurs

Nous avons déjà parlé de certains décorateurs dans les sections précédentes, mais pas de décorateurs pour créer plus de décorateurs. Un tel décorateur est functools.wraps. Pour comprendre pourquoi vous en avez besoin, regardons un exemple :

def decorator(func): def actual_func(*args, **kwargs): """Inner function within decorator, which does the actual work""" print(f"Before Calling {func.__name__}") func(*args, **kwargs) print(f"After Calling {func.__name__}") return actual_func @decorator
def greet(name): """Says hello to somebody""" print(f"Hello, {name}!") greet("Martin")
# Before Calling greet
# Hello, Martin!
# After Calling greet

Cet exemple montre comment un décorateur simple peut être implémenté. Nous enveloppons une fonction qui exécute une tâche spécifique ( actual_func) avec un décorateur externe, et il devient un décorateur lui-même, qui peut ensuite être appliqué à d'autres fonctions, par exemple, comme c'est le cas avec greet. Lorsque vous appelez la fonction, greet vous verrez qu'il imprime les messages à la fois de actual_funcet tout seul. Ça a l'air bien, n'est-ce pas ? Mais que se passe-t-il si nous faisons ceci :

print(greet.__name__)
# actual_func
print(greet.__doc__)
# Inner function within decorator, which does the actual work

Lorsque nous appelons le nom et la documentation de la fonction décorée, nous nous rendons compte qu'ils ont été remplacés par des valeurs de la fonction décoratrice. C'est mauvais car nous ne pouvons pas réécrire tous nos noms de fonctions et notre documentation lorsque nous utilisons un décorateur. Comment ce problème peut-il être résolu ? Bien sûr, avec functools.wraps:

from functools import wraps def decorator(func): @wraps(func) def actual_func(*args, **kwargs): """Inner function within decorator, which does the actual work""" print(f"Before Calling {func.__name__}") func(*args, **kwargs) print(f"After Calling {func.__name__}") return actual_func @decorator
def greet(name): """Says hello to somebody""" print(f"Hello, {name}!") print(greet.__name__)
# greet
print(greet.__doc__)
# Says hello to somebody

Le seul but de la fonction wrapsconsiste à copier le nom, la documentation, la liste des arguments, etc., pour éviter l'écrasement. Étant donné que wrapsc'est aussi un décorateur, vous pouvez simplement l'ajouter à notre actual_func, et le problème est résolu !

Réduire

Le dernier mais non le moindre dans le module functools est cette réduction. Peut-être que d'autres langues, vous le connaissez peut-être comme fold(Haskell). Cette fonction prend un itérable et replie (ajoute) toutes ses valeurs en une seule. Il existe de nombreuses applications pour cela, par exemple :

from functools import reduce
import operator def product(iterable): return reduce(operator.mul, iterable, 1) def factorial(n): return reduce(operator.mul, range(1, n)) def sum(numbers): # Use `sum` function from standard library instead return reduce(operator.add, numbers, 1) def reverse(iterable): return reduce(lambda x, y: y+x, iterable) print(product([1, 2, 3]))
# 6
print(factorial(5))
# 24
print(sum([2, 6, 8, 3]))
# 20
print(reverse("hello"))
# olleh

Comme vous pouvez le voir sur le code, reduce peut simplifier ou condenser le code en une seule ligne, qui serait autrement beaucoup plus longue. Cela dit, c'est généralement une mauvaise idée d'abuser de cette fonction juste pour réduire le code, le rendant "plus intelligent", car il devient rapidement effrayant et illisible. Pour cette raison, à mon avis, il faut l'utiliser avec parcimonie.

Et si vous vous souvenez qu'il reduce raccourcit souvent tout à une seule ligne, il peut être parfaitement combiné avec partial:

product = partial(reduce, operator.mul) print(product([1, 2, 3]))
# 6

Et enfin, si vous avez besoin de plus que le résultat final "effondré", vous pouvez utiliser accumulate– d'un autre excellent module itertools. Pour calculer le maximum, il peut être utilisé comme suit :

from itertools import accumulate data = [3, 4, 1, 3, 5, 6, 9, 0, 1] print(list(accumulate(data, max)))
# [3, 4, 4, 4, 5, 6, 9, 9, 9]

Conclusion

Comme vous pouvez le voir dans functools, il existe de nombreuses fonctions et décorateurs utiles qui peuvent vous faciliter la vie, mais ce n'est que la pointe de l'iceberg. Comme je l'ai dit au début, il existe de nombreuses fonctions dans la bibliothèque standard Python qui vous aident à écrire un meilleur code, donc en plus des fonctions que nous avons couvertes ici, vous pouvez prêter attention à d'autres modules, tels que operatoror itertool. Pour toute question, vous pouvez cliquer sur ma boîte de commentaires. Je ferai de mon mieux pour résoudre vos entrées et j'espère vous donner les sorties souhaitées. Vous pouvez également me joindre sur LinkedIn :-

https://www.linkedin.com/in/shivani-sharma-aba6141b6/

Les médias présentés dans cet article ne sont pas la propriété d'Analytics Vidhya et sont utilisés à la discrétion de l'auteur.

Source : https://www.analyticsvidhya.com/blog/2021/08/functools-the-power-of-higher-order-functions-in-python/

Horodatage:

Plus de Analytique Vidhya