La programmation orienté objet en Python

a marqué ce sujet comme résolu.

Tout le monde se secoue ! :D

J'ai commencé (dimanche 17 avril 2016 à 17h49) la rédaction d'un tutoriel au doux nom de « La programmation orienté objet en Python » et j'ai dans l'objectif de proposer en validation un texte aux petits oignons. Je fais donc appel à votre bonté sans limite pour dénicher le moindre pépin, que ce soit à propos du fond ou de la forme. Vous pourrez consulter la bêta à votre guise à l'adresse suivante :

Merci !


Cours annoncé depuis un moment, c'est Smokiev qui en a initié la rédaction en avril dernier, rejoint peu de temps après par nohar et moi-même. Ce cours s'inscrit dans la collection de tutoriels Python, pour se placer à une place charnière entre l'apprentissage des notions de base et les spécialisations.

L'écriture a bien avancé depuis, et le cours est quasi-complet (il manque un chapitre sur les bonnes pratiques objet, par rapport au plan initial).

Je le propose donc en beta, afin de réunir vos premiers avis.

Chouette !

J'ai lu le premier chapitre et, à part quelques remarques de détail sur lesquelles je reviendrai plus tard, j'ai trouvé l'approche un peu abrupte. Les explications sont très claires, il n'y a pas de souci de ce côté-là, mais le débutant se posera fatalement la question de l'intérêt de cette notion d'objet. Y a-t-il une raison particulière pour laquelle vous avez abandonné cette approche ? :)

+0 -0

Beau travail ! J'ai lu ça rapidement, quelques remarques :

Il me semble qu'une petite erreur s'est glissée ici (c.bar() devrait renvoyer ?) :

1
2
3
4
5
>>> c = C()
>>> c.foo()
'!'
>>> c.bar()
'!'

Sinon, les explications sur super dans un héritage multiple sont trop courtes, je pense que peu de lecteurs vont saisir la subtilité du truc. Il faudrait au moins indiquer que c'est bel et bien le super().__init__() de la classe A qui va appeler celui de la classe B.

on lui préférera dans le cas présent la fonction builtin êepr.

repr

+0 -0

Merci à vous deux.

J'ai lu le premier chapitre et, à part quelques remarques de détail sur lesquelles je reviendrai plus tard, j'ai trouvé l'approche un peu abrupte. Les explications sont très claires, il n'y a pas de souci de ce côté-là, mais le débutant se posera fatalement la question de l'intérêt de cette notion d'objet. Y a-t-il une raison particulière pour laquelle vous avez abandonné cette approche ? :)

Vayel

Le parti pris est que le lecteur connaît Python et a déjà rencontré la notion d'objet, et que l'on peut donc se permettre de passer assez rapidement sur ces termes et leur signification. Pour citer nohar :

Or dans mon tuto, on parle d'objet dès le début. Les objets sont les choses que l'on manipule dans nos programmes. Et de type en second. Il n'y a donc probablement pas besoin de s'attarder sur la notion de type ou d'objet. Si l'on considère que le lecteur a lu un bon tuto d'intro, il aura déjà touché à ces notions pendant tout son apprentissage, et il ne reste plus qu'à virer l'emballage pour expliquer, au fond, que la POO consiste d'abord à créer ses propres types de données en python.

nohar

Pour l'approche que tu cites, je ne la trouvais pas adaptée. Elle est plus complexe à mes yeux que d'instancier directement un objet et de commencer à travailler avec.

Et j'ajouterai une réaction que j'avais faite plus tôt sur ce code :

Je ne suis pas sûr qu'il soit intéressant d'avoir les mêmes méthodes pour récupérer et modifier les attributs, je trouve ça plutôt confus. Surtout avec des opérations qui servent à la fois à la récupération et à la modification. Je pense à format(age(personne1, 56)) dont l'équivalent objet serait une erreur de syntaxe (format(personne1.age = 56)).

entwanne

(où format était un équivalent de saluer dans le code que tu donnes)

@yoch, je corrigerai les erreurs que tu cites. Quant à super, je vais voir ce que je peux faire, tu as peut-être des idées ?

@yoch, Quant à super, je vais voir ce que je peux faire, tu as peut-être des idées ?

entwanne

Éventuellement ajouter : "étant donnée la classe en premier paramètre (ou la classe courante si appelée sans paramètres), il se charge de localiser la classe juste à sa droite dans le MRO [de l'instance]". C'est pas forcement super clair comme formulation, mais sans ça la phrase prête a confusion (on peut comprendre que c'est le MRO de la classe en premier paramètre qui est employé).

