Introducción
Al escribir clases, es común tratar de encapsular los detalles internos. Esta sección presenta algunos modos de programación en Python para ello, incluyendo variables y propiedades privadas.
This tutorial is from open-source community. Access the source code
💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí
Al escribir clases, es común tratar de encapsular los detalles internos. Esta sección presenta algunos modos de programación en Python para ello, incluyendo variables y propiedades privadas.
Uno de los principales roles de una clase es encapsular los datos y los detalles de implementación internos de un objeto. Sin embargo, una clase también define una interfaz pública que el mundo exterior debe utilizar para manipular el objeto. Esta distinción entre los detalles de implementación y la interfaz pública es importante.
En Python, casi todo lo relativo a clases y objetos es abierto.
Ese es un problema cuando intentas aislar los detalles de la implementación interna.
Python se basa en convenciones de programación para indicar el uso previsto de algo. Estas convenciones se basan en la nomenclatura. Existe una actitud general de que es responsabilidad del programador observar las reglas en lugar de que el lenguaje las imponga.
Cualquier nombre de atributo que comience con _
se considera privado.
class Person(object):
def __init__(self, name):
self._name = 0
Como se mencionó anteriormente, esto es solo un estilo de programación. Aún puedes acceder y cambiarlo.
>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>
Como regla general, cualquier nombre que comience con _
se considera parte de la implementación interna, ya sea que se trate de una variable, una función o un nombre de módulo. Si te encuentras usando directamente tales nombres, probablemente estás haciendo algo mal. Busca una funcionalidad de mayor nivel.
Considera la siguiente clase.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Una característica sorprendente es que puedes establecer los atributos en cualquier valor:
>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>
Podrías ver eso y pensar que quieres algunas comprobaciones adicionales.
s.shares = '50' ## Genera un TypeError, esto es una cadena
¿Cómo lo harías?
Un enfoque: introducir métodos accesores.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.set_shares(shares)
self.price = price
## Función que implementa la operación "get"
def get_shares(self):
return self._shares
## Función que implementa la operación "set"
def set_shares(self, value):
if not isinstance(value, int):
raise TypeError('Se esperaba un int')
self._shares = value
Qué lástima que esto rompa todo nuestro código existente. s.shares = 50
se convierte en s.set_shares(50)
Existe un enfoque alternativo al patrón anterior.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Se esperaba un int')
self._shares = value
El acceso a atributos normales ahora activa los métodos getter y setter debajo de @property
y @shares.setter
.
>>> s = Stock('IBM', 50, 91.1)
>>> s.shares ## Activa @property
50
>>> s.shares = 75 ## Activa @shares.setter
>>>
Con este patrón, no es necesario hacer ningún cambio en el código fuente. El nuevo setter también se llama cuando hay una asignación dentro de la clase, incluyendo dentro del método __init__()
.
class Stock:
def __init__(self, name, shares, price):
...
## Esta asignación llama al setter debajo
self.shares = shares
...
...
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Se esperaba un int')
self._shares = value
A menudo hay confusión entre una propiedad y el uso de nombres privados. Aunque una propiedad utiliza internamente un nombre privado como _shares
, el resto de la clase (no la propiedad) puede continuar utilizando un nombre como shares
.
Las propiedades también son útiles para atributos de datos calculados.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def cost(self):
return self.shares * self.price
...
Esto te permite omitir los paréntesis adicionales, ocultando el hecho de que en realidad es un método:
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## Variable de instancia
100
>>> s.cost ## Valor calculado
49010.0
>>>
El último ejemplo muestra cómo poner una interfaz más uniforme en un objeto. Si no haces esto, un objeto puede resultar confuso de usar:
>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## Método
49010.0
>>> b = s.shares ## Atributo de datos
100
>>>
¿Por qué se requiere ()
para el costo, pero no para las acciones? Una propiedad puede solucionar esto.
La sintaxis @
se conoce como "decoración". Especifica un modificador que se aplica a la definición de función que sigue inmediatamente.
...
@property
def cost(self):
return self.shares * self.price
Se dan más detalles en la Sección 7.
__slots__
Puedes restringir el conjunto de nombres de atributos.
class Stock:
__slots__ = ('name','_shares','price')
def __init__(self, name, shares, price):
self.name = name
...
Generará un error para otros atributos.
>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in?
AttributeError: 'Stock' object has no attribute 'prices'
Aunque esto evita errores y restringe el uso de objetos, en realidad se utiliza para mejorar el rendimiento y hacer que Python utilice la memoria de manera más eficiente.
No te vayas al extremo con atributos privados, propiedades, ranuras, etc. Tienen un propósito específico y los puedes ver al leer otros códigos de Python. Sin embargo, no son necesarios para la mayoría de la codificación cotidiana.
Las propiedades son una forma útil de agregar "atributos calculados" a un objeto. En stock.py
, creaste un objeto Stock
. Observa que en tu objeto hay una ligera inconsistencia en cómo se extraen diferentes tipos de datos:
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>
En particular, observa cómo tienes que agregar los paréntesis extra a cost
porque es un método.
Puedes eliminar los paréntesis extra en cost()
si lo conviertes en una propiedad. Toma tu clase Stock
y modifíquela de modo que el cálculo del costo funcione así:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>
Intenta llamar a s.cost()
como una función y observa que ya no funciona ahora que cost
se ha definido como una propiedad.
>>> s.cost()
... falla...
>>>
Hacer este cambio probablemente romperá tu programa pcost.py
anterior. Es posible que tengas que volver y eliminar los paréntesis del método cost()
.
Modifica el atributo shares
de modo que el valor se almacene en un atributo privado y que se utilicen un par de funciones de propiedad para garantizar que siempre se establezca en un valor entero. Aquí hay un ejemplo del comportamiento esperado:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: expected an integer
>>>
Modifica la clase Stock
de modo que tenga un atributo __slots__
. Luego, verifica que no se puedan agregar nuevos atributos:
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... ver lo que pasa...
>>>
Cuando se utiliza __slots__
, Python utiliza una representación interna más eficiente de los objetos. ¿Qué pasa si intentas inspeccionar el diccionario subyacente de s
anterior?
>>> s.__dict__
... ver lo que pasa...
>>>
Es importante destacar que __slots__
se utiliza con más frecuencia como una optimización en clases que sirven como estructuras de datos. Usar ranuras hará que estos programas utilicen mucha menos memoria y ejecuten un poco más rápido. Sin embargo, probablemente debas evitar __slots__
en la mayoría de las otras clases.
¡Felicitaciones! Has completado el laboratorio de Clases y Encapsulación. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.