Functools -הכוח של פונקציות מסדר גבוה יותר בפייתון

צומת המקור: 1865357

מאמר זה פורסם כחלק מה- בלוגאת מדע הנתונים

מבוא

הספרייה הסטנדרטית של פייתון כוללת הרבה מודולים נהדרים שיעזרו לך לשמור על קוד נקי ופשוט יותר בהחלט אחד מ

מטמון

נתחיל בכמה מהפונקציות הפשוטות אך העוצמתיות ביותר של פונקציונלי המודול. נתחיל בפונקציות המטמון (כמו גם בעיטורים) - lru_cache,cache ו cached_property. הראשון מביניהם - lru_cache מספק מטמון של התוצאות האחרונות של ביצוע פונקציות, או במילים אחרות, זוכר את תוצאת עבודתן:

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}

בדוגמה זו, אנו מבקשים בקשות GET ומחסנים את תוצאותיהם (עד 32 תוצאות) באמצעות מעצב @lru_cache. כדי לבדוק אם המטמון אכן עובד, תוכל לבדוק את פרטי מטמון הפונקציות בשיטה cache_infoשמראה את מספר ההתאמות וההתאמות במטמון. מעצב מספק גם שיטות clear_cacheו cache_parametersלביטול התוצאות במטמון ופרמטרי הבדיקה, בהתאמה.

אם אתה צריך יותר מטמון גרגירי יותר, תוכל לכלול ארגומנט אופציונלי הקלד = true, המאפשר לך לשמור מטמון מסוגים שונים בנפרד.

מעצב נוסף לאחסון במטמון ב- functools הוא פונקציה הנקראת בפשטות cache. זהו עטיפה פשוטה lru_cacheזה משמיט את הארגומנט max_size, מקטין אותו ואינו מסיר את הערכים הישנים.

מעצב נוסף שתוכל להשתמש בו למטמון הוא cached_property. כפי שהשם מרמז, הוא משמש למטמון התוצאות של תכונות הכיתה. המכונאי הזה מאוד שימושי אם יש לכם נכס יקר לחישוב, אבל זה נשאר אותו דבר.

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

דוגמה פשוטה זו מראה. כפי שניתן להשתמש בו, נכס במטמון, למשל, במטמון דף HTML מעובד שצריך להציג בפני המשתמש שוב ושוב. אותו הדבר ניתן לעשות לגבי שאילתות מסד נתונים מסוימות או חישובי מתמטיקה ארוכים.

יופי נוסף cached_propertyהוא שהוא פועל רק בחיפוש, ולכן הוא מאפשר לנו לשנות את ערך התכונה. לאחר שינוי המאפיין, הערך שנשמר בעבר לא ישתנה, במקום זאת, הערך החדש יחושב ויטמון. תוכל גם לנקות את המטמון, וכל שעליך לעשות הוא להסיר את התכונה.

אני רוצה לסיים את הסעיף הזה עם סייג לגבי כל המעצבים לעיל - אל תשתמש בהם אם לתפקוד שלך יש כמה תופעות לוואי או אם הוא יוצר אובייקטים הניתנים לשינוי בכל פעם שהוא נקרא מכיוון שברור שלא מדובר בפונקציות שאתה רוצה לשמור במטמון .

השוואה והזמנה

אתה בטח כבר יודע שאתה יכול ליישם אופרטורי השוואה ב- Python כגון <, >=or ==עם lt, gtor eq. עם זאת, זה יכול להיות די מתסכל להבין כל אחת מהן eq, lt, le, gtor ge. למרבה המזל, functools יש מעצב @total_orderingשיכול לעזור לנו בכך, כי כל מה שאנחנו צריכים ליישם הוא eqאחת מהשיטות הנותרות, ושאר המעצב ייווצר אוטומטית.

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

בדרך זו, נוכל ליישם את כל פעולות ההשוואה המורחבות, למרות שיש לי רק eq ו ביד. היתרון הברור ביותר הוא הנוחות, כלומר שאתה לא צריך לכתוב את כל שיטות הקסם הנוספות האלה, אבל כנראה שחשוב יותר להפחית את כמות הקוד ואת הקריאות הטובה יותר שלו.

להעמיס יותר מדי

קרוב לוודאי שלימדנו את כולנו שאין עומס יתר ב- Python, אבל למעשה יש דרך קלה ליישם אותו באמצעות שתי פונקציות מ- functools, כלומר שיגור יחיד ו/או שיטת שיגור אחת. פונקציות אלה עוזרות לנו ליישם את מה שהיינו מכנים אלגוריתם שיגור מרובה המאפשר לשפות תכנות שהוקלדו באופן דינמי כגון Python להבחין בין סוגים בזמן ריצה.

חלקי

כולנו עובדים עם ספריות או מסגרות חיצוניות שונות, שרבות מהן מספקות פונקציות וממשקים המחייבים אותנו להעביר שיחות טלפון, כגון פעולות אסינכרוניות או האזנה לאירועים. זה לא דבר חדש, אבל מה אם נצטרך להעביר כמה טיעונים יחד עם החזרה. זה המקום שבו הוא מגיע בפונקציות שימושיות.partial. יכול לשמש partialלהקפיא חלק מהארגומנטים (או כולם) לפונקציה על ידי יצירת אובייקט חדש עם חתימת פונקציה פשוטה. מְבוּלבָּל? בואו נסתכל על כמה דוגמאות מעשיות:

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

