Fundamentos del Sistema de Objetos de Python

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

El sistema de objetos de Python se basa en gran medida en una implementación que involucra diccionarios. En esta sección se aborda eso.

Diccionarios, revisados

Recuerda 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 intérprete y pueden ser el tipo de datos más importante en Python.

Diccionarios y Módulos

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

## foo.py

x = 42
def bar():
  ...

def spam():
  ...

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

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

Diccionarios y Objetos

Los objetos definidos por el usuario también utilizan diccionarios tanto para los datos de instancia como para las clases. De hecho, todo el sistema de objetos es en gran medida una capa adicional que se coloca encima de los diccionarios.

Un diccionario contiene los datos de instancia, __dict__.

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

Se llena este diccionario (y la 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 instancia, self.__dict__, se ven así:

{
    '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 creas 100 instancias de una clase determinada, hay 100 diccionarios almacenando datos.

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 se encuentra en Stock.__dict__.

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

Instancias y Clases

Las instancias y las clases están vinculadas. El atributo __class__ se refiere 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 instancia contiene datos únicos para cada instancia, mientras que el diccionario de clase contiene datos compartidos colectivamente por todas las instancias.

Acceso a Atributos

Cuando trabajas con objetos, accedes a datos y métodos utilizando el operador ..

x = obj.name          ## Obtener
obj.name = value      ## Establecer
del obj.name          ## Eliminar

Estas operaciones están directamente vinculadas a los diccionarios que se encuentran por debajo.

Modificando 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       ## Estableciendo
>>> s.date = '6/7/2007' ## Estableciendo
>>> s.__dict__
{ 'name': 'GOOG','shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares        ## Eliminando
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

Leyendo Atributos

Supongamos que lees un atributo en una instancia.

x = obj.name

El atributo puede existir en dos lugares:

  • Diccionario local de instancia.
  • Diccionario de clase.

Ambos diccionarios deben ser verificados. Primero, verifica en __dict__ local. Si no se encuentra, busca en __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 se comparten entre todas las instancias.

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

Leyendo Atributos con Herencia

Lógicamente, el proceso de encontrar un atributo es el siguiente. Primero, verifica en __dict__ local. Si no se encuentra, busca en __dict__ de la clase. Si no se encuentra en la clase, busca en las clases base a través de __bases__. Sin embargo, hay algunos aspectos sutiles de esto que se discuten a continuación.

Leyendo Atributos con Herencia Simple

En jerarquías de herencia, los atributos se encuentran recorriendo el árbol de herencia en orden ascendente.

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

Con herencia simple, hay un solo camino hacia la cima. Se detiene con la primera coincidencia.

Orden de Resolución de Métodos o MRO

Python precomputa una cadena de herencia y la almacena en el atributo MRO de la clase. Puedes verla.

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

Esta cadena se llama Orden de Resolución de Métodos. Para encontrar un atributo, Python recorre el MRO en orden. La primera coincidencia gana.

MRO en Herencia Múltiple

Con herencia múltiple, no hay un solo camino hacia la cima. Echemos un vistazo a un ejemplo.

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

¿Qué pasa cuando se accede a un atributo?

e = E()
e.attr

Se lleva a cabo un proceso de búsqueda de atributos, pero ¿cuál es el orden? Ese es el problema.

Python utiliza la herencia múltiple cooperativa que obedece algunas reglas sobre el orden de las clases.

  • Los hijos siempre se comprueban antes que los padres
  • Los padres (si hay varios) siempre se comprueban en el orden en que se listan.

El MRO se calcula ordenando todas las clases de 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 llama "Algoritmo de Linealización C3". Los detalles precisos no son importantes siempre y cuando recuerdes que una jerarquía de clases obedece las mismas reglas de orden que seguirías si tu casa estuviera en llamas y tuvieras que evacuar: los hijos primero, seguidos de los padres.

Un Uso Extraño de Reutilización de Código (Involucrando Herencia Múltiple)

Considera dos objetos completamente no relacionados:

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

    def chase(self):
        return 'Chasing!'

class LoudDog(Dog):
    def noise(self):
        ## Código en común con LoudBike (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):
        ## Código en común con LoudDog (arriba)
        return super().noise().upper()

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

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 es usable por sí sola. Se mezcla con otras clases a través de la herencia.

class LoudDog(Loud, Dog):
    pass

class LoudBike(Loud, Bike):
    pass

Míraculosamente, la alta volumen 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.

¿Por qué super()?

Siempre utiliza super() cuando sobrescribes métodos.

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

super() delega a la siguiente clase en el MRO.

Lo complicado es que no sabes cuál es. En especial, no lo sabes si se está utilizando herencia múltiple.

Algunas Precauciones

La herencia múltiple es una herramienta poderosa. Recuerda que con el poder viene la responsabilidad. A veces, los marcos / bibliotecas la utilizan para características avanzadas que implican la composición de componentes. Ahora, olvida que has visto eso.

En la Sección 4, definiste una clase Stock que representaba una posesión de acciones. En este ejercicio, usaremos esa clase. Reinicia el intérprete y crea 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 la shell interactiva, inspecciona los diccionarios subyacentes de las dos instancias que creaste:

>>> goog.__dict__
... mira la salida...
>>> ibm.__dict__
... mira la salida...
>>>

Ejercicio 5.2: Modificación de Datos de Instancia

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

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

En la salida anterior, notarás que la instancia goog tiene un atributo date, mientras que la instancia ibm no lo tiene. Es importante destacar que Python realmente no impone ninguna restricción sobre los atributos. Por ejemplo, los atributos de una instancia no se limitan a los establecidos en el método __init__().

En lugar de establecer un atributo, intenta 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 simplemente una capa sobre un diccionario. Nota: debe enfatizarse 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. Observe que todas las instancias tienen un enlace de vuelta a su clase asociada:

>>> goog.__class__
... mira la salida...
>>> ibm.__class__
... mira la salida...
>>>

Intenta llamar a un método en las instancias:

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

Observe que el nombre 'cost' no está definido en goog.__dict__ ni en ibm.__dict__. En su lugar, está siendo suministrado por el diccionario de la clase. Prueba esto:

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

Intenta 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 se está llamando a la función definida en la definición de la clase y cómo el argumento self obtiene la instancia.

Intenta 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, observe que no es parte del diccionario de la instancia:

>>> goog.__dict__
... mira la salida y observa que no hay un atributo 'foo'...
>>>

La razón por la cual se 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 instancia misma.

Nota: Esta parte del ejercicio ilustra algo conocido como una variable de clase. Suponga, 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 clase misma, es una "variable de clase". Es compartida por todas las instancias que se crean. Por ejemplo:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a          ## Inspecciona la variable de clase (es la misma para ambas instancias)
13
>>> g.a
13
>>> f.b          ## Inspecciona la variable de instancia (es diferente)
10
>>> g.b
20
>>> Foo.a = 42   ## Cambia el valor de la variable de clase
>>> f.a
42
>>> g.a
42
>>>

Ejercicio 5.4: Métodos vinculados

Una característica sutil de Python es que invocar un método implica dos pasos y algo conocido como un 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, guardan 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 de Stock.

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

Los métodos vinculados también registran la instancia, que es el argumento self.

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

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

>>> s.__func__(s.__self__, 25)    ## Lo mismo que s(25)
>>> goog.shares
50
>>>

Ejercicio 5.5: Herencia

Crea una nueva clase que herede de Stock.

>>> 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 extendiendo 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 atributos.

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

Aquí está cómo se encontraría el método cost() de la instancia n anterior:

>>> for cls in n.__class__.__mro__:
        if 'cost' in cls.__dict__:
            break

>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

Resumen

¡Felicitaciones! Has completado el laboratorio de Diccionarios Revisados. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.