Comment inspecter les objets en Python ? 9 fonctions à connaître

Pour améliorer considérablement votre manière de développer

Python est un langage orienté objet représentatif, dans lequel tout est organisé autour d’objets – classes intégrées et personnalisées, instances, modules et fonctions. Lorsque nous traitons avec des objets, il est souvent nécessaire de vérifier ce qu’ils sont afin de pouvoir les utiliser en conséquence.

Dans cet article, j’aimerais présenter neuf fonctions permettant d’inspecter les objets sous divers aspects. Il est important de noter que, le cas échéant, j’aborderai également les caractéristiques pertinentes liées à chaque fonction.

Sans plus tarder, commençons.

1. print

Pendant la phase de développement, nous pouvons utiliser la fonction print pour inspecter un objet ou ses attributs spécifiques dans la console. Le code suivant vous montre le message d’impression pour certaines instances de types de données intégrés :

numbers_tuple = (1, 2, 3)
numbers_dict = {1: "one", 2: "two", 3: "three"}
>>> print(numbers_tuple)
(1, 2, 3)
>>> print(numbers_dict)
{1: 'one', 2: 'two', 3: 'three'}

Comme indiqué ci-dessus, les messages imprimés ressemblent aux données d’origine. Cependant, lorsque vous imprimez une instance d’une classe personnalisée, l’aspect peut être différent :

class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id

>> student = Student("John Smith", 19302)
>>> print(student)
<__main__.Student object at 0x7fce50a3fd30>

Il ne vous montre pas les données que l’instance stocke. Au lieu de cela, elle vous indique seulement qu’il s’agit d’un objet instance de Student à l’adresse mémoire spécifiée. Comment faire en sorte que la fonction print fonctionne avec une classe personnalisée ? Vous devez définir la méthode __str__ dans la classe :

class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
    def __str__(self):
        return f"Student #{self.student_id}: {self.name}"
>> student = Student("John Smith", 19302)
>>> print(student)
Student #19302: John Smith

Pour la méthode __str__, nous retournons une valeur de chaîne de caractères qui décrit l’objet de l’instance. Dans ce cas, les informations clés sont le nom et le numéro d’identification de l’étudiant. Après avoir implémenté __str__, l’appel à print sur cet objet d’instance permet d’imprimer des données plus significatives.

2. type

La fonction intégrée de type renvoie le nom de la classe qui implémente l’objet. Pour les objets d’instance d’une classe, vous obtiendrez la classe correspondante.

> type(numbers_tuple)
<class 'tuple'>
>>> type(numbers_dict)
<class 'dict'>

Lorsque vous comparez le type d’une instance avec un type spécifique, vous devez utiliser is comme comparaison au lieu de ==. Quelques exemples sont présentés ci-dessous :

>>> type(numbers_tuple) is tuple
True
>>> type(numbers_dict) is dict
True

Veuillez noter que bien que vous obteniez les mêmes résultats d’évaluation si vous utilisez ==, les types sont considérés comme des objets singletons et vous devriez utiliser le test d’identité (is).

Vous pouvez comparer le type d’une instance avec un type spécifique, comme indiqué ci-dessus. Cependant, ce n’est généralement pas la meilleure idée. Nous préférons plutôt utiliser la fonction suivante – isinstance, lorsque nous voulons savoir si un objet est une instance d’une classe.

3. isinstance

La fonction isinstance renvoie une valeur booléenne pour indiquer qu’un objet est une instance d’une classe. Quelques exemples sont présentés ci-dessous :

> isinstance(numbers_tuple, tuple)
True
>>> isinstance(numbers_dict, dict)
True

Notamment, vous ne pouvez pas spécifier qu'une seule classe - comme une fonctionnalité pratique, vous pouvez spécifier plusieurs classes si nécessaire, comme indiqué ci-dessous :

>> isinstance(numbers_tuple, (list, tuple, set))
True

Veuillez noter que si l’objet est une instance de n’importe quelle classe dans les classes spécifiées, et l’équivalent du code ci-dessus est montré ci-dessous et l’évaluation est True.

isinstance(numbers_tuple, list) or isinstance(numbers_tuple, tuple) or isinstance(numbers_tuple, set)
Une raison importante pour laquelle vous préférez utiliser isinstance plutôt que la comparaison de types est qu’il peut gérer la sous-classification dès le départ. C’est-à-dire que lorsque vous créez un objet d’instance d’une sous-classe, isinstance évalue également True, si vous vérifiez si l’objet est une instance de la super-classe, comme indiqué ci-dessous :

class CollegeStudent(Student):
    pass
>>> college_student = CollegeStudent("John Smith", 19302)
>>> isinstance(college_student, Student)
True

