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.
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í
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.
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.
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>
}
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.
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>
}
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.
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.
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' }
>>>
Supongamos que lees un atributo en una instancia.
x = obj.name
El atributo puede existir en dos lugares:
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.
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.
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.
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.
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.
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.
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.
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 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.
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.
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)
>>>
En la shell interactiva, inspecciona los diccionarios subyacentes de las dos instancias que creaste:
>>> goog.__dict__
... mira la salida...
>>> ibm.__dict__
... mira la salida...
>>>
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 (.).
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
>>>
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
>>>
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>
>>>
¡Felicitaciones! Has completado el laboratorio de Diccionarios Revisados. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.