Functools -Moč funkcij višjega reda v Pythonu

Izvorno vozlišče: 1865357

Ta članek je bil objavljen kot del Blogathon o znanosti o podatkih

Predstavitev

Standardna knjižnica Python ima veliko odličnih modulov, ki pomagajo ohranjati vašo kodo čistejšo in enostavnejšo, funkcionalna orodja pa so zagotovo eden izmed

Predpomnjenje

Začnimo z nekaterimi najpreprostejšimi, a zmogljivimi funkcijami funkcijskih orodij modula. Začnimo s funkcijami predpomnjenja (pa tudi z dekoratorji) – lru_cache,cache in cached_property. Prvi od njih – lru_cache zagotavlja predpomnilnik zadnjih rezultatov izvajanja funkcij ali z drugimi besedami zapomni rezultat njihovega dela:

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}

V tem primeru naredimo zahteve GET in predpomnimo njihove rezultate (do 32 rezultatov) z uporabo dekoratorja @lru_cache. Če želite preveriti, ali predpomnjenje dejansko deluje, lahko preverite informacije o predpomnilniku funkcije z metodo cache_infoki prikazuje število zadetkov in zadetkov predpomnilnika. Dekorater ponuja tudi metode clear_cachein cache_parametersza preklic predpomnjenih rezultatov oziroma testnih parametrov.

Če potrebujete bolj natančno predpomnjenje, lahko vključite izbirni argument typed=true, ki vam omogoča ločeno predpomnjenje različnih vrst argumentov.

Drug dekorater za predpomnjenje v funkcijskih orodjih je funkcija, imenovana preprosto cache. To je preprost ovoj lru_cacheki izpusti argument max_size, ga zmanjša in ne odstrani starih vrednosti.

Drug dekorater, ki ga lahko uporabite za predpomnjenje, je cached_property. Kot pove že ime, se uporablja za predpomnilnik rezultatov atributov razreda. Ta mehanika je zelo uporabna, če imate lastnost, ki je draga za izračun, vendar ta ostaja enaka.

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

Ta preprost primer kaže. Kot je mogoče uporabiti lastnost cached, na primer za predpomnjenje upodobljene strani HTML, ki jo je treba uporabniku vedno znova prikazati. Enako je mogoče storiti za določene poizvedbe v bazi podatkov ali dolgotrajne matematične izračune.

Še ena lepotica cached_propertyje, da se izvaja samo pri iskanju, zato nam omogoča spreminjanje vrednosti atributa. Po spremembi atributa se prej shranjena vrednost ne bo spremenila, namesto tega bo izračunana in predpomnjena nova vrednost. Prav tako lahko počistite predpomnilnik in vse, kar morate storiti, je odstraniti atribut.

Ta razdelek želim zaključiti s opozorilom o vseh zgornjih dekoratorjih – ne uporabljajte jih, če ima vaša funkcija nekaj stranskih učinkov ali če ob vsakem klicu ustvari spremenljive predmete, saj to očitno niso funkcije, ki jih želite predpomniti. .

Primerjava in naročanje

Verjetno že veste, da lahko v Pythonu implementirate primerjalne operatorje, kot je npr <, >=or ==s lt, gtor eq. Vendar pa je lahko zelo frustrirajoče spoznanje vsakega od njih eq, lt, le, gtor ge. Na srečo, functools obstaja dekorater @total_orderingki nam lahko pri tem pomaga, kajti vse, kar moramo implementirati, je eqena od preostalih metod, preostali del dekoratorja pa bo ustvarjen samodejno.

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

Na ta način lahko izvajamo vse razširjene primerjalne operacije, čeprav imamo samo eq in ročno lt. Najbolj očitna prednost je priročnost, ki je, da vam ni treba pisati vseh teh dodatnih magičnih metod, verjetno pa je bolj pomembno zmanjšati količino kode in njeno boljšo berljivost.

preobremenitev

Verjetno so nas vsi naučili, da v Pythonu ni preobremenitve, vendar je pravzaprav preprost način, da ga implementirate z uporabo dveh funkcij iz funkcijskih orodij, in sicer z enojno odpremo in/ali enojno metodo pošiljanja. Te funkcije nam pomagajo pri implementaciji tega, kar bi imenovali algoritem za večkratno odpremo, ki omogoča dinamično tipkanim programskim jezikom, kot je Python, da razlikujejo med vrstami med izvajanjem.

Delno

Vsi delamo z različnimi zunanjimi knjižnicami ali ogrodji, od katerih mnoge zagotavljajo funkcije in vmesnike, ki od nas zahtevajo posredovanje povratnih klicev, na primer za asinhrone operacije ali poslušanje dogodkov. To ni nič novega, a kaj, če moramo poleg povratnega klica posredovati tudi nekaj argumentov. Tukaj pride v poštev funkcionalna orodja.partial. Je lahko uporabljen partialzamrzniti nekatere (ali vse) argumente funkcije z ustvarjanjem novega predmeta s poenostavljenim podpisom funkcije. Zmedeni? Oglejmo si nekaj praktičnih primerov:

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

