Functools: il potere delle funzioni di ordine superiore in Python

Nodo di origine: 1865357

Questo articolo è stato pubblicato come parte di Blogathon sulla scienza dei dati

Introduzione

La libreria standard di Python ha molti fantastici moduli per aiutarti a mantenere il tuo codice più pulito e semplice e functools lo è sicuramente uno dei

Caching

Cominciamo con alcune delle funzioni più semplici ma potenti del modulo functools. Cominciamo con le funzioni di memorizzazione nella cache (così come i decoratori) – lru_cache,cache ed cached_property. Il primo di loro - lru_cache fornisce una cache degli ultimi risultati dell'esecuzione delle funzioni, o in altre parole, ricorda il risultato del loro lavoro:

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}

In questo esempio, effettuiamo richieste GET e memorizziamo nella cache i loro risultati (fino a 32 risultati) utilizzando un decoratore @lru_cache. Per vedere se la memorizzazione nella cache funziona davvero, puoi controllare le informazioni sulla cache della funzione utilizzando un metodo cache_infoche mostra il numero di riscontri e riscontri nella cache. Un decoratore fornisce anche metodi clear_cacheed cache_parametersrispettivamente per la cancellazione dei risultati memorizzati nella cache e dei parametri del test.

Se hai bisogno di una memorizzazione nella cache più granulare, puoi includere un argomento facoltativo typed=true, che ti consente di memorizzare nella cache diversi tipi di argomenti separatamente.

Un altro decoratore per la memorizzazione nella cache in functools è una funzione chiamata simply cache. È un semplice involucro lru_cacheche omette l'argomento max_size, diminuendolo, e non rimuove i vecchi valori.

Un altro decoratore che puoi usare per la memorizzazione nella cache è cached_property. Come suggerisce il nome, viene utilizzato per memorizzare nella cache i risultati degli attributi di classe. Questa meccanica è molto utile se si dispone di una proprietà costosa da calcolare, ma che rimane la stessa.

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

Questo semplice esempio mostra. Come può essere utilizzata, la proprietà cached, ad esempio, per memorizzare nella cache una pagina HTML renderizzata che deve essere mostrata all'utente più e più volte. Lo stesso può essere fatto per determinate query di database o lunghi calcoli matematici.

Un'altra bellezza cached_propertyè che funziona solo durante la ricerca, quindi ci consente di modificare il valore dell'attributo. Dopo aver modificato l'attributo, il valore precedentemente memorizzato nella cache non cambierà, ma il nuovo valore verrà calcolato e memorizzato nella cache. Puoi anche cancellare la cache e tutto ciò che devi fare è rimuovere l'attributo.

Voglio terminare questa sezione con un avvertimento su tutti i decoratori di cui sopra: non usarli se la tua funzione ha alcuni effetti collaterali o se crea oggetti mutabili ogni volta che viene chiamata poiché queste chiaramente non sono le funzioni che vuoi memorizzare nella cache .

Confronto e ordinamento

Probabilmente sai già che puoi implementare operatori di confronto in Python come <, >=or ==, con lt, gtor eq. Tuttavia, può essere piuttosto frustrante realizzare ciascuno dei eq, lt, le, gtor ge. Fortunatamente, functools c'è un decoratore @total_orderingche può aiutarci in questo, perché tutto ciò che dobbiamo implementare è equno dei restanti metodi e il resto del decoratore verrà generato automaticamente.

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

In questo modo, possiamo implementare tutte le operazioni di confronto esteso, pur avendo solo eq ea mano lt. Il vantaggio più ovvio è la comodità, ovvero che non devi scrivere tutti questi metodi magici aggiuntivi, ma è probabilmente più importante ridurre la quantità di codice e la sua migliore leggibilità.

Sovraccarico

Probabilmente ci è stato insegnato che non c'è sovraccarico in Python, ma in realtà c'è un modo semplice per implementarlo usando due funzioni di functools, vale a dire single dispatch e/o single dispatch method. Queste funzioni ci aiutano a implementare quello che chiameremmo un algoritmo di invio multiplo che consente a linguaggi di programmazione tipizzati dinamicamente come Python di distinguere tra i tipi in fase di esecuzione.

Parziale

Lavoriamo tutti con varie librerie o framework esterni, molti dei quali forniscono funzioni e interfacce che ci richiedono di passare callback, ad esempio per operazioni asincrone o ascolto di eventi. Questa non è una novità, ma cosa succede se abbiamo anche bisogno di passare alcuni argomenti insieme al callback. È qui che entrano in gioco utili functools.partial. Può essere utilizzato partialper congelare alcuni (o tutti) gli argomenti di una funzione creando un nuovo oggetto con una firma di funzione semplificata. Confuso? Vediamo alcuni esempi pratici:

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()

