Functools – Die Macht der Funktionen höherer Ordnung in Python

Quellknoten: 1865357

Dieser Artikel wurde als Teil des veröffentlicht Data-Science-Blogathon

Einleitung

Die Python-Standardbibliothek enthält viele großartige Module, die dabei helfen, Ihren Code sauberer und einfacher zu halten und functools ist definitiv einer von

Caching

Beginnen wir mit einigen der einfachsten und doch leistungsfähigsten Funktionen des Moduls functools. Beginnen wir mit den Caching-Funktionen (sowie Dekoratoren) – lru_cache,cache und cached_property. Der erste von ihnen – lru_cache stellt einen Cache der letzten Ergebnisse der Ausführung von Funktionen bereit, oder mit anderen Worten, erinnert sich an das Ergebnis ihrer Arbeit:

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 diesem Beispiel stellen wir GET-Anfragen und cachen ihre Ergebnisse (bis zu 32 Ergebnisse) mit einem Dekorator @lru_cache. Um zu sehen, ob das Caching tatsächlich funktioniert, können Sie die Cache-Informationen der Funktion mit einer Methode überprüfen cache_infodas zeigt die Anzahl der Cache-Treffer und -Treffer. Ein Dekorateur bietet auch Methoden clear_cacheund cache_parameterszum Löschen der zwischengespeicherten Ergebnisse bzw. Testparameter.

Wenn Sie ein genaueres Caching benötigen, können Sie ein optionales Argument typed=true einschließen, mit dem Sie verschiedene Argumenttypen separat zwischenspeichern können.

Ein weiterer Decorator zum Caching in functools ist eine Funktion namens simply cache. Es ist ein einfacher Wrapper lru_cachedas das Argument max_size auslässt, es verkleinert und die alten Werte nicht entfernt.

Ein weiterer Decorator, den Sie zum Caching verwenden können, ist cached_property. Wie der Name schon sagt, wird es verwendet, um die Ergebnisse von Klassenattributen zwischenzuspeichern. Dieser Mechanismus ist sehr nützlich, wenn Sie eine Eigenschaft haben, deren Berechnung teuer ist, die aber gleich bleibt.

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

Dieses einfache Beispiel zeigt. Als Cached-Eigenschaft kann beispielsweise verwendet werden, um eine gerenderte HTML-Seite zwischenzuspeichern, die dem Benutzer immer wieder angezeigt werden muss. Dasselbe kann für bestimmte Datenbankabfragen oder langwierige mathematische Berechnungen durchgeführt werden.

Eine andere Schönheit cached_propertyist, dass es nur bei der Suche ausgeführt wird, sodass wir den Wert des Attributs ändern können. Nach dem Ändern des Attributs ändert sich der zuvor zwischengespeicherte Wert nicht, stattdessen wird der neue Wert berechnet und zwischengespeichert. Sie können auch den Cache leeren und müssen lediglich das Attribut entfernen.

Ich möchte diesen Abschnitt mit einem Vorbehalt zu allen oben genannten Dekoratoren beenden – verwenden Sie sie nicht, wenn Ihre Funktion einige Nebenwirkungen hat oder wenn sie bei jedem Aufruf veränderliche Objekte erstellt, da dies eindeutig nicht die Funktionen sind, die Sie zwischenspeichern möchten .

Vergleich und Bestellung

Sie wissen wahrscheinlich bereits, dass Sie Vergleichsoperatoren in Python implementieren können, wie z <, >=or ==, mit lt, gtor eq. Es kann jedoch ziemlich frustrierend sein, jedes der eq, lt, le, gtor ge. Zum Glück gibt es bei functools einen Dekorateur @total_orderingdas kann uns dabei helfen, denn alles was wir umsetzen müssen ist eqeine der verbleibenden Methoden, und der Rest des Dekorators wird automatisch generiert.

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

Auf diese Weise können wir alle erweiterten Vergleichsoperationen implementieren, obwohl wir nur eq und von Hand lt haben. Der offensichtlichste Vorteil ist die Bequemlichkeit, die darin besteht, dass Sie nicht all diese zusätzlichen magischen Methoden schreiben müssen, aber es ist wahrscheinlich wichtiger, die Menge an Code zu reduzieren und seine Lesbarkeit zu verbessern.

Überlastung

Wir haben wahrscheinlich alle gelernt, dass es in Python keine Überladung gibt, aber es gibt tatsächlich eine einfache Möglichkeit, dies mit zwei Funktionen von functools zu implementieren, nämlich Single Dispatch und/oder Single Dispatch-Methode. Diese Funktionen helfen uns bei der Implementierung eines sogenannten Multiple-Dispatch-Algorithmus, der es dynamisch typisierten Programmiersprachen wie Python ermöglicht, zur Laufzeit zwischen Typen zu unterscheiden.

Teil-

Wir alle arbeiten mit verschiedenen externen Bibliotheken oder Frameworks, von denen viele Funktionen und Schnittstellen bereitstellen, die Rückrufe erfordern, z. B. für asynchrone Operationen oder das Abhören von Ereignissen. Dies ist nichts Neues, aber was ist, wenn wir zusammen mit dem Rückruf auch einige Argumente übergeben müssen. Hier kommt es in praktische Funktools.partial. Kann verwendet werden partialeinige (oder alle) Argumente einer Funktion einzufrieren, indem ein neues Objekt mit einer vereinfachten Funktionssignatur erstellt wird. Verwirrt? Schauen wir uns einige praktische Beispiele an:

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