Et aussi, insister sur le fait que les __init__ des parents ne sont pas appelés automatiquement comme on peut le penser (puisqu'il est surchargé dans les classes filles), c'est bien le chaînage des super().__init__() qui est mis en oeuvre. (dans le cas ou la classe fille n'a pas de __init__, le même comportement sera observé pour les __init__ des parents, mais pour des raisons bien différentes.)

Tout ça me fait penser qu'il faut peut être insister un peu plus sur le mécanisme de __init__ en python, et expliquer en quoi il differe des constructeurs des autres langages. Peut-être évoquer __new__ aussi. En fait, je crois que le probleme principal de l'exemple est qu'il s'appuie justement sur __init__.

+0 -0

Faudra que je relise, mais j'ai l'impression qu'on n'a pas de définition réelle de ce qu'est la POO, son principe. Ce qui est dommage.

artragis

Je ressens aussi ce manque. C'est actuellement l'introduction qui joue ce rôle, mais elle est très courte. Une section dédiée dans le premier chapitre ne serait pas de trop.

Éventuellement ajouter : "étant donnée la classe en premier paramètre (ou la classe courante si appelée sans paramètres), il se charge de localiser la classe juste à sa droite dans le MRO [de l'instance]". C'est pas forcement super clair comme formulation, mais sans ça la phrase prête a confusion (on peut comprendre que c'est le MRO de la classe en premier paramètre qui est employé).

Et aussi, insister sur le fait que les __init__ des parents ne sont pas appelés automatiquement comme on peut le penser (puisqu'il est surchargé dans les classes filles), c'est bien le chaînage des super().__init__() qui est mis en oeuvre. (dans le cas ou la classe fille n'a pas de __init__, le même comportement sera observé pour les __init__ des parents, mais pour des raisons bien différentes.)

yoch

D'accord, je vais tourner ça comme ça. Et insister sur l'appel explicite aux initialisateurs parents, oui.

Tout ça me fait penser qu'il faut peut être insister un peu plus sur le mécanisme de __init__ en python, et expliquer en quoi il differe des constructeurs des autres langages. Peut-être évoquer __new__ aussi. En fait, je crois que le probleme principal de l'exemple est qu'il s'appuie justement sur __init__.

yoch

Là ça me paraît hors du champ de ce tuto, qui ne vise pas à expliquer en détails le modèle objet mis en place par Python. J'y consacre déjà une partie dans le cours sur les notions avancées.

Bonjour entwanne,

Dans cette partie, et dans la sous partie mixins, j'ai vu que tes classes ne respectent pas la fameuse norme que j'ai apprise en C++ : A hérite de B ssi A est un B. Est-ce justement parce que ces fameux mixins sont des classes à part dans l'esprit Python ? J'aurais plutôt eu tendance à ajouter un objet ProfilePicture dans User par exemple. En python on respecte la norme que j'ai citée pour toutes les classes sauf les mixins ?

Comme tu n'as pas mentionné cette fameuse norme, je me demande si c'est l'esprit de Python qui change ou alors si c'est particulié aux mixins. Peut être que quelqu'un avec peu de connaissances en POO fera des héritages n'importe comment si tu ne précises pas cette petite norme.

J'édite pour signaler que tu as écris êepr au lieux de repr au début du chapitre suivant.

+1 -0

C'est l'esprit qui change entre les deux langages.

A hérite de B signifie que A reproduit une grosse partie des comportements de B. La relation est un n'a pas beaucoup de sens en Python (sauf cas très particulier), parce que c'est un langage dynamique qui repose sur le duck-typing (si ça marche comme un canard, etc.).

En C++, à l'opposé, la relation est un est importante parce que c'est par elle que passe le polymorphisme. Hériter d'une classe est le seul moyen de changer le comportement du code qui utilise cette classe.

Au fond, l'héritage en Python est une notion beaucoup plus relâchée qu'en C++ ou n'importe quel autre langage OO de la même trempe (C#, Java, D…). C'est avant tout une façon d'éviter un copier coller, et les mixins sont une façon d'utiliser ce mecanisme pour assembler des classescomme des lego, là où on utiliserait des templates en, C++, ou plus généralement le pattern Strategy.

+1 -0

Pas grand chose à ajouter, l'héritage n'a pas le même but en Python qu'en C++.

Déjà parce que la plupart du temps on ne s'intéresse pas plus que ça au type des valeurs, seulement à leur interface. Et quand bien même on s'attarde sur le type, il est possible d'être un A, sans que le type de l'objet n'hérite de A (cf collections.abc).

C'est pour ça que l'héritage dans ce cours est abordé sous l'angle de la réutilisation de code. Le sous-typage y est évoqué, sans que l'accent ne soit mis dessus.

Rendons à César ce qui est à César, ma contribution en l'état au tutoriel est minime (disons même nulle pour l'instant) par manque de dispo. Mais je ne désespère pas participer aux corrections/améliorations/ajouts à venir !

+0 -0

Salut,

Je commence une relecture :

Comment veux-tu que je t'encapsule ?

sécuriser le stockge de nos mots

stockage

en ajoutant une méthode servant à le hasher.

Je pense que ce serait bien d'expliquer le terme hasher ici.

Certains langages^[C++, Java, Ruby] implémenent

Note de bas de page ratée ? & implémentent

Tu aimes les glaces, canard ?

les attributs qu'ils contient

il

receva t un corps de message en paramètre et retournant

recevant

Une classe avec deux mamans

foo et `bar.

bar

Mixins

Les mixins sont des classes dédiées à une fonctionnalité particulière, utilisable

utilisables


Pour le fond, pas grand chose à ajouter, ça me semble pythonique et pertinent jusqu'à présent et l'exemple User/Post/etc. offre un bon fil rouge. Pour les débutants avec la notion même d'objet, c'est vrai qu'il faudrait le pont (le tutoriel pour débutant de nohar normalement) ce qui peut s'avérer problématique tant que ce pont n'existe pas.

+0 -0

De rien,

J'ai continué ma relecture, mais j'ai malheureusement perdu mon message, donc je poste les remarques générales dont je me souviens et je repasserai pour les problèmes d'orthographe :

Il est maintenant temps de nous intéresser aux opérateurs du langage Python (+, -, *, etc.). En effet, un code respectant la philosophie se doit de les utiliser à bon escient.

Je trouve cela pas assez clair, on ne comprend pas trop ce que ça veut dire qu'un code respectant la philosophie.

Après, tu dis qu'il est préférable d'utiliser les builtin aux méthodes spéciales (lors de l'exemple de repr et __repr__, d'ailleurs y'a une faute de typo sur la première) mais pourquoi ? C'est juste synthaxique ?

Du coup la méthode non spéciale fait juste appel à la méthode spéciale, par exemple avec le module operator : add fait juste appel à __add__ ?

Enfin, à certains moments, tu parles de termes (décorateur & mutable) que le public visé ne connaîtra sans doute pas à mon avis, sans en expliquer le sens même grossièrement pour que le lecteur puisse comprendre les exemples.

Voilà, à bientôt donc.

+0 -0

J'ai commencé la correction suite à vos différentes remarques, je voulais juste revenir sur certains points.

Sinon, les explications sur super dans un héritage multiple sont trop courtes, je pense que peu de lecteurs vont saisir la subtilité du truc. Il faudrait au moins indiquer que c'est bel et bien le super().__init__() de la classe A qui va appeler celui de la classe B.

yoch

J'ai reformulé le passage concernant super, et complété mon exemple, qui montre maintenant bien à quel moment est appelé quel initialiseur, et par qui.

Les mixins sont des classes dédiées à une fonctionnalité particulière, utilisable

utilisables

Smokiev

« utilisable » fait ici référence à la fonctionnalité, d'où l'emploi du singulier.

Après, tu dis qu'il est préférable d'utiliser les builtin aux méthodes spéciales (lors de l'exemple de repr et __repr__, d'ailleurs y'a une faute de typo sur la première) mais pourquoi ? C'est juste synthaxique ?

Smokiev

Il y a déjà la différence syntaxique, mais aussi que les méthodes avec double-underscores font plutôt référence à des méthodes internes de l'objet.

Du coup la méthode non spéciale fait juste appel à la méthode spéciale, par exemple avec le module operator : add fait juste appel à __add__ ?

Smokiev

Pas exactement. operator.add fait appel à __add__ avec un fallback sur __radd__ de l'opérande droit en cas de NotImplemented.

Enfin, à certains moments, tu parles de termes (décorateur & mutable) que le public visé ne connaîtra sans doute pas à mon avis, sans en expliquer le sens même grossièrement pour que le lecteur puisse comprendre les exemples.

Smokiev

Je vais essayer de détailler au mieux « mutable », même si c'est vrai que c'est une notion pas facile à appréhender en Python (on pense souvent initialement que tous les objets sont mutables puisque les variables sont réassignables).

Mais pour ce qui est des décorateurs, leur utilisation devrait être expliquée dans le cours sur les bases du langage, je prends donc la notion comme acquise.

Quant aux notes de bas de page, faut que je regarde à nouveau la syntaxe du zmarkdown pour les faire, ce n'est pas celle dont j'ai l'habitude.

Bonjour les agrumes !

La bêta a été mise à jour et décante sa pulpe à l'adresse suivante :

Merci d'avance pour vos commentaires.


  • Corrections et précisions suite aux retours ;
  • Nouvelle section dédiées aux fonctions getattr/hasattr/etc.

La surcharge est un concept qui permet de redéfinir une méthode du parent.

Il n'y a pas d'overloading (surcharge) en Python. La coexistence n'est pas possible. Les méthodes sont supplantées et bel et bien overridden (redéfinies en français) quand on regarde les dictionnaires des classes enfants/ou des objets enfant. Je ne doute pas qu'il y moyen de retrouver le parent ou le type pour aller chercher le "défaut", mais ce n'est pas ce que l'on appelle surcharger.

Botter en touche le sous-typage parce que l'on ne s'intéresse qu'aux interfaces me laisse perplexe. Si nos interfaces et instances compatibles ne respectent pas un soupçon de LSP, on aura des soucis en Python aussi. Il y a toujours un minimum de contrat à respecter en duck-typing.

Je ne vois plus l'encapsulation comme un truc qui sert à cacher, mais comme un truc qui sert à protéger les invariants. La nuance est subtile.

À l'usage, dans aucun gros projet sur lequel j'ai travaillé ces dernières années, on n'a utilisé le sous-typage comme levier pour nous donner des garanties en Python.

Ce qui se rapproche le plus des contrats dont tu parles (et que j'utilise très souvent en revanche) est le module standard abc qui permet de définir des interfaces abstraites dont les implémentations sont vérifiées statiquement.

+0 -0

Il n'y a pas d'overloading (surcharge) en Python. La coexistence n'est pas possible. Les méthodes sont supplantées et bel et bien overridden (redéfinies en français) quand on regarde les dictionnaires des classes enfants/ou des objets enfant. Je ne doute pas qu'il y moyen de retrouver le parent ou le type pour aller chercher le "défaut", mais ce n'est pas ce que l'on appelle surcharger.

lmghs

Oui, je n'ai pas fait attention pour l'utilisation du terme « surcharge », que j'ai l'habitude d'utiliser à la fois pour l'overloading et l'overriding.

Botter en touche le sous-typage parce que l'on ne s'intéresse qu'aux interfaces me laisse perplexe. Si nos interfaces et instances compatibles ne respectent pas un soupçon de LSP, on aura des soucis en Python aussi. Il y a toujours un minimum de contrat à respecter en duck-typing.

lmghs

Le sous-typage est évoqué, juste que l'accent n'est pas mis dessus, parce que ça ne me semble pas primordial en Python. Notamment par qu'il n'y a pas d'équivalence entre héritage et sous-typage. On peut être un sous-type d'une classe sans en hériter (collections.abc, typing), et inversement (bien que ce soit plus rare/louche).

Quant au LSP, une section lui est dédiée dans le dernier chapitre, regroupant les bonnes pratiques de programmation orientée objet, qui ne figure pas dans la version actuelle du cours car son état actuel est lamentable (suite de définitions).

Je ne vois plus l'encapsulation comme un truc qui sert à cacher, mais comme un truc qui sert à protéger les invariants. La nuance est subtile.

lmghs

J'aborde l'encapsulation et la visibilité dans le même temps. Mais je pensais justement avoir tourné les choses autour des états interne de l'objet de l'objet et de leur protection. Pourrais-tu me dire ce qui pose problème dans la partie dédiée ?

Merci.

Après réflexion, je me dis de plus en plus que le LSP s'applique (ou devrait s'appliquer) à tous les polymorphismes. Pas seulement à celui des objets. I.e., il est aussi valide avec le polymorphisme paramétrique.

Dans "On nomme encapsulation cette notion de visibilité des attributs et méthodes d'un objet.", je dirai plutôt: "On nomme encapsulation cette notion de protection des attributs d'un objet pour garantir le respect des invariants de l'objet en offrant une interface dédiée de manipulation de l'objet." (très mal dit). Bref, on ne cache pas pas pour cacher. On cache pour être sûr qu'un tiers couillon, ou nous après une soirée bien arrosée, ne vienne pas altérer, depuis n'importe où et n'importe comment, des données qui participent à une certaine stabilité.

Exemples :

  • changer un mot de passe crypté sans passer par la procédure sécurisée qui vérifie l'ancien mot de passe ;
  • altérer le dénominateur d'un nombre rationnel alors qu'on l'exploite en supposant qu'il soit toujours strictement positif et la fraction réduite ;
  • changer directement (ou mettre directement) un élément à l'intérieur d'une séquence triée

Après techniquement, l'encapsulation s'appuie souvent sur la notion de visibilité pour être mise en oeuvre. À noter que de son coté Eiffel autorise l'accès en lecture aux attributs, mais pas celui en écriture. On a un type d'accessibilité encore différent du public/protégé/privé/package que l'on trouve ailleurs.

Bref, j'ai trouvé la présentation de l'encapsulation trop axée comment (visibilité) et pas assez finalité (à quoi ça sert, pourquoi…). Encapsulation et invariant de classe/instance sont deux notions intimement liées.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte