Functools - Kraften ved funktioner af højere orden i Python

Kildeknude: 1865357

Denne artikel blev offentliggjort som en del af Data Science Blogathon

Introduktion

Python Standard Library har mange gode moduler til at hjælpe med at holde din kode renere og enklere, og funktionsværktøjer er helt sikkert en af ​​de

Caching

Lad os starte med nogle af de enkleste, men kraftfulde funktioner i modulfunktionsværktøjerne. Lad os starte med caching-funktionerne (såvel som dekoratører) - lru_cache,cache , cached_property. Den første af dem - lru_cache giver en cache af de sidste resultater af udførelsen af ​​funktioner, eller med andre ord husker resultatet af deres arbejde:

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}

I dette eksempel laver vi GET-anmodninger og cacher deres resultater (op til 32 resultater) ved hjælp af en dekorator @lru_cache. For at se, om caching rent faktisk virker, kan du kontrollere funktionen cache-oplysninger ved hjælp af en metode cache_infoder viser antallet af cache-hits og -hits. En dekoratør giver også metoder clear_cache, cache_parameterstil annullering af henholdsvis cachelagrede resultater og testparametre.

Hvis du har brug for mere detaljeret caching, kan du inkludere et valgfrit argument typed=true, som giver dig mulighed for at cache forskellige typer argumenter separat.

En anden dekorator til caching i functools er en funktion, der kaldes simpelt cache. Det er en simpel indpakning lru_cacheder udelader argumentet max_size, reducerer det og fjerner ikke de gamle værdier.

En anden dekorator du kan bruge til caching er cached_property. Som navnet antyder, bruges det til at cache resultaterne af klasseattributter. Denne mekaniker er meget nyttig, hvis du har en ejendom, der er dyr at beregne, men som forbliver den samme.

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

Dette enkle eksempel viser. Som kan bruges, cachelagret egenskab, for eksempel, til at cache en gengivet HTML-side, der skal vises til brugeren igen og igen. Det samme kan gøres for visse databaseforespørgsler eller lange matematiske beregninger.

Endnu en skønhed cached_propertyer, at det kun kører ved opslag, så det giver os mulighed for at ændre værdien af ​​attributten. Efter ændring af attributten vil den tidligere cachelagrede værdi ikke ændre sig, i stedet vil den nye værdi blive beregnet og cachelagret. Du kan også rydde cachen, og alt hvad du skal gøre er at fjerne attributten.

Jeg vil afslutte dette afsnit med en advarsel om alle ovennævnte dekoratører – brug dem ikke, hvis din funktion har nogle bivirkninger, eller hvis den skaber foranderlige objekter, hver gang den kaldes, da det tydeligvis ikke er de funktioner, du vil cache .

Sammenligning og bestilling

Du ved sikkert allerede, at du kan implementere sammenligningsoperatorer i Python som f.eks <, >=or ==, med lt, gtor eq. Det kan dog være ret frustrerende at indse hver af de eq, lt, le, gtor ge. Heldigvis, functools der er en dekoratør @total_orderingdet kan hjælpe os med dette, for det eneste vi skal implementere er eqen af ​​de resterende metoder, og resten af ​​dekoratøren vil blive genereret automatisk.

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

På denne måde kan vi implementere alle de udvidede sammenligningsoperationer, på trods af at vi kun har eq og manuelt lt. Den mest åbenlyse fordel er bekvemmeligheden, som er, at du ikke behøver at skrive alle disse ekstra magiske metoder, men det er nok vigtigere at reducere mængden af ​​kode og dens bedre læsbarhed.

Overbelastning

Vi er nok alle blevet undervist i, at der ikke er nogen overbelastning i Python, men der er faktisk en nem måde at implementere det på ved hjælp af to funktioner fra functools, nemlig single dispatch og/eller single dispatch metode. Disse funktioner hjælper os med at implementere, hvad vi ville kalde en multiple dispatch-algoritme, der tillader dynamisk indtastede programmeringssprog såsom Python at skelne mellem typer under kørsel.

Delvis

Vi arbejder alle med forskellige eksterne biblioteker eller rammer, hvoraf mange leverer funktioner og grænseflader, der kræver, at vi sender tilbagekald, såsom til asynkrone operationer eller lytning efter begivenheder. Dette er ikke noget nyt, men hvad nu hvis vi også skal sende nogle argumenter sammen med tilbagekaldet. Det er her, det kommer i praktiske funktionsværktøjer.partial. Kan bruges partialat fryse nogle (eller alle) argumenterne til en funktion ved at oprette et nyt objekt med en forenklet funktionssignatur. Forvirret? Lad os tage et kig på nogle praktiske eksempler:

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