En revanche, la fonction de type ne peut pas gérer directement la comparaison impliquant le sous-classement, comme indiqué ci-dessous, et elle est donc moins flexible.

>>> type(college_student) is Student
False

4. issubclass

Puisque nous venons de parler de sous-classement, vous devez savoir que nous pouvons utiliser issubclass pour déterminer si une classe est une sous-classe de la superclasse. L’utilisation devrait être très simple ; elle utilise ce qui suit :

>> issubclass(CollegeStudent, Student)
True

Le seul point délicat de cette fonction est de savoir quel argument passe en premier – la sous-classe ou la superclasse. Mon conseil est que vous faites simplement une phrase : Je veux utiliser issubclass pour vérifier si a est une sous-classe de b, et en utilisant cet ordre, nous aurons ce qui suit :

issubclass(a, b)

5. callable

Nous avons parlé des classes. Comme vous le savez peut-être, les classes sont une sorte de callable, ce qui signifie que nous pouvons appeler la classe à l’aide de l’opérateur d’appel – une paire de parenthèses. En général, si un objet peut être appelé, on dit qu’il est callable. Pour vérifier si un objet est callable, nous pouvons utiliser la fonction callable.

>> callable(tuple)
True
>>> callable(Student)
True

La raison pour laquelle une classe est appelable est que nous pouvons appeler la classe pour construire une instance. Un autre type important de callable est celui des fonctions, comme indiqué ci-dessous :

def triple(number):
    return number * 3
>> callable(triple)
True

Outre les classes et les fonctions, vous pouvez même rendre une instance d’une classe personnalisée appelable en surchargeant la méthode __call__, comme illustré ci-dessous :

class Student:
    def __init__(self, name, student_id):
        self.name = name
        self.student_id = student_id
    def __call__(self):
        print(f"{self.name} completed the semester.")
>>> student = Student("John Smith", 19302)
>>> student()
John Smith completed the semester.

6. hashable

Puisque nous avons parlé de callable, il est raisonnable de parler d’un autre -able – hashable. Hashable indique si un objet peut être haché. Mais que signifie hacher ? Hacher signifie que vous utilisez une fonction prédéfinie (appelée fonction de hachage) pour convertir un objet en une autre représentation appelée valeurs de hachage ou simplement hachages. En Python, les valeurs de hachage sont des entiers. Vous pouvez obtenir les valeurs de hachage à l’aide de la fonction de hachage, comme indiqué ci-dessous :

>>> hash(numbers_tuple)
529344067295497451

Please note that this number can be different on your computer.

Notably, not all objects are hashable. For the built-in data types, the mutable objects, such as a dict, are unhashable.

>>> hash(numbers_dict)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'dict'

La raison en est que les valeurs de hachage sont des correspondances biunivoques avec leurs données d’origine. Pour les objets mutables, s’ils étaient hachables, disons que vous obtenez une valeur de hachage initialement. Plus tard, vous mettez à jour l’objet, l’objet mis à jour devrait avoir une valeur de hachage différente, ce qui viole l’exigence de correspondance biunivoque. Ainsi, pour résoudre ce dilemme, Python exige que l’on ne puisse pas hacher un objet mutable.

Ainsi, pour inspecter la possibilité de hachage d’un objet, il suffit d’appeler la fonction de hachage sur l’objet. S’il est hachable, vous obtiendrez la valeur de hachage – sinon, vous rencontrerez une exception TypeError.

Si vous souhaitez inspecter directement la capacité de hachage d’un objet sans vous fier à une éventuelle levée d’exception, vous pouvez utiliser l’alternative
suivante :

from collections.abc import Hashable
> isinstance(numbers_tuple, Hashable)
True
>>> isinstance(numbers_dict, Hashable)
False

Le Hashable représente une classe abstraite, qui définit un comportement générique pour les classes dont les objets d’instance peuvent être hachés.

7. id

Lorsque vous souhaitez obtenir l’identité d’un objet, vous pouvez utiliser la fonction id, qui renvoie l’adresse mémoire de l’objet. Voir quelques exemples ci-dessous :

>> id(numbers_tuple)
140524092985536
>>> id(numbers_dict)
140522741441408

À tout moment, chaque adresse mémoire doit identifier un objet de manière unique. C’est pourquoi on peut utiliser l’adresse mémoire pour vérifier l’identité. Si deux objets ont la même adresse mémoire, il s’agit du même objet :

if id(a) == id(b):
assert a is b

Outre l’obtention de l’identité d’un objet, la fonction id peut être utile pour connaître la différence entre une copie superficielle et une copie profonde, ce qui peut être délicat lorsque vous traitez des objets mutables, tels que des listes.

