Python Language Motivation


Exemple

Vous venez donc de créer votre première classe en Python, une petite classe intéressante qui encapsule une carte à jouer:

class Card:
    def __init__(self, suit, pips):
        self.suit = suit
        self.pips = pips

Ailleurs dans votre code, vous créez quelques instances de cette classe:

ace_of_spades = Card('Spades', 1)
four_of_clubs = Card('Clubs',  4)
six_of_hearts = Card('Hearts', 6)

Vous avez même créé une liste de cartes afin de représenter une "main":

my_hand = [ace_of_spades, four_of_clubs, six_of_hearts]

Maintenant, pendant le débogage, vous voulez voir à quoi ressemble votre main, alors vous faites ce qui vient naturellement et écrivez:

print(my_hand)

Mais ce que vous obtenez est un tas de charabia:

[<__main__.Card instance at 0x0000000002533788>, 
 <__main__.Card instance at 0x00000000025B95C8>, 
 <__main__.Card instance at 0x00000000025FF508>]

Confus, vous essayez d'imprimer une seule carte:

print(ace_of_spades)

Et encore une fois, vous obtenez cette sortie bizarre:

<__main__.Card instance at 0x0000000002533788>

N'ai pas peur. Nous sommes sur le point de résoudre ce problème.

Tout d'abord, il est important de comprendre ce qui se passe ici. Lorsque vous avez écrit print(ace_of_spades) vous avez dit à Python que vous souhaitiez imprimer des informations sur l'instance Card votre code appelle ace_of_spades . Et pour être juste, ça l'a fait.

Cette sortie est composée de deux bits importants: le type de l'objet et son id . La deuxième partie seule (le nombre hexadécimal) est suffisante pour identifier de manière unique l'objet au moment de l'appel d' print . [1]

Ce qui s'est vraiment passé, c'est que vous avez demandé à Python de "mettre en mots" l'essence de cet objet et de vous l'afficher ensuite. Une version plus explicite de la même machine pourrait être:

string_of_card = str(ace_of_spades)
print(string_of_card)

Dans la première ligne, vous essayez de transformer votre instance de Card en chaîne et, dans le second, vous l’affichez.

Le problème

Le problème que vous rencontrez est dû au fait que, même si vous avez dit à Python tout ce qu'il fallait savoir sur la classe Card pour créer des cartes, vous ne lui avez pas indiqué comment convertir les instances Card en chaînes.

Et comme il ne savait pas, lorsque vous (implicitement) avez écrit str(ace_of_spades) , cela vous a donné ce que vous avez vu, une représentation générique de l'instance de la Card .

La solution (partie 1)

Mais nous pouvons dire à Python comment nous voulons que les instances de nos classes personnalisées soient converties en chaînes. Et la façon dont nous le faisons est avec la __str__ "dunder" (pour double-underscore) ou "magic".

Chaque fois que vous demandez à Python de créer une chaîne à partir d'une instance de classe, celle-ci recherche une méthode __str__ sur la classe et l'appelle.

Considérez la version mise à jour suivante de notre classe de Card :

class Card:
    def __init__(self, suit, pips):
        self.suit = suit
        self.pips = pips

    def __str__(self):
        special_names = {1:'Ace', 11:'Jack', 12:'Queen', 13:'King'}

        card_name = special_names.get(self.pips, str(self.pips))

        return "%s of %s" % (card_name, self.suit)

Ici, nous avons maintenant défini la méthode __str__ sur notre classe Card qui, après une simple recherche dans les dictionnaires, retourne une chaîne formatée selon notre __str__ .

