Lo que comparten todos los objetos: atributos de clase
En resumen: Un atributo de clase pertenece a la clase, no al objeto: todos los objetos ven un único valor compartido, y @staticmethod y @classmethod son métodos sin objeto.
Problema
Todos los productos son el n.º 1
La tienda quiere que cada producto nuevo reciba el número siguiente: 1, 2, 3… Pones un contador _next_id en el producto. __init__ le da al producto el número actual y aumenta el contador:
class Product: def __init__(self, name): self._next_id = 1 self._name = name self._id = self._next_id self._next_id += 1
def __str__(self): return f"n.º {self._id} {self._name}"
bread = Product("Pan")milk = Product("Leche")coffee = Product("Café")print(bread)print(milk)print(coffee)n.º 1 Pann.º 1 Lechen.º 1 CaféTodos son el n.º 1. La razón es que _next_id es un atributo del objeto, igual que el nombre. Cada producto nuevo recibe su propio contador, que empieza en 1. El contador del pan nunca llega a la leche.
La idea
Un atributo de la clase, no del objeto
El contador no es parte de un producto. Lo comparten todos los productos. Escríbelo en la propia clase, fuera de __init__:
class Product: _next_id = 1Un atributo escrito en la clase es un atributo de clase. Hay uno solo, sin importar cuántos objetos crees, y todos los objetos lo comparten. En __init__ llegas a él a través del nombre de la clase: Product._next_id.
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
def __str__(self): return f"n.º {self._id} {self._name}"
bread = Product("Pan")milk = Product("Leche")coffee = Product("Café")print(bread)print(milk)print(coffee)n.º 1 Pann.º 2 Lechen.º 3 CaféEl pan toma el 1 y deja el contador en 2. La leche ve el mismo contador, toma el 2 y lo deja en 3.
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
bread = Product("Pan")milk = Product("Leche")coffee = Product("Café")Paso a paso, en palabras
- _next_id es un atributo de clase: hay uno solo, no uno en cada objeto.
- Product("Pan"): un objeto nuevo. __init__ guarda el nombre.
- self._id = Product._next_id: el objeto toma el número actual, 1.
- Product._next_id += 1 cambia el contador compartido: ahora es 2.
- Leche: el mismo contador. Toma el 2 y lo deja en 3.
- Café: toma el 3 y lo deja en 4.
- Hay un solo contador para todos, así que los números van en orden.
Más precisamente, para curiosos
Cuidado con self. Leer self._next_id encuentra el atributo de clase, pero escribir self._next_id += 1 crea un atributo nuevo en el objeto, y el contador compartido no se mueve:
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = self._next_id self._next_id += 1
def __str__(self): return f"n.º {self._id} {self._name}"
bread = Product("Pan")milk = Product("Leche")print(bread)print(milk)print(Product._next_id)n.º 1 Pann.º 1 Leche1Por eso el valor compartido solo lo cambias a través del nombre de la clase: Product._next_id += 1.
Pruébalo
Compartido y propio
¿Qué imprimirá el programa?
class Order: count = 0
def __init__(self): Order.count += 1 self.items = 0 self.items += 1
a = Order()b = Order()c = Order()print(Order.count, c.items)count es un atributo de clase: un contador para todos los pedidos, aumentado tres veces. items es un atributo del objeto: cada pedido tiene el suyo, aumentado una vez.La idea
@staticmethod: trabajar sin objeto
Lo mismo vale para los métodos. Un método con @staticmethod encima es un método de la clase. No tiene self, y lo llamas a través del nombre de la clase sin crear un objeto:
class Calculator: @staticmethod def add(a, b): return a + b
@staticmethod def average(values): total = 0 for v in values: total += v return total / len(values)
print(Calculator.add(10, 20))grades = [5, 6, 4, 6]print(Calculator.average(grades))print(max(3, 8))305.258Calculator.add no necesita un objeto: todo lo que necesita llega como parámetros. En Python, un trabajo así suele ser simplemente una función fuera de la clase, como max y math.sqrt. Lo pones en una clase cuando está muy ligado a la clase y lo quieres bajo el nombre de la clase.
Python no tiene un main en una clase desde el que arranque el programa: el programa son las líneas del archivo, de arriba abajo. Por eso las funciones del módulo «Funciones» no necesitaban una clase.
Más precisamente, para curiosos
También existe @classmethod. Recibe la propia clase, cls, como primer parámetro, y llega a los atributos de clase sin objeto:
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
@classmethod def created(cls): return cls._next_id - 1
bread = Product("Pan")milk = Product("Leche")print(Product.created())2@staticmethod no recibe nada, @classmethod recibe la clase, y un método normal recibe el objeto (self).
Pruébalo
Encuentra el error
Elige la línea con el error.
label() lee self._name, y el nombre pertenece a un producto concreto. Un @staticmethod no recibe self, así que no sabe de quién tomar el nombre:NameError: name 'self' is not defined@staticmethod y añade self como parámetro: la etiqueta pertenece al producto, así que es un método de instancia.La idea
Constantes
El IVA es del 20 % para todos los productos, y no cambia mientras el programa se ejecuta. Un valor así es una constante:
class Prices: VAT_RATE = 0.20
@staticmethod def with_vat(price): return price * (1 + Prices.VAT_RATE)
print(Prices.with_vat(50))print(f"IVA: {Prices.VAT_RATE * 100}%")60.0IVA: 20.0%Python no tiene constantes. Hay una convención: un nombre en mayúsculas con guiones bajos, VAT_RATE, significa «esto no cambia». Escrito en la clase, es un atributo de clase.
Si alguien intenta cambiarla,
Prices.VAT_RATE = 0.25nada la protege, y Prices.VAT_RATE ahora es:
0.25Lo que te da la convención es que el número 0.20 está escrito en un solo lugar con un nombre claro. Las mayúsculas le dicen a todo el que lee el código «no tocar», pero proteger el valor depende de ti.
Más precisamente, para curiosos
Final del módulo typing parece una constante, pero Python no lo comprueba. Solo el editor o una herramienta como mypy te avisan:
from typing import Final
class Prices: VAT_RATE: Final = 0.20
Prices.VAT_RATE = 0.25print(Prices.VAT_RATE)0.25A veces el valor es distinto para cada objeto, pero no debe cambiar después de crear el objeto. El número del producto es así. Hazlo una propiedad de solo lectura, @property sin setter (la lección «Encapsulamiento»):
class Product: _next_id = 1
def __init__(self, name): self._name = name self._id = Product._next_id Product._next_id += 1
@property def id(self): return self._idcoffee.id lee el número, y coffee.id = 5 se detiene con un AttributeError, porque la propiedad no tiene setter.
Pruébalo
¿Qué falta?
class Calendar:
= 12MONTHS: las mayúsculas dicen que es una constante. Python no la protege, pero todo el que lee el código sabe que no debe tocarla.La idea
Cuándo en la clase y cuándo en el objeto
Pregúntate: «¿Esto pertenece a un objeto concreto?»
- Sí (el saldo de la cuenta, el nombre del producto, retirar de una cuenta): un atributo del objeto en
__init__, o un método conself. - No (el número de todas las cuentas, el IVA, la suma de dos números): un atributo de clase,
@staticmethodo@classmethod.
Un error frecuente es una lista como atributo de clase. Hay una sola lista para todos los objetos, así que lo que pones en un carrito aparece en el otro:
class Cart: items = []
def add(self, name): self.items.append(name)
first = Cart()second = Cart()first.add("Pan")print(second.items)['Pan']La lista pertenece a cada carrito, así que su lugar está en __init__: self.items = []. Si no, todos los «objetos» comparten los mismos datos, y vuelves al problema de las listas paralelas de la primera lección.
Ejercicio
Lo que comparten todos los objetos
Resuelve las tareas en tu editor.
1. Pedidos. Una clase Order con un cliente y un importe. Cada pedido nuevo recibe el número siguiente de un contador que es un atributo de clase. Un método count() con @classmethod devuelve cuántos pedidos se han creado.
orders = [ Order("María", 45.5), Order("Pablo", 120), Order("Elena", 18.9),]for o in orders: print(o)print(f"Pedidos: {Order.count()}")Pedido n.º 1: María, 45.5 €Pedido n.º 2: Pablo, 120 €Pedido n.º 3: Elena, 18.9 €Pedidos: 32. Calculadora. Una clase Calculator con métodos con @staticmethod para sumar, restar y multiplicar números enteros, y para dividir números decimales.
print(Calculator.add(10, 20))print(Calculator.subtract(10, 15))print(Calculator.multiply(6, 7))print(Calculator.divide(5, 2))30-5422.53. Cambio de divisas. Una clase Converter con constantes para el tipo de cambio: 1 € = 1,08 dólares estadounidenses y 1 € = 0,85 libras esterlinas (los tipos son de ejemplo). Métodos con @staticmethod convierten un importe en euros a dólares y a libras. Para 20 €:
20 EUR = 21.6 USD20 EUR = 17.0 GBPMás tareas
- Cuentas numeradas. Añade a la
Accountde la lección «Encapsulamiento» un número de cuenta que sale de un contador que es un atributo de clase, y una propiedad de solo lectura para el número. - Cambio en los dos sentidos. Añade a
Convertermétodos que convierten de dólares y de libras a euros. Convierte 20 € a dólares y de vuelta a euros. ¿Obtienes exactamente 20? - Un límite. La tienda acepta como mucho 3 productos en un carrito. Añade una constante para eso, y haz que el método de añadir rechace el cuarto.
En resumen
El módulo en resumen
- Una clase es tu propio tipo, y un objeto es una instancia concreta de ella, creada con
Product(). La variable apunta al objeto. Clases y objetos __init__se asegura de que el objeto se cree completo.selfes el objeto actual. Constructores y self- Los métodos de instancia trabajan con los atributos del objeto.
__str__dice cómo se ve como texto. Métodos de instancia _nombredice «interno», y@propertycon un setter deja pasar los cambios solo después de una comprobación. Encapsulamiento- Los atributos de clase,
@staticmethody@classmethodson lo que comparte la clase: contadores, métodos auxiliares y constantes.
Los datos y sus reglas en un solo lugar, con una marca clara de lo que es interno: eso es la programación orientada a objetos. En el siguiente módulo, las clases empezarán a compartir código entre sí.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Clases y objetos · Constructores y self · Métodos de instancia · Encapsulamiento
- Continúa en: Principios de la POO · Lanzar excepciones y crear las tuyas · Expresiones regulares: grupos y reemplazo · Datos en archivos
- Ejercicios sobre el tema: Clases y objetos