Introduction
Le système d'objets Python est largement basé sur une implémentation impliquant des dictionnaires. Cette section en discute.
This tutorial is from open-source community. Access the source code
💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici
Le système d'objets Python est largement basé sur une implémentation impliquant des dictionnaires. Cette section en discute.
Rappelez-vous qu'un dictionnaire est une collection de valeurs nommées.
stock = {
'name' : 'GOOG',
'shares' : 100,
'price' : 490.1
}
Les dictionnaires sont couramment utilisés pour les structures de données simples. Cependant, ils sont utilisés pour des parties critiques de l'interpréteur et peuvent être le type de données le plus important en Python.
Dans un module, un dictionnaire contient toutes les variables et fonctions globales.
## foo.py
x = 42
def bar():
...
def spam():
...
Si vous examinez foo.__dict__
ou globals()
, vous verrez le dictionnaire.
{
'x' : 42,
'bar' : <function bar>,
'spam' : <function spam>
}
Les objets définis par l'utilisateur utilisent également des dictionnaires pour les données d'instance et les classes. En fait, tout le système d'objets est principalement une couche supplémentaire qui est placée au-dessus des dictionnaires.
Un dictionnaire contient les données d'instance, __dict__
.
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG','shares' : 100, 'price': 490.1 }
Vous remplit ce dictionnaire (et l'instance) en affectant à self
.
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Les données d'instance, self.__dict__
, ressemblent à ceci :
{
'name': 'GOOG',
'shares': 100,
'price': 490.1
}
Chaque instance obtient son propre dictionnaire privé.
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 vous avez créé 100 instances d'une certaine classe, il y a 100 dictionnaires qui contiennent des données.
Un dictionnaire séparé contient également les méthodes.
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
Le dictionnaire se trouve dans Stock.__dict__
.
{
'cost': <function>,
'sell': <function>,
'__init__': <function>
}
Les instances et les classes sont liées ensemble. L'attribut __class__
renvoie à la classe.
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG','shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>
Le dictionnaire d'instance contient des données uniques à chaque instance, tandis que le dictionnaire de classe contient des données collectivement partagées par toutes les instances.
Lorsque vous travaillez avec des objets, vous accédez aux données et aux méthodes en utilisant l'opérateur .
.
x = obj.name ## Obtenir
obj.name = value ## Définir
del obj.name ## Supprimer
Ces opérations sont directement liées aux dictionnaires qui se trouvent sous les couvertures.
Les opérations qui modifient un objet mettent à jour le dictionnaire sous-jacent.
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG','shares': 100, 'price': 490.1 }
>>> s.shares = 50 ## Définition
>>> s.date = '6/7/2007' ## Définition
>>> s.__dict__
{ 'name': 'GOOG','shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares ## Suppression
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>
Supposons que vous l lisez un attribut sur une instance.
x = obj.name
L'attribut peut exister en deux endroits :
Les deux dictionnaires doivent être vérifiés. D'abord, vérifiez dans __dict__
local. Si non trouvé, cherchez dans __dict__
de la classe via __class__
.
>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>
Ce schéma de recherche est la manière dont les membres d'une classe sont partagés par toutes les instances.
Les classes peuvent hériter d'autres classes.
class A(B, C):
...
Les classes de base sont stockées dans un tuple dans chaque classe.
>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>
Cela fournit un lien vers les classes parentes.
Logiquement, le processus de recherche d'un attribut est le suivant. Tout d'abord, vérifiez dans __dict__
local. Si non trouvé, cherchez dans __dict__
de la classe. Si non trouvé dans la classe, cherchez dans les classes de base via __bases__
. Cependant, il y a quelques aspects subtils de ceci qui seront discutés ci-dessous.
Dans les hiérarchies d'héritage, les attributs sont trouvés en remontant l'arbre d'héritage dans l'ordre.
class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass
Avec l'héritage simple, il existe un seul chemin vers le sommet. Vous arrêtez au premier match.
Python calcule à l'avance une chaîne d'héritage et la stocke dans l'attribut MRO de la classe. Vous pouvez la visualiser.
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)
>>>
Cette chaîne est appelée Ordre de résolution des méthodes. Pour trouver un attribut, Python parcourt l'ordre MRO. Le premier match remporte le combat.
Avec l'héritage multiple, il n'y a pas de chemin unique vers le sommet. Jetons un coup d'œil à un exemple.
class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass
Qu'est-ce qui se passe lorsque vous accédez à un attribut?
e = E()
e.attr
Un processus de recherche d'attribut est effectué, mais quel est l'ordre? C'est un problème.
Python utilise l'héritage multiple coopératif qui obéit à certaines règles concernant l'ordre des classes.
L'ordre de résolution des méthodes (MRO) est calculé en triant toutes les classes d'une hiérarchie selon ces règles.
>>> E.__mro__
(
<class 'E'>,
<class 'C'>,
<class 'A'>,
<class 'D'>,
<class 'B'>,
<class 'object'>)
>>>
L'algorithme de base est appelé "Algorithme de linéarisation C3". Les détails précis ne sont pas importants tant que vous vous souvenez qu'une hiérarchie de classes obéit aux mêmes règles d'ordre que vous pourriez suivre si votre maison était en feu et que vous deviez évacuer - les enfants d'abord, suivis des parents.
Considérez deux objets complètement non liés :
class Dog:
def noise(self):
return 'Bark'
def chase(self):
return 'Chasing!'
class LoudDog(Dog):
def noise(self):
## Code commun avec LoudBike (ci-dessous)
return super().noise().upper()
Et
class Bike:
def noise(self):
return 'On Your Left'
def pedal(self):
return 'Pedaling!'
class LoudBike(Bike):
def noise(self):
## Code commun avec LoudDog (ci-dessus)
return super().noise().upper()
Il y a une similitude de code dans l'implémentation de LoudDog.noise()
et LoudBike.noise()
. En fait, le code est exactement le même. Naturellement, un code comme celui-ci est certainement susceptible d'attirer les ingénieurs logiciels.
Le motif Mixin est une classe avec un fragment de code.
class Loud:
def noise(self):
return super().noise().upper()
Cette classe ne peut pas être utilisée isolément. Elle est combinée avec d'autres classes via l'héritage.
class LoudDog(Loud, Dog):
pass
class LoudBike(Loud, Bike):
pass
Miracle, la capacité à être bruyant a maintenant été implémentée une seule fois et réutilisée dans deux classes complètement non liées. Ce genre de tour de passe-passe est l'une des principales utilisations de l'héritage multiple en Python.
super()
Utilisez toujours super()
lorsqu'il s'agit de surcharger des méthodes.
class Loud:
def noise(self):
return super().noise().upper()
super()
délègue à la classe suivante dans l'ordre MRO.
Le point délicat est que vous ne savez pas laquelle c'est. Vous ne savez surtout pas laquelle c'est si l'héritage multiple est utilisé.
L'héritage multiple est un outil puissant. Rappelez-vous que avec le pouvoir vient la responsabilité. Certains frameworks / bibliothèques l'utilisent parfois pour des fonctionnalités avancées impliquant la composition de composants. Maintenant, oubliez que vous avez lu ça.
Dans la Section 4, vous avez défini une classe Stock
qui représentait une position en actions. Dans cet exercice, nous allons utiliser cette classe. Redémarrez l'interpréteur et créez quelques instances :
>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm = Stock('IBM',50, 91.23)
>>>
Au shell interactif, examinez les dictionnaires sous-jacents des deux instances que vous avez créées :
>>> goog.__dict__
... regardez la sortie...
>>> ibm.__dict__
... regardez la sortie...
>>>
Essayez de définir un nouvel attribut sur l'une des instances ci-dessus :
>>> goog.date = '6/11/2007'
>>> goog.__dict__
... regardez la sortie...
>>> ibm.__dict__
... regardez la sortie...
>>>
Dans la sortie ci-dessus, vous remarquerez que l'instance goog
a un attribut date
tandis que l'instance ibm
n'en a pas. Il est important de noter que Python ne pose vraiment aucune restriction sur les attributs. Par exemple, les attributs d'une instance ne sont pas limités à ceux définis dans la méthode __init__()
.
Au lieu de définir un attribut, essayez de placer une nouvelle valeur directement dans l'objet __dict__
:
>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>
Ici, vous remarquez vraiment le fait qu'une instance n'est qu'une couche au-dessus d'un dictionnaire. Notez : il faut souligner que la manipulation directe du dictionnaire est peu courante - vous devriez toujours écrire votre code pour utiliser la syntaxe (.).
Les définitions qui composent une définition de classe sont partagées par toutes les instances de cette classe. Remarquez que toutes les instances ont un lien vers leur classe associée :
>>> goog.__class__
... regardez la sortie...
>>> ibm.__class__
... regardez la sortie...
>>>
Essayez d'appeler une méthode sur les instances :
>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>
Remarquez que le nom 'cost' n'est pas défini dans goog.__dict__
ni dans ibm.__dict__
. Au lieu de cela, il est fourni par le dictionnaire de la classe. Essayez ceci :
>>> Stock.__dict__['cost']
... regardez la sortie...
>>>
Essayez d'appeler la méthode cost()
directement via le dictionnaire :
>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>
Remarquez comment vous appelez la fonction définie dans la définition de classe et comment l'argument self
reçoit l'instance.
Essayez d'ajouter un nouvel attribut à la classe Stock
:
>>> Stock.foo = 42
>>>
Remarquez comment ce nouvel attribut apparaît maintenant sur toutes les instances :
>>> goog.foo
42
>>> ibm.foo
42
>>>
Cependant, remarquez qu'il n'est pas partie du dictionnaire d'instance :
>>> goog.__dict__
... regardez la sortie et remarquez qu'il n'y a pas d'attribut 'foo'...
>>>
La raison pour laquelle vous pouvez accéder à l'attribut foo
sur les instances est que Python vérifie toujours le dictionnaire de la classe s'il ne trouve pas quelque chose sur l'instance elle-même.
Note : Cette partie de l'exercice illustre ce qu'on appelle une variable de classe. Par exemple, imaginez que vous avez une classe comme ceci :
class Foo(object):
a = 13 ## Variable de classe
def __init__(self,b):
self.b = b ## Variable d'instance
Dans cette classe, la variable a
, assignée dans le corps de la classe elle-même, est une "variable de classe". Elle est partagée par toutes les instances qui sont créées. Par exemple :
>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a ## Vérifiez la variable de classe (identique pour les deux instances)
13
>>> g.a
13
>>> f.b ## Vérifiez la variable d'instance (différente)
10
>>> g.b
20
>>> Foo.a = 42 ## Changez la valeur de la variable de classe
>>> f.a
42
>>> g.a
42
>>>
Une caractéristique subtile de Python est que l'appel d'une méthode implique en fait deux étapes et quelque chose appelé une méthode liée. Par exemple :
>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>
Les méthodes liées contiennent en fait tous les éléments nécessaires pour appeler une méthode. Par exemple, elles conservent une trace de la fonction qui implémente la méthode :
>>> s.__func__
<function sell at 0x10049af50>
>>>
C'est la même valeur que celle trouvée dans le dictionnaire de Stock
.
>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>
Les méthodes liées enregistrent également l'instance, qui est l'argument self
.
>>> s.__self__
Stock('GOOG',75,490.1)
>>>
Lorsque vous appelez la fonction en utilisant ()
, tous les éléments se rassemblent. Par exemple, appeler s(25)
fait en réalité ceci :
>>> s.__func__(s.__self__, 25) ## Identique à s(25)
>>> goog.shares
50
>>>
Créez une nouvelle classe qui hérite de Stock
.
>>> class NewStock(Stock):
def yow(self):
print('Yow!')
>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>
L'héritage est implémenté en étendant le processus de recherche d'attributs. L'attribut __bases__
a un tuple des parents directs :
>>> NewStock.__bases__
(<class'stock.Stock'>,)
>>>
L'attribut __mro__
a un tuple de tous les parents, dans l'ordre dans lequel ils seront recherchés pour des attributs.
>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class'stock.Stock'>, <class 'object'>)
>>>
Voici comment la méthode cost()
de l'instance n
ci-dessus serait trouvée :
>>> for cls in n.__class__.__mro__:
if 'cost' in cls.__dict__:
break
>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>
Félicitations ! Vous avez terminé le laboratoire Dictionnaires revisités. Vous pouvez pratiquer d'autres laboratoires sur LabEx pour améliorer vos compétences.