? Conoce Propiedades
¡Bienvenidos! En este artículo, aprenderá a trabajar con el @property
decorador en Python.
Aprenderás:
- Las ventajas de trabajar con propiedades en Python.
- Los conceptos básicos de las funciones de decorador: qué son y cómo se relacionan con @property.
- Cómo puede usar @property para definir captadores, definidores y eliminadores.
1️⃣ Ventajas de las propiedades en Python
Comencemos con un poco de contexto. ¿Por qué usarías propiedades en Python?
Las propiedades pueden considerarse la forma "Pythonic" de trabajar con atributos porque:
- La sintaxis utilizada para definir propiedades es muy concisa y legible.
- Puede acceder a los atributos de instancia exactamente como si fueran atributos públicos mientras utiliza la "magia" de los intermediarios (captadores y definidores) para validar nuevos valores y evitar acceder o modificar los datos directamente.
- Al usar @property, puede "reutilizar" el nombre de una propiedad para evitar crear nuevos nombres para los captadores, definidores y eliminadores.
Estas ventajas hacen que las propiedades sean una herramienta realmente increíble para ayudarlo a escribir código más conciso y legible. ?
2️⃣ Introducción a los decoradores
Una función decoradora es básicamente una función que agrega nueva funcionalidad a una función que se pasa como argumento. ¿Usar una función decoradora es como agregar chispas de chocolate a un helado ?. Nos permite agregar una nueva funcionalidad a una función existente sin modificarla.
En el siguiente ejemplo, puede ver cómo se ve una función decoradora típica en Python:
def decorator(f): def new_function(): print("Extra Functionality") f() return new_function @decorator def initial_function(): print("Initial Functionality") initial_function()
Analicemos estos elementos en detalle:
- Primero encontramos la función decoradora
def decorator(f)
(los sprinkles ✨) que toma una funciónf
como argumento.
def decorator(f): def new_function(): print("Extra Functionality") f() return new_function
- Esta función decorador tiene una función anidada,
new_function
. Observe cómof
se llama dentronew_function
para lograr la misma funcionalidad mientras agrega una nueva funcionalidad antes de la llamada a la función (también podríamos agregar una nueva funcionalidad después de la llamada a la función). - La función decoradora en sí misma devuelve la función anidada
new_function
. - Luego (abajo), encontramos la función que se decorará (¿el helado?)
initial_function
. Observe la sintaxis muy peculiar (@decorator
) sobre el encabezado de la función.
@decorator def initial_function(): print("Initial Functionality") initial_function()
Si ejecutamos el código, vemos esta salida:
Extra Functionality Initial Functionality
Observe cómo se ejecuta la función decoradora incluso si solo estamos llamando initial_function()
. ¿Esta es la magia de agregar @decorator ?.
?Nota: En general, escribiríamos @
, reemplazando el nombre de la función decoradora después del símbolo @.
Sé que puede estar preguntando: ¿cómo se relaciona esto con @property? @Property es un decorador integrado para la función property () en Python. Se utiliza para dar una funcionalidad "especial" a ciertos métodos para que actúen como captadores, definidores o eliminadores cuando definimos propiedades en una clase.
Ahora que está familiarizado con los decoradores, ¡veamos un escenario real del uso de @property!
? Escenario del mundo real: @property
Digamos que esta clase es parte de su programa. Está modelando una casa con una House
clase (por el momento, la clase solo tiene definido un atributo de instancia de precio ):
class House: def __init__(self, price): self.price = price
Este atributo de instancia es público porque su nombre no tiene un guión bajo al principio. Dado que el atributo es público actualmente, es muy probable que usted y otros desarrolladores de su equipo hayan accedido y modificado el atributo directamente en otras partes del programa utilizando la notación de puntos, como esta:
# Access value obj.price # Modify value obj.price = 40000
? Consejo: obj representa una variable que hace referencia a una instancia de House
.
Hasta ahora todo está funcionando muy bien, ¿verdad? Pero digamos que se le pide que haga que este atributo esté protegido (no público) y valide el nuevo valor antes de asignarlo . Específicamente, debe verificar si el valor es un valor flotante positivo. ¿Cómo lo harías tú? Veamos.
Cambiar su código
En este punto, si decide agregar getters y setters, probablemente usted y su equipo entrarán en pánico. Esto se debe a que cada línea de código que accede o modifica el valor del atributo tendrá que modificarse para llamar al getter o al setter, respectivamente. De lo contrario, el código se romperá ⚠️.
# Changed from obj.price obj.get_price() # Changed from obj.price = 40000 obj.set_price(40000)
Pero ... ¡Las propiedades vienen al rescate! Con @property
, usted y su equipo no necesitarán modificar ninguna de esas líneas porque podrán agregar getters y setters "detrás de escena" sin afectar la sintaxis que usó para acceder o modificar el atributo cuando era público.
Impresionante, ¿verdad?
? @property: Sintaxis y lógica
Si decide usarlo @property
, su clase se verá como el siguiente ejemplo:
class House: def __init__(self, price): self._price = price @property def price(self): return self._price @price.setter def price(self, new_price): if new_price > 0 and isinstance(new_price, float): self._price = new_price else: print("Please enter a valid price") @price.deleter def price(self): del self._price
Específicamente, puede definir tres métodos para una propiedad:
- Un captador : para acceder al valor del atributo.
- Un setter : para establecer el valor del atributo.
- Un eliminador : para eliminar el atributo de instancia.
El precio ahora está "protegido"
Please note that the price attribute is now considered "protected" because we added a leading underscore to its name in self._price
:
self._price = price
In Python, by convention, when you add a leading underscore to a name, you are telling other developers that it should not be accessed or modified directly outside of the class. It should only be accessed through intermediaries (getters and setters) if they are available.
? Getter
Here we have the getter method:
@property def price(self): return self._price
Notice the syntax:
@property
- Used to indicate that we are going to define a property. Notice how this immediately improves readability because we can clearly see the purpose of this method.def price(self)
- The header. Notice how the getter is named exactly like the property that we are defining: price. This is the name that we will use to access and modify the attribute outside of the class. The method only takes one formal parameter, self, which is a reference to the instance.return self._price
- This line is exactly what you would expect in a regular getter. The value of the protected attribute is returned.
Here is an example of the use of the getter method:
>>> house = House(50000.0) # Create instance >>> house.price # Access value 50000.0
Notice how we access the price attribute as if it were a public attribute. We are not changing the syntax at all, but we are actually using the getter as an intermediary to avoid accessing the data directly.
? Setter
Now we have the setter method:
@price.setter def price(self, new_price): if new_price > 0 and isinstance(new_price, float): self._price = new_price else: print("Please enter a valid price")
Notice the syntax:
@price.setter
- Used to indicate that this is the setter method for the price property. Notice that we are not using @property.setter, we are using @price.setter. The name of the property is included before .setter.def price(self, new_price):
- The header and the list of parameters. Notice how the name of the property is used as the name of the setter. We also have a second formal parameter (new_price), which is the new value that will be assigned to the price attribute (if it is valid).- Finally, we have the body of the setter where we validate the argument to check if it is a positive float and then, if the argument is valid, we update the value of the attribute. If the value is not valid, a descriptive message is printed. You can choose how to handle invalid values according the needs of your program.
This is an example of the use of the setter method with @property:
>>> house = House(50000.0) # Create instance >>> house.price = 45000.0 # Update value >>> house.price # Access value 45000.0
Notice how we are not changing the syntax, but now we are using an intermediary (the setter) to validate the argument before assigning it. The new value (45000.0) is passed as an argument to the setter :
house.price = 45000.0
If we try to assign an invalid value, we see the descriptive message. We can also check that the value was not updated:
>>> house = House(50000.0) >>> house.price = -50 Please enter a valid price >>> house.price 50000.0
? Tip: This proves that the setter method is working as an intermediary. It is being called "behind the scenes" when we try to update the value, so the descriptive message is displayed when the value is not valid.
? Deleter
Finally, we have the deleter method:
@price.deleter def price(self): del self._price
Notice the syntax:
@price.deleter
- Used to indicate that this is the deleter method for the price property. Notice that this line is very similar to @price.setter, but now we are defining the deleter method, so we write @price.deleter.def price(self):
- The header. This method only has one formal parameter defined, self.del self._price
- The body, where we delete the instance attribute.
? Tip: Notice that the name of the property is "reused" for all three methods.
This is an example of the use of the deleter method with @property:
# Create instance >>> house = House(50000.0) # The instance attribute exists >>> house.price 50000.0 # Delete the instance attribute >>> del house.price # The instance attribute doesn't exist >>> house.price Traceback (most recent call last): File "", line 1, in house.price File "", line 8, in price return self._price AttributeError: 'House' object has no attribute '_price'
The instance attribute was deleted successfully ?. When we try to access it again, an error is thrown because the attribute doesn't exist anymore.
? Some final Tips
You don't necessarily have to define all three methods for every property. You can define read-only properties by only including a getter method. You could also choose to define a getter and setter without a deleter.
If you think that an attribute should only be set when the instance is created or that it should only be modified internally within the class, you can omit the setter.
You can choose which methods to include depending on the context that you are working with.
? In Summary
- You can define properties with the @property syntax, which is more compact and readable.
- @property can be considered the "pythonic" way of defining getters, setters, and deleters.
- By defining properties, you can change the internal implementation of a class without affecting the program, so you can add getters, setters, and deleters that act as intermediaries "behind the scenes" to avoid accessing or modifying the data directly.
I really hope you liked my article and found it helpful. To learn more about Properties and Object Oriented Programming in Python, check out my online course, which includes 6+ hours of video lectures, coding exercises, and mini projects.