Menu
CoddyTech

Mixins

Parte de la sección Programación orientada a objetos del Journey de Python de Coddy. Lección 38 de 64.

Los mixins son un tipo especial de herencia múltiple que se utiliza para «incorporar» funcionalidades adicionales a las clases. Proporcionan métodos específicos sin ser clases completas por sí mismos.

Aquí tienes un ejemplo de un mixin sencillo:

class JSONSerializableMixin:
    def to_json(self):
        import json
        return json.dumps(self.__dict__)

Desglosemos lo que hace este mixin:

  • self.__dict__ - Este atributo especial contiene un diccionario con todos los atributos del objeto y sus valores
  • json.dumps() - Esta función convierte el diccionario de Python en una cadena con formato JSON
  • El mixin incorpora esta funcionalidad de serialización JSON en cualquier clase que herede de él

Ahora veámoslo en acción:

class User(JSONSerializableMixin):
    def __init__(self, name, email):
        self.name = name
        self.email = email

El mixin añade funcionalidad JSON a cualquier clase que herede de él:

user = User("Alice", "alice@example.com")
print(user.to_json())

Salida:

{"name": "Alice", "email": "alice@example.com"}

La idea clave: ahora la clase User tiene el método to_json() sin definirlo directamente. ¡El mixin incorporó esa funcionalidad!

Crea varios mixins para distintas funcionalidades:

class PrintableMixin:
    def pretty_print(self):
        for key, value in self.__dict__.items():
            print(f"{key}: {value}")

class ComparableMixin:
    def __eq__(self, other):
        return self.__dict__ == other.__dict__

Cada mixin accede a self.__dict__ para trabajar con los atributos del objeto, independientemente de qué clase use el mixin. Este es el poder de los mixins: proporcionan funcionalidad reutilizable que funciona con los atributos de cualquier clase.

Combina varios mixins en una clase:

class Product(JSONSerializableMixin, PrintableMixin, ComparableMixin):
    def __init__(self, name, price):
        self.name = name
        self.price = price

product1 = Product("Laptop", 999)
product2 = Product("Laptop", 999)

Usa todas las funcionalidades de los mixins:

print(product1.to_json())         # De JSONSerializableMixin
product1.pretty_print()          # De PrintableMixin
print(product1 == product2)      # De ComparableMixin

Salida:

{"name": "Laptop", "price": 999}
name: Laptop
price: 999
True

Características clave de los mixins:

  • No están pensados para instanciarse por sí solos
  • Proporcionan funcionalidades específicas y reutilizables
  • Normalmente no tienen métodos __init__
  • Los nombres suelen terminar en "Mixin" o "able"
  • Se pueden combinar con la herencia múltiple
  • Trabajan con self.__dict__ u otras características comunes de los objetos para ser flexibles

Punto clave: Los mixins ofrecen una forma de compartir funcionalidades entre distintas jerarquías de clases sin crear árboles de herencia complejos. Te permiten incorporar capacidades específicas, como la serialización, la comparación o la impresión, a cualquier clase que las necesite. Esto fomenta la reutilización del código y permite que las clases se centren en sus responsabilidades principales.

challenge icon

Desafío

Intermedio

En este desafío, implementarás un sistema de comercio electrónico sencillo usando mixins e herencia.

Implementa las clases requeridas en estos archivos (sigue los comentarios TODO de cada archivo):

  • printablemixin.py - Crea PrintableMixin con funcionalidad de salida con formato
  • discountmixin.py - Implementa DiscountMixin para calcular reducciones de precio
  • shippablemixin.py - Crea ShippableMixin para gestionar el peso y los costos de envío
  • product.py - Desarrolla la clase base Product con la herencia de mixins adecuada
  • physicalproduct.py - Crea la clase PhysicalProduct que extiende Product
  • digitalproduct.py - Implementa la clase DigitalProduct con un comportamiento especial de descuento
FORMATO DE SALIDA REQUERIDO:

Pruébalo tú mismo

from product import Product
from physicalproduct import PhysicalProduct
from digitalproduct import DigitalProduct
from printablemixin import PrintableMixin
from discountmixin import DiscountMixin
from shippablemixin import ShippableMixin

def test_basic_functionality():
    # Prueba la funcionalidad básica de todas las clases
    p = Product("Laptop", 1000)
    assert p.print_details() == "Product: Laptop, Price: $1000", f"Print details failed: {p.print_details()}"
    assert p.apply_discount(10) == 900, f"Discount calculation failed: {p.apply_discount(10)}"
    
    physical = PhysicalProduct("Desk", 500)
    physical.set_weight(30)
    assert physical.calculate_shipping() == 15, f"Shipping calculation failed: {physical.calculate_shipping()}"
    
    digital = DigitalProduct("Software", 200)
    assert digital.apply_discount(10) == 180, f"Digital discount failed: {digital.apply_discount(10)}"
    print("Basic functionality test passed!")

