El módulo «operator» y la programación funcional

El módulo «operator» y la programación funcional

La programación funcional es un paradigma que consiste en construir un programa a partir de pequeñas funciones que realizan tareas específicas y que pueden ser pasadas como argumentos a otras funciones para expresar operaciones complejas. El módulo estándar operator facilita la implementación de programas según el paradigma funcional (junto con los módulos functools e itertools) al proveer los operadores de Python (por ejemplo, el operador de suma + o el operador de acceso a un elemento []) como funciones. Esto es útil porque las funciones pueden guardarse en variables y ser pasadas como argumentos, a diferencia de los operadores.

No haremos un recorrido por todas las funciones disponibles en operator. En su lugar, concentrémonos en tres de ellas: itemgetter(), attrgetter() y methodcaller(). La primera encapsula en una función la operación de acceso a un elemento (como diccionario[clave] o lista[indice]), la segunda implementa la operación de acceso a un atributo (objeto.atributo) y la tercera la operación de ejecución de un método (objeto.metodo()).

Veamos un ejemplo. Supongamos que disponemos de la siguiente lista con un conjunto de productos:

productos = [
    {"nombre": "Mouse", "precio": 10},
    {"nombre": "Teclado", "precio": 20},
    {"nombre": "Monitor", "precio": 60},
    {"nombre": "Auriculares", "precio": 5},
]

Cada producto está representado por un diccionario con dos claves: "nombre" y "precio". Ahora queremos imprimir la información de estos productos ordenados de menor a mayor precio, para lo cual primero debemos ordenar la lista. Para ello podemos usar la función incorporada sorted():

productos = [
    {"nombre": "Mouse", "precio": 10},
    {"nombre": "Teclado", "precio": 20},
    {"nombre": "Monitor", "precio": 60},
    {"nombre": "Auriculares", "precio": 5},
]
productos_ordenados = sorted(
    productos,
    key=lambda producto: producto["precio"]
)
for producto in productos_ordenados:
    print(f'{producto["nombre"]} (${producto["precio"]})')

La expresión lambda producto: producto["precio"] crea una función lambda que obtiene el precio de cada producto, ya que ese es el criterio (key) para ordenar la lista. Este es un típico código de programación funcional. Sin embargo, el módulo operator nos libra de la necesidad de crear una función cuyo único objetivo sea acceder al elemento de una colección al disponer de la función itemgetter(). El código anterior se puede simplificar así:

from operator import itemgetter

productos = [
    {"nombre": "Mouse", "precio": 10},
    {"nombre": "Teclado", "precio": 20},
    {"nombre": "Monitor", "precio": 60},
    {"nombre": "Auriculares", "precio": 5},
]
productos_ordenados = sorted(productos, key=itemgetter("precio"))
for producto in productos_ordenados:
    print(f'{producto["nombre"]} (${producto["precio"]})')

Como se observa, itemgetter("precio") es equivalente a lambda producto: producto["precio"].

¿Qué ocurre si, en lugar de guardar los productos en diccionarios, tenemos una clase específica para representar productos?

from dataclasses import dataclass

@dataclass
class Producto:
    nombre: str
    precio: float

productos = [
    Producto("Mouse", 10),
    Producto("Teclado", 20),
    Producto("Monitor", 60),
    Producto("Auriculares", 5),
]

(Sobre el decorador @dataclass, véase este artículo).

Ahora nuestro código para ordenar esta lista de productos con una función lambda debería verse así:

productos_ordenados = sorted(
    productos,
    key=lambda producto: producto.precio
)
for producto in productos_ordenados:
    print(f'{producto.nombre} (${producto.precio})')

Nuevamente, tenemos una función lambda producto: producto.precio cuyo solo propósito es acceder al atributo precio del objeto pasado como argumento. Podemos reemplazar esa expresión por la función attrgetter():

from dataclasses import dataclass
from operator import attrgetter

@dataclass
class Producto:
    nombre: str
    precio: float

productos = [
    Producto("Mouse", 10),
    Producto("Teclado", 20),
    Producto("Monitor", 60),
    Producto("Auriculares", 5),
]
productos_ordenados = sorted(productos, key=attrgetter("precio"))
for producto in productos_ordenados:
    print(f'{producto.nombre} (${producto.precio})')

¿Cómo hacemos si la clase Producto tiene un método precio_con_impuestos() que devuelve el precio del producto con un recargo impositivo y que queremos usar como criterio para ordenar con sorted()?

@dataclass
class Producto:
    nombre: str
    precio: float

    def precio_con_impuestos(self):
        return self.precio * 1.05

De nuevo, no es necesario recurrir a una función lambda, sino que tenemos la función methodcaller():

from dataclasses import dataclass
from operator import methodcaller

@dataclass
class Producto:
    nombre: str
    precio: float

    def precio_con_impuestos(self):
        return self.precio * 1.05

productos = [
    Producto("Mouse", 10),
    Producto("Teclado", 20),
    Producto("Monitor", 60),
    Producto("Auriculares", 5),
]
productos_ordenados = sorted(
    productos,
    key=methodcaller("precio_con_impuestos")
)
for producto in productos_ordenados:
    print(f'{producto.nombre} (${producto.precio_con_impuestos()})')

Así como hicimos con sorted(), hay una gran cantidad de funciones en el lenguaje que permiten expresar nuestros algoritmos según el paradigma funcional y aprovechar el módulo operator. Por ejemplo, si queremos calcular la suma de todos los productos con los impuestos incluidos:

total_precios = sum(map(methodcaller("precio_con_impuestos"), productos))

(Véase una explicación sobre las funciones sum() y map()).

Sin duda mejor que:

total_precios = 0
for producto in productos:
    total_precios += producto.precio_con_impuestos()

Curso online 👨‍💻

¡Ya lanzamos el curso oficial de Recursos Python en Udemy! Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter en 2024.

Consultoría 💡

Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.

Deja una respuesta