Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Orientation de l’objet et classes

Leverage the power of Python by writing new classes. For interactive reading and executing code blocks Binder and find b09-classes.ipynb, or install Python and JupyterLab locally.

La classe des classes

Python est un langage intrinsèquement orienté objet et rend le déploiement de classes et d’objets extrêmement facile. Ce chapitre introduit le concept de classes Python et commence par des définitions essentielles.

Qu’est-ce que la programmation orientée objet (OPO)?

La programmation orientée objet est un paradigme de programmation qui aligne l’architecture du logiciel sur la réalité. L’orientation de l’objet commence par la conception du logiciel, où un modèle structuré est établi. Le modèle structuré contient des informations sur les objets et leurs abstractions. Le développement et la mise en œuvre de logiciels orientés objet nécessitent une façon structurée de penser et une compréhension conceptuelle des classes, de l’héritage, du polymorphisme et de l’encapsulation.

Objets et catégories

Dans le langage informatique, un objet est une instance qui contient des données sous forme de champs (appelés attributs ou propriétés) et de code sous forme de fonctionnalités (fonctions ou méthodes). Dans Python, un objet est une instance d’une classe. Une classe définit le comportement et peut agir comme un plan pour ses instances. Les attributs stockent l’état de l’objet, et les méthodes sont des fonctions définies sur la classe qui fonctionne sur les instances. Par convention, le premier paramètre d’une méthode d’instance s’appelle self; Python fournit l’instance pour cet argument lorsque la méthode est appelée par une instance.

Les classes elles-mêmes sont des objets et consomment de la mémoire. Une classe peut définir __init__ pour initialiser une nouvelle instance, mais une méthode explicite __init__ n’est pas requise.

La forme la plus simple d’une classe en Python comprend seulement quelques énoncés fondamentaux, et il est fortement recommandé d’ajouter une instruction __init__ dans laquelle les variables de classe sont définies. Nous reviendrons sur la déclaration __init__ plus tard dans la section sur les méthodes magic. L’exemple suivant montre l’une des structures de classe les plus simples possibles avec une méthode __init__. Notez l’utilisation de self dans la classe, qui devient object_name.attribute pour les cas de la classe IceCream.

class IceCream:
    def __init__(self, *args, **kwargs):
        self.flavors=["vanilla", "chocolate", "bread"]
    
    def add_flavor(self, flavor):
        self.flavors.append(flavor)
        
    def print_flavors(self):
        print(", ".join(self.flavors))

# create an instance of IceCream and use the print_flavors method
some_scoops = IceCream()
some_scoops.add_flavor("lemon")

# the following statements have similar effects
some_scoops.print_flavors()
print(some_scoops.flavors)
vanilla, chocolate, bread, lemon
['vanilla', 'chocolate', 'bread', 'lemon']

Héritage

Le Cambridge Dictionary définit l’héritage (biologie) comme “Caractéristiques particulières reçues des parents par les gènes”. De même, l’héritage dans l’OOP décrit la relation hiérarchique entre les classes avec est-un-type-de relations. Par exemple, une classe appelée Salmon peut hériter d’une classe appelée Fish. Dans ce cas, Fish est la classe parent (ou super-classe) et Salmon est la classe enfant (ou sous-classe), où Fish pourrait définir des attributs comme preferred_flow_depth ou preferred_flow_velocity avec des méthodes de fuzzification décrivant d’autres préférences d’habitat. Un tel héritage de classe pourrait ressembler à ceci:

# define the parent class Fish
class Fish:
  def __init__(self, *args, **kwargs):
        self.preferred_flow_depth = float()
        self.preferred_flow_velocity = float()
        self.species = ""
        self.xy_position = tuple()
        
  def print_habitat(self):
        print("The species {0} prefers {1}m deep and {2}m/s fast flowing waters.".format(self.species, str(self.preferred_flow_depth), str(self.preferred_flow_velocity)))
        
  def swim_to_position(self, new_position=()):
        self.xy_position = new_position


# define the child class Salmon, which inherits (is-a-type-of) from Fish
class Salmon(Fish):
  def __init__(self, species, *args, **kwargs):
	  super().__init__(*args, **kwargs)
	  self.family = "Salmonidae"
	  self.species = species

        
  def habitat_function(self, depth, velocity):
        self.preferred_flow_depth = depth
        self.preferred_flow_velocity = velocity

