5. Funcionamiento interno del objeto Python

En esta sección aprenderemos sobre el funcionamiento interno de los objetos en Python. Algunos programadores con experiencia en otros lenguajes de programación a menudo encuentran que la noción de clases en Python carece de ciertas características. Por ejemplo, no existe la noción de control de accesso (e.g. privado, protegido), el argumento self lo encuentran extraño, y trabajar con objetos se siente como una actividad con mucho libertinaje. Conoceremos como todo funciona y algunos patrones comunes para la encapsulación interna de objetos.

5.1 Diccionarios, Otra Visita

El sistema de objetos de Python se basa en gran medida en una implementación que incluye diccionarios. Esta sección trata sobre eso.

5.1.1 Diccionarios

Recuerde que un diccionario es una colección de valores con nombre.

stock = {
    'name' : 'GOOG',
    'shares' : 100,
    'price' : 490.1
}

Los diccionarios se utilizan comúnmente para estructuras de datos simples. Sin embargo, se utilizan para partes críticas del interpretador y pueden ser el tipo de datos más importante en Python .

5.1.2 Dicts y Modulos

Dentro de un módulo, un diccionario contiene todas las variables y funciones globales.

# foo.py
x = 42
def bar():
    ...

def spam():
    ...

Si inspecciona foo.__dict__ o globals(), verá el diccionario.

{
    'x' : 42,
    'bar' : <function bar>,
    'spam' : <function spam>
}

5.1.3 Dicts y objetos

Los objetos definidos por el usuario también utilizan diccionarios para datos de instancia y clases. De hecho, todo el sistema de objetos es principalmente una capa adicional que se coloca encima de los diccionarios.

Un diccionario contiene los datos ejemplo, __dict__.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG', 'shares' : 100, 'price': 490.1 }

Popule este dict (e instancia) al asignar a self.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Los datos de la instancia que se encuentran en self.__dict__ tienen el siguiente aspecto:

{
    'name': 'GOOG',
    'shares': 100,
    'price': 490.1
}

Cada instancia tiene su propio diccionario privado.

s = Stock('GOOG', 100, 490.1)  # {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45)  # {'name' : 'AAPL','shares' : 50, 'price': 123.45 }

Si creó 100 instancias de alguna clase, hay 100 diccionarios que contienen datos.

5.1.4 Miembros de la clase

Un diccionario separado también contiene los métodos.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

El diccionario está en Stock.__dict__.

{
    'cost': <function>,
    'sell': <function>,
    '__init__': <function>
}

5.1.5 Instancias y clases

Las instancias y clases están vinculadas entre sí. El atributo __class__ hace referencia a la clase.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'> >>>
````

El diccionario de instancias contiene datos únicos para cada instancia, mientras que el diccionario de clase contiene datos compartidos colectivamente por todas las instancias.

### 5.1.6 Acceso a atributos

Cuando trabaja con objetos, acceda a datos y métodos utilizando el operador `.`.