הקוד למעלה מראה כיצד אתה יכול להשתמש בו partialלהעביר פונקציה ( output_result) יחד עם טיעון ( log=logger) כחיוג חוזר. במקרה זה, נשתמש ב- multiprocessing.apply_async, אשר מחשב באופן אסינכרוני את תוצאת הפונקציה ( concat) ומחזירה את תוצאת החזרה. למרות זאת, apply_asyncזה תמיד יעביר את התוצאה כארגומנט הראשון, ואם נרצה לכלול טיעונים נוספים כלשהם, כמו במקרה של log = logger, עלינו להשתמש בחלק.

שקלנו מקרה שימוש מתקדם למדי, ודוגמה פשוטה יותר תהיה יצירה רגילה של פונקציה שכותבת ב- stderr במקום stdout:

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

בעזרת הטריק הפשוט הזה, יצרנו פונקציה חדשה להתקשרות שתמיד תחלוף file=sys.stderrכארגומנט בשם לפלט, המאפשר לנו לפשט את הקוד שלנו ולא צריך לציין את הערך של הארגומנט בשם בכל פעם.

ודוגמה טובה אחרונה. אנחנו יכולים להשתמש partialבשילוב עם פונקציה לא ידועה iterכדי ליצור איטרציה על ידי העברת אובייקט שניתן להתקשר אליו ו sentinelin iter, שניתן ליישם כך:

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

בדרך כלל, בעת קריאת קובץ, אנו רוצים לחזור על שורות, אך במקרה של נתונים בינאריים, ייתכן שנצטרך לחזור על רשומות בגודל קבוע. אתה יכול לעשות זאת על ידי יצירת אובייקט שניתן להתקשר באמצעות partialשקורא את נתח הנתונים שצוין ומעביר אותם פנימה iterכדי ליצור איטרציה. איטרציה זו קוראת לאחר מכן לפונקציית הקריאה עד שהיא מגיעה לסוף הקובץ, ותמיד לוקחת רק את גודל הנתח שצוין ( RECORD_SIZE). לבסוף, כאשר מגיעים לסוף הקובץ, הערך sentinel(ב ") מוחזר והאיטרציה מפסיקה.

decorators

כבר דיברנו על כמה מעצבים בסעיפים הקודמים, אבל לא על מעצבים ליצירת יותר מעצבים. מעצב אחד כזה הוא functools.wraps. כדי להבין מדוע אתה צריך את זה, בואו רק נסתכל על דוגמה:

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

דוגמה זו מראה כיצד ניתן ליישם מעצב פשוט. אנו עוטפים פונקציה המבצעת משימה מסוימת ( actual_func) עם מעצב חיצוני, והוא הופך למעצב עצמו, שאפשר ליישם אותו על פונקציות אחרות, למשל, כפי שקורה עם greet. כאשר אתה קורא לפונקציה, greet תראה שהוא מדפיס הודעות משניהם actual_funcומעצמו. נראה בסדר, לא? אבל מה יקרה אם נעשה זאת:

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

כאשר אנו קוראים לשם ותיעוד הפונקציה המעוטרת, אנו מבינים כי הם הוחלפו בערכים מפונקציית המעצבים. זה רע מכיוון שאיננו יכולים לשכתב את כל שמות הפונקציות והתיעוד שלנו כאשר אנו משתמשים במעצב כלשהו. כיצד ניתן לפתור בעיה זו? כמובן, עם פונקציות.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

המטרה היחידה של הפונקציה wrapsהוא להעתיק את השם, התיעוד, רשימת הטיעונים וכו ', כדי למנוע החלפה. בהתחשב בזה wrapsהוא גם מעצב, אתה יכול פשוט להוסיף אותו ל- actual_func שלנו, והבעיה נפתרה!

להפחית

אחרון חביב בפונקציונאליות של המודולים הוא הפחתה זו. אולי משפות אחרות, אתה יכול לדעת את זה כ fold(האסקל). פונקציה זו לוקחת איטרציה ומקפלת (מוסיפה) את כל ערכיה לאחד. ישנם יישומים רבים לכך, למשל:

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

כפי שאתה יכול לראות מהקוד, reduce יכול לפשט או לצמצם את הקוד לשורה אחת, שאחרת הייתה ארוכה הרבה יותר. עם זאת, לרוב זה רעיון גרוע להתעלל בפונקציה זו רק לשם צמצום הקוד, מה שהופך אותו ל"חכם יותר ", מכיוון שהוא הופך במהירות למפחיד ובלתי קריא. מסיבה זו, לדעתי, יש להשתמש בה במשורה.

ואם אתה זוכר את זה reduce לעתים קרובות מקצר הכל לשורה אחת, ניתן לשלב אותו בצורה מושלמת partial:

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

ולבסוף, אם אתה צריך יותר מסתם התוצאה הסופית של "התמוטטות", תוכל להשתמש accumulate- ממודול נהדר אחר itertools. לחישוב המקסימום ניתן להשתמש בו כדלקמן:

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]

סיכום

כפי שאתה יכול לראות פונקציות, ישנם הרבה פונקציות שימושיות ומעצבים שיכולים להקל על חייך, אך זהו רק קצה הקרחון. כפי שאמרתי בהתחלה, ישנן פונקציות רבות בספרייה הסטנדרטית של פייתון שעוזרות לך לכתוב קוד טוב יותר, כך שבנוסף לפונקציות שסיקרנו כאן, תוכל לשים לב למודולים אחרים, כגון operatoror itertool. לכל שאלה אתה יכול ללחוץ על תיבת ההערות שלי. אנסה כמיטב יכולתי לפתור את התשומות שלך ומקווה לתת לך את התפוקות הרצויות. אתה יכול גם להגיע אלי בלינקדאין:-

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

אמצעי התקשורת המוצגים במאמר זה אינם בבעלות Analytics Vidhya ומשמשים את שיקול הדעת של המחבר.

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

בול זמן:

עוד מ אנליטיקה וידיה