atlantic_salmon = Salmon("Salmo salar")
atlantic_salmon.habitat_function(depth=0.4, velocity=0.5)
atlantic_salmon.print_habitat()

pacific_salmon = Salmon("Oncorhynchus tshawytscha")
pacific_salmon.habitat_function(depth=0.6, velocity=0.8)
pacific_salmon.print_habitat()
The species Salmo salar prefers 0.4m deep and 0.5m/s fast flowing waters.
The species Oncorhynchus tshawytscha prefers 0.6m deep and 0.8m/s fast flowing waters.

Polymorphisme

Le polymorphisme permet à différents types d’objets de supporter la même interface. Une sous-classe qui remplace une méthode héritée en est un exemple. Python utilise également couramment le typage du canard : des classes non liées peuvent être utilisées par la même opération lorsqu’elles fournissent les méthodes ou attributs requis. Par exemple, le polymorphisme dans Python est quand nous redéfinissons la fonction swim_to_position de la classe de parent ci-dessus montrée Fish dans la classe d’enfant Salmon.

Encapsulation (Attributs publics et non publics)

Le concept d’encapsulation combine les données et les fonctions pour manipuler les données, en vertu desquelles les données et les fonctions sont protégées contre les interférences et les manipulations externes. L’encapsulation est également la référence de data cached en informatique, qui sépare les décisions de conception dans les logiciels concernant les objets susceptibles de changer.

L’un des aspects les plus importants de l’encapsulation est la différenciation entre les variables private et public class. Un attribut private ne peut pas être modifié de l’extérieur (c’est-à-dire qu’il est protégé et ne peut pas être modifié pour une instance d’une classe). Python n’applique pas vraiment les attributs d’instance privée. Un nom tel que _family est non-public par convention. Un nom tel que __family est name-mangled pour inclure le nom de classe, principalement pour réduire les affrontements accidentels dans les sous-classes; il peut toujours être consulté délibérément. Les propriétés peuvent fournir des fonctions de lecture, d’écriture et de suppression gérées via getter, setter et deleter.

Dans l’exemple ci-dessus de la classe Salmon, nous utilisons une variable publique appelée self.family. Cependant, l’attribut famille de la classe Salmon est un attribut qui ne devrait pas être modifiable. Un comportement similaire serait souhaitable pour un attribut appelé self.aggregate_state = "frozen" de la classe IceCream. Pour se familiariser avec le concept, le bloc de code suivant définit un autre enfant de la classe Fish avec un attribut non public __family. L’attribut __family n’est pas directement accessible pour les cas de la nouvelle classe enfant appelée Carp. Néanmoins, nous voulons que la classe Carp ait un attribut family et nous voulons pouvoir imprimer sa valeur. C’est pourquoi nous avons besoin d’une méthode spéciale def family(self), qui a le même nom que l’attribut protégé et un décorateur @property. L’exemple ci-dessous comporte une méthode spéciale supplémentaire appelée def family(self, value) qui est embrassée avec un décorateur @property.setter et qui permet de redéfinir la propriété non publique __family (même si c’est logiquement absurde parce que nous ne voulons pas permettre de renommer la propriété __family).

class Carp(Fish):
    def __init__(self, species, *args, **kwargs):
        Fish.__init__(self)
        self.__family = "cyprinidae"
        self.species = species
        
    @property
    def family(self):
        return self.__family
    
    @family.setter
    def family(self, value):
        self.__family = value
        print("family set to \'%s\'" % self.__family)
        
        
european_carp = Carp("Cyprinus carpio carpio")
print(european_carp.family)

try:
    print(european_carp.__family)
except AttributeError:
    print("__family is not directly accessible.")

# re-definition of __family through @family.setter method
european_carp.family="lamnidae"
cyprinidae
__family is not directly accessible.
family set to 'lamnidae'

Décors

Le décorateur @property définit un getter, donc european_carp.family lit la valeur retournée par la méthode. Le décorateur @family.setter définit ce qui se passe lorsque le code client exécute une mission comme european_carp.family = "Lamnidae".

Jusqu’ici, nous ne connaissons que les décorateurs comme un moyen efficace de simplifier les fonctions. Cependant, les décorateurs sont un outil encore plus puissant dans la programmation de classes orientées objet, dans lequel les décorateurs peuvent être utilisés pour envelopper des méthodes de classe similaires aux fonctions. Définons un autre enfant de la classe Fish pour explorer le décorateur @property avec ses méthodes deleter, getter et setter.