Koden ovenfor viser, hvordan du kan bruge den partialat bestå en funktion ( output_result) sammen med et argument ( log=logger) som et tilbagekald. I dette tilfælde vil vi bruge multiprocessing.apply_async, som asynkront beregner resultatet af funktionen ( concat) og returnerer resultatet af tilbagekaldet. Imidlertid, apply_asyncdet vil altid sende resultatet som det første argument, og hvis vi vil inkludere yderligere argumenter, som det er tilfældet med log=logger, skal vi bruge partial.

Vi har overvejet en ret avanceret use case, og et enklere eksempel ville være den sædvanlige oprettelse af en funktion, der skriver i stderr i stedet for stdout:

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

Med dette enkle trick har vi skabt en ny funktion, der kan kaldes, som altid vil bestå file=sys.stderrsom et navngivet argument til output, hvilket giver os mulighed for at forenkle vores kode og ikke behøver at angive værdien af ​​det navngivne argument hver gang.

Og et sidste godt eksempel. Vi kan bruge partiali forbindelse med en lidet kendt funktion iterat oprette en iterator ved at sende et kaldbart objekt ind og sentinelin iter, som kan anvendes på denne måde:

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

Normalt, når vi læser en fil, ønsker vi at iterere over linjer, men i tilfælde af binære data, skal vi muligvis iterere over poster med en fast størrelse. Du kan gøre dette ved at oprette et kaldbart objekt vha partialder læser den angivne del af data og sender den ind iterfor at oprette en iterator. Denne iterator kalder derefter læsefunktionen, indtil den når slutningen af ​​filen, og tager altid kun den angivne chunkstørrelse ( RECORD_SIZE). Til sidst, når slutningen af ​​filen er nået, værdien sentinel(b ”) returneres, og iterationen stopper.

dekoratører

Vi har allerede talt om nogle dekoratører i de foregående afsnit, men ikke dekoratører for at skabe flere dekoratører. En sådan dekoratør er functools.wraps. For at forstå hvorfor du har brug for det, lad os bare se på et eksempel:

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

Dette eksempel viser, hvordan en simpel dekoratør kan implementeres. Vi ombryder en funktion, der udfører en bestemt opgave ( actual_func) med en ekstern dekoratør, og den bliver selv en dekoratør, som så kan anvendes til andre funktioner, f.eks. som det er tilfældet med greet. Når du kalder funktionen, greet du vil se, at den udskriver beskeder både fra actual_funcog på egen hånd. Ser okay ud, gør det ikke? Men hvad sker der, hvis vi gør dette:

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

Når vi kalder den dekorerede funktions navn og dokumentation, indser vi, at de er blevet erstattet med værdier fra dekoratørfunktionen. Dette er dårligt, da vi ikke kan omskrive alle vores funktionsnavne og dokumentation, når vi bruger en eller anden dekoratør. Hvordan kan dette problem løses? Selvfølgelig med 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

Funktionens eneste formål wrapser at kopiere navn, dokumentation, argumentliste osv. for at forhindre overskrivning. Overvejer det wrapsdet er også en dekoratør, du kan blot tilføje det til vores actual_func, og problemet er løst!

Reducer

Sidst men ikke mindst i modulets funktionsværktøjer er denne reduktion. Måske fra andre sprog, kender du det måske som fold(Haskell). Denne funktion tager en iterabel og folder (tilføjer) alle dens værdier til én. Der er mange applikationer til dette, for eksempel:

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

Som du kan se af koden, reduce kan forenkle eller kondensere koden til én linje, som ellers ville være meget længere. Når det er sagt, er det normalt en dårlig idé at misbruge denne funktion bare for at formindske koden, så den bliver "smartere", da den hurtigt bliver skræmmende og ulæselig. Af denne grund bør det efter min mening bruges sparsomt.

Og hvis du husker det reduce ofte forkorter alt til én linje, det kan perfekt kombineres med partial:

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

Og endelig, hvis du har brug for mere end blot det endelige "kollapserede" resultat, så kan du bruge accumulate– fra et andet godt modul itertools. For at beregne maksimum, kan det bruges som følger:

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]

Konklusion

Som du kan se functools, er der mange nyttige funktioner og dekoratorer, der kan gøre dit liv lettere, men dette er kun toppen af ​​isbjerget. Som jeg sagde i starten, er der mange funktioner i Python standardbiblioteket, der hjælper dig med at skrive bedre kode, så udover de funktioner, som vi dækkede her, kan du være opmærksom på andre moduler, som f.eks. operatoror itertool. For eventuelle spørgsmål kan du trykke på mit kommentarfelt. Jeg vil gøre mit bedste for at løse dine input og håber at give dig ønskede output. Du kan også kontakte mig på LinkedIn:-

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

Medierne vist i denne artikel ejes ikke af Analytics Vidhya og bruges efter forfatterens skøn.

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

Tidsstempel:

Mere fra Analyse Vidhya