Ir al contenido
wannacode

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 total
order = Order()
order.add(Order.Line("Café en grano", 2, 12.5))
order.add(Order.Line("Taza", 4, 3.0))
print(f"Total: {order.total()} €")
Salida
Total: 37.0 €
  • Desde fuera, el nombre es Order.Line. Order.Line(...) se crea sin un objeto Order, como una clase normal.
  • Dentro de los métodos de Order también escribes Order.Line. Ahí no se ve solo Line: 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__)
Salida
NameError: name 'Line' is not defined
Order.Line

En 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 = 0
for line in order["lines"]:
total += line["quantity"] * line["price"]
print(order["lines"][0]["product"])
print(f"Total: {total} €")
Salida
Café en grano
Total: 37.0 €
  • order["lines"] es la lista, order["lines"][0] es la primera línea, y order["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])
Salida
{'Sevilla': ['Juan', 'Ana'], 'Valencia': ['María']}
Ana
Más precisamente, para curiosos
  • Una trampa con las listas anidadas: * 3 repite la referencia, no crea tres listas. Las tres filas son una misma lista:
grid = [[0] * 3] * 3
grid[0][0] = 1
print(grid)
grid = []
for row in range(3):
grid.append([0] * 3)
grid[0][0] = 1
print(grid)
Salida
[[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()
Salida
Auriculares: 72.0 €
Funda: 18.0 €
Java tiene la clase interna: cada objeto suyo guarda una referencia oculta al objeto externo. Python no tiene algo así, por eso la referencia se ve: 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
  1. El carrito es un objeto Cart con un descuento del 10 %.
  2. 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.
  3. El segundo Item apunta al mismo carrito.
  4. final_price de «Auriculares» sigue la flecha self.cart y lee _discount_percent: 80 * (100 - 10) / 100 = 72.0.
  5. 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 Item se crea con el carrito como primer argumento. Normalmente la clase externa crea las internas ella misma, como add aquí:
cart = Cart(10)
cable = Cart.Item(cart, "Cable", 10)
print(cable.final_price())
Salida
9.0
  • El campo cart es justo lo que hace el Cart.this oculto 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()
Respuesta

Pruébalo

Encuentra el error

La línea debería mostrar la moneda del pedido. Con print(Order.Line("Café", 12.5).show()) el programa se detiene:
Error
NameError: name 'currency' is not defined

¿En qué línea está el problema?

Elige la línea con el error.

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)
Salida
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):
pass

Y 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))
Salida
70.0
100
  • Una función es un valor. checkout(100, black_friday) pasa la función, sin paréntesis, y checkout la llama con discount(price).
  • checkout no pregunta qué es discount. Solo necesita que se pueda llamar con un precio.
  • no_discount(100) devuelve el número que recibió, así que es 100, no 100.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)
Error
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))
Salida
[María 6.0, Juan 5.5, Ana 5.5, Pedro 4.75]
  • key=by_grade pasa la función, sin paréntesis. reverse=True invierte 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. sorted no intercambia elementos iguales, ni siquiera con reverse=True.
  • print de una lista muestra los elementos con __repr__, por eso Student lo tiene.
  • Para texto y números en orden inverso basta con reverse=True:
cities = ["Sevilla", "Valencia", "Bilbao"]
print(sorted(cities, reverse=True))
Salida
['Valencia', 'Sevilla', 'Bilbao']
Más precisamente, para curiosos
  • students.sort(key=by_grade, reverse=True) ordena la propia lista, y sorted devuelve 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__. key es para los casos en que hay varios órdenes.
  • Una función de comparación como compare(a, b) de Java también es posible, con functools.cmp_to_key. Pero en Python key es 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)
Respuesta

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.

Salida
Productos: 3
Total: 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.

Salida
Solicitudes: 5
200: 3
401: 1
500: 1
Correctas: 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.

Salida
Apartamento 101: 3 habitaciones, propietario: Fernández
No 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.

Salida
María 6.0
Ana 5.5
Juan 5.5
Pedro 4.75

Más tareas

  • Configurador de computadoras. Una clase Computer con las clases anidadas CPU (marca, velocidad en GHz), RAM (GB) y Storage (GB) y los métodos set_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.Destination que 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 Contact de la tarea de la lección sobre diccionarios dentro de ContactNetwork. ¿Debería Contact conocer su red?
  • Cálculo mental, otra vez. Mueve Question de la tarea de la lección sobre listas dentro de MathGame.
  • 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

1. class Line está dentro de class Order. ¿Ve Line los atributos de un pedido concreto?
2. ¿Por qué show() de Order.Line se detiene con NameError: name 'currency' is not defined?
3. ¿Qué usa Python en lugar de una clase anónima con un solo método, como el descuento?
4. ¿Qué hace sorted(students, key=by_grade)?
5. ¿Cuál ordena a los alumnos por nota, de la más alta a la más baja?
6. grid = [[0] * 3] * 3 y después grid[0][0] = 1. ¿Qué hay en grid?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·