class Bullhead(Fish):
  def __init__(self, species, *args, **kwargs):
	  Fish.__init__(self)
	  self.__family = "cottidae"
	  self.species = species
	  self.__length = 7.0
        
  @property
  def length(self):
	  return self.__length

  @length.setter
  def length(self, value):
	  try:
		  self.__length = float(value)
	  except (TypeError, ValueError):
		  print("Error: Value is not a real number.")

  @length.deleter
  def length(self):
	  del self.__length

# Read through the getter, assign through the setter, and delete through the deleter

        
european_bullhead = Bullhead("Cottus gobio")

# make use of @property.getter, which directly results from the @property-embraced def length method
print(european_bullhead.length)

# make use of @property.setter method
european_bullhead.length = 6.5
print(european_bullhead.length)

# make use of @property.delete method
del european_bullhead.length
try:
    print(european_bullhead.length)
except AttributeError:
    print("Error: You cannot print a nonexistent property.")
7.0
6.5
Error: You cannot print a nonexistent property.

Surcharge et méthodes magiques

Les exemples ci-dessus ont introduit une méthode spéciale ou magique appelée __init__. Nous avons déjà vu que __init__ n’est rien de magique en soi et il y a beaucoup d’autres méthodes prédéfinies dans Python. Avant d’arriver aux méthodes magiques, il est important de comprendre le concept de surcharge en Python. Vous êtes-vous déjà demandé pourquoi le même opérateur peut avoir des effets différents selon le type de données? Par exemple, l’opérateur + concatenates strings, mais résume les types de données numériques :

a_string = "vanilla"
b_string = "cream"
print("+ operator applied to strings: " + str(a_string + b_string))

a_number = 50
b_number = 30
print("+ operator applied to integers: " + str(a_number + b_number))
+ operator applied to strings: vanillacream
+ operator applied to integers: 80

Ce comportement est appelé operator (ou fonction) surcharge en Python et surcharge est possible en raison de noms prédéfinis de méthodes magiques en Python. Maintenant, nous sommes prêts à plonger dans la piscine de méthodes magiques.

Les méthodes magiques sont l’un des éléments clés qui rendent Python facile et clair à utiliser. En raison de leur déclaration à l’aide de doubles soulignés (__this_is_magic__), les méthodes magiques sont également appelées dunder (double underscore). Les méthodes magiques sont des méthodes spéciales avec des noms fixes et leur magic nom est parce qu’ils n’ont pas besoin d’être directement invoqués. Dans les coulisses, Python utilise constamment des méthodes magiques, par exemple lorsqu’une nouvelle instance d’une classe est attribuée : Quand var = MyClass() est évalué, Python appelle MyClass.__new__ pour créer une instance. Si __new__ retourne une instance de MyClass, Python appelle alors MyClass.__init__ pour l’initialiser. Pour a + b, Python essaie d’abord a.__add__(b); si cela retourne NotImplemented, Python peut essayer l’opération réfléchie b.__radd__(a) avant de soulever TypeError.

Les sections suivantes énumèrent certaines méthodes de magie documentées pour une utilisation dans les classes et les paquets. Ce ne sont que quelques-unes des méthodes magiques les plus courantes et des objets ou attributs magiques plus documentés existent.

Opérateur (binaire) et méthodes d’assignation

Pour toute nouvelle classe que nous voulons pouvoir traiter avec un opérateur (par exemple, pour permettre de résumer des objets avec result = object1 + object2), nous devons implémenter (surcharger) les méthodes suivantes.

Operator (1)Method (1)Operator (2)Method (2)
+object.__add__(self, other)+=object.__iadd__(self, other)
-object.__sub__(self, other)-=object.__isub__(self, other)
*object.__mul__(self, other)*=object.__imul__(self, other)
//object.__floordiv__(self, other)//=object.__ifloordiv__(self, other)
/object.__truediv__(self, other)/=object.__itruediv__(self, other)
%object.__mod__(self, *args, **kwargs)%=object.__imod__(self, *args, **kwargs)
**object.__pow__(self, *args, **kwargs)**=object.__ipow__(self, *args, **kwargs)
<<object.__lshift__(self, *args, **kwargs)<<=object.__ilshift__(self, *args, **kwargs)
>>object.__rshift__(self, *args, **kwargs)>>=object.__irshift__(self, *args, **kwargs)
&object.__and__(self, *args, **kwargs)&=object.__iand__(self, *args, **kwargs)
^object.__xor__(self, *args, **kwargs)^=object.__ixor__(self, *args, **kwargs)
|object.__or__(self, *args, **kwargs)|=object.__ior__(self, *args, **kwargs)