Il codice sopra mostra come puoi usarlo partialpassare una funzione ( output_result) insieme a un argomento ( log=logger) come richiamo. In questo caso, utilizzeremo multiprocessing.apply_async, che calcola in modo asincrono il risultato della funzione ( concat) e restituisce il risultato della richiamata. Tuttavia, apply_asyncpasserà sempre il risultato come primo argomento, e se vogliamo includere altri argomenti, come nel caso di log=logger, dobbiamo usare partial.

Abbiamo considerato un caso d'uso abbastanza avanzato, e un esempio più semplice sarebbe la solita creazione di una funzione che scrive in stderr invece che in stdout:

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

Con questo semplice trucco, abbiamo creato una nuova funzione richiamabile che passerà sempre file=sys.stderrcome argomento denominato per l'output, che ci consente di semplificare il nostro codice e non dover specificare ogni volta il valore dell'argomento denominato.

E un ultimo buon esempio. Possiamo usare partialinsieme a una funzione poco conosciuta iterper creare un iteratore passando un oggetto chiamabile e sentinelin iter, che può essere applicato in questo modo:

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...

Di solito, durante la lettura di un file, vogliamo iterare su righe, ma nel caso di dati binari, potremmo aver bisogno di iterare su record di dimensioni fisse. Puoi farlo creando un oggetto richiamabile usando partialche legge il blocco di dati specificato e lo passa iterper creare un iteratore. Questo iteratore quindi chiama la funzione read finché non raggiunge la fine del file, prendendo sempre solo la dimensione del blocco specificata ( RECORD_SIZE). Infine, quando viene raggiunta la fine del file, il valore sentinel(B ") viene restituito e l'iterazione si interrompe.

decoratori

Abbiamo già parlato di alcuni decoratori nelle sezioni precedenti, ma non di decoratori per creare altri decoratori. Uno di questi decoratori è functools.wraps. Per capire perché ne hai bisogno, diamo solo un'occhiata a un esempio:

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

Questo esempio mostra come implementare un semplice decoratore. Avvolgiamo una funzione che esegue un compito specifico ( actual_func) con un decoratore esterno, e diventa esso stesso un decoratore, che può poi essere applicato ad altre funzioni, ad esempio, come nel caso di greet. Quando chiami la funzione, greet vedrai che stampa i messaggi sia da actual_funce da solo. Sembra a posto, vero? Ma cosa succede se facciamo questo:

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

Quando chiamiamo il nome e la documentazione della funzione decorata, ci rendiamo conto che sono stati sostituiti con i valori della funzione decoratore. Questo è negativo in quanto non possiamo riscrivere tutti i nomi delle nostre funzioni e la documentazione quando usiamo un decoratore. Come si può risolvere questo problema? Naturalmente, con 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

L'unico scopo della funzione wrapsconsiste nel copiare il nome, la documentazione, l'elenco degli argomenti, ecc., per impedire la sovrascrittura. Considerando che wrapsè anche un decoratore, puoi semplicemente aggiungerlo al nostro actual_func e il problema è risolto!

Riduce

Ultimo ma non meno importante nel modulo functools c'è questo reduce. Forse da altre lingue, potresti conoscerlo come fold(Haskell). Questa funzione accetta un iterabile e ripiega (aggiunge) tutti i suoi valori in uno solo. Ci sono molte applicazioni per questo, ad esempio:

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

Come puoi vedere dal codice, reduce può semplificare o condensare il codice in una riga, che altrimenti sarebbe molto più lunga. Detto questo, di solito è una cattiva idea abusare di questa funzione solo per il gusto di ridurre il codice, rendendolo "più intelligente", in quanto diventa rapidamente spaventoso e illeggibile. Per questo, secondo me, va usato con parsimonia.

E se te lo ricordi reduce spesso accorcia tutto a una riga, può essere perfettamente combinato con partial:

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

E infine, se hai bisogno di qualcosa di più del semplice risultato finale "compresso", puoi usare accumulate– da un altro grande modulo itertools. Per calcolare il massimo, può essere utilizzato come segue:

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]

Conclusione

Come puoi vedere functools, ci sono molte funzioni utili e decoratori che possono semplificarti la vita, ma questa è solo la punta dell'iceberg. Come ho detto all'inizio, ci sono molte funzioni nella libreria standard di Python che ti aiutano a scrivere codice migliore, quindi oltre alle funzioni che abbiamo trattato qui, puoi prestare attenzione ad altri moduli, come operatoror itertool. Per qualsiasi domanda, puoi colpire la mia casella dei commenti. Farò del mio meglio per risolvere i tuoi input e spero di darti i risultati desiderati. Puoi anche raggiungermi su LinkedIn: -

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

I media mostrati in questo articolo non sono di proprietà di Analytics Vidhya e vengono utilizzati a discrezione dell'autore.

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

Timestamp:

Di più da Analisi Vidhya