Clases anidadas y datos
En resumen: Una clase anidada es una clase declarada dentro de otra porque solo tiene sentido ahí; en Python los datos también se anidan a menudo como diccionarios y listas unos dentro de otros.
Problema
Una clase que solo tiene sentido en un lugar
En la lección sobre la lista enlazada, Node era una clase aparte. Cualquiera en el programa puede crear nodos, pero solo tienen sentido en la lista. Lo mismo pasa con una línea de pedido:
class OrderLine: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price
class Order: def __init__(self): self.lines = []OrderLine sin Order no tiene sentido. El nombre es largo para que no choque con la línea de una factura, InvoiceLine, ni con la línea de un almacén. Y nada dice que las dos clases van juntas. ¿Puede la línea vivir dentro del pedido?
La idea
Clase anidada
Puede. Una clase declarada dentro de otra clase se llama clase anidada:
class Order: class Line: def __init__(self, product, quantity, price): self.product = product self.quantity = quantity self.price = price
def total(self): return self.quantity * self.price
def __init__(self): self._lines = []
def add(self, line): self._lines.append(line)
def total(self): total = 0 for line in self._lines: total += line.total() return totalorder = Order()order.add(Order.Line("Café en grano", 2, 12.5))order.add(Order.Line("Taza", 4, 3.0))print(f"Total: {order.total()} €")Total: 37.0 €- Desde fuera, el nombre es
Order.Line.Order.Line(...)se crea sin un objetoOrder, como una clase normal. - Dentro de los métodos de
Ordertambién escribesOrder.Line. Ahí no se ve soloLine: la clase no es un ámbito para sus métodos. - Una clase anidada en Python no está ligada a un pedido concreto y no ve sus atributos. En Java una clase así se llama estática.
- Si es
_Line, por convención dice «solo para uso interno», como_lines.
class Order: class Line: pass
def make(self): return Line()
try: Order().make()except NameError as e: print(f"NameError: {e}")print(type(Order.Line()).__qualname__)NameError: name 'Line' is not definedOrder.LineEn Python las clases anidadas son menos comunes que en Java. A menudo la clase pequeña simplemente está al lado de la grande, en el mismo archivo, o en lugar de una clase hay un diccionario. De eso trata la siguiente tarjeta.
La idea
Datos dentro de datos: diccionarios y listas
Si la línea de pedido solo guarda valores y no tiene métodos, en Python a menudo no hay una clase para ella. El pedido es un diccionario que guarda una lista de diccionarios:
order = { "customer": "Fernández", "lines": [ {"product": "Café en grano", "quantity": 2, "price": 12.5}, {"product": "Taza", "quantity": 4, "price": 3.0}, ],}
total = 0for line in order["lines"]: total += line["quantity"] * line["price"]print(order["lines"][0]["product"])print(f"Total: {total} €")Café en granoTotal: 37.0 €order["lines"]es la lista,order["lines"][0]es la primera línea, yorder["lines"][0]["product"]es el producto que hay en ella. Cada[...]baja un nivel más.- El valor en un diccionario puede ser cualquier cosa: un número, texto, una lista, otro diccionario.
- Una clase es mejor opción cuando los datos tienen comportamiento (un método
total) o reglas que hay que cumplir.
Un diccionario de listas reúne cosas en grupos:
people = [("Juan", "Sevilla"), ("María", "Valencia"), ("Ana", "Sevilla")]by_city = {}for name, city in people: if city not in by_city: by_city[city] = [] by_city[city].append(name)print(by_city)print(by_city["Sevilla"][1]){'Sevilla': ['Juan', 'Ana'], 'Valencia': ['María']}AnaMás precisamente, para curiosos
- Una trampa con las listas anidadas:
* 3repite la referencia, no crea tres listas. Las tres filas son una misma lista:
grid = [[0] * 3] * 3grid[0][0] = 1print(grid)
grid = []for row in range(3): grid.append([0] * 3)grid[0][0] = 1print(grid)[[1, 0, 0], [1, 0, 0], [1, 0, 0]][[1, 0, 0], [0, 0, 0], [0, 0, 0]]La idea
El objeto anidado guarda al externo
El carrito tiene un descuento para todos los productos que hay en él. Cada producto tiene que conocer el descuento de su propio carrito. En Python una clase anidada no ve al objeto externo, así que el carrito se pasa de forma explícita:
class Cart: class Item: def __init__(self, cart, name, price): self.cart = cart self.name = name self.price = price
def final_price(self): return self.price * (100 - self.cart._discount_percent) / 100
def __init__(self, discount_percent): self._discount_percent = discount_percent self._items = []
def add(self, name, price): self._items.append(Cart.Item(self, name, price))
def print_items(self): for item in self._items: print(f"{item.name}: {item.final_price()} €")cart = Cart(10)cart.add("Auriculares", 80)cart.add("Funda", 20)cart.print_items()Auriculares: 72.0 €Funda: 18.0 €add pasa self, e Item la guarda en self.cart. final_price lee el _discount_percent del carrito en el que está. El guion bajo dice «interno», e Item está dentro de Cart, así que está permitido.cart = Cart(10)cart.add("Auriculares", 80)cart.add("Funda", 20)cart.print_items()Paso a paso, en palabras
- El carrito es un objeto Cart con un descuento del 10 %.
- add crea un Item y le pasa el carrito, self. Además de name y price, cada Item guarda en cart una flecha a su propio carrito.
- El segundo Item apunta al mismo carrito.
- final_price de «Auriculares» sigue la flecha self.cart y lee _discount_percent: 80 * (100 - 10) / 100 = 72.0.
- final_price de «Funda» sigue la flecha self.cart y lee _discount_percent: 20 * (100 - 10) / 100 = 18.0.
Más precisamente, para curiosos
- Desde fuera, un objeto
Itemse crea con el carrito como primer argumento. Normalmente la clase externa crea las internas ella misma, comoaddaquí:
cart = Cart(10)cable = Cart.Item(cart, "Cable", 10)print(cable.final_price())9.0- El campo
cartes justo lo que hace elCart.thisoculto en Java. Aquí simplemente se ve. - Si la clase anidada no usa los campos de la clase externa, no le des el objeto externo. Así no guarda una referencia que no necesita.
Pruébalo
Dos carritos
La clase Cart es la misma. Un carrito no tiene descuento, y el otro tiene un 20 % para los socios del club.
¿Qué imprimirá el programa?
regular = Cart(0)member = Cart(20)regular.add("Libro", 15)member.add("Libro", 15)regular.print_items()member.print_items()Cada Item ve el descuento del carrito que lo creó. El libro de regular tiene un 0 %, y el de member, un 20 %.
Pruébalo
Encuentra el error
print(Order.Line("Café", 12.5).show()) el programa se detiene:NameError: name 'currency' is not defined¿En qué línea está el problema?
Elige la línea con el error.
currency es un atributo de un objeto Order, y Line no tiene pedido. Python busca un nombre suelto currency en el método y en todo el archivo, y no lo encuentra. El error solo aparece cuando se llama a show, no antes. La corrección es que Line reciba el pedido, Order.Line(order, "Café", 12.5), y lea self.order.currency. Otra forma es que currency sea un atributo de clase, Order.currency, si la moneda es la misma para todos los pedidos.La idea
Una clase dentro de una función
Una clase también se puede declarar dentro de una función. Entonces solo se ve ahí y se llama clase local:
def print_best_seller(names, sold): class Sale: def __init__(self, name, count): self.name = name self.count = count
best = Sale(names[0], sold[0]) for i in range(1, len(names)): if sold[i] > best.count: best = Sale(names[i], sold[i]) print(f"Más vendido: {best.name} ({best.count} vendidos)")names = ["Café", "Té", "Agua"]sold = [120, 45, 300]print_best_seller(names, sold)Más vendido: Agua (300 vendidos)Sale mantiene juntos el nombre y la cantidad. Nadie la usa fuera de print_best_seller, así que vive ahí. Estas clases son raras, pero conviene conocerlas.
La idea
Una función en lugar de una clase
Los descuentos de la tienda pueden ser una clase abstracta con un método, como en el módulo «Herencia y polimorfismo»:
class Discount(ABC): @abstractmethod def apply(self, price): passY una clase para cada descuento: class BlackFriday(Discount): .... En Java, para un descuento que se usa en un solo lugar, existe la clase anónima: una clase sin nombre, escrita justo ahí. Python no tiene clases anónimas. No las necesita: cuando la «clase» tiene un solo método, en Python pasas una función:
def black_friday(price): return price * 70 / 100
def no_discount(price): return price
def checkout(price, discount): return discount(price)
print(checkout(100, black_friday))print(checkout(100, no_discount))70.0100- Una función es un valor.
checkout(100, black_friday)pasa la función, sin paréntesis, ycheckoutla llama condiscount(price). checkoutno pregunta qué esdiscount. Solo necesita que se pueda llamar con un precio.no_discount(100)devuelve el número que recibió, así que es100, no100.0.
Más precisamente, para curiosos
- En el módulo «Python moderno» verás una forma más corta de escribir funciones pequeñas como
no_discount, justo donde se pasan. - Una función declarada dentro de otra función ve sus variables. Eso hace que las funciones sean aún más prácticas para estos casos.
La idea
key: ordenar con tu propia regla
sorted ordenaba texto y números. Pero no puede ordenar alumnos:
class Student: def __init__(self, name, grade): self.name = name self.grade = grade
def __repr__(self): return f"{self.name} {self.grade}"students = [ Student("Juan", 5.50), Student("María", 6.00), Student("Pedro", 4.75), Student("Ana", 5.50),]sorted(students)TypeError: '<' not supported between instances of 'Student' and 'Student'Python no sabe por qué ordenar a los alumnos: ¿por nombre, por nota, por edad? < entre dos objetos Student no está definido.
La regla se da como una función en key. Recibe un elemento y devuelve el valor con el que comparar. sorted la llama para cada alumno y ordena según el resultado:
def by_grade(student): return student.grade
print(sorted(students, key=by_grade, reverse=True))[María 6.0, Juan 5.5, Ana 5.5, Pedro 4.75]key=by_gradepasa la función, sin paréntesis.reverse=Trueinvierte el orden, así que la nota más alta va primero.- Juan y Ana tienen la misma nota y se quedan en el orden en que estaban.
sortedno intercambia elementos iguales, ni siquiera conreverse=True. printde una lista muestra los elementos con__repr__, por esoStudentlo tiene.- Para texto y números en orden inverso basta con
reverse=True:
cities = ["Sevilla", "Valencia", "Bilbao"]print(sorted(cities, reverse=True))['Valencia', 'Sevilla', 'Bilbao']Más precisamente, para curiosos
students.sort(key=by_grade, reverse=True)ordena la propia lista, ysorteddevuelve una nueva.- El texto y los números se ordenan sin
key, porque tienen<. Tu clase también puede tener un orden natural, si escribes__lt__.keyes para los casos en que hay varios órdenes. - Una función de comparación como
compare(a, b)de Java también es posible, confunctools.cmp_to_key. Pero en Pythonkeyes la forma habitual: es más corta y más rápida.
Pruébalo
¿Cuál va primero?
¿Qué imprimirá el programa?
products = [ Product("Monitor", 220), Product("Ratón", 25), Product("Teclado", 60),]
def by_price(product): return product.price
for p in sorted(products, key=by_price): print(p.name)by_price devuelve el precio, y sorted ordena del más pequeño: 25, 60, 220. Para poner primero el más caro, agrega reverse=True.Ejercicio
Clases que van juntas
Resuelve las tareas en tu editor.
1. Carrito de compras. Una clase ShoppingCart con una clase anidada Item (nombre y precio) y los métodos add_item(item), get_total() y count(). Agrega ShoppingCart.Item("Leche", 2.5), pan por 1,20 € y café por 7,50 €. Imprime el total con dos cifras después del punto.
Productos: 3Total: 11.20 €2. Monitor de un servidor web. Una clase WebServer con una clase anidada Request (dirección y código de estado) y los métodos add_request(r) y print_stats(). Las estadísticas dicen cuántas solicitudes hay, cuántas hay de cada código de estado (ordenados) y qué porcentaje son correctas, es decir, con un código del 200 al 299. Guarda los conteos en un diccionario. Las solicitudes son /home 200, /cart 200, /login 401, /home 200, /api/orders 500.
Solicitudes: 5200: 3401: 1500: 1Correctas: 60%3. Administrador de un edificio. Una clase Building con una clase anidada Apartment (número, número de habitaciones, propietario) y los métodos add_apartment(a) y get_apartment_info(number). Guarda los apartamentos en un diccionario de número a apartamento. Agrega el 101, de 3 habitaciones, de Fernández, y el 102, de 2 habitaciones, de Pérez. Busca el 101 y el 999.
Apartamento 101: 3 habitaciones, propietario: FernándezNo existe el apartamento 999.4. Clasificación. Ordena a los alumnos de la lección por nota, de la más alta a la más baja. Cuando las notas son iguales, ordénalos por nombre, en orden alfabético. Todo va en una sola función key: que devuelva una tupla (-grade, name). Las tuplas se comparan por el primer elemento, y cuando es igual, por el segundo. El signo menos invierte solo el orden de la nota.
María 6.0Ana 5.5Juan 5.5Pedro 4.75Más tareas
- Configurador de computadoras. Una clase
Computercon las clases anidadasCPU(marca, velocidad en GHz),RAM(GB) yStorage(GB) y los métodosset_cpu,set_ram,set_storage.__str__describe la configuración:Intel 3.5 GHz, 16 GB RAM, 512 GB. - El planificador de viajes, otra vez. Haz la tarea de la lección sobre diccionarios una vez con una clase anidada
TripPlanner.Destinationque guarde el nombre y la lista de actividades, y otra vez solo con un diccionario de listas. ¿Cuál es más práctica? - La red de contactos, otra vez. Mueve
Contactde la tarea de la lección sobre diccionarios dentro deContactNetwork. ¿DeberíaContactconocer su red? - Cálculo mental, otra vez. Mueve
Questionde la tarea de la lección sobre listas dentro deMathGame. - Productos de dos formas. Ordena una lista de productos por precio, del más caro, y por nombre cuando el precio es el mismo. Después ordena la misma lista solo por nombre, en orden inverso.
- Tu propio descuento. Escribe tres funciones de descuento: un 10 % a partir de 100 €, 5 € fijos de descuento y «el segundo producto a mitad de precio». Aplícalas a un mismo carrito con un mismo
checkout.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Cadenas · Listas: append, remove y sort · Pila, cola y lista enlazada · dict y set
- Continúa en: Lambdas · Listas por comprensión y generadores · Patrones de diseño · Tests con unittest
- Ejercicios sobre el tema: Colecciones