Clases y encapsulación

PythonPythonBeginner
Practicar Ahora

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í

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.

Público vs Privado

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.

Un problema

En Python, casi todo lo relativo a clases y objetos es abierto.

  • Puedes inspeccionar fácilmente los detalles internos de un objeto.
  • Puedes cambiar las cosas a voluntad.
  • No existe una noción fuerte de control de acceso (es decir, miembros de clase privados)

Ese es un problema cuando intentas aislar los detalles de la implementación interna.

Encapsulación en Python

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.

Atributos privados

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.

Atributos simples

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?

Atributos administrados

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)

Propiedades

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

Acceso uniforme

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.

Sintaxis del decorador

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.

Atributo __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.

Comentarios finales sobre la encapsulación

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.

Ejercicio 5.6: Propiedades simples

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().

✨ Revisar Solución y Practicar

Ejercicio 5.7: Propiedades y setters

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
>>>
✨ Revisar Solución y Practicar

Ejercicio 5.8: Agregando ranuras

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.

✨ Revisar Solución y Practicar

Resumen

¡Felicitaciones! Has completado el laboratorio de Clases y Encapsulación. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.