Functools - O poder das funções de ordem superior em Python

Nó Fonte: 1865357

Este artigo foi publicado como parte do Blogathon de Ciência de Dados

Introdução

A Biblioteca Padrão Python tem muitos módulos excelentes para ajudar a manter seu código mais limpo e simples e functools é definitivamente um dos

Cache

Vamos começar com algumas das funções mais simples, porém poderosas, do módulo functools. Vamos começar com as funções de cache (assim como os decoradores) – lru_cache,cache e cached_property. O primeiro deles – lru_cache fornece um cache dos últimos resultados da execução das funções, ou seja, lembra o resultado do seu trabalho:

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}

Neste exemplo, fazemos solicitações GET e armazenamos em cache seus resultados (até 32 resultados) usando um decorador @lru_cache. Para ver se o cache realmente funciona, você pode verificar as informações do cache da função usando um método cache_infoque mostra o número de acessos e acessos ao cache. Um decorador também fornece métodos clear_cachee cache_parameterspara o cancelamento dos resultados armazenados em cache e dos parâmetros de teste, respectivamente.

Se precisar de cache mais granular, você pode incluir um argumento opcional typed=true, que permite armazenar em cache diferentes tipos de argumentos separadamente.

Outro decorador para cache em functools é uma função chamada simplesmente cache. É um invólucro simples lru_cacheque omite o argumento max_size, diminuindo-o, e não remove os valores antigos.

Outro decorador que você pode usar para cache é cached_property. Como o nome sugere, ele é usado para armazenar em cache os resultados dos atributos da classe. Essa mecânica é muito útil se você tiver uma propriedade que é cara para calcular, mas que permanece a mesma.

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

Este exemplo simples mostra. Como pode ser usada, a propriedade cached, por exemplo, para armazenar em cache uma página HTML renderizada que precisa ser mostrada ao usuário repetidamente. O mesmo pode ser feito para determinadas consultas de banco de dados ou cálculos matemáticos demorados.

Outra beleza cached_propertyé que ele só roda na pesquisa, então nos permite alterar o valor do atributo. Após alterar o atributo, o valor armazenado em cache anteriormente não será alterado; em vez disso, o novo valor será calculado e armazenado em cache. Você também pode limpar o cache e tudo o que precisa fazer é remover o atributo.

Quero terminar esta seção com uma advertência sobre todos os decoradores acima – não os use se sua função tiver alguns efeitos colaterais ou se ela criar objetos mutáveis ​​toda vez que for chamada, pois essas claramente não são as funções que você deseja armazenar em cache .

Comparação e pedido

Você provavelmente já sabe que pode implementar operadores de comparação em Python, como <, >=or ==, com lt, gtor eq. No entanto, pode ser bastante frustrante perceber cada um dos eq, lt, le, gtor ge. Felizmente, functools existe um decorador @total_orderingque pode nos ajudar com isso, porque tudo o que precisamos implementar é equm dos métodos restantes, e o restante do decorador será gerado automaticamente.

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

Desta forma, podemos implementar todas as operações de comparação estendida, apesar de termos apenas eq e manualmente lt. O benefício mais óbvio é a conveniência, que é que você não precisa escrever todos esses métodos mágicos adicionais, mas provavelmente é mais importante reduzir a quantidade de código e sua melhor legibilidade.

Sobrecarga

Provavelmente todos aprendemos que não há sobrecarga em Python, mas na verdade existe uma maneira fácil de implementá-la usando duas funções do functools, ou seja, despacho único e/ou método de despacho único. Essas funções nos ajudam a implementar o que chamaríamos de algoritmo de despacho múltiplo que permite que linguagens de programação de tipo dinâmico, como Python, distingam entre tipos em tempo de execução.

Parcial

Todos nós trabalhamos com várias bibliotecas ou estruturas externas, muitas das quais fornecem funções e interfaces que exigem a passagem de retornos de chamada, como para operações assíncronas ou escuta de eventos. Isso não é novidade, mas e se também precisarmos passar alguns argumentos junto com o retorno de chamada. É aqui que as funções úteis se tornam úteis.partial. Pode ser usado partialcongelar alguns (ou todos) os argumentos de uma função criando um novo objeto com uma assinatura de função simplificada. Confuso? Vejamos alguns exemplos práticos:

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

