Functools: el poder de las funciones de orden superior en Python

Nodo de origen: 1865357

Este artículo fue publicado como parte del Blogatón de ciencia de datos

Introducción

La biblioteca estándar de Python tiene muchos módulos excelentes para ayudar a mantener su código más limpio y simple, y las funciones son definitivamente uno de los

Almacenamiento en caché

Comencemos con algunas de las funciones más simples pero poderosas de las funciones del módulo. Comencemos con las funciones de almacenamiento en caché (así como con los decoradores): lru_cache,cache y cached_property. El primero de ellos - lru_cache proporciona un caché de los últimos resultados de la ejecución de funciones, es decir, recuerda el resultado de su trabajo:

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}

En este ejemplo, hacemos solicitudes GET y almacenamos en caché sus resultados (hasta 32 resultados) usando un decorador @lru_cache. Para ver si el almacenamiento en caché realmente funciona, puede verificar la información de la memoria caché de la función usando un método cache_infoque muestra el número de aciertos y aciertos de caché. Un decorador también proporciona métodos clear_cachey cache_parameterspara la cancelación de los resultados almacenados en caché y los parámetros de prueba, respectivamente.

Si necesita un almacenamiento en caché más granular, puede incluir un argumento opcional typed = true, que le permite almacenar en caché diferentes tipos de argumentos por separado.

Otro decorador para el almacenamiento en caché en functools es una función llamada simplemente cache. Es un envoltorio simple lru_cacheque omite el argumento max_size, disminuyéndolo y no elimina los valores antiguos.

Otro decorador que puede utilizar para el almacenamiento en caché es cached_property. Como sugiere el nombre, se utiliza para almacenar en caché los resultados de los atributos de clase. Esta mecánica es muy útil si tiene una propiedad que es costosa de calcular, pero que sigue siendo la misma.

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 simple ejemplo muestra. Como se puede utilizar, propiedad almacenada en caché, por ejemplo, para almacenar en caché una página HTML renderizada que debe mostrarse al usuario una y otra vez. Lo mismo se puede hacer para ciertas consultas de bases de datos o cálculos matemáticos extensos.

Otra belleza cached_propertyes que solo se ejecuta en la búsqueda, por lo que nos permite cambiar el valor del atributo. Después de cambiar el atributo, el valor previamente almacenado en caché no cambiará, en cambio, el nuevo valor será calculado y almacenado en caché. También puede borrar la caché y todo lo que necesita hacer es eliminar el atributo.

Quiero terminar esta sección con una advertencia sobre todos los decoradores anteriores: no los use si su función tiene algunos efectos secundarios o si crea objetos mutables cada vez que se llama, ya que claramente estas no son las funciones que desea almacenar en caché .

Comparación y pedido

Probablemente ya sepa que puede implementar operadores de comparación en Python como <, >=or ==, con las lt, gtor eq. Sin embargo, puede ser bastante frustrante darse cuenta de cada uno de los eq, lt, le, gtor ge. Afortunadamente, functools hay un decorador @total_orderingque puede ayudarnos con esto, porque todo lo que necesitamos implementar es equno de los métodos restantes, y el resto del decorador se generará automáticamente.

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

De esta forma, podemos implementar todas las operaciones de comparación extendidas, a pesar de tener solo eq y de forma manual lt. El beneficio más obvio es la conveniencia, que es que no tiene que escribir todos estos métodos mágicos adicionales, pero probablemente sea más importante reducir la cantidad de código y su mejor legibilidad.

Sobrecarga

Probablemente a todos nos han enseñado que no hay sobrecarga en Python, pero en realidad hay una manera fácil de implementarlo usando dos funciones de functools, a saber, envío único y / o método de envío único. Estas funciones nos ayudan a implementar lo que llamaríamos un algoritmo de envío múltiple que permite a los lenguajes de programación escritos dinámicamente, como Python, distinguir entre tipos en tiempo de ejecución.

Parcial

Todos trabajamos con varias bibliotecas o marcos externos, muchos de los cuales proporcionan funciones e interfaces que requieren que pasemos devoluciones de llamada, como para operaciones asincrónicas o escucha de eventos. Esto no es nada nuevo, pero ¿qué pasa si también necesitamos pasar algunos argumentos junto con la devolución de llamada? Aquí es donde entran en juego funciones útiles.partial. Puede ser usado partialpara congelar algunos (o todos) los argumentos de una función creando un nuevo objeto con una firma de función simplificada. ¿Confundido? Echemos un vistazo a algunos ejemplos prácticos:

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