Pour une copie superficielle, vous obtiendrez une copie de la liste la plus haute, mais pas des listes internes. Prenons l’exemple suivant :

from copy import copy, deepcopy

numbers = [[1, 2, 3], [4, 5, 6]]
numbers_copy = copy(numbers)

assert id(numbers) != id(numbers_copy)
assert numbers is not numbers_copy

Ici, j’utilise la fonction copy du module copy au lieu de la méthode copy d’une liste, car le module copy peut être utilisé pour copier des objets autres que des listes et a une meilleure généricité. Pour une copie superficielle, les listes internes ne sont pas copiées. Observez cet effet ci-dessous :

assert id(numbers[0]) == id(numbers_copy[0])
assert id(numbers[1]) == id(numbers_copy[1])

En revanche, une copie profonde créera une copie distincte pour les listes internes en plus de la liste la plus éloignée :

numbers_deepcopy = deepcopy(numbers)

assert id(numbers) != id(numbers_deepcopy)
assert id(numbers[0]) != id(numbers_deepcopy[0])
assert id(numbers[1]) != id(numbers_deepcopy[1])

8. hasattr

Si vous voulez vérifier si un objet possède un attribut spécifique, vous pouvez utiliser la fonction hasattr. Quelques exemples sont présentés ci-dessous :

Le choix du style de codage est lié à cette fonction : LBYL (regarde avant de sauter) vs EAFP (il est plus facile de demander pardon que de demander la permission). Lorsque vous utilisez la fonction hasattr pour vérifier l’attribut particulier d’un objet, il s’agit du style LBYL : suis-je sûr de pouvoir effectuer une telle opération sur cet objet, comme ci-dessous ?

if hasattr(obj, "specific_attr"):
# do something with the obj
else:
# can't do such with the object

En revanche, lorsque vous utilisez l’EAFP, vous exécutez simplement l’opération directement et vous gérez l’exception éventuelle lorsque cette opération n’est pas possible avec l’objet, comme ci-dessous :

try:
obj.specific_attr() # suppose it's a method call
except AttributeError:
# can't do such with the object

Comme indiqué ci-dessus, nous utilisons l’instruction try…except…, dans laquelle la clause try tente d’exécuter le code qui pourrait ne pas fonctionner. Si une exception est levée, dans ce cas une AttributeError, nous la traitons dans la clause except.

En général, nous préférons utiliser le style EAFP, car il est généralement plus rapide que le style LBYL, qui exige la vérification de la condition avant de pouvoir effectuer l’opération. En revanche, l’EAFP l’exécute simplement sans avoir besoin de vérifier la condition.

9. dir

Si vous souhaitez connaître toutes les méthodes et tous les attributs d’un objet, vous pouvez utiliser la fonction dir. Lorsque vous travaillez avec un objet d’un nouveau type, par exemple dans une bibliothèque tierce, et que vous ne connaissez pas les méthodes et les attributs disponibles pour ce type, vous pouvez utiliser cette fonction.

Par exemple, nous utilisons souvent pandas pour le traitement des données, et la structure de données clé est DataFrame, et nous pouvons découvrir ce que nous pouvons faire avec un objet DataFrame. Veuillez noter que je ne fournis qu’une liste abrégée des attributs, car elle est trop longue !

>>> import pandas as pd
>>> df = pd.DataFrame()
>>> dir(df)
['T', '_AXIS_LEN', '_AXIS_ORDERS', '_AXIS_REVERSED', '_AXIS_TO_AXIS_NUMBER', '_HANDLED_TYPES', '__abs__', '__add__', '__and__', '__annotations__', ..., 'value_counts', 'values', 'var', 'where', 'xs']

Lorsque la liste est excessivement longue, comment pouvons-nous l’utiliser ? Supposons que je veuille trier les données, mais vous pouvez simplement filtrer les attributs qui contiennent le mot « tri », comme indiqué ci-dessous :

> [x for x in dir(df) if x.find("sort") >= 0]
['sort_index', 'sort_values']

La liste étant beaucoup plus courte, il vous suffit de consulter les informations relatives à ces deux méthodes pour vous familiariser avec le tri des objets DataFrame.

Conclusion

Les objets sont au cœur de toute application. Si vous maîtrisez ces fonctions, vous n’aurez aucun problème à inspecter les objets et à appliquer des opérations spécifiques en conséquence.

J’espère que vous avez apprécié cet article.

[Cet article est une traduction de l’article « How To Inspect Objects in Python? 9 Functions To Know » écrit par Yong Cui]


Laisser un commentaire

fr_FRFrançais