(Notez que "return" est en gras ici, pour souligner l’importance de retourner une chaîne, et non simplement l’imprimer. L'impression peut sembler fonctionner, mais la carte est imprimée lorsque vous faites quelque chose comme str(ace_of_spades) , sans même avoir un appel de fonction d’impression dans votre programme principal. Pour être clair, assurez-vous que __str__ renvoie une chaîne.).

La méthode __str__ est une méthode, donc le premier argument sera self et il ne devrait ni accepter, ni transmettre des arguments supplémentaires.

Revenons à notre problème d'affichage de la carte d'une manière plus conviviale, si nous courons à nouveau:

ace_of_spades = Card('Spades', 1)
print(ace_of_spades)

Nous verrons que notre production est bien meilleure:

Ace of Spades

Tellement génial, nous avons fini, non?

Eh bien, juste pour couvrir nos bases, vérifions que nous avons résolu le premier problème rencontré, en imprimant la liste des instances de la Card , la hand .

Nous vérifions donc le code suivant:

my_hand = [ace_of_spades, four_of_clubs, six_of_hearts]
print(my_hand)

Et, à notre grande surprise, nous obtenons à nouveau ces codes hexadécimaux amusants:

[<__main__.Card instance at 0x00000000026F95C8>, 
 <__main__.Card instance at 0x000000000273F4C8>, 
 <__main__.Card instance at 0x0000000002732E08>]

Que se passe-t-il? Nous avons dit à Python comment nous voulions que nos instances de Card soient affichées, pourquoi semble-t-il apparemment oublier?

La solution (partie 2)

Eh bien, la machine en arrière-plan est un peu différente lorsque Python veut obtenir la représentation sous forme de chaîne des éléments dans une liste. Il s'avère que Python ne se soucie pas de __str__ à cette fin.

Au lieu de cela, il recherche une méthode différente, __repr__ , et si ce n'est pas trouvé, il retombe sur la "chose hexadécimale". [2]

Donc, vous dites que je dois faire deux méthodes pour faire la même chose? Une pour quand je veux print ma carte seule et une autre quand elle est dans une sorte de conteneur?

Non, mais d'abord, regardons ce que serait notre classe si nous implémentions les méthodes __str__ et __repr__ :

class Card:
    special_names = {1:'Ace', 11:'Jack', 12:'Queen', 13:'King'}

    def __init__(self, suit, pips):
        self.suit = suit
        self.pips = pips

    def __str__(self):
        card_name = Card.special_names.get(self.pips, str(self.pips))
        return "%s of %s (S)" % (card_name, self.suit)

    def __repr__(self):
        card_name = Card.special_names.get(self.pips, str(self.pips))
        return "%s of %s (R)" % (card_name, self.suit)

Ici, l'implémentation des deux méthodes __str__ et __repr__ sont exactement les mêmes, sauf que pour différencier les deux méthodes, (S) est ajouté aux chaînes renvoyées par __str__ et (R) est ajouté aux chaînes renvoyées par __repr__ .

Notez que, tout comme notre méthode __str__ , __repr__ n'accepte aucun argument et renvoie une chaîne.

Nous pouvons voir maintenant quelle méthode est responsable de chaque cas:

ace_of_spades = Card('Spades', 1)
four_of_clubs = Card('Clubs',  4)
six_of_hearts = Card('Hearts', 6)

my_hand = [ace_of_spades, four_of_clubs, six_of_hearts]

print(my_hand)           # [Ace of Spades (R), 4 of Clubs (R), 6 of Hearts (R)]

print(ace_of_spades)     # Ace of Spades (S)

Comme nous l'avons vu, la méthode __str__ été appelée lors de l' print notre instance Card et la méthode __repr__ été appelée lorsque nous avons transmis une liste de nos instances à print .

À ce stade , il est intéressant de souligner que , tout comme nous pouvons créer explicitement une chaîne à partir d' une instance de classe personnalisée en utilisant str() comme nous l'avons fait précédemment, nous pouvons aussi créer explicitement une représentation de chaîne de notre classe avec une fonction intégrée appelée repr() .

Par exemple:

str_card = str(four_of_clubs)
print(str_card)                     # 4 of Clubs (S)

repr_card = repr(four_of_clubs)
print(repr_card)                    # 4 of Clubs (R)

De plus, si elle est définie, nous pourrions appeler les méthodes directement (même si cela semble peu clair et inutile):

print(four_of_clubs.__str__())     # 4 of Clubs (S)

print(four_of_clubs.__repr__())    # 4 of Clubs (R)

À propos de ces fonctions dupliquées ...

Les développeurs Python ont réalisé, dans le cas où vous vouliez que des chaînes identiques soient renvoyées par str() et repr() vous pourriez avoir à dupliquer de manière fonctionnelle des méthodes - quelque chose que personne n'aime.

Au lieu de cela, il existe un mécanisme pour éliminer ce besoin. Un que je vous ai fait passer jusqu'à ce point. Il s'avère que si une classe implémente la méthode __repr__ mais pas la méthode __str__ , et que vous passez une instance de cette classe à str() (implicitement ou explicitement), Python utilisera votre implémentation __repr__ .

Donc, pour être clair, considérez la version suivante de la classe Card :

class Card:
    special_names = {1:'Ace', 11:'Jack', 12:'Queen', 13:'King'}

    def __init__(self, suit, pips):
        self.suit = suit
        self.pips = pips

    def __repr__(self):
        card_name = Card.special_names.get(self.pips, str(self.pips))
        return "%s of %s" % (card_name, self.suit)

Notez que cette version implémente uniquement la méthode __repr__ . Néanmoins, les appels à str() donnent lieu à la version conviviale:

print(six_of_hearts)            # 6 of Hearts  (implicit conversion)
print(str(six_of_hearts))       # 6 of Hearts  (explicit conversion)

comme le font les appels à repr() :

print([six_of_hearts])          #[6 of Hearts] (implicit conversion)
print(repr(six_of_hearts))      # 6 of Hearts  (explicit conversion)

Résumé

Pour que vous puissiez autoriser vos instances de classe à "se montrer" de manière conviviale, vous devez envisager d'implémenter au moins la méthode __repr__ votre classe. Si la mémoire est __repr__ Raymond Hettinger a dit lors d'une conversation que la mise en œuvre des classes avec __repr__ est l'une des premières choses qu'il cherche à faire lors de la révision du code Python. La quantité d'informations que vous auriez pu ajouter aux instructions de débogage, aux rapports d'erreur ou aux fichiers journaux avec une méthode simple est écrasante par rapport aux informations dérisoires et souvent peu utiles (type, id) fournies par défaut.

Si vous souhaitez des représentations différentes pour, par exemple, à l’intérieur d’un conteneur, vous devez implémenter les méthodes __repr__ et __str__ . (Plus d'informations sur la manière d'utiliser ces deux méthodes différemment ci-dessous).