El código anterior muestra cómo puede usarlo partialpasar una función output_result) junto con un argumento ( log=logger) como devolución de llamada. En este caso, usaremos multiprocessing.apply_async, que calcula de forma asincrónica el resultado de la función ( concat) y devuelve el resultado de la devolución de llamada. Sin embargo, apply_asyncsiempre pasará el resultado como el primer argumento, y si queremos incluir argumentos adicionales, como es el caso de log = logger, necesitamos usar parcial.

Hemos considerado un caso de uso bastante avanzado, y un ejemplo más simple sería la creación habitual de una función que escribe en stderr en lugar de stdout:

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

Con este simple truco, creamos una nueva función invocable que siempre pasará file=sys.stderrcomo un argumento con nombre para la salida, lo que nos permite simplificar nuestro código y no tener que especificar el valor del argumento con nombre cada vez.

Y un último buen ejemplo. Nosotros podemos usar partialjunto con una función poco conocida iterpara crear un iterador pasando un objeto invocable y sentinelin iter, que se puede aplicar así:

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

Por lo general, cuando leemos un archivo, queremos iterar sobre líneas, pero en el caso de datos binarios, es posible que necesitemos iterar sobre registros de un tamaño fijo. Puede hacer esto creando un objeto invocable usando partialque lee el fragmento de datos especificado y lo pasa iterpara crear un iterador. Este iterador luego llama a la función de lectura hasta que llega al final del archivo, siempre tomando solo el tamaño de fragmento especificado ( RECORD_SIZE). Finalmente, cuando se llega al final del archivo, el valor sentinel(B ") se devuelve y la iteración se detiene.

decoradores

Ya hemos hablado de algunos decoradores en los apartados anteriores, pero no de decoradores para crear más decoradores. Uno de esos decoradores es functools.wraps. Para entender por qué lo necesita, veamos un ejemplo:

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 ejemplo muestra cómo se puede implementar un decorador simple. Envolvemos una función que realiza una tarea específica ( actual_func) con un decorador externo, y se convierte en decorador él mismo, que luego se puede aplicar a otras funciones, por ejemplo, como es el caso de greet. Cuando llamas a la función, greet verás que imprime mensajes tanto de actual_funcy por sí solo. Se ve bien, ¿no? Pero que pasa si hacemos esto:

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

Cuando llamamos el nombre y la documentación de la función decorada, nos damos cuenta de que han sido reemplazados por valores de la función decoradora. Esto es malo ya que no podemos reescribir todos nuestros nombres de funciones y documentación cuando usamos algún decorador. ¿Cómo se puede solucionar este problema? Por supuesto, con 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

El único propósito de la función wrapses copiar el nombre, la documentación, la lista de argumentos, etc., para evitar que se sobrescriban. Teniendo en cuenta que wrapstambién es un decorador, simplemente puede agregarlo a nuestro actual_func, ¡y el problema está resuelto!

La disminución

Por último, pero no menos importante, en las funciones del módulo se reduce. Quizás de otros idiomas, es posible que lo conozca como fold(Haskell). Esta función toma un iterable y dobla (agrega) todos sus valores en uno. Hay muchas aplicaciones para esto, por ejemplo:

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 puede ver en el código, reduce puede simplificar o condensar el código en una línea, que de otro modo sería mucho más larga. Dicho esto, por lo general es una mala idea abusar de esta función solo para reducir el código, haciéndolo "más inteligente", ya que rápidamente se vuelve aterrador e ilegible. Por esta razón, en mi opinión, debería usarse con moderación.

Y si lo recuerdas reduce a menudo acorta todo a una línea, se puede combinar perfectamente con partial:

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

Y, por último, si necesita algo más que el resultado final "contraído", puede utilizar accumulate- de otro gran módulo itertools. Para calcular el máximo, se puede utilizar de la siguiente manera:

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]

Conclusión

Como puede ver, existen muchas funciones útiles y decoradores que pueden hacer su vida más fácil, pero esto es solo la punta del iceberg. Como dije al principio, hay muchas funciones en la biblioteca estándar de Python que te ayudan a escribir mejor código, así que además de las funciones que cubrimos aquí, puedes prestar atención a otros módulos, como operatoror itertool. Para cualquier consulta, puede presionar mi cuadro de comentarios. Haré todo lo posible para resolver sus entradas y espero brindarle las salidas deseadas. También puede comunicarse conmigo en LinkedIn: -

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

Los medios que se muestran en este artículo no son propiedad de Analytics Vidhya y se utilizan a discreción del autor.

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

Sello de tiempo:

Mas de Analítica Vidhya