Functools -De kracht van functies van een hogere orde in Python

Bronknooppunt: 1865357

Dit artikel is gepubliceerd als onderdeel van het Data Science-blogathon

Introductie

De Python Standard Library heeft veel geweldige modules om je code schoner en eenvoudiger te houden en functools is zeker een van de

Caching

Laten we beginnen met enkele van de eenvoudigste maar krachtige functies van de module functools. Laten we beginnen met de caching-functies (evenals decorateurs) - lru_cache,cache en cached_property. De eerste van hen - lru_cache biedt een cache van de laatste resultaten van de uitvoering van functies, of met andere woorden, onthoudt het resultaat van hun werk:

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 dit voorbeeld doen we GET-verzoeken en cachen hun resultaten (maximaal 32 resultaten) met behulp van een decorateur @lru_cache. Om te zien of caching echt werkt, kun je de functiecache-informatie controleren met behulp van een methode cache_infodat het aantal cache-hits en -hits toont. Een decorateur biedt ook methoden clear_cacheen cache_parametersvoor het annuleren van respectievelijk de in de cache opgeslagen resultaten en testparameters.

Als u meer gedetailleerde caching nodig heeft, kunt u een optioneel argument typed=true toevoegen, waarmee u verschillende typen argumenten afzonderlijk in de cache kunt opslaan.

Een andere decorateur voor caching in functools is een functie die eenvoudig wordt genoemd cache. Het is een eenvoudige verpakking lru_cachedat het argument max_size weglaat, het verkleint en de oude waarden niet verwijdert.

Een andere decorateur die u kunt gebruiken voor caching is cached_property. Zoals de naam al doet vermoeden, wordt het gebruikt om de resultaten van klasseattributen in de cache op te slaan. Deze monteur is erg handig als je een eigenschap hebt die duur is om te berekenen, maar die hetzelfde blijft.

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

Dit eenvoudige voorbeeld laat zien. Zoals kan worden gebruikt, cached property, bijvoorbeeld om een ​​gerenderde HTML-pagina in de cache op te slaan die steeds opnieuw aan de gebruiker moet worden getoond. Hetzelfde kan worden gedaan voor bepaalde databasequery's of langdurige wiskundige berekeningen.

Nog een schoonheid cached_propertyis dat het alleen wordt uitgevoerd bij het opzoeken, dus het stelt ons in staat om de waarde van het attribuut te wijzigen. Nadat het kenmerk is gewijzigd, verandert de eerder in de cache opgeslagen waarde niet, maar wordt de nieuwe waarde berekend en in de cache opgeslagen. U kunt ook de cache wissen en u hoeft alleen het kenmerk te verwijderen.

Ik wil dit gedeelte eindigen met een waarschuwing over alle bovenstaande decorateurs - gebruik ze niet als uw functie enkele bijwerkingen heeft of als het veranderlijke objecten creëert elke keer dat het wordt aangeroepen, aangezien dit duidelijk niet de functies zijn die u in de cache wilt opslaan .

Vergelijken en bestellen

U weet waarschijnlijk al dat u vergelijkingsoperatoren in Python kunt implementeren, zoals: <, >=or ==met lt, gtor eq. Het kan echter behoorlijk frustrerend zijn om elk van de eq, lt, le, gtor ge. Gelukkig is er bij functools een decorateur @total_orderingdie ons hierbij kan helpen, want alles wat we hoeven te implementeren is eqeen van de resterende methoden, en de rest van de decorateur wordt automatisch gegenereerd.

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

Op deze manier kunnen we alle uitgebreide vergelijkingsbewerkingen implementeren, ondanks dat we alleen eq en met de hand hebben. Het meest voor de hand liggende voordeel is het gemak, dat is dat je niet al deze extra magische methoden hoeft te schrijven, maar het is waarschijnlijk belangrijker om de hoeveelheid code en de betere leesbaarheid ervan te verminderen.

Overbelasten

We hebben waarschijnlijk allemaal geleerd dat Python niet overbelast is, maar er is eigenlijk een gemakkelijke manier om het te implementeren met behulp van twee functies van functools, namelijk single dispatch en/of single dispatch methode. Deze functies helpen ons bij het implementeren van wat we een meervoudig verzendingsalgoritme zouden noemen waarmee dynamisch getypte programmeertalen zoals Python tijdens runtime onderscheid kunnen maken tussen typen.

Partieel

We werken allemaal met verschillende externe bibliotheken of frameworks, waarvan vele functies en interfaces bieden waarvoor we callbacks moeten doorgeven, zoals voor asynchrone bewerkingen of het luisteren naar gebeurtenissen. Dit is niets nieuws, maar wat als we ook enkele argumenten moeten doorgeven samen met de callback. Dit is waar het van pas komt in handige functools.partial. Kan worden gebruikt partialom sommige (of alle) argumenten voor een functie te bevriezen door een nieuw object te maken met een vereenvoudigde functiehandtekening. Verward? Laten we een paar praktische voorbeelden bekijken:

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

De bovenstaande code laat zien hoe je het kunt gebruiken partialeen functie doorgeven ( output_result) samen met een argument ( log=logger) als terugroepactie. In dit geval gebruiken we multiprocessing.apply_async, dat asynchroon het resultaat van de functie berekent ( concat) en retourneert het resultaat van de callback. Echter, apply_asynchet zal het resultaat altijd doorgeven als het eerste argument, en als we extra argumenten willen opnemen, zoals het geval is met log=logger, moeten we partieel gebruiken.

We hebben een redelijk geavanceerde use-case overwogen, en een eenvoudiger voorbeeld zou de gebruikelijke creatie van een functie zijn die schrijft in stderr in plaats van stdout:

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

Met deze eenvoudige truc hebben we een nieuwe aanroepbare functie gemaakt die altijd zal slagen file=sys.stderrals een genoemd argument om uit te voeren, waardoor we onze code kunnen vereenvoudigen en niet elke keer de waarde van het genoemde argument hoeven op te geven.

En nog een laatste goed voorbeeld. We kunnen gebruiken partialin combinatie met een weinig bekende functie iterom een ​​iterator te maken door een oproepbaar object door te geven en sentinelin iter, die als volgt kan worden toegepast:

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

Meestal willen we bij het lezen van een bestand over regels herhalen, maar in het geval van binaire gegevens moeten we mogelijk records met een vaste grootte herhalen. U kunt dit doen door een oproepbaar object te maken met partialdat het gespecificeerde stuk gegevens leest en het doorgeeft iterom een ​​iterator te maken. Deze iterator roept vervolgens de leesfunctie aan totdat deze het einde van het bestand bereikt, waarbij altijd alleen de opgegeven chunk-grootte wordt gebruikt ( RECORD_SIZE). Ten slotte, wanneer het einde van het bestand is bereikt, wordt de waarde sentinel(B ") wordt geretourneerd en de iteratie stopt.

decorateurs

We hebben in de vorige secties al over enkele decorateurs gesproken, maar niet over decorateurs om meer decorateurs te maken. Een van die decorateurs is functools.wraps. Laten we eens naar een voorbeeld kijken om te begrijpen waarom je het nodig hebt:

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

Dit voorbeeld laat zien hoe een eenvoudige decorateur kan worden geïmplementeerd. We wrappen een functie die een specifieke taak uitvoert ( actual_func) met een externe decorateur, en het wordt zelf een decorateur, die vervolgens kan worden toegepast op andere functies, zoals bijvoorbeeld het geval is bij greet. Als je de functie aanroept, greet u zult zien dat het berichten afdrukt van zowel actual_funcen op zichzelf. Ziet er goed uit, niet? Maar wat gebeurt er als we dit doen:

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

Wanneer we de naam en documentatie van de gedecoreerde functie aanroepen, realiseren we ons dat deze zijn vervangen door waarden uit de decorateurfunctie. Dit is slecht omdat we niet al onze functienamen en documentatie kunnen herschrijven als we een decorateur gebruiken. Hoe kan dit probleem worden opgelost? Natuurlijk met 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

Het enige doel van de functie: wrapsis om de naam, documentatie, argumentenlijst, enz. te kopiëren om overschrijven te voorkomen. overwegende dat wrapshet is ook een decorateur, je kunt het gewoon toevoegen aan onze actual_func, en het probleem is opgelost!

Verminderen

Last but not least in de module functools is dit te verminderen. Misschien uit andere talen, je kent het misschien als fold(Hassel). Deze functie neemt een iterable en vouwt (voegt toe) al zijn waarden in één. Hier zijn veel toepassingen voor, bijvoorbeeld:

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

Zoals je aan de code kunt zien, reduce kan de code vereenvoudigen of samenvatten tot één regel, die anders veel langer zou zijn. Dat gezegd hebbende, is het meestal een slecht idee om deze functie te misbruiken om de code te verkleinen, waardoor deze "slimmer" wordt, omdat deze snel eng en onleesbaar wordt. Om deze reden moet het naar mijn mening spaarzaam worden gebruikt.

En als je je herinnert dat het reduce verkort vaak alles tot één regel, het is perfect te combineren met partial:

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

En tot slot, als je meer nodig hebt dan alleen het uiteindelijke "samengevouwen" resultaat, dan kun je gebruik maken van accumulate– van een andere geweldige module itertools. Om het maximum te berekenen, kan het als volgt worden gebruikt:

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]

Conclusie

Zoals je kunt zien, zijn er veel handige functies en decorateurs die je leven gemakkelijker kunnen maken, maar dit is slechts het topje van de ijsberg. Zoals ik in het begin al zei, zijn er veel functies in de Python-standaardbibliotheek die je helpen betere code te schrijven, dus naast de functies die we hier hebben behandeld, kun je aandacht besteden aan andere modules, zoals operatoror itertool. Voor vragen kunt u op mijn opmerkingenveld klikken. Ik zal mijn best doen om uw invoer op te lossen en hoop u de gewenste uitvoer te geven. U kunt mij ook bereiken op LinkedIn:-

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

De media die in dit artikel worden getoond, zijn geen eigendom van Analytics Vidhya en worden naar goeddunken van de auteur gebruikt.

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

Tijdstempel:

Meer van Analytics Vidhya