Functools — moc funkcji wyższego rzędu w Pythonie

Węzeł źródłowy: 1865357

Ten artykuł został opublikowany jako część Blogathon Data Science

Wprowadzenie

Biblioteka standardowa Pythona zawiera wiele świetnych modułów, które pomagają w utrzymaniu czystszego i prostszego kodu, a functools są zdecydowanie jeden z

buforowanie

Zacznijmy od najprostszych, ale potężnych funkcji functools modułu. Zacznijmy od funkcji buforowania (a także dekoratorów) – lru_cache,cache i cached_property. Pierwszy z nich – lru_cache zapewnia pamięć podręczną ostatnich wyników wykonania funkcji, czyli zapamiętuje wynik ich pracy:

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}

W tym przykładzie wykonujemy żądania GET i buforujemy ich wyniki (do 32 wyników) za pomocą dekoratora @lru_cache. Aby sprawdzić, czy buforowanie rzeczywiście działa, możesz sprawdzić informacje o buforze funkcji za pomocą metody cache_infoktóry pokazuje liczbę trafień i trafień w pamięci podręcznej. Dekorator zapewnia również metody clear_cachei cache_parametersodpowiednio do anulowania zbuforowanych wyników i parametrów testu.

Jeśli potrzebujesz bardziej szczegółowego buforowania, możesz dołączyć opcjonalny argument typed=true, który umożliwia oddzielne buforowanie różnych typów argumentów.

Innym dekoratorem do buforowania w functools jest funkcja o nazwie simple cache. To proste opakowanie lru_cachektóry pomija argument max_size, zmniejszając go i nie usuwa starych wartości.

Innym dekoratorem, którego możesz użyć do buforowania, jest cached_property. Jak sama nazwa wskazuje, służy do buforowania wyników atrybutów klas. Ta mechanika jest bardzo przydatna, jeśli masz właściwość, której obliczenie jest drogie, ale pozostaje takie samo.

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

Ten prosty przykład pokazuje. Jak można użyć, właściwość buforowana, na przykład do buforowania renderowanej strony HTML, która musi być wyświetlana użytkownikowi w kółko. To samo można zrobić w przypadku niektórych zapytań do bazy danych lub długich obliczeń matematycznych.

Kolejne piękno cached_propertyjest to, że działa tylko podczas wyszukiwania, więc pozwala nam zmienić wartość atrybutu. Po zmianie atrybutu poprzednio zbuforowana wartość nie ulegnie zmianie, zamiast tego zostanie obliczona i zbuforowana nowa wartość. Możesz także wyczyścić pamięć podręczną, a wszystko, co musisz zrobić, to usunąć atrybut.

Chcę zakończyć tę sekcję zastrzeżeniem dotyczącym wszystkich powyższych dekoratorów – nie używaj ich, jeśli twoja funkcja ma jakieś skutki uboczne lub jeśli tworzy zmienne obiekty za każdym razem, gdy jest wywoływana, ponieważ wyraźnie nie są to funkcje, które chcesz buforować .

Porównanie i zamawianie

Prawdopodobnie już wiesz, że możesz zaimplementować operatory porównania w Pythonie, takie jak <, >=or ==Z lt, gtor eq. Jednak uświadomienie sobie każdego z nich może być dość frustrujące eq, lt, le, gtor ge. Na szczęście functools jest dekorator @total_orderingktóre mogą nam w tym pomóc, ponieważ wszystko, co musimy wdrożyć, to: eqjedną z pozostałych metod, a reszta dekoratora zostanie wygenerowana automatycznie.

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

W ten sposób możemy zaimplementować wszystkie rozszerzone operacje porównawcze, pomimo posiadania tylko eq i ręcznie lt. Najbardziej oczywistą korzyścią jest wygoda, która polega na tym, że nie musisz pisać tych wszystkich dodatkowych magicznych metod, ale prawdopodobnie ważniejsze jest zmniejszenie ilości kodu i jego lepsza czytelność.

Przeciążać

Prawdopodobnie wszyscy nauczono nas, że w Pythonie nie ma przeciążania, ale w rzeczywistości istnieje prosty sposób na zaimplementowanie tego za pomocą dwóch funkcji z functools, a mianowicie pojedynczej wysyłki i/lub pojedynczej metody wysyłania. Te funkcje pomagają nam zaimplementować coś, co nazwalibyśmy algorytmem wielokrotnego wysyłania, który umożliwia dynamicznie typowanym językom programowania, takim jak Python, rozróżnianie typów w czasie wykonywania.

Częściowa

Wszyscy pracujemy z różnymi zewnętrznymi bibliotekami lub frameworkami, z których wiele udostępnia funkcje i interfejsy wymagające od nas przekazywania wywołań zwrotnych, takich jak operacje asynchroniczne lub nasłuchiwanie zdarzeń. Nie jest to nic nowego, ale co, jeśli wraz z wywołaniem zwrotnym musimy również przekazać kilka argumentów. Tutaj przydają się przydatne funkcje.partial. Może być użyte partialzamrozić niektóre (lub wszystkie) argumenty funkcji przez utworzenie nowego obiektu z uproszczoną sygnaturą funkcji. Zdezorientowany? Rzućmy okiem na kilka praktycznych przykładów:

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