O código acima mostra como você pode usá-lo partialpara passar uma função ( output_result) junto com um argumento ( log=logger) como um retorno de chamada. Neste caso, usaremos multiprocessing.apply_async, que calcula de forma assíncrona o resultado da função ( concat) e retorna o resultado do retorno de chamada. No entanto, apply_asyncsempre passará o resultado como primeiro argumento, e se quisermos incluir algum argumento adicional, como é o caso de log=logger, precisamos usar parcial.

Consideramos um caso de uso bastante avançado, e um exemplo mais simples seria a criação usual de uma função que escreve em stderr em vez de stdout:

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

Com este truque simples, criamos uma nova função que pode ser chamada que sempre passará file=sys.stderrcomo um argumento nomeado para saída, o que nos permite simplificar nosso código e não ter que especificar o valor do argumento nomeado todas as vezes.

E um último bom exemplo. Podemos usar partialem conjunto com uma função pouco conhecida iterpara criar um iterador passando um objeto que pode ser chamado e sentinelin iter, que pode ser aplicado assim:

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

Normalmente, ao ler um arquivo, queremos iterar sobre linhas, mas no caso de dados binários, podemos precisar iterar sobre registros de tamanho fixo. Você pode fazer isso criando um objeto que pode ser chamado usando partialque lê o pedaço de dados especificado e o transmite iterpara criar um iterador. Este iterador então chama a função de leitura até chegar ao final do arquivo, sempre pegando apenas o tamanho do bloco especificado ( RECORD_SIZE). Finalmente, quando o final do arquivo é atingido, o valor sentinel(b”) é retornado e a iteração é interrompida.

decoradores

Já falamos sobre alguns decoradores nas seções anteriores, mas não sobre decoradores para criar mais decoradores. Um desses decoradores é o functools.wraps. Para entender por que você precisa disso, vejamos apenas um exemplo:

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

Este exemplo mostra como um decorador simples pode ser implementado. Envolvemos uma função que executa uma tarefa específica ( actual_func) com um decorador externo, e passa a ser ele próprio um decorador, que pode então ser aplicado a outras funções, por exemplo, como é o caso de greet. Quando você chama a função, greet você verá que ele imprime mensagens tanto de actual_funce por conta própria. Parece bom, não é? Mas o que acontece se fizermos isso:

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

Quando chamamos o nome e a documentação da função decorada, percebemos que eles foram substituídos por valores da função decoradora. Isso é ruim porque não podemos reescrever todos os nossos nomes de funções e documentação quando usamos algum decorador. Como esse problema pode ser resolvido? Claro, com 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

O único propósito da função wrapsé copiar o nome, a documentação, a lista de argumentos, etc., para evitar a substituição. Considerando que wrapstambém é um decorador, você pode simplesmente adicioná-lo ao nosso actual_func e o problema está resolvido!

Reduzir

Por último, mas não menos importante, no módulo functools está a redução. Talvez de outras línguas, você possa conhecê-lo como fold(Haskell). Esta função pega um iterável e soma (adiciona) todos os seus valores em um. Existem muitas aplicações para isso, por exemplo:

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

Como você pode ver no código, reduce pode simplificar ou condensar o código em uma linha, que de outra forma seria muito mais longa. Dito isso, geralmente é uma má ideia abusar dessa função apenas para reduzir o código, tornando-o “mais inteligente”, pois rapidamente se torna assustador e ilegível. Por esta razão, na minha opinião, deve ser usado com moderação.

E se você se lembrar disso reduce muitas vezes encurta tudo para uma linha, pode ser perfeitamente combinado com partial:

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

E, finalmente, se você precisar de mais do que apenas o resultado final “recolhido”, poderá usar accumulate– de outro ótimo módulo itertools. Para calcular o máximo, pode ser usado da seguinte forma:

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]

Conclusão

Como você pode ver no functools, existem muitas funções e decoradores úteis que podem facilitar sua vida, mas isso é apenas a ponta do iceberg. Como eu disse no início, existem muitas funções na biblioteca padrão do Python que ajudam você a escrever um código melhor, portanto, além das funções que abordamos aqui, você pode prestar atenção em outros módulos, como operatoror itertool. Para qualquer dúvida, você pode acessar minha caixa de comentários. Tentarei o meu melhor para resolver suas sugestões e espero fornecer os resultados desejados. Você também pode entrar em contato comigo no LinkedIn: -

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

As mídias mostradas neste artigo não são propriedade da Analytics Vidhya e são usadas a critério do autor.

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

Carimbo de hora:

Mais de Análise Vidhya