Zgornja koda prikazuje, kako jo lahko uporabite partialprenesti funkcijo ( output_result) skupaj z argumentom ( log=logger) kot povratni klic. V tem primeru bomo uporabili multiprocessing.apply_async, ki asinhrono izračuna rezultat funkcije ( concat) in vrne rezultat povratnega klica. Vendar pa apply_asyncvedno bo posredoval rezultat kot prvi argument, in če želimo vključiti dodatne argumente, kot je v primeru log=logger, moramo uporabiti delni.

Upoštevali smo precej napreden primer uporabe, preprostejši primer pa bi bilo običajno ustvarjanje funkcije, ki piše v stderr namesto v stdout:

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

S tem preprostim trikom smo ustvarili novo klicno funkcijo, ki bo vedno prestala file=sys.stderrkot poimenovani argument za izhod, kar nam omogoča, da poenostavimo našo kodo in nam ni treba vsakič navesti vrednosti poimenovanega argumenta.

In še zadnji dober primer. Lahko uporabimo partialv povezavi z malo znano funkcijo iterustvariti iterator s posredovanjem klicljivega predmeta in sentinelin iter, ki se lahko uporablja takole:

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

Običajno želimo pri branju datoteke ponoviti po vrsticah, v primeru binarnih podatkov pa bomo morda morali ponoviti zapise fiksne velikosti. To lahko storite tako, da ustvarite klicni objekt z uporabo partialki prebere določen kos podatkov in ga posreduje iterda ustvarite iterator. Ta iterator nato pokliče funkcijo branja, dokler ne doseže konca datoteke, pri čemer vedno vzame samo določeno velikost kosa ( RECORD_SIZE). Končno, ko je dosežen konec datoteke, vrednost sentinel(b”) se vrne in ponovitev se ustavi.

Decorators

V prejšnjih razdelkih smo že govorili o nekaterih dekoraterjih, ne pa tudi o dekoratorjih, ki bi ustvarili več dekoraterjev. Eden takšnih okrasnikov so functools.wraps. Če želite razumeti, zakaj ga potrebujete, poglejmo samo primer:

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

Ta primer prikazuje, kako je mogoče izvesti preprost dekorater. Zavijemo funkcijo, ki izvaja določeno nalogo ( actual_func) z zunanjim dekoratorjem in postane sam dekorater, ki ga lahko nato uporabimo za druge funkcije, na primer greet. Ko pokličete funkcijo, greet videli boste, da natisne sporočila iz obeh actual_funcin samostojno. Izgleda v redu, kajne? Toda kaj se zgodi, če naredimo to:

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

Ko pokličemo ime in dokumentacijo okrašene funkcije, ugotovimo, da so bili zamenjani z vrednostmi iz funkcije dekoraterja. To je slabo, saj ne moremo prepisati vseh imen naših funkcij in dokumentacije, ko uporabljamo kakšen dekorator. Kako je mogoče rešiti ta problem? Seveda s funkcijskimi orodji.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

Edini namen funkcije wrapsje kopirati ime, dokumentacijo, seznam argumentov itd., da preprečite prepisovanje. Glede na to wrapsje tudi dekorater, lahko ga preprosto dodate v naš fact_func in problem je rešen!

Zmanjšaj

Nenazadnje v funkcijskih orodjih modula je to zmanjšanje. Morda iz drugih jezikov, morda ga poznate kot fold(Haskell). Ta funkcija vzame iterable in zloži (doda) vse njegove vrednosti v eno. Za to obstaja veliko aplikacij, na primer:

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

Kot lahko vidite iz kode, reduce lahko poenostavi ali zgosti kodo v eno vrstico, ki bi bila sicer veliko daljša. Glede na to je običajno slaba ideja zlorabljati to funkcijo samo zaradi krčenja kode in jo narediti "pametnejšo", saj hitro postane strašljiva in neberljiva. Zato ga je po mojem mnenju treba uporabljati zmerno.

In če se spomnite tega reduce pogosto skrajša vse na eno vrstico, se lahko odlično kombinira z partial:

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

In končno, če potrebujete več kot le končni "strt" rezultat, potem lahko uporabite accumulate– iz še enega odličnega modula itertools. Za izračun največje vrednosti se lahko uporabi na naslednji način:

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]

zaključek

Kot lahko vidite funkcijska orodja, obstaja veliko uporabnih funkcij in okrasnikov, ki vam lahko olajšajo življenje, vendar je to le vrh ledene gore. Kot sem rekel na začetku, je v standardni knjižnici Python veliko funkcij, ki vam pomagajo pisati boljšo kodo, zato lahko poleg funkcij, ki smo jih tukaj obravnavali, posvetite pozornost še drugim modulom, kot je npr. operatoror itertool. Za kakršna koli vprašanja lahko pritisnete moje polje za komentar. Poskušal bom rešiti vaše vložke in upam, da vam bom dal želene rezultate. Lahko me kontaktirate tudi na LinkedInu:-

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

Mediji, prikazani v tem članku, niso v lasti Analytics Vidhya in se uporabljajo po presoji avtorja.

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

Časovni žig:

Več od Analitika Vidhya