Ir al contenido
wannacode

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)
Salida
n.º 1 Pan
n.º 1 Leche
n.º 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 = 1

Un 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.

shop.py
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)
Salida
n.º 1 Pan
n.º 2 Leche
n.º 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
  1. _next_id es un atributo de clase: hay uno solo, no uno en cada objeto.
  2. Product("Pan"): un objeto nuevo. __init__ guarda el nombre.
  3. self._id = Product._next_id: el objeto toma el número actual, 1.
  4. Product._next_id += 1 cambia el contador compartido: ahora es 2.
  5. Leche: el mismo contador. Toma el 2 y lo deja en 3.
  6. Café: toma el 3 y lo deja en 4.
  7. 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)
Salida
n.º 1 Pan
n.º 1 Leche
1

Por 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)
Respuesta

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))
Salida
30
5.25
8

Calculator.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())
Salida
2

@staticmethod no recibe nada, @classmethod recibe la clase, y un método normal recibe el objeto (self).

Pruébalo

Encuentra el error

tea.label() detiene el programa. ¿Qué línea tiene el error?

Elige la línea con el error.

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}%")
Salida
60.0
IVA: 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.25

nada la protege, y Prices.VAT_RATE ahora es:

Salida
0.25

Lo 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.25
print(Prices.VAT_RATE)
Salida
0.25

A 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._id

coffee.id lee el número, y coffee.id = 5 se detiene con un AttributeError, porque la propiedad no tiene setter.

Pruébalo

¿Qué falta?

Un año tiene 12 meses, y eso no va a cambiar. ¿Qué nombre va en el hueco?
class Calendar:
     = 12

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 con self.
  • No (el número de todas las cuentas, el IVA, la suma de dos números): un atributo de clase, @staticmethod o @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)
Salida
['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.

El programa
orders = [
Order("María", 45.5),
Order("Pablo", 120),
Order("Elena", 18.9),
]
for o in orders:
print(o)
print(f"Pedidos: {Order.count()}")
Salida
Pedido n.º 1: María, 45.5 €
Pedido n.º 2: Pablo, 120 €
Pedido n.º 3: Elena, 18.9 €
Pedidos: 3

2. Calculadora. Una clase Calculator con métodos con @staticmethod para sumar, restar y multiplicar números enteros, y para dividir números decimales.

El programa
print(Calculator.add(10, 20))
print(Calculator.subtract(10, 15))
print(Calculator.multiply(6, 7))
print(Calculator.divide(5, 2))
Salida
30
-5
42
2.5

3. 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 €:

Salida
20 EUR = 21.6 USD
20 EUR = 17.0 GBP

Más tareas

  • Cuentas numeradas. Añade a la Account de 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 Converter mé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. self es 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
  • _nombre dice «interno», y @property con un setter deja pasar los cambios solo después de una comprobación. Encapsulamiento
  • Los atributos de clase, @staticmethod y @classmethod son 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

1. Creas 5 objetos de una clase con el atributo de clase count = 0. ¿Cuántas copias de count hay en la memoria?
2. ¿Qué NO puede hacer un @staticmethod?
3. ¿Qué recibe un @classmethod como primer parámetro?
4. La clase tiene _next_id = 1. ¿Qué hace self._next_id += 1 en __init__?
5. ¿Qué pasa con Prices.VAT_RATE = 0.25?
6. ¿Qué es un buen candidato para un @staticmethod?

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