Méthodes de l’opérateur (inactif) et du comparateur

De plus, les opérateurs ponctuels ou comparés peuvent être définis ou surchargés. Les opérateurs uniques ne traitent qu’une seule entrée, contrairement aux opérateurs binaires susmentionnés. Un opérateur unique agit sur un seul opérande, comme la négation -x, unary plus +x, ou l’inversion bitwise ~x (à noter que Python n’a pas ++ ou -- incrément/décret opers). De plus, les opérateurs comparatifs (comparateurs) utilisent des méthodes magiques, comme __ne__, comme synonyme pour not equal.

OperatorMethodComparatorMethod
-object.__neg__(self)<object.__lt__(self, *args, **kwargs)
+object.__pos__(self)<=object.__le__(self, *args, **kwargs)
abs()object.__abs__(self)==object.__eq__(self, *args, **kwargs)
~object.__invert__(self)!=object.__ne__(self, *args, **kwargs)
complex()object.__complex__(self)>=object.__ge__(self, *args, **kwargs)
int()object.__int__(self)>object.__gt__(self, *args, **kwargs)
float()object.__float__(self)

Un résumé complet et inclusif des méthodes magiques est fourni dans le Python docs.

Néanmoins, vous vous demandez peut-être comment, dans la pratique, une classe ressemble à qui est capable d’utiliser, par exemple, l’opérateur + avec une méthode __add__? À cette fin, définissons un autre enfant de la classe Fish pour construire un essaim :

class Mackerel(Fish):
    def __init__(self, species, *args, **kwargs):
        Fish.__init__(self)
        self.__family = "scombridae"
        self.species = species
        self.count = 1
        
    def __add__(self, value):
        self.count += value
        return self.count
    
    def __mul__(self, multiplier):
        self.count *= multiplier
        return self.count
        
atlantic_mackerel = Mackerel("Scomber scombrus")
print(atlantic_mackerel + 1)
print(atlantic_mackerel * 10)
2
20

Modèle de classe Python personnalisé

Cette section comporte un modèle pour une classe Python3 personnalisée. Le modèle peut être étendu avec des propriétés publiques et non publiques, et des personnalisations de méthodes magiques pour permettre l’utilisation d’opérateurs tels que + ou <=. En fin de compte, il existe de nombreuses options pour écrire une classe personnalisée, mais toutes les classes personnalisées devraient au moins intégrer les méthodes suivantes:

  • __init__(self, [...) est l’initialisateur de classe (magique), qui est appelé quand une instance de la classe est créée. Plus précisément, il est appelé avec la méthode __new__(cls, [...), qui, en revanche, est rarement utilisée (lire davantage à python.org). L’initialisateur obtient les arguments passés avec lesquels l’objet a été appelé. Par exemple, lorsque var = MyClass(1, 'vanilla' ), la méthode __init__(self, [...) reçoit 1 et 'vanilla'.

  • __call__(self, [...) permet d’appeler directement une instance de classe. Par exemple, var('cherry') (correspondants à var.__call__('cherry')) peut être utilisé pour changer de 'vanilla' à 'cherry'.

Ainsi, un squelette modèle de classe robuste ressemble à ceci:

class NewClass:
  def __init__(self, *args, **kwargs):
	  # Initialize instance attributes here.
	  pass

  def method_1(self, *args, **kwargs):
	  pass

  def __call__(self, *args, **kwargs):
	  print(f"Class info: {type(self).__name__}")
	  print(dir(self))

La compréhension du pouvoir et de la structure des classes et de l’orientation des objets prend du temps et nécessite une pratique. À cette fin, les chapitres sur Graphical User Interfaces et Geospatial Python fournissent d’autres exemples de cours pour se familiariser avec les concepts.

Vérification de la réussite en apprentissage

Prenez le test de réussite d’apprentissage pour ce carnet Jupyter.