Classes — données et comportement ensemble
`class`, `__init__` et `self`, méthodes, une forme imprimable avec `__repr__`, égalité avec `__eq__`, niveau classe contre niveau instance, et quand une classe n'est pas voulue.
Comment voulez-vous apprendre ?
Le problème que nous résolvons
Jusqu'ici nous avons gardé plusieurs faits sur une chose dans un dictionnaire.
order = {"name": "pen", "price": 15.0, "quantity": 3}
print(order["nmae"])KeyError: 'nmae'L'erreur a été attrapée, ce qui est déjà quelque chose — mais seulement quand cette ligne s'exécute. Cachée dans une condition, elle pourrait n'apparaître qu'un mois plus tard.
Et le problème n'est pas seulement l'orthographe. N'importe quelle clé peut être mise dans un dictionnaire, donc rien nulle part ne dit ce qu'une ligne de commande doit contenir. La fonction qui la totalise est ailleurs, et personne ne vérifie que le dictionnaire a tous les champs avant de l'appeler. Les données sont dans un endroit, ce qu'on peut en faire dans un autre, et ce qui appartient à quoi existe seulement dans votre tête.
class OrderLine:
def __init__(self, name, price, quantity):
self.name = name
self.price = price
self.quantity = quantity
def total(self):
return round(self.price * self.quantity, 2)
line = OrderLine("pen", 15.0, 3)
print(line.name)
print(line.total())pen
45.0Une classe est un moule : elle dit ce qu'une chose de ce genre contient, et ce qu'on peut en faire.
À la fin de ce chapitre vous pourrez
- Écrire une
class, et dire ce que font__init__etself - Écrire des méthodes, et dire en quoi elles diffèrent des fonctions ordinaires
- Donner à un objet une forme imprimable avec
__repr__ - Dire ce qui sépare un attribut de classe d'un attribut d'instance — et éviter un piège familier
- Rendre deux objets comparables avec
__eq__ - Dire quand une classe n'est pas voulue
Prérequis : JSON et CSV.
Ce qu'est vraiment self
self n'est pas un mot magique — c'est le premier paramètre, et ce qui arrive dedans est l'objet sur lequel la méthode a été appelée.
Écrire line.total() signifie « lance la méthode total de OrderLine, et donne-lui line comme self ». C'est pourquoi self.price à l'intérieur donne le prix de cette ligne particulière.
Oubliez self et le message est confus au premier abord :
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())TypeError: OrderLine.shout() takes 0 positional arguments but 1 was given« 1 was given » — mais les parenthèses étaient vides. Ce qui a été donné est line lui-même, silencieusement, et shout n'a laissé aucune place pour le recevoir.
__init__ est une méthode ordinaire aussi, avec un nom spécial : Python l'appelle juste après la création de l'objet. À l'intérieur, self.name = name définit les attributs.
Une faute d'orthographe donne une erreur plus claire
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(line.nmae)AttributeError: 'OrderLine' object has no attribute 'nmae'Pour être honnête : une classe ne détecte pas cette erreur plus tôt. Une AttributeError, tout comme une KeyError, apparaît seulement quand la ligne s'exécute. Cachée dans une condition, celle-ci pourrait aussi n'apparaître qu'un mois plus tard.
Alors où est le gain ? En trois endroits.
- L'erreur dit plus. Le message nomme la classe —
'OrderLine' object— donc vous pouvez voir sur quel genre de chose le mauvais nom a été cherché. - Ce qui devrait être là est écrit en un seul endroit. Lire
__init__montre quels attributs unOrderLinea. Un dictionnaire n'a pas une telle liste. - Votre éditeur peut aider. Un éditeur lit la classe et suggère les noms d'attributs quand vous tapez
line., et beaucoup d'éditeurs soulignent un nom inconnu — avant que le code ne s'exécute du tout. Avec des clés de dictionnaire, un éditeur ne peut généralement pas faire cela.
Rappelez-vous la limite aussi : une faute d'orthographe n'est pas attrapée lors de l'écriture. Tout comme order["nmae"] = 5 crée silencieusement une nouvelle clé, line.nmae = 5 crée silencieusement un nouvel attribut.
Et ce qui est à l'intérieur peut être regardé :
class OrderLine:
def __init__(self, name):
self.name = name
line = OrderLine("pen")
print(type(line))
print(isinstance(line, OrderLine))
print(line.__dict__)<class '__main__.OrderLine'>
True
{'name': 'pen'}__dict__ montre qu'il y a vraiment un dictionnaire à l'intérieur de l'objet. La classe n'a pas supprimé le dictionnaire — elle lui a donné un nom et une forme.
__repr__ — ce que l'impression montre
Sans __repr__ :
class OrderLine:
def __init__(self, name):
self.name = name
print(OrderLine("pen"))<__main__.OrderLine object at 0x79b769f03830>Le numéro à la fin est une adresse mémoire, et il diffère à chaque exécution — vous verrez un numéro différent sur votre écran. Pour le débogage, il ne sert à rien.
class OrderLine:
def __init__(self, name, price):
self.name = name
self.price = price
def __repr__(self):
return f"OrderLine(name={self.name!r}, price={self.price!r})"
line = OrderLine("pen", 15.0)
print(line)
print([line, line])OrderLine(name='pen', price=15.0)
[OrderLine(name='pen', price=15.0), OrderLine(name='pen', price=15.0)]La seconde ligne est le vrai bénéfice : cela fonctionne aussi dans une liste. Quand Python imprime une liste il appelle le __repr__ de chaque élément, pas son __str__. C'est pourquoi, des deux, __repr__ est celui à écrire en premier.
La convention est de retourner une chaîne qui ressemble au code qui a construit l'objet. Utiliser !r met des « guillemets » autour des chaînes à l'intérieur — pour la même raison que dans le chapitre 24.
Égalité — le défaut n'est pas ce que vous voulez
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)False
TrueLes deux objets contiennent la même chose, et pourtant == donne False. Parce que l'égalité par défaut est l'identité — == se comporte exactement comme is, demandant « est-ce le tout même objet ? ».
Le changer signifie dire ce que veut dire égal :
class OrderLine:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a is b)True
FalseMaintenant == dit « semblables » et is dit « le même » — la distinction du chapitre 21, cette fois dans votre propre type.
Une mise en garde : écrire __eq__ laisse l'objet non hachable par défaut, donc il ne peut pas aller dans un ensemble ni être une clé de dictionnaire. Le dataclass du prochain chapitre fait les deux en une ligne.
Niveau classe et niveau instance
class Basket:
TAX_RATE = 0.15
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.items is b.items)['pen', 'bag']
['pen', 'bag']
TrueDeux paniers séparés, mais une seule liste.
Vous le reconnaissez ? C'est le piège du défaut mutable du chapitre 21, dans un nouvel endroit. Le items = [] juste sous la ligne class s'exécute une fois, quand la classe est créée — pas une fois par objet. Donc la liste appartient à la classe, et chaque instance la partage.
La correction est du même genre : ce qui appartient à chaque objet doit être construit dans __init__.
class Basket:
TAX_RATE = 0.15
def __init__(self):
self.items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)
print(a.TAX_RATE, b.TAX_RATE)['pen']
['bag']
0.15 0.15TAX_RATE est resté au niveau classe, et à juste titre — c'est le même pour chaque panier, et nous ne voulons pas le changer. Mais notez que Python ne vous empêche pas de le changer : Basket.TAX_RATE = 0.2 ne lève aucune erreur. Le nom en majuscules n'est qu'une convention qui dit au lecteur « traitez ceci comme une constante, ne le changez pas ». La règle en une ligne : constantes au niveau classe ; tout ce qui change dans __init__.
Quand ne pas écrire une classe
Une fois que vous avez appris les classes, il est tentant de tout faire en classe. Quelques signes que la classe n'est pas nécessaire :
La classe n'a qu'un __init__ et une méthode. C'est une fonction, coupée en deux sans raison.
Pas de méthodes du tout, seulement des données. Le dataclass du prochain chapitre est fait exactement pour cela, en bien moins de lignes.
L'objet ne change jamais et ne contient qu'une chose. Un tuple, ou une valeur simple, suffit.
Une classe est le bon outil là où données et comportement appartiennent vraiment ensemble : certaines données qui changent, et quelques opérations qui tournent toujours sur ces données.
Un exemple complet
"""An order built from two small classes."""
TAX_RATE = 0.15
class OrderLine:
"""One item on an order: what it is, what it costs, how many."""
def __init__(self, name, price, quantity):
if quantity < 1:
raise ValueError(f"quantity must be at least 1: {quantity}")
self.name = name
self.price = price
self.quantity = quantity
def total(self):
"""Price for this line, tax included."""
return round(self.price * self.quantity * (1 + TAX_RATE), 2)
def __repr__(self):
return f"OrderLine({self.name!r}, {self.price!r}, {self.quantity!r})"
class Order:
"""A named collection of lines, and the questions you ask about it."""
def __init__(self, customer):
self.customer = customer
self.lines = []
def add(self, name, price, quantity):
self.lines.append(OrderLine(name, price, quantity))
return self
def total(self):
return round(sum(line.total() for line in self.lines), 2)
def largest(self):
"""The most expensive line, or None for an empty order."""
if not self.lines:
return None
return max(self.lines, key=lambda line: line.total())
def __repr__(self):
return f"Order({self.customer!r}, {len(self.lines)} lines)"
def main():
order = Order("rafi")
order.add("pen", 15.0, 3)
order.add("bag", 850.0, 1)
order.add("ink", 120.0, 2)
for line in order.lines:
print(f"{line.name:<6} {line.total():>9.2f}")
print(f"{'total':<6} {order.total():>9.2f}")
print()
print(order)
print("largest:", order.largest())
try:
order.add("clip", 5.0, 0)
except ValueError as err:
print("rejected:", err)
if __name__ == "__main__":
main()pen 51.75
bag 977.50
ink 276.00
total 1305.25
Order('rafi', 3 lines)
largest: OrderLine('bag', 850.0, 1)
rejected: quantity must be at least 1: 0Quatre choses à remarquer.
__init__ valide. Avec quantity < 1 l'objet n'est jamais créé du tout. C'est la chose la plus utile qu'une classe offre : avoir un OrderLine veut dire que sa quantité est au moins un — nulle part où il est utilisé n'a besoin de vérifier à nouveau. Un dictionnaire ne peut pas donner cette garantie.
Order ne sait pas comment le prix d'une ligne est calculé. Il appelle line.total() et c'est tout. Comment la taxe s'applique est l'affaire de OrderLine, et le changer ne change qu'un seul endroit.
Les deux classes ont un __repr__, et la sortie de largest() montre pourquoi ça compte. Sans, cette dernière ligne aurait été une adresse mémoire, qui ne vous dit rien.
add retourne lui-même (return self). Ça rend order.add(...).add(...) possible. Ce n'est pas utilisé ici, délibérément — avoir l'option et l'utiliser à chaque ligne sont deux choses différentes.
Quand ça casse
TypeError: ... takes 0 positional arguments but 1 was given La méthode manque self. Ajoutez self comme premier paramètre.
AttributeError: 'X' object has no attribute 'y' Une faute d'orthographe, ou self.y n'a jamais été défini dans __init__. print(obj.__dict__) montre ce qui est réellement là.
TypeError: X() takes no arguments Il n'y a pas de __init__, ou le nom est mal orthographié (__init__, pas __int__, deux underscores de chaque côté).
Chaque objet partage une liste La liste est au niveau classe. Déplacez-la dans __init__.
Deux objets contiennent la même chose et == donne False L'égalité par défaut est l'identité. Écrivez __eq__ — ou utilisez le dataclass du prochain chapitre.
Après avoir écrit __eq__ il ne peut pas aller dans un ensemble Écrire __eq__ enlève le hachage par défaut. dataclass(frozen=True) donne les deux.
L'affichage donne <__main__.X object at 0x...> Il n'y a pas de __repr__.
Étape 4 sur 6 — Prédire
Vérifiez votre compréhension
shout() est appelée sans arguments. Que se passe-t-il ?
class OrderLine:
def __init__(self, name):
self.name = name
def shout():
return "hello"
line = OrderLine("pen")
print(line.shout())- AUne `TypeError` — elle prend un, et zéro a été donné
- BUne `TypeError` — elle prend zéro, et un a été donné
- CUne `AttributeError`
- D`hello` s'imprime
Deux paniers séparés, un article chacun. Que s'imprime-t-il ?
class Basket:
items = []
def add(self, name):
self.items.append(name)
a = Basket()
b = Basket()
a.add("pen")
b.add("bag")
print(a.items)
print(b.items)- A['pen', 'bag'] []
- B['pen'] ['pen', 'bag']
- C['pen'] ['bag']
- D['pen', 'bag'] ['pen', 'bag']
Les deux objets contiennent le même nom. Que s'imprime-t-il ?
class OrderLine:
def __init__(self, name):
self.name = name
a = OrderLine("pen")
b = OrderLine("pen")
print(a == b)
print(a == a)- AFalse True
- BUne `TypeError`
- CFalse False
- DTrue True
Répondre nécessite un compte
Connectez-vous pour vérifier vos réponses
Les questions sont ci-dessus, et les résoudre de tête est ce qui compte vraiment. Connectez-vous pour voir les réponses, les explications et les indices à trois niveaux.
À vous de jouer
Écrivez deux classes.
Book — construit à partir de title, author et pages. Il lève une ValueError quand pages est inférieur à 1. Donnez-lui un __repr__, et un __eq__ qui dit que deux livres sont égaux quand le titre et l'auteur correspondent.
Shelf — collecte des livres, et offre add(book), total_pages(), longest(), et by_author(name) retournant une liste.
Puis cinq expériences :
- Déplacez
books = []deShelfhors de__init__et sous la ligneclass. Faites deux étagères séparées, ajoutez un livre à l'une, puis imprimez l'autre. Que voyez-vous ? - Supprimez le
__repr__et imprimez une liste de livres. Combien le débogage est-il plus difficile ? - Supprimez
__eq__et comparez deux livres avec le même titre et auteur. - Enlevez
selfd'une méthode et appelez-la. Combien d'arguments le message indique-t-il avoir reçus ? - Faites un
Booket écrivezbook.pagse = 100(mal orthographié exprès). Y a-t-il une erreur ? Puisprint(book.__dict__)— que voyez-vous ?
Ce dernier point est important car il montre la limite d'une classe. Un mauvais nom lève une erreur à la lecture, pas à l'écriture — définir un nouvel attribut est toujours permis. Le prochain chapitre montre que les annotations de type et un vérificateur de types peuvent combler cet écart aussi.
Étape 6 sur 6
Défi — le quiz du chapitre
Dix questions, de la plus facile à la plus difficile. Les dernières sont difficiles exprès.
Connectez-vous pour faire le quiz