```python
x = obj.name  # Conseguir
obj.name = value  # Definir
del obj.name  # Remover

Estas operaciones están directamente vinculadas al diccionario subyacente del objeto.

5.1.7 Modificar instancias

Las operaciones que modifican un objeto actualizan el diccionario subyacente.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG', 'shares': 100, 'price': 490.1 }
>>> s.shares = 50       # Setting >>> s.date = '6/7/2007' # Setting >>> s.__dict__
{ 'name': 'GOOG', 'shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares        # Deleting >>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

5.1.8 Atributos de lectura

Suponga que lee un atributo en una instancia.

x = obj.name

El atributo puede existir en dos lugares:

  • Diccionario de la instancia local.
  • Diccionario de la clase.

Se deben revisar ambos diccionarios. Primero, verifique en el __dict__ local. Si no se encuentra ahí, busque en el __dict__ de la clase, a través de __class__.

>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>

Este esquema de búsqueda es cómo los miembros de una clase son compartidos por todas las instancias.

5.1.9 Cómo funciona la herencia

Las clases pueden heredar de otras clases.

class A(B, C):
    ...

Las clases base se almacenan en una tupla en cada clase.

>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>

Esto proporciona un enlace a las clases padres.

5.1.10 Lectura de atributos con herencia

Lógicamente, el proceso de búsqueda de un atributo es el siguiente. Primero, verifique en local __dict__. Si no lo encuentra, busque en el __dict__ de la clase. Si no se encuentra en la clase, busque en las clases base a través de __bases__. Sin embargo, hay algunos aspectos sutiles de esto que se comentan a continuación.

5.1.11 Lectura de atributos con herencia única

En las jerarquías de herencia, los atributos se encuentran subiendo por el árbol de herencia en orden.

class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass

Con herencia única, hay un camino único a la cima.

5.1.12 Orden de resolución de métodos o MRO

Python calcula previamente una cadena de herencia y la almacena en el atributo MRO de la clase. Puedes verlo.

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
 <class '__main__.B'>, <class '__main__.A'>,
 <type 'object'>)
>>>

Esta cadena se denomina Orden de resolución de métodos . Para encontrar un atributo, Python recorre el MRO en orden.

5.1.13 MRO en herencia múltiple

Con la herencia múltiple, no hay un camino único hacia la cima.

Un ejemplo:

class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass

¿Qué sucede cuando accedes a un atributo?

e = E()
e.attr

Se realiza un proceso de búsqueda de atributos, pero ¿cuál es el orden? Eso es un problema.

Python usa herencia múltiple cooperativa que obedece a algunas reglas sobre el orden de clases.

  • Los niños siempre son controlados antes que los padres
  • Los padres (si son múltiples) siempre se marcan en el orden indicado.

El MRO se calcula ordenando todas las clases en una jerarquía de acuerdo con esas reglas.

>>> E.__mro__
(
    <class 'E'>,
    <class 'C'>,
    <class 'A'>,
    <class 'D'>,
    <class 'B'>,
    <class 'object'>)
>>>

El algoritmo subyacente se denomina "Algoritmo de linealización C3". Los detalles precisos no son importantes siempre que recuerde que una jerarquía de clases obedece las mismas reglas de ordenamiento que podría seguir si su casa se incendiara y tuviera que evacuar: los niños primero, seguidos de los padres.

5.1.14 Una reutilización de código extraño (que implica herencia múltiple)

Considere dos objetos completamente no relacionados:

class Dog:
    def noise(self):
        return 'Bark'

    def chase(self):
        return 'Chasing!'

class LoudDog(Dog):
    def noise(self):
        # Codigo comun a LoudDog (abajo)
        return super().noise().upper()

Y

class Bike:
    def noise(self):
        return 'On Your Left'

    def pedal(self):
        return 'Pedaling!'

class LoudBike(Bike):
    def noise(self):
        # Codigo comun a LoudDog (arriba)
        return super().noise().upper()

Hay un código común en la implementación de LoudDog.noise() y LoudBike.noise(). De hecho, el código es exactamente el mismo. Naturalmente, un código como ese seguramente atraerá a los ingenieros de software.

5.1.15 El patrón "Mixin"

El patrón Mixin es una clase con un fragmento de código.

class Loud:
    def noise(self):
        return super().noise().upper()

Esta clase no se puede utilizar de forma aislada. Se mezcla con otras clases por herencia.

class LoudDog(Loud, Dog):
    pass

class LoudBike(Loud, Bike):
    pass

Milagrosamente, el altavoz ahora se implementó solo una vez y se reutilizó en dos clases completamente no relacionadas. Este tipo de truco es uno de los usos principales de la herencia múltiple en Python.

5.1.16 Por qué super()

Use super() siempre que esta sobreescribiendo métodos.

class Loud:
    def noise(self):
        return super().noise().upper()

super() delegates to the next class on the MRO.

El truco es que no sabes qué es. En especial, no sabe qué es si se utiliza la herencia múltiple.

5.1.17 Algunas precauciones

La herencia múltiple es una herramienta poderosa. Recuerde que con el poder viene la responsabilidad. Los marcos de desarrollo / bibliotecas / librerias / modulos a veces lo usan para funciones avanzadas que involucran la composición de componentes. Ok, ahora puedes olvidarte de que lo leíste.

5.1.18 Ejercicios

En la Sección 4, definió una clase Stock que representaba una tenencia de acciones. En este ejercicio usaremos esa clase. Reinicie el intérprete y realice algunas instancias:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm  = Stock('IBM',50, 91.23)
>>>

Ejercicio 5.1: Representación de instancias

En el shell interactivo, inspeccione los diccionarios subyacentes de las dos instancias que creó:



>>> goog.__dict__
... mire la salida ...
>>> ibm.__dict__
... mire la salida ...
>>>

Ejercicio 5.2: Modificación de datos de instancia

Intente establecer un nuevo atributo en una de las instancias anteriores:

>>> goog.date = '6/11/2007'
>>> goog.__dict__
... mire la salida ...
>>> ibm.__dict__
... mire la salida ...
>>>

En el resultado anterior, notará que la instancia goog tiene un atributo, date, mientras que la instancia ibm no. Es importante tener en cuenta que Python realmente no impone restricciones a los atributos. Por ejemplo, los atributos de una instancia no se limitan a los configurados en el método __init__().

En lugar de establecer un atributo, intente colocar un nuevo valor directamente en el objeto __dict__:

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>

Aquí, realmente se nota el hecho de que una instancia es solo una capa sobre un diccionario. Nota: vale destacar que la manipulación directa del diccionario es poco común; siempre debe escribir su código para usar la sintaxis (.).

Ejercicio 5.3: El papel de las clases

Las definiciones que componen una definición de clase son compartidas por todas las instancias de esa clase. Tenga en cuenta que todas las instancias tienen un enlace a su clase asociada:

>>> goog.__class__
... mire la salida ...
>>> ibm.__class__
... mire la salida ...
>>>

Intente llamar a un método en las instancias:

>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>

Tenga en cuenta que el nombre de 'costo' no se define en cualquier goog.__dict__ o ibm.__dict__. En cambio, lo proporciona el diccionario de la clase. Prueba esto:

>>> Stock.__dict__['cost']
... mire la salida ...
>>>

Intente llamar al método cost() directamente a través del diccionario:

>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>

Observe cómo está llamando a la función definida en la definición de clase y cómo el argumento self obtiene la instancia.

Intente agregar un nuevo atributo a la clase Stock:

>>> Stock.foo = 42
>>>

Observe cómo este nuevo atributo ahora aparece en todas las instancias:

>>> goog.foo
42
>>> ibm.foo
42
>>>

Sin embargo, tenga en cuenta que no forma parte del diccionario de la instancia:

>>> goog.__dict__
... mire la salida y verifique que no hay atributo 'foo' ...
>>>

La razón por la que puede acceder al atributo foo en las instancias es que Python siempre verifica el diccionario de la clase si no puede encontrar algo en la propia instancia.

Nota: Esta parte del ejercicio ilustra algo conocido como variable de clase. Supongamos, por ejemplo, que tiene una clase como esta:

class Foo(object):
     a = 13                 # variable de Clase
     def __init__(self,b):
        self.b = b          # variable de Instancia

En esta clase, la variable a, asignada en el cuerpo de la propia clase, es una "variable de clase". Lo comparten todas las instancias que se crean. Por ejemplo:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a
13
>>> g.a
13
>>> f.b          # Inspeccione la variable de instancia (difiere)
10
>>> g.b
20
>>> Foo.a = 42   # Change the value of the class variable
>>> f.a
42
>>> g.a
42
>>>

Ejercicio 5.4: Métodos vinculados

Una característica sutil de Python es que la invocación de un método en realidad implica dos pasos y algo conocido como método vinculado. Por ejemplo:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>

Los métodos vinculados en realidad contienen todas las piezas necesarias para llamar a un método. Por ejemplo, mantienen un registro de la función que implementa el método:

>>> s.__func__
<function sell at 0x10049af50>
>>>

Este es el mismo valor que se encuentra en el diccionario Stock.

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

Los métodos enlazados también registran la instancia, que es el selfargumento.

>>> s.__self__
Stock('GOOG',75,490.1)
>>>

Cuando se invoca la función utilizando ()todas las piezas se unen. Por ejemplo, llamar s(25)realmente hace esto:

>>> s.__func__(s.__self__, 25)    # Same as s(25) >>> goog.shares
50
>>>

Ejercicio 5.5: Herencia

Crea una nueva clase que herede de Stocks.

>>> class NewStock(Stock):
...     def yow(self):
...         print('Yow!')
...
>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>

La herencia se implementa ampliando el proceso de búsqueda de atributos. El atributo __bases__ tiene una tupla de los padres inmediatos:

>>> NewStock.__bases__
(<class 'stock.Stock'>,)
>>>

El atributo __mro__ tiene una tupla de todos los padres, en el orden en que se buscarán los atributos.

>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class 'stock.Stock'>, <class 'object'>) >>>

Así es como se encontraría el método cost() de la instancia anterior:

>>> for cls in n.__class__.__mro__:
...     if 'cost' in cls.__dict__:
...         break
...
>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

5.2 Clases y encapsulación

Al escribir clases, es común intentar encapsular detalles internos. Esta sección presenta algunos modismos de programación de Python para esto, incluyendo propiedades y variables privadas.

5.2.1 Público vs Privado

Uno de los roles principales de una clase es encapsular datos y detalles de implementación de un objeto. Sin embargo, una clase también define un Interfaz * pública * que se supone que el mundo exterior debe usar para manipular el objeto. Esta distinción entre implementación Los detalles y la interfaz pública son importantes.

5.2.2 Un problema

En Python, casi todo lo relacionado con clases y objetos es abierto.

  • Puede inspeccionar fácilmente el interior de los objetos.
  • Puedes cambiar las cosas a su antojo.
  • No existe una noción fuerte de control de acceso (es decir, miembros de clases privadas)

Esto es un problema cuando intenta aislar detalles de la implementación interna.

5.2.3 Encapsulación de Python

Python se basa en convenciones de programación para indicar el uso previsto de algo. Estas convenciones se basan en la denominación o nombramiento. El lenguaje no impone ciertas reglas, sino que le delega esa responsabilidad al programador. El programador debe observar y cumplir ciertas reglas, sin que el lenguaje lo obligue hacerlo. En ese sentido, Python se distingue de otros lenguajes de programación que tienen reglas estrictas sobre lo que es público y lo que es privado.

5.2.4 Atributos privados

Cualquier nombre de atributo con "_" inicial se considera privado.

clase Persona (objeto):
    def __init__ (self, nombre):
        self._name = 0

Como se mencionó anteriormente, este es solo un estilo de programación. Todavia puede acceder y modificarlo.

>>> p = Persona('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

Como regla general, cualquier nombre con un "_" inicial se considera implementación interna ya sea una variable, una función o un nombre de módulo. Si se encuentra usando tales nombres directamente, probablemente esté haciendo algo mal. Busque una funcionalidad de nivel superior.

5.2.5 Atributos simples

Considere 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 puede establecer los atributos en cualquier valor:

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>

Puede mirar eso y pensar que quiere algunas verificaciones adicionales.

s.shares = '50'     # Alza un TypeError, esto es una cadena

¿Como lo harias?

5.2.6 Atributos gestionados

Un enfoque: introducir métodos de acceso.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.set_shares(shares)
        self.price = price

    # Metodo que encapa la operación de acceso al atributo (get)
    def get_shares(self):
        return self._shares

    # Metodo que encapa la operacion de definición del atributo (set)
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Esperaba un int')
        self._shares = value

Lástima que esto rompa todo nuestro código existente. s.shares = 50 se convierte en s.set_shares(50)

5.2.7 Propiedades

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('Expected int')
        self._shares = value

El acceso normal a los atributos ahora activa los métodos getter y setter en @propertyy @shares.setter.

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares         # Provoca @property
50
>>> s.shares = 75    # Prooca @shares.setter
>>>

Con este patrón, no se necesitan cambios en el código fuente. El nuevo setter también se llama cuando hay una asignación dentro de la clase, incluyendo en el interior del __init__() método.

class Stock:
    def __init__(self, name, shares, price):
        ...
        # La asignación usa el método setter de abajo
        self.shares = shares
        ...

    ...
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

A menudo existe una confusión entre una propiedad y el uso de nombres privados. Aunque una propiedad usa internamente un nombre privado como _shares, el resto de la clase (no la propiedad) puede continuar usando un nombre como shares.

Las propiedades también son útiles para los 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 le permite eliminar los paréntesis adicionales, ocultando el hecho de que en realidad es un método:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares # Instance variable 100
>>> s.cost   # Computed Value 49010.0
>>>

5.2.8 Acceso uniforme

El último ejemplo muestra cómo poner una interfaz más uniforme en un objeto. Si no hace esto, un objeto puede resultar confuso de usar:

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() # Method
49010.0
>>> b = s.shares # Data attribute
100
>>>

¿Por qué se requiere () para el costo, pero no para las acciones? Una propiedad puede arreglar esto.

5.2.9 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

More details are given in Section 7.

5.2.10 slots Atributo

Puede 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 usa para el rendimiento y hace que Python use la memoria de manera más eficiente.

5.2.11 Comentarios finales sobre encapsulación

No se exceda con atributos privados, propiedades, espacios, etc. Sirven para un propósito específico y es posible que los vea al leer otro código de Python. Sin embargo, no son necesarios para la mayor parte de la codificación diaria.

5.2.12 Ejercicios

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. Observe que en su objeto hay una ligera inconsistencia en cómo se extraen los 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
>>>

Específicamente, observe cómo tiene que agregar extra () a cost porque es un método.

Puede deshacerse del extra () en cost() si lo convierte en una propiedad. Tome su clase Stock y modifíquela para que el cálculo del costo funcione así:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

Intente llamar s.cost() como una función y observe que no funciona ahora que cost se ha definido como una propiedad.

>>> s.cost()
... fracasa ...
>>>

Hacer este cambio probablemente romperá su programa pcost.py anterior . Es posible que deba volver atrás y deshacerse del método () en cost().

Ejercicio 5.7: Propiedades y establecedores

Modifique el atributo shares para 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. A continuación, se muestra 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
>>>

Ejercicio 5.8: Agregar ranuras

Modifique la clase Stock para que tenga un atributo __slots__. Luego, verifique que no se puedan agregar nuevos atributos:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... mire lo que sucede ...
>>>

Cuando lo usa __slots__, Python usa una representación interna de objetos más eficiente. ¿Qué sucede si intenta inspeccionar el diccionario subyacente de s arriba?

>>> s.__dict__
... mire lo que sucede ...
>>>

Cabe señalar que __slots__ se usa más comúnmente como optimización en clases que sirven como estructuras de datos. El uso de ranuras hará que dichos programas usen mucha menos memoria y se ejecuten un poco más rápido. Sin embargo , probablemente debería evitar el uso de __slots__ en la mayoría de las otras clases.