Der obige Code zeigt, wie Sie ihn verwenden können partialeine Funktion übergeben ( output_result) zusammen mit einem Argument ( log=logger) als Rückruf. In diesem Fall verwenden wir multiprocessing.apply_async, das das Ergebnis der Funktion asynchron berechnet ( concat) und gibt das Ergebnis des Rückrufs zurück. Jedoch, apply_asynces wird immer das Ergebnis als erstes Argument übergeben, und wenn wir zusätzliche Argumente einschließen wollen, wie es bei log=logger der Fall ist, müssen wir partiell verwenden.

Wir haben einen ziemlich fortgeschrittenen Anwendungsfall betrachtet, und ein einfacheres Beispiel wäre die übliche Erstellung einer Funktion, die in stderr statt in stdout schreibt:

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

Mit diesem einfachen Trick haben wir eine neue aufrufbare Funktion erstellt, die immer bestanden wird file=sys.stderrals benanntes Argument für die Ausgabe, was es uns ermöglicht, unseren Code zu vereinfachen und nicht jedes Mal den Wert des benannten Arguments angeben zu müssen.

Und noch ein letztes gutes Beispiel. Wir können benutzen partialin Verbindung mit einer wenig bekannten Funktion itereinen Iterator erstellen, indem ein aufrufbares Objekt übergeben wird und sentinelin iter, die wie folgt angewendet werden kann:

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

Normalerweise möchten wir beim Lesen einer Datei über Zeilen iterieren, aber im Fall von Binärdaten müssen wir möglicherweise über Datensätze einer festen Größe iterieren. Sie können dies tun, indem Sie mit ein aufrufbares Objekt erstellen partialdas den angegebenen Datenblock liest und übergibt itereinen Iterator zu erstellen. Dieser Iterator ruft dann die Lesefunktion auf, bis das Ende der Datei erreicht ist, wobei immer nur die angegebene Chunk-Größe ( RECORD_SIZE). Schließlich, wenn das Ende der Datei erreicht ist, wird der Wert sentinel(B ") wird zurückgegeben und die Iteration wird beendet.

Dekorateure

Wir haben in den vorherigen Abschnitten bereits über einige Dekorateure gesprochen, aber nicht über Dekoratoren, um weitere Dekorateure zu erstellen. Ein solcher Dekorateur ist functools.wraps. Um zu verstehen, warum Sie es brauchen, schauen wir uns einfach ein Beispiel an:

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

Dieses Beispiel zeigt, wie ein einfacher Dekorator implementiert werden kann. Wir verpacken eine Funktion, die eine bestimmte Aufgabe ausführt ( actual_func) mit einem externen Dekorator, und er wird selbst zum Dekorator, der dann auf andere Funktionen angewendet werden kann, z. B. wie bei greet. Wenn Sie die Funktion aufrufen, greet Sie werden sehen, dass Nachrichten sowohl von ausgedruckt werden actual_funcund auf eigene Faust. Sieht okay aus, nicht wahr? Aber was passiert, wenn wir dies tun:

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

Wenn wir den Namen und die Dokumentation der dekorierten Funktion aufrufen, stellen wir fest, dass sie durch Werte aus der Decorator-Funktion ersetzt wurden. Dies ist schlecht, da wir nicht alle unsere Funktionsnamen und die Dokumentation neu schreiben können, wenn wir einen Dekorator verwenden. Wie kann dieses Problem gelöst werden? Natürlich mit Funktools.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

Der einzige Zweck der Funktion wrapsbesteht darin, den Namen, die Dokumentation, die Argumentliste usw. zu kopieren, um ein Überschreiben zu verhindern. Bedenkt, dass wrapses ist auch ein Dekorateur, Sie können es einfach zu unserer aktuellen_func hinzufügen, und das Problem ist gelöst!

Reduziert

Nicht zuletzt im Modul functools wird dies reduziert. Vielleicht aus anderen Sprachen, Sie kennen es vielleicht als fold(Haskell). Diese Funktion nimmt ein Iterable und faltet (addiert) alle seine Werte zu einem. Dafür gibt es viele Anwendungen, zum Beispiel:

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

Wie Sie dem Code entnehmen können, reduce kann den Code in einer Zeile vereinfachen oder verdichten, die sonst viel länger wäre. Abgesehen davon ist es normalerweise eine schlechte Idee, diese Funktion nur zu missbrauchen, um Code zu verkleinern, um ihn „intelligenter“ zu machen, da er schnell beängstigend und unlesbar wird. Aus diesem Grund sollte meiner Meinung nach sparsam damit umgegangen werden.

Und wenn du dich daran erinnerst reduce verkürzt oft alles auf eine Zeile, es lässt sich perfekt kombinieren mit partial:

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

Und schließlich, wenn Sie mehr als nur das endgültige „zusammengeklappte“ Ergebnis benötigen, können Sie accumulate– von einem anderen tollen Modul itertools. Um das Maximum zu berechnen, kann es wie folgt verwendet werden:

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]

Zusammenfassung

Wie Sie sehen, gibt es bei functools viele nützliche Funktionen und Dekorateure, die Ihnen das Leben erleichtern können, aber dies ist nur die Spitze des Eisbergs. Wie ich eingangs sagte, gibt es in der Python-Standardbibliothek viele Funktionen, die Ihnen helfen, besseren Code zu schreiben. Sie können also zusätzlich zu den hier behandelten Funktionen auf andere Module achten, wie z operatoror itertool. Bei Fragen können Sie mein Kommentarfeld drücken. Ich werde mein Bestes geben, um Ihre Eingaben zu lösen und hoffe, Ihnen die gewünschten Ausgaben zu geben. Sie erreichen mich auch auf LinkedIn:-

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

Die in diesem Artikel gezeigten Medien sind nicht Eigentum von Analytics Vidhya und werden nach Ermessen des Autors verwendet.

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

Zeitstempel:

Mehr von Analytics-Vidhya