def test_edge_cases():
    # Prueba casos límite como cero y valores negativos
    p = Product("Free Item", 0)
    assert p.apply_discount(10) == 0, f"Zero price discount failed: {p.apply_discount(10)}"
    
    p_neg = Product("Negative Item", -100)
    assert p_neg.apply_discount(10) == -90, f"Negative price discount failed: {p_neg.apply_discount(10)}"
    
    physical = PhysicalProduct("Empty Box", 10)
    physical.set_weight(0)
    assert physical.calculate_shipping() == 0, f"Zero weight shipping failed: {physical.calculate_shipping()}"
    
    physical_neg = PhysicalProduct("Anti-Gravity Item", 10)
    physical_neg.set_weight(-5)
    assert physical_neg.calculate_shipping() == -2.5, f"Negative weight shipping failed: {physical_neg.calculate_shipping()}"
    print("Edge cases test passed!")

def test_large_values():
    # Prueba con valores muy grandes
    p = Product("Expensive Item", 1000000)
    assert p.apply_discount(10) == 900000, f"Large value discount failed: {p.apply_discount(10)}"
    
    physical = PhysicalProduct("Heavy Item", 500)
    physical.set_weight(1000)
    assert physical.calculate_shipping() == 500, f"Large weight shipping failed: {physical.calculate_shipping()}"
    print("Large values test passed!")

def test_inheritance():
    # Prueba las relaciones de herencia
    p = Product("Test", 100)
    physical = PhysicalProduct("Test", 100)
    digital = DigitalProduct("Test", 100)
    
    assert isinstance(p, PrintableMixin), "Product should inherit from PrintableMixin"
    assert isinstance(p, DiscountMixin), "Product should inherit from DiscountMixin"
    
    assert isinstance(physical, Product), "PhysicalProduct should inherit from Product"
    assert isinstance(physical, ShippableMixin), "PhysicalProduct should inherit from ShippableMixin"
    assert isinstance(physical, PrintableMixin), "PhysicalProduct should inherit from PrintableMixin through Product"
    assert isinstance(physical, DiscountMixin), "PhysicalProduct should inherit from DiscountMixin through Product"
    
    assert isinstance(digital, Product), "DigitalProduct should inherit from Product"
    assert isinstance(digital, PrintableMixin), "DigitalProduct should inherit from PrintableMixin through Product"
    assert isinstance(digital, DiscountMixin), "DigitalProduct should inherit from DiscountMixin through Product"
    print("Inheritance test passed!")

def test_method_overriding():
    # Prueba el comportamiento de la sobrescritura de métodos
    p = Product("Regular Product", 100)
    digital = DigitalProduct("Digital Product", 100)
    
    # Mismo precio, mismo porcentaje de descuento, resultados diferentes
    assert p.apply_discount(20) == 80, f"Regular discount calculation failed: {p.apply_discount(20)}"
    assert digital.apply_discount(20) == 90, f"Digital fixed discount failed: {digital.apply_discount(20)}"
    
    # El producto digital siempre debe aplicar un descuento del 10% independientemente del parámetro
    assert digital.apply_discount(0) == 90, "Digital product should apply 10% discount even with 0%"
    assert digital.apply_discount(50) == 90, "Digital product should apply 10% discount even with 50%"
    print("Method overriding test passed!")

def test_polymorphism():
    # Prueba el comportamiento polimórfico con una lista de distintos tipos de productos
    products = [
        Product("Regular", 100),
        PhysicalProduct("Physical", 100),
        DigitalProduct("Digital", 100)
    ]
    
    # Establece el peso del producto físico
    products[1].set_weight(10)
    
    # Resultados esperados para apply_discount(20)
    expected_discounts = [80, 80, 90]
    
    for i, product in enumerate(products):
        # Todos deben tener el método print_details
        assert "Product:" in product.print_details(), f"Polymorphic print_details failed for {type(product)}"
        
        # Todos deben tener el método apply_discount, pero con distintas implementaciones
        assert product.apply_discount(20) == expected_discounts[i], f"Polymorphic apply_discount failed for {type(product)}"
    print("Polymorphism test passed!")

def test_attribute_access():
    # Prueba los patrones de acceso a los atributos
    p = Product("Test Product", 100)
    assert p.name == "Test Product", "Name attribute not properly set in Product"
    assert p.price == 100, "Price attribute not properly set in Product"
    
    physical = PhysicalProduct("Physical Product", 200)
    assert physical.name == "Physical Product", "Name attribute not properly set in PhysicalProduct"
    assert physical.price == 200, "Price attribute not properly set in PhysicalProduct"
    
    # El atributo de peso no debería existir antes de llamar a set_weight
    try:
        weight = physical.weight
        assert False, "Weight attribute should not exist before set_weight is called"
    except AttributeError:
        pass
    
    physical.set_weight(15)
    assert physical.weight == 15, "Weight attribute not properly set in PhysicalProduct"
    
    digital = DigitalProduct("Digital Product", 300)
    assert digital.name == "Digital Product", "Name attribute not properly set in DigitalProduct"
    assert digital.price == 300, "Price attribute not properly set in DigitalProduct"
    print("Attribute access test passed!")

# Ejecutor principal de pruebas
test_case = input()

if test_case == "basic_test":
    test_basic_functionality()
elif test_case == "edge_cases":
    test_edge_cases()
elif test_case == "large_values":
    test_large_values()
elif test_case == "inheritance":
    test_inheritance()
elif test_case == "method_overriding":
    test_method_overriding()
elif test_case == "polymorphism":
    test_polymorphism()
elif test_case == "attribute_access":
    test_attribute_access()
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación orientada a objetos

Practica por tu cuenta: Compilador de Python online