Python @staticmethod y @classmethod
Aprende cómo funcionan los decoradores @staticmethod y @classmethod de Python, cuándo usar cada uno y cómo escribir métodos de fábrica y funciones auxiliares.
Python asigna a cada método dentro de una clase uno de tres estilos de vinculación: puede estar vinculado a una instancia, a la clase en sí, o a ninguna. Los decoradores @classmethod y @staticmethod controlan estos dos últimos estilos.
Este capítulo cubre:
- Los tres tipos de métodos y lo que los diferencia
@staticmethod— una función normal almacenada dentro de una clase@classmethod— un método que recibe la clase como primer argumento- Métodos de fábrica: el uso más común en el mundo real de
@classmethod - Constructores alternativos y cómo interactúan con la herencia
- Cuándo elegir
@staticmethodvs@classmethodvs una función a nivel de módulo - Errores comunes
Antes de leer, asegúrate de estar familiarizado con las clases y objetos de Python y la herencia en Python. Para el acceso a atributos calculados, consulta @property. Para un análisis profundo de cómo funcionan los decoradores en general, consulta Decoradores de Python.
Los Tres Tipos de Métodos
Antes de examinar cada decorador, aquí tienes una comparación lado a lado:
| Método de instancia | @classmethod | @staticmethod | |
|---|---|---|---|
| Primer parámetro | self (la instancia) | cls (la clase) | ninguno |
| ¿Recibe la instancia? | Sí | No | No |
| ¿Recibe la clase? | Vía type(self) | Sí (directamente) | No |
| Se llama sobre una instancia | Sí | Sí | Sí |
| Se llama sobre la clase | Sí (pero self falta) | Sí | Sí |
| Uso típico | Operar sobre datos de instancia | Métodos de fábrica, estado a nivel de clase | Funciones de utilidad/auxiliares |
class Demo:
def instance_method(self):
return f"instance method — self is {self}"
@classmethod
def class_method(cls):
return f"class method — cls is {cls}"
@staticmethod
def static_method():
return "static method — no self, no cls"
d = Demo()
print(d.instance_method()) # instance method — self is <__main__.Demo object at 0x...>
print(d.class_method()) # class method — cls is <class '__main__.Demo'>
print(d.static_method()) # static method — no self, no cls
# All three can also be called directly on the class:
print(Demo.class_method()) # class method — cls is <class '__main__.Demo'>
print(Demo.static_method()) # static method — no self, no cls@staticmethod
Un método estático es el más simple de los tres. Es simplemente una función normal que reside dentro del espacio de nombres de una clase. Python no pasa self ni cls automáticamente.
class MathUtils:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def is_even(n):
return n % 2 == 0
print(MathUtils.add(3, 4)) # 7
print(MathUtils.is_even(10)) # TrueCuándo usar @staticmethod
Usa @staticmethod cuando un auxiliar pertenece lógicamente a una clase — por claridad, agrupación o espacio de nombres — pero no necesita leer ni modificar el estado de la instancia ni el de la clase:
- Auxiliares de validación llamados antes de construir un objeto.
- Funciones puras de conversión o cálculo que solo tienen sentido en el contexto de una clase.
- Funciones de utilidad usadas por varios métodos de la misma clase pero en ningún otro lugar.
class Temperature:
def __init__(self, celsius):
if not Temperature._is_valid(celsius):
raise ValueError(f"Temperature {celsius} °C is below absolute zero")
self.celsius = celsius
@staticmethod
def _is_valid(celsius):
return celsius >= -273.15
@staticmethod
def celsius_to_fahrenheit(celsius):
return celsius * 9 / 5 + 32
t = Temperature(100)
print(Temperature.celsius_to_fahrenheit(100)) # 212.0
print(Temperature._is_valid(-300)) # FalseObserva que _is_valid tiene el prefijo _ para indicar que es interno a la clase. Los llamadores que solo necesitan objetos Temperature nunca lo ven — simplemente obtienen un ValueError si pasan un valor imposible.
@staticmethod vs una función a nivel de módulo
Una función a nivel de módulo y un @staticmethod son casi idénticos en comportamiento. La diferencia es dónde vive la función:
- Si la función solo es relevante para
Temperature(o se llama exclusivamente desde dentro deTemperature), ponla dentro de la clase como@staticmethod. - Si es una utilidad general usada en todo tu módulo, ponla a nivel de módulo.
No hay diferencia de rendimiento. Esta es puramente una decisión organizativa.
@classmethod
Un método de clase recibe la clase como primer argumento (llamado cls por convención — pero al igual que self, el nombre es una convención, no una palabra clave). Como tiene una referencia a la clase, puede:
- Leer o modificar atributos a nivel de clase.
- Crear y devolver nuevas instancias de la clase (métodos de fábrica).
- Funcionar correctamente con subclases (fábricas polimórficas).
class Counter:
_count = 0 # class-level attribute
def __init__(self):
Counter._count += 1
@classmethod
def get_count(cls):
return cls._count
@classmethod
def reset(cls):
cls._count = 0
Counter()
Counter()
Counter()
print(Counter.get_count()) # 3
Counter.reset()
print(Counter.get_count()) # 0Métodos de fábrica — el caso de uso más importante
El uso más común y valioso de @classmethod es como método de fábrica (también llamado constructor alternativo). Un método de fábrica crea instancias a partir de diferentes tipos de entrada sin saturar __init__ con lógica condicional.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
"""Create a Date from an ISO 8601 string, e.g. '2024-03-15'."""
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day)
@classmethod
def from_tuple(cls, date_tuple):
"""Create a Date from a (year, month, day) tuple."""
return cls(*date_tuple)
d1 = Date(2024, 3, 15)
d2 = Date.from_string("2024-03-15")
d3 = Date.from_tuple((2024, 3, 15))
print(d1) # Date(2024, 3, 15)
print(d2) # Date(2024, 3, 15)
print(d3) # Date(2024, 3, 15)__init__ permanece simple — solo almacena tres enteros. Los métodos de clase manejan la lógica de conversión. Esto es más limpio que un único __init__ con múltiples parámetros opcionales y ramas if/elif.
Por qué cls importa para la herencia
Cuando un método de clase de fábrica llama a cls(...) en lugar de codificar el nombre de la clase, crea una instancia de cualquier clase sobre la que se llamó el método — incluso una subclase. Por eso siempre debes preferir cls(...) sobre ClassName(...) dentro de un @classmethod.
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"{type(self).__name__}({self.year}, {self.month}, {self.day})"
@classmethod
def from_string(cls, date_string):
year, month, day = (int(p) for p in date_string.split("-"))
return cls(year, month, day) # uses cls, not Date
class DateTime(Date):
pass # inherits from_string
dt = DateTime.from_string("2024-03-15")
print(dt) # DateTime(2024, 3, 15) — correct subclass
print(type(dt)) # <class '__main__.DateTime'>Si from_string hubiera codificado return Date(year, month, day), llamar a DateTime.from_string(...) devolvería un Date, no un DateTime — rompiendo silenciosamente el contrato de herencia.
Modificar el estado a nivel de clase
Los métodos de clase también pueden actuar como constructores con nombre y efectos secundarios, o pueden manipular variables de clase que rastrean el estado compartido:
class Registry:
_instances = []
def __init__(self, name):
self.name = name
Registry._instances.append(self)
@classmethod
def all(cls):
return list(cls._instances)
@classmethod
def clear(cls):
cls._instances.clear()
Registry("alice")
Registry("bob")
Registry("carol")
print([r.name for r in Registry.all()]) # ['alice', 'bob', 'carol']
Registry.clear()
print(Registry.all()) # []Llamar desde una Instancia vs la Clase
Tanto @staticmethod como @classmethod pueden llamarse sobre una instancia o sobre la clase. Python maneja cualquiera de las dos formas:
class Circle:
PI = 3.14159265
def __init__(self, radius):
self.radius = radius
def area(self):
return Circle.PI * self.radius ** 2
@classmethod
def unit_circle(cls):
"""Return a circle with radius 1."""
return cls(1)
@staticmethod
def describe():
return "A circle is a round plane figure."
c = Circle(5)
# staticmethod — callable on instance or class
print(c.describe()) # A circle is a round plane figure.
print(Circle.describe()) # A circle is a round plane figure.
# classmethod — callable on instance or class
unit = c.unit_circle()
print(unit.radius) # 1
print(Circle.unit_circle().radius) # 1Llamarlos sobre la clase suele ser más claro — indica al lector que no hay datos de instancia involucrados.
Combinar @classmethod y @staticmethod
Un método de clase puede delegar el trabajo de validación a un método estático, porque el método de clase tiene acceso a cls para llamarlo:
class PositiveNumber:
def __init__(self, value):
self.value = value
def __repr__(self):
return f"PositiveNumber({self.value})"
@staticmethod
def _validate(value):
if value <= 0:
raise ValueError(f"Expected a positive number, got {value!r}")
@classmethod
def create(cls, value):
cls._validate(value)
return cls(value)
n = PositiveNumber.create(42)
print(n) # PositiveNumber(42)
try:
PositiveNumber.create(-5)
except ValueError as e:
print(e) # Expected a positive number, got -5Referencia Rápida: ¿Qué Decorador Debo Usar?
| Situación | Recomendación |
|---|---|
El método lee o escribe self | Método de instancia normal |
| El método crea una nueva instancia | @classmethod (fábrica / constructor alternativo) |
| El método lee o escribe un atributo de clase | @classmethod |
| El método es un auxiliar puro que no necesita datos de clase o instancia | @staticmethod (o función a nivel de módulo) |
| El método valida la entrada antes de la construcción | @staticmethod |
| El método debe funcionar correctamente en subclases | @classmethod (usa cls, no el nombre de clase codificado) |
Errores Comunes
Olvidar cls dentro de @classmethod
Si codificas el nombre de la clase en lugar de usar cls, la herencia se rompe silenciosamente:
class Animal:
@classmethod
def create(cls):
return cls() # correct — returns an instance of the actual class
class Dog(Animal):
pass
print(type(Dog.create())) # <class '__main__.Dog'> — correctSiempre usa cls(...), nunca Animal(...), dentro de un método de clase.
Acceder a self o cls en un @staticmethod
Un @staticmethod no recibe ningún primer argumento implícito. Intentar referenciar self o cls dentro de él es un error:
class Bad:
label = "bad"
@staticmethod
def show():
# print(cls.label) # NameError: name 'cls' is not defined
print("use @classmethod if you need cls")
Bad.show() # use @classmethod if you need clsSi te encuentras necesitando cls en lo que pensabas que era un método estático, cámbialo a un @classmethod.
Confundir los decoradores
Los métodos @classmethod deben tener cls como primer parámetro explícito, y los métodos @staticmethod deben tener ninguno. Intercambiarlos provoca un TypeError en el momento de la llamada, no en el momento de la definición — lo que puede ser sorprendente:
class Broken:
@staticmethod
def forgot_cls(cls): # cls is just a regular positional argument here
return cls
# Broken.forgot_cls() # TypeError: forgot_cls() missing 1 required positional argument: 'cls'Sobreescribir en subclases
Ambos decoradores funcionan con super() y pueden ser sobreescritos:
class Base:
@classmethod
def who(cls):
return f"Base.who called with cls={cls.__name__}"
class Child(Base):
@classmethod
def who(cls):
parent = super().who()
return f"Child.who — parent said: {parent}"
print(Child.who())
# Child.who — parent said: Base.who called with cls=ChildObserva que cls en Base.who sigue siendo Child — porque el método fue despachado desde Child.
Ejemplo del Mundo Real: Una Clase User
Aquí tienes un ejemplo completo que reúne métodos de instancia, un método de clase de fábrica y un validador de método estático:
import re
class User:
_all_users = []
def __init__(self, name, email):
User._validate_email(email)
self.name = name
self.email = email
User._all_users.append(self)
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r})"
# --- instance method ---
def greet(self):
return f"Hello, my name is {self.name}."
# --- factory / alternative constructor ---
@classmethod
def from_dict(cls, data):
"""Create a User from a dict like {'name': 'Alice', 'email': '[email protected]'}."""
return cls(data["name"], data["email"])
# --- class-level query ---
@classmethod
def count(cls):
return len(cls._all_users)
# --- pure helper, no instance or class data needed ---
@staticmethod
def _validate_email(email):
pattern = r"^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, email):
raise ValueError(f"Invalid email address: {email!r}")
# Create via normal constructor
u1 = User("Alice", "[email protected]")
# Create via factory
u2 = User.from_dict({"name": "Bob", "email": "[email protected]"})
print(u1.greet()) # Hello, my name is Alice.
print(u2.greet()) # Hello, my name is Bob.
print(User.count()) # 2
try:
User("Carol", "not-an-email")
except ValueError as e:
print(e) # Invalid email address: 'not-an-email'Este patrón — __init__ para la construcción normal, @classmethod para constructores alternativos, @staticmethod para auxiliares — aparece en toda la biblioteca estándar de Python (véase datetime.date.today(), datetime.date.fromisoformat(), int.from_bytes()).
Resumen
- Un método de instancia recibe
selfy tiene acceso completo al estado del objeto. - Un
@classmethodrecibecls— la clase en sí — en lugar de una instancia. Úsalo para métodos de fábrica y todo lo que opere sobre el estado a nivel de clase. Siempre usacls(...)dentro de él para que las subclases funcionen correctamente. - Un
@staticmethodno recibe niselfnicls. Úsalo para lógica de utilidad pura que pertenece al espacio de nombres de la clase pero no necesita datos del objeto ni de la clase.
Para atributos calculados que parecen acceso a atributos normales, consulta @property. Para el mecanismo completo de decoradores que hace funcionar los tres, consulta Decoradores de Python.