Functools - Puterea funcțiilor de ordin superior în Python

Nodul sursă: 1865357

Acest articol a fost publicat ca parte a Blogathon Data Science

Introducere

Biblioteca standard Python are multe module excelente pentru a vă ajuta să vă păstrați codul mai curat și mai simplu, iar functools este cu siguranta unul dintre

Caching

Să începem cu unele dintre cele mai simple, dar puternice funcții ale functools modulului. Să începem cu funcțiile de caching (precum și decoratorii) – lru_cache,cache și cached_property. Primul dintre ei - lru_cache oferă un cache al ultimelor rezultate ale execuției funcțiilor sau, cu alte cuvinte, își amintește rezultatul muncii lor:

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}

În acest exemplu, facem cereri GET și memorăm în cache rezultatele acestora (până la 32 de rezultate) folosind un decorator @lru_cache. Pentru a vedea dacă memorarea în cache funcționează, puteți verifica informațiile din memoria cache a funcției folosind o metodă cache_infocare arată numărul de accesări și accesări în cache. Un decorator oferă și metode clear_cacheși cache_parameterspentru anularea rezultatelor memorate în cache și, respectiv, a parametrilor de testare.

Dacă aveți nevoie de stocare în cache mai granulară, puteți include un argument opțional typed=true, care vă permite să stocați în cache diferite tipuri de argumente separat.

Un alt decorator pentru memorarea în cache în functools este o funcție numită simplu cache. Este un simplu ambalaj lru_cachecare omite argumentul max_size, micșorându-l și nu elimină vechile valori.

Un alt decorator pe care îl puteți folosi pentru stocarea în cache este cached_property. După cum sugerează și numele, este folosit pentru a stoca în cache rezultatele atributelor de clasă. Această mecanică este foarte utilă dacă aveți o proprietate care este costisitoare de calculat, dar rămâne aceeași.

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

Acest exemplu simplu arată. După cum poate fi folosită, proprietatea stocată în cache, de exemplu, pentru a stoca în cache o pagină HTML redată care trebuie să fie afișată utilizatorului din nou și din nou. Același lucru se poate face pentru anumite interogări de baze de date sau calcule matematice lungi.

O altă frumusețe cached_propertyeste că rulează doar pe căutare, deci ne permite să schimbăm valoarea atributului. După modificarea atributului, valoarea stocată anterior în cache nu se va modifica, în schimb, noua valoare va fi calculată și stocată în cache. De asemenea, puteți șterge memoria cache și tot ce trebuie să faceți este să eliminați atributul.

Vreau să închei această secțiune cu o avertizare despre toți decoratorii de mai sus - nu-i utilizați dacă funcția dvs. are unele efecte secundare sau dacă creează obiecte mutabile de fiecare dată când este apelată, deoarece acestea nu sunt în mod clar funcțiile pe care doriți să le memorați în cache. .

Comparație și ordonare

Probabil știți deja că puteți implementa operatori de comparare în Python, cum ar fi <, >=or ==, Cu lt, gtor eq. Cu toate acestea, poate fi destul de frustrant să realizezi fiecare dintre ele eq, lt, le, gtor ge. Din fericire, functools există un decorator @total_orderingcare ne poate ajuta cu asta, pentru că tot ce trebuie să punem în aplicare este equna dintre metodele rămase, iar restul decoratorului va fi generat automat.

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

În acest fel, putem implementa toate operațiile de comparare extinse, în ciuda faptului că avem doar eq și manual lt. Cel mai evident beneficiu este comoditatea, și anume că nu trebuie să scrieți toate aceste metode magice suplimentare, dar probabil că este mai important să reduceți cantitatea de cod și o mai bună lizibilitate a acestuia.

Suprasarcină

Probabil cu toții am fost învățați că nu există supraîncărcare în Python, dar există de fapt o modalitate ușoară de a o implementa folosind două funcții din functools, și anume metoda de expediere unică și/sau metoda de expediere unică. Aceste funcții ne ajută să implementăm ceea ce am numi un algoritm de expediere multiplă care permite limbajelor de programare tipizate dinamic, cum ar fi Python, să facă distincția între tipuri în timpul execuției.

Parțial

Cu toții lucrăm cu diferite biblioteci sau cadre externe, multe dintre acestea oferind funcții și interfețe care ne solicită să transmitem apeluri inverse, cum ar fi pentru operațiuni asincrone sau pentru a asculta evenimente. Nu este nimic nou, dar ce se întâmplă dacă trebuie să transmitem și câteva argumente împreună cu apelul invers. Aici vine în functool-uri utile.partial. Poate fi folosit partialpentru a îngheța unele (sau toate) argumentele unei funcții prin crearea unui nou obiect cu o semnătură simplificată a funcției. Confuz? Să aruncăm o privire la câteva exemple practice:

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

