Por otro lado, las propiedades permiten controlar el acceso a los atributos de instancia, proporcionando una interfaz para obtener, establecer o eliminar valores con lógica personalizada.
Osea que más allá de los clásicos atributos de clase, tenemos los atributos de instancia, que se definen al momento de realizar una instancia, es decir, crear un objeto a partir de una clase.
🔍 Atributos de clase vs. atributos de instancia
Recordemos que instancia es la creación de un objeto a partir de una clase.
Los atributos de clase se definen directamente en la clase y son compartidos por todas las instancias.
En cambio, los atributos de instancia se definen dentro del método __init__ y son únicos para cada objeto. Veamos una comparación de ambos tipos de atributos:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Atributos de clase y atributos de instancia - OOP
# pythones.net
class Empleado:
aumento = 1.05 # Atributo de clase
def __init__(self, nombre, salario):
self.nombre = nombre # Atributo de instancia
self.salario = salario # Atributo de instancia
En este ejemplo, aumento es un atributo de clase que se aplica a todos los empleados, mientras que nombre y salario son atributos de instancia específicos de cada empleado.
Por lo que al crear un empleado lo haríamos de la siguiente manera:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Atributos de clase y atributos de instancia - OOP
# pythones.net
class Empleado:
aumento = 1.05 # Atributo de clase
def __init__(self, nombre, salario):
self.nombre = nombre # Atributo de instancia
self.salario = salario # Atributo de instancia
#Instanciar:
Marcos = Empleado('Marcos', 120000)
Marta = Empleado('Marta', 15000)
#Imprimir atributos de instancia
print(Marcos.nombre, Marcos.salario)
print(Marta.nombre, Marta.salario)
#Imprimir atributos de clase
print(Marcos.aumento)
print(Marta.aumento)
Resultado:
Marcos 120000
Marta 15000
1.05
1.05
Al momento de la instancia definimos los atributos de instancia, es decir, aquellos que en la clase están especificados dentro del __init__ como requeridos para la creación o inicialización del objeto. Y finalmente tenemos en cada objeto el atributo “aumento” que es un atributo de clase, por lo que será común a todos los objetos creados a partir de esa clase. Hasta aquí supongo ya habrás comprendido muy bien la diferencia, pero vamos a repasarlas:
📊 Diferencias clave en resumen
- ✔️ Atributos de clase: compartidos entre todas las instancias.
- ✔️ Atributos de instancia: únicos por objeto, definidos en
__init__. - ✔️ Modificación desde instancia: crea un nuevo atributo de instancia que oculta el de clase.
- ✔️ Modificación desde clase: afecta a todas las instancias (si no han sobrescrito el valor).
Fíjate en el siguiente ejemplo que modificamos estos atributos de clase y de instancia en cada objeto, siguiendo el código anterior:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Atributos de clase y atributos de instancia - OOP
# pythones.net
class Empleado:
aumento = 1.05 # Atributo de clase
def __init__(self, nombre, salario):
self.nombre = nombre # Atributo de instancia
self.salario = salario # Atributo de instancia
#Instanciar:
Marcos = Empleado('Marcos', 120000)
Marta = Empleado('Marta', 15000)
Juan = Empleado('Juan', 180000)
#Imprimir atributos de instancia
print(Marcos.nombre, Marcos.salario)
print(Marta.nombre, Marta.salario)
#Imprimir atributos de clase
print(Marcos.aumento)
print(Marta.aumento)
#Modificacion de atributos
#De clase:
Juan.aumento = 2.00
print (Juan.aumento)
print (Marta.aumento)
#De instancia:
Marta.salario = 170000
print(Marta.salario)
Resultado:
Marcos 120000
Marta 15000
1.05
1.05
2.0
1.05
170000
👥 Cómo se accede a los atributos de clase
Podemos acceder a un atributo de clase tanto desde la clase como desde una instancia. Sin embargo, si se modifica desde una instancia, se creará un atributo de instancia con el mismo nombre, sin alterar el valor compartido.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Atributos de clase y atributos de instancia - OOP
# pythones.net
class Empleado:
aumento = 1.05 # Atributo de clase
def __init__(self, nombre, salario):
self.nombre = nombre # Atributo de instancia
self.salario = salario # Atributo de instancia
emp1 = Empleado("Ana", 50000)
emp2 = Empleado("Luis", 60000)
print(emp1.aumento) # Imprime 1.05
print(emp2.aumento) # Imprime 1.05
Empleado.aumento = 1.10 # Cambia el valor del atributo de clase
print(emp1.aumento) # Ahora imprime 1.10
print(emp2.aumento) # También imprime 1.10
Resultado:
1.05
1.05
1.1
1.1
Como puedes ver, al modificar el atributo directamente desde la clase, se refleja en todas las instancias que no hayan sobrescrito ese valor. Por lo que si deseáramos modificar una atributo para todas las clases lo haríamos directamente llamando a la clase como objeto y asignando el nuevo valor, el cual se verá reflejado en todos los objetos que hayan sido creados a partir de ella.
⚠️ ¡Cuidado al modificar desde una instancia!
emp1.aumento = 1.20 # Esto crea un nuevo atributo de instancia print(emp1.aumento) # Imprime 1.20 (el de instancia) print(emp2.aumento) # Sigue siendo 1.10 (el de clase)
Es importante entender esta diferencia para evitar errores al manejar datos compartidos entre objetos.
Que modifiques un atributo en un objeto no hará que cambie para los demás, a menos que modifiques el atributo desde la clase, creo que ha quedado bastante claro!.
🏷️ Usando @property para definir propiedades
En Python, podemos usar el decorador @property para crear métodos que se comportan como atributos. Esto nos permite controlar el acceso a los valores, aplicando lógica si es necesario, sin cambiar la forma en que los llamamos.
La función integrada property() nos permite interceptar la escritura, lectura y borrado de los atributos, además de incorporar documentación sobre los mismos.
Volvamos a nuestro ejemplo de empleados. Supongamos que queremos que el email del empleado se genere automáticamente a partir del nombre, pero que se actualice si cambiamos ese nombre. Usaremos @property para lograrlo.
class Empleado:
aumento = 1.05
def __init__(self, nombre, salario):
self.nombre = nombre
self.salario = salario
@property
def email(self):
return f"{self.nombre.lower()}@empresa.com"
Ahora podemos acceder al email como si fuera un atributo:
emp1 = Empleado("Carlos", 55000)
print(emp1.email) # Imprime carlos@empresa.com
Si luego cambiamos el nombre del empleado, el email reflejará ese cambio automáticamente:
emp1.nombre = "Roberto" print(emp1.email) # Imprime roberto@empresa.com
Gracias al decorador @property, podemos mantener la lógica encapsulada dentro de un método, pero usando una sintaxis limpia y natural.
🔧 ¿Qué son getter, setter y deleter en las propiedades?
Cuando usamos el decorador @property en una clase, podemos controlar cómo se accede, modifica o elimina un atributo. Esto se logra usando tres métodos especiales:
- Getter → Se encarga de interceptar la lectura del atributo. (get = obtener). Se define con
@property. - Setter → Se encarga de interceptar cuando se escribe. (set = definir o escribir). Se define con
@nombre.setter. - Deleter → Se encarga de interceptar cuando se borra. (delete = borrar). Se define con
@nombre.deleter. - doc → Recibe una cadena para documentar el atributo. (doc = documentación)
🔹 Ejemplo 1 de propiedades de clase en python:
class Empleado:
def __init__(self, nombre):
self.nombre = nombre
@property
def nombre(self):
return self.nombre
@nombre.setter
def nombre(self, valor):
self.nombre = valor.title() # Capitaliza el nombre
@nombre.deleter
def nombre(self):
print("Eliminando nombre...")
del self.nombre
Veamos cómo se comporta esta clase en la práctica:
emp = Empleado("ana")
print(emp.nombre) # Ana
emp.nombre = "luis" # Setter en acción
print(emp.nombre) # Luis
del emp.nombre # Se ejecuta el deleter
🔹 Ejemplo 2 de propiedades de clase en python:
Veamos ahora con otro ejemplo completo, con animalitos:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# pythones.net
class Perros:
def __init__(self, nombre, peso):
self.nombre = nombre
self.peso = peso
@property
def nombre(self):
"""Documentación del método nombre: Retorna el nombre del perro."""
return self.nombre
@nombre.setter
def nombre(self, nuevo):
print("Modificando nombre...")
self.nombre = nuevo
print("El nombre se ha modificado por")
print(self.nombre)
@nombre.deleter
def nombre(self):
print("Borrando nombre...")
del self.nombre
@property
def peso(self):
return self.peso
# Instanciamos
Tomas = Perros('Tom', 27)
print(Tomas.nombre) # Imprime Tom
Tomas.nombre = 'Tomasito' # Cambiamos el nombre usando setter
del Tomas.nombre # Borramos el nombre usando deleter
Resultado:
Tom
Modificando nombre…
El nombre se ha modificado por
Tomasito
Borrando nombre…
📌 Resumen rápido:
| Acción | Decorador | Uso |
|---|---|---|
| Leer | @property |
print(obj.propiedad) |
| Escribir | @propiedad.setter |
obj.propiedad = valor |
| Eliminar | @propiedad.deleter |
del obj.propiedad |
💡 Cuándo usar propiedades y atributos de clase en tus proyectos
Comprender la diferencia entre atributos de clase y de instancia no solo es útil, es crucial para evitar errores comunes al modelar datos. Por ejemplo, los atributos de clase son ideales para valores estándar, tasas fijas o configuraciones predeterminadas que se aplican globalmente, mientras que las propiedades son una excelente forma de encapsular lógica sin exponerla directamente al exterior.
Además, usar @property te da la flexibilidad de cambiar la implementación interna de una clase sin afectar el código que la utiliza, lo que es una gran ventaja en proyectos que evolucionan constantemente.
- 🔹 Las propiedades de clase se definen fuera del constructor y son compartidas entre todas las instancias.
- 🔹 Se pueden modificar directamente desde la clase o desde una instancia.
- 🔹 El decorador
@propertypermite crear métodos que se comportan como atributos, muy útil para definir valores derivados o controlados. - 🔹 Usar
@propertymejora la legibilidad y el mantenimiento del código.
