Functools - возможности функций высшего порядка в Python

Исходный узел: 1865357

Эта статья была опубликована в рамках Блогатон по Data Science

Введение

В стандартной библиотеке Python есть множество отличных модулей, которые помогут сделать ваш код более чистым и простым, а функции functools - определенно один из

Кэширование

Начнем с некоторых простейших, но мощных функций модуля functools. Начнем с функций кеширования (а также декораторов) - 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для отмены кешированных результатов и параметров теста соответственно.

Если вам нужно более детальное кэширование, вы можете включить необязательный аргумент typed = 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 и вручную lt. Наиболее очевидным преимуществом является удобство, заключающееся в том, что вам не нужно писать все эти дополнительные магические методы, но, вероятно, более важно уменьшить объем кода и улучшить его читаемость.

Перегрузка

Наверное, всех нас учили, что в 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, нам нужно использовать partial.

Мы рассмотрели довольно продвинутый вариант использования, и более простым примером может быть обычное создание функции, которая пишет в 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(б") возвращается, и итерация останавливается.

Декораторы

В предыдущих разделах мы уже говорили о некоторых декораторах, но не о декораторах для создания дополнительных декораторов. Один из таких декораторов - 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

Когда мы вызываем имя и документацию декорированной функции, мы понимаем, что они были заменены значениями из функции декоратора. Это плохо, поскольку мы не можем переписать все имена наших функций и документацию, когда используем какой-либо декоратор. Как решить эту проблему? Конечно, с помощью 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

Единственная цель функции wrapsзаключается в копировании имени, документации, списка аргументов и т. д. для предотвращения перезаписи. Учитывая, что wrapsэто тоже декоратор, вы можете просто добавить его в наш actual_func, и проблема решена!

Уменьшить

И последнее, но не менее важное в модуле functools - это сокращение. Возможно, вы знаете это из других языков как 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]

Заключение

Как видите functools, существует множество полезных функций и декораторов, которые могут облегчить вашу жизнь, но это лишь верхушка айсберга. Как я сказал в начале, в стандартной библиотеке Python есть много функций, которые помогут вам писать лучший код, поэтому в дополнение к функциям, которые мы здесь рассмотрели, вы можете обратить внимание на другие модули, такие как operatoror itertool. По любым вопросам вы можете нажать на поле для комментариев. Я постараюсь изо всех сил решить ваши входные данные и надеюсь дать вам желаемый результат. Вы также можете связаться со мной в LinkedIn: -

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/

Отметка времени:

Больше от Аналитика Видхья