Codul de mai sus arată cum îl puteți folosi partiala trece o funcție ( output_result) împreună cu un argument ( log=logger) ca un apel invers. În acest caz, vom folosi multiprocessing.apply_async, care calculează asincron rezultatul funcției ( concat) și returnează rezultatul apelului invers. In orice caz, apply_asyncva trece întotdeauna rezultatul ca prim argument, iar dacă dorim să includem orice argument suplimentar, așa cum este cazul log=logger, trebuie să folosim parțial.

Am luat în considerare un caz de utilizare destul de avansat, iar un exemplu mai simplu ar fi crearea obișnuită a unei funcții care scrie în stderr în loc de stdout:

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

Cu acest truc simplu, am creat o nouă funcție apelabilă care va trece întotdeauna file=sys.stderrca argument numit la ieșire, ceea ce ne permite să ne simplificăm codul și să nu trebuie să specificăm de fiecare dată valoarea argumentului numit.

Și un ultim exemplu bun. Putem folosi partialîn legătură cu o funcţie puţin cunoscută iterpentru a crea un iterator prin trecerea unui obiect apelabil și sentinelin iter, care poate fi aplicat astfel:

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

De obicei, când citim un fișier, dorim să iterăm peste linii, dar în cazul datelor binare, este posibil să fie nevoie să iterăm peste înregistrări de dimensiune fixă. Puteți face acest lucru creând un obiect apelabil folosind partialcare citește porțiunea specificată de date și o transmite iterpentru a crea un iterator. Acest iterator apelează apoi funcția de citire până când ajunge la sfârșitul fișierului, luând întotdeauna doar dimensiunea specificată ( RECORD_SIZE). În cele din urmă, când se ajunge la sfârșitul fișierului, valoarea sentinel(b”) este returnat și iterația se oprește.

decoratorii

Am vorbit deja despre unii decoratori în secțiunile anterioare, dar nu despre decoratori pentru a crea mai mulți decoratori. Un astfel de decorator este functools.wraps. Pentru a înțelege de ce aveți nevoie de el, să ne uităm doar la un exemplu:

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

Acest exemplu arată cum poate fi implementat un decorator simplu. Încheiem o funcție care îndeplinește o anumită sarcină ( actual_func) cu un decorator extern și devine în sine un decorator, care poate fi apoi aplicat altor funcții, de exemplu, cum este cazul cu greet. Când apelați funcția, greet veți vedea că imprimă mesaje ambele de la actual_funcși pe cont propriu. Pare în regulă, nu-i așa? Dar ce se întâmplă dacă facem asta:

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

Când numim numele și documentația funcției decorate, ne dăm seama că au fost înlocuite cu valori din funcția de decorator. Acest lucru este rău, deoarece nu putem rescrie toate numele funcțiilor și documentația noastră atunci când folosim un decorator. Cum poate fi rezolvată această problemă? Desigur, cu 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

Unicul scop al funcției wrapseste să copiați numele, documentația, lista de argumente etc., pentru a preveni suprascrierea. Având în vedere că wrapseste și un decorator, pur și simplu îl poți adăuga la actual_func-ul nostru și problema este rezolvată!

Reduce

Nu în ultimul rând, în modulul functools este această reducere. Poate din alte limbi, s-ar putea să-l cunoașteți ca fold(Haskell). Această funcție ia un iterabil și pliază (adaugă) toate valorile sale într-una singură. Există multe aplicații pentru aceasta, de exemplu:

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

După cum puteți vedea din cod, reduce poate simplifica sau condensa codul într-o singură linie, care altfel ar fi mult mai lungă. Acestea fiind spuse, este de obicei o idee proastă să abuzați de această funcție doar de dragul micșorării codului, făcându-l „mai inteligent”, deoarece devine rapid înfricoșător și imposibil de citit. Din acest motiv, în opinia mea, ar trebui să fie folosit cu moderație.

Și dacă îți amintești asta reduce deseori scurtează totul la o singură linie, poate fi combinat perfect cu partial:

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

Și, în sfârșit, dacă aveți nevoie de mai mult decât de rezultatul final „restrâns”, atunci puteți utiliza accumulate– dintr-un alt modul grozav itertools. Pentru a calcula maximul, acesta poate fi utilizat după cum urmează:

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]

Concluzie

După cum puteți vedea functools, există multe funcții utile și decoratori care vă pot face viața mai ușoară, dar acesta este doar vârful aisbergului. După cum am spus la început, în biblioteca standard Python există multe funcții care vă ajută să scrieți un cod mai bun, așa că, pe lângă funcțiile pe care le-am acoperit aici, puteți acorda atenție altor module, cum ar fi operatoror itertool. Pentru orice întrebări, puteți apăsa caseta mea de comentarii. Voi încerca tot posibilul să vă rezolv intrările și sper să vă ofer rezultatele dorite. Ma puteti contacta si pe LinkedIn:-

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

Mediile prezentate în acest articol nu sunt deținute de Analytics Vidhya și sunt utilizate la discreția autorului.

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

Timestamp-ul:

Mai mult de la Analize Vidhya