Powyższy kod pokazuje, jak możesz go użyć partialprzekazać funkcję ( output_result) wraz z argumentem ( log=logger) jako wywołanie zwrotne. W tym przypadku użyjemy multiprocessing.apply_async, który asynchronicznie oblicza wynik funkcji ( concat) i zwraca wynik wywołania zwrotnego. Jednakże, apply_asynczawsze przekaże wynik jako pierwszy argument, a jeśli chcemy dołączyć dodatkowe argumenty, jak ma to miejsce w przypadku log=logger, musimy użyć częściowego.

Rozważaliśmy dość zaawansowany przypadek użycia, a prostszym przykładem byłoby zwykłe utworzenie funkcji, która pisze w stderr zamiast na stdout:

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

Dzięki tej prostej sztuczce stworzyliśmy nową funkcję wywoływalną, która zawsze będzie przechodzić file=sys.stderrjako nazwany argument do wyjścia, co pozwala nam uprościć nasz kod i nie musi za każdym razem określać wartości nazwanego argumentu.

I ostatni dobry przykład. Możemy użyć partialw połączeniu z mało znaną funkcją iterutworzyć iterator, przekazując obiekt wywoływalny i sentinelin iter, który można zastosować w następujący sposób:

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

Zwykle podczas odczytu pliku chcemy iterować po wierszach, ale w przypadku danych binarnych może być konieczne iterowanie po rekordach o ustalonym rozmiarze. Możesz to zrobić, tworząc wywoływalny obiekt za pomocą partialktóry odczytuje określoną porcję danych i przekazuje ją iterstworzyć iterator. Ten iterator następnie wywołuje funkcję read, aż dotrze do końca pliku, zawsze przyjmując tylko określony rozmiar porcji ( RECORD_SIZE). Wreszcie, po osiągnięciu końca pliku, wartość sentinel(b ") jest zwracany i iteracja zostaje zatrzymana.

dekoratorzy

Mówiliśmy już o niektórych dekoratorach w poprzednich sekcjach, ale nie o dekoratorach, aby stworzyć więcej dekoratorów. Jednym z takich dekoratorów są functools.wraps. Aby zrozumieć, dlaczego tego potrzebujesz, spójrzmy na przykład:

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

Ten przykład pokazuje, jak można zaimplementować prosty dekorator. Opakowujemy funkcję, która wykonuje określone zadanie ( actual_func) z dekoratorem zewnętrznym i sam staje się dekoratorem, który można następnie zastosować do innych funkcji, na przykład, jak w przypadku greet. Kiedy wywołujesz funkcję, greet zobaczysz, że wypisuje wiadomości zarówno od actual_funci samodzielnie. Wygląda dobrze, prawda? Ale co się stanie, jeśli to zrobimy:

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

Kiedy przywołamy nazwę i dokumentację zdobionej funkcji, zdajemy sobie sprawę, że zostały one zastąpione wartościami z funkcji dekoratora. To jest złe, ponieważ nie możemy przepisać wszystkich naszych nazw funkcji i dokumentacji, gdy używamy jakiegoś dekoratora. Jak rozwiązać ten problem? Oczywiście z functoolami.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

Jedynym celem funkcji wrapsjest skopiowanie nazwy, dokumentacji, listy argumentów itp., aby zapobiec nadpisaniu. Biorąc pod uwagę, że wrapsjest również dekoratorem, możesz go po prostu dodać do naszego current_func i problem zostanie rozwiązany!

Zredukować

Ostatnia, ale nie mniej ważna funkcja w module functools to redukcja. Być może z innych języków znasz to jako fold(Haskell). Ta funkcja pobiera iterację i składa (dodaje) wszystkie jej wartości w jedną. Istnieje wiele zastosowań do tego, na przykład:

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

Jak widać z kodu, reduce może uprościć lub skondensować kod do jednej linii, która w innym przypadku byłaby znacznie dłuższa. Mając to na uwadze, zwykle złym pomysłem jest nadużywanie tej funkcji tylko ze względu na zmniejszenie kodu, dzięki czemu jest „mądrzejszy”, ponieważ szybko staje się przerażający i nieczytelny. Z tego powodu moim zdaniem należy go używać oszczędnie.

A jeśli pamiętasz, że to reduce często skraca wszystko do jednej linii, można to doskonale połączyć z partial:

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

I wreszcie, jeśli potrzebujesz czegoś więcej niż tylko końcowego „zwiniętego” wyniku, możesz użyć accumulate– z kolejnego świetnego modułu itertools. Aby obliczyć maksimum, można go użyć w następujący sposób:

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]

Wnioski

Jak widać functools, istnieje wiele przydatnych funkcji i dekoratorów, które mogą ułatwić Ci życie, ale to tylko wierzchołek góry lodowej. Jak wspomniałem na początku, w standardowej bibliotece Pythona jest wiele funkcji, które pomagają pisać lepszy kod, więc oprócz funkcji, które tu omówiliśmy, możesz zwrócić uwagę na inne moduły, takie jak operatoror itertool. W przypadku jakichkolwiek pytań możesz kliknąć moje pole komentarza. Postaram się jak najlepiej rozwiązać twoje dane wejściowe i mam nadzieję, że dam ci pożądane wyniki. Możesz również skontaktować się ze mną na LinkedIn:-

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

Media pokazane w tym artykule nie są własnością Analytics Vidhya i są używane według uznania Autora.

Źródło: https://www.analyticsvidhya.com/blog/2021/08/functools-the-power-of-higher-order-functions-in-python/

Znak czasu:

Więcej z Analityka Widhja