Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Apprentissage Python

4 réponses
Avatar
jvpic
Bonjour,

Je pratique Python depuis peu et j'ai voulu m'intéresser à des concepts
plus sophistiqués tels :

fermetures, modèle objet nouveau avec le constructeur __new__, la
surcharge de type() par __metaclass__, les descripteurs, property et les
__slots__ et autres décorateurs.

En dehors de __slots__ qui permet une économie de mémoire (et encore
sans doute assez faible), je me pose des questions sur la réelle utilité
de ces mécanismes.

Les documents consultés (bouquin de tarek Ziadé et Pyton en concentré de
Martelly) exposent les mécanismes mais ne montrent pas l'intérêt de ces
techniques ni d'exemples concrets.

La question est vaste, mais pouvez-vous me conseiller sur l'intérêt de
ces concepts et/ou me guider vers un site qui pourrait m'aider ?

Merci beaucoup

Jacques Picard

4 réponses

Avatar
Bruno Desthuilliers
jvpic a écrit :
Bonjour,

Je pratique Python depuis peu et j'ai voulu m'intéresser à des concepts
plus sophistiqués tels :

fermetures, modèle objet nouveau avec le constructeur __new__, la
surcharge de type() par __metaclass__, les descripteurs, property et les
__slots__ et autres décorateurs.



Tu oublies les lambda, les expressions de liste, les générateurs, etc !-)

En dehors de __slots__ qui permet une économie de mémoire (et encore
sans doute assez faible),



Hmmm.... Pas si faible que ça quand il s'agit de dizaines de milliers
(ou plus) d'objet.

je me pose des questions sur la réelle utilité
de ces mécanismes.



Pas moi - je les utilise tous, régulièrement, directement (à l'exception
des slots - pas eu de cas d'utilisation jusque là) et/ou indirectement.

Ceci étant, si tu n'es familier avec aucun de ces concepts, je peux
comprendre tes interrogations - personnellement, la première fois que
j'ai fait connaissance avec les fonctions anonymes (via l'instruction
lambda de Python d'ailleurs), je me suis franchement demandé à quoi ça
pouvait bien servir.

Les documents consultés (bouquin de tarek Ziadé et Pyton en concentré de
Martelly) exposent les mécanismes mais ne montrent pas l'intérêt de ces
techniques ni d'exemples concrets.

La question est vaste, mais pouvez-vous me conseiller sur l'intérêt de
ces concepts et/ou me guider vers un site qui pourrait m'aider ?



Concernant les fermetures, c'est un mécanisme de base de la
programmation fonctionnelle. D'une manière générale, c'est une autre
('autre' par rapport à l'OO) façon de partager un état entre plusieurs
fonctions sans rendre cet état global. Dans la pratique - en Python,
j'entend -, il y a pas de mal de cas où le choix d'utiliser l'un ou
l'autre mécanisme relève plus d'un choix d'implémentation que d'autre
chose. L'avantage d'une fermeture est qu'elle ne nécessite pas de
définir une nouvelle classe - c'est donc un mécanisme beaucoup plus
léger pour des choses simples. Au dela d'un certain niveau de
complexité, il peut être plus intéressant de définir une classe, ne
serait-ce que pour la lisibilité. L'évaluation partielle, par exemple,
peut être - et a été - implémentée des deux façons, la version
finalement intégrée au language (functools.partial) étant basée sur une
classe.

La raison première du constructeur __new__ est de permettre d'hériter de
types builtin immutables (str, int, tuple etc) qui n'utilisent pas
l'initialiseur __init__. Mais ce mécanisme permet aussi de prendre le
controle sur l'instanciation, par exemple pour implémenter un "cache"
d'objets (pool, singleton, ....)

Les métaclasses permettent d'intervenir sur la création des classes, et
sont d'une utilité incroyable pour implémenter des frameworks. Je te
recommande la lecture du code source de packages comme FormEncode ou
Django pour comprendre à quel point ce mécanisme simplifie non seulement
la vie de l'auteur du framework, mais aussi et surtout celle de
l'utilisateur (cette remarque vaut d'ailleurs aussi bien pour les
descripteurs et décorateurs).

Le protocole descripteur est AMHA une idée absolument géniale. C'est un
mécanisme très simple qui permet, d'une manière générale, d'implémenter
tout types d'attributs calculés - et honnêtement, si tu ne vois pas
l'intérêt des attributs calculés, je te recommende l'assembleur, les
langages de haut niveau ne sont pas pour toi !-)... (je plaisante...
non, pas taper...).

Les deux applications "standard" sont les types 'property' et 'method'
(dans ce dernier cas, c'est le type function qui implémente le protocole
descripteur, et se charge de fournir l'objet method encapsulant la
fonction et l'objet sur lequel elle est appelée, ce qui permet d'éviter
de faire des méthodes un cas particulier), mais il n'y a guère de
limites aux applications particulières qu'on peut trouver au mécanisme.
Là encore, regarde le code source de Django (et notament la partie ORM)
pour voir des exemples d'application pratique.

Quant à la syntaxe "décorateur", il ne s'agit jamais que de sucre
syntaxique pour des fonctions d'ordre supérieur (un autre mécanisme de
base de la programmation fonctionnelle, d'ailleurs usuellement utilisé
en combinaison avec les fermertures). En bref:

@un_decorateur
def une_fonction(...):
code_ici()

est un raccourci pour:

def une_fonction(...):
code_ici()

une_fonction = un_decorateur(une_fonction)


Les cas d'utilisation sont innombrables (à commencer par les classmethod
et staticmethod). Juste pour en citer un ou deux...

* Encore une fois dans le cadre d'un framework: la gestion des
permissions. Dans Zope2, par exemple, pour donner une permission sur une
méthode, il faut faire ceci:

class UneRessource(...):
# divers truc ici

security.declareProtected('une_methode', 'une_perm')
def une_methode(self, ...):
code_ici()

Une erreur courante consiste à se tromper dans le nom de la méthode dans
l'appel à security.declareProtected - soit parce qu'on fait une typo,
soit parce qu'on renomme la méthode mais qu'on oublie de reporter ça
dans la déclaration, soit (très souvent), parce qu'on a copié/collé la
déclaration et oublié de la mettre à jour.

En utilisant un décorateur à la place, on évite cette violation de
principe de non-répétition:

@security.declareProtected('une_perm')
def une_methode(self...):
code_ici()

* D'une manière plus générique : un bout de code qu'on veut exécuter
systématiquement avant / après / autour de certaines fonctions ou
méthodes. Par exemple, une journalisation. Sans les décorateurs, il faut
insérer manuellement le code en question dans toutes les fonctions
concernées, aux bons endroits - encore une violation du principe de non
répétition, et de celui d'ouverture / fermeture. Avec les décorateurs,
il suffit de décorer les fonctions concernées, et c'est tout - pas
besoin de toucher au code des fonctions elles-mêmes.

Accessoirement, on peut aussi combiner décorateurs et métaclasses pour
'instrumentaliser' un code existant sans même avoir à ajouter
manuellement les @decoration dans le code source - ce qui permet par
exemple de decider dynamiquement d'installer ou non certains décorateurs
(en fonction d'options de configuration par exemple).

D'une manière générale, Python expose (et permet de prendre la main sur)
l'essentiel de son modèle objet, et c'est clairement une des grandes
forces de ce langage. Ca permet de solutionner simplement des problèmes
complexes (et souvent récurrents sur certains types d'applications).
Sans ces mécanismes, on est souvent contraint au mieux d'écrire (et donc
de tester, maintenir etc) 10 à 20 fois plus de code - voire parfois de
forker une base de code complète (et complexe), ce qui est tout
simplement prohibitif en termes de coûts de maintenance.

HTH
Avatar
jvpic
Bruno Desthuilliers a écrit :
jvpic a écrit :
Bonjour,

Je pratique Python depuis peu et j'ai voulu m'intéresser à des concepts
plus sophistiqués tels :

fermetures, modèle objet nouveau avec le constructeur __new__, la
surcharge de type() par __metaclass__, les descripteurs, property et
les __slots__ et autres décorateurs.



Tu oublies les lambda, les expressions de liste, les générateurs, etc !-)

En dehors de __slots__ qui permet une économie de mémoire (et encore
sans doute assez faible),



Hmmm.... Pas si faible que ça quand il s'agit de dizaines de milliers
(ou plus) d'objet.

je me pose des questions sur la réelle utilité de ces mécanismes.



Pas moi - je les utilise tous, régulièrement, directement (à l'exception
des slots - pas eu de cas d'utilisation jusque là) et/ou indirectement.

Ceci étant, si tu n'es familier avec aucun de ces concepts, je peux
comprendre tes interrogations - personnellement, la première fois que
j'ai fait connaissance avec les fonctions anonymes (via l'instruction
lambda de Python d'ailleurs), je me suis franchement demandé à quoi ça
pouvait bien servir.

Les documents consultés (bouquin de tarek Ziadé et Pyton en concentré
de Martelly) exposent les mécanismes mais ne montrent pas l'intérêt de
ces techniques ni d'exemples concrets.

La question est vaste, mais pouvez-vous me conseiller sur l'intérêt de
ces concepts et/ou me guider vers un site qui pourrait m'aider ?



Concernant les fermetures, c'est un mécanisme de base de la
programmation fonctionnelle. D'une manière générale, c'est une autre
('autre' par rapport à l'OO) façon de partager un état entre plusieurs
fonctions sans rendre cet état global. Dans la pratique - en Python,
j'entend -, il y a pas de mal de cas où le choix d'utiliser l'un ou
l'autre mécanisme relève plus d'un choix d'implémentation que d'autre
chose. L'avantage d'une fermeture est qu'elle ne nécessite pas de
définir une nouvelle classe - c'est donc un mécanisme beaucoup plus
léger pour des choses simples. Au dela d'un certain niveau de
complexité, il peut être plus intéressant de définir une classe, ne
serait-ce que pour la lisibilité. L'évaluation partielle, par exemple,
peut être - et a été - implémentée des deux façons, la version
finalement intégrée au language (functools.partial) étant basée sur une
classe.

La raison première du constructeur __new__ est de permettre d'hériter de
types builtin immutables (str, int, tuple etc) qui n'utilisent pas
l'initialiseur __init__. Mais ce mécanisme permet aussi de prendre le
controle sur l'instanciation, par exemple pour implémenter un "cache"
d'objets (pool, singleton, ....)

Les métaclasses permettent d'intervenir sur la création des classes, et
sont d'une utilité incroyable pour implémenter des frameworks. Je te
recommande la lecture du code source de packages comme FormEncode ou
Django pour comprendre à quel point ce mécanisme simplifie non seulement
la vie de l'auteur du framework, mais aussi et surtout celle de
l'utilisateur (cette remarque vaut d'ailleurs aussi bien pour les
descripteurs et décorateurs).

Le protocole descripteur est AMHA une idée absolument géniale. C'est un
mécanisme très simple qui permet, d'une manière générale, d'implémenter
tout types d'attributs calculés - et honnêtement, si tu ne vois pas
l'intérêt des attributs calculés, je te recommende l'assembleur, les
langages de haut niveau ne sont pas pour toi !-)... (je plaisante...
non, pas taper...).

Les deux applications "standard" sont les types 'property' et 'method'
(dans ce dernier cas, c'est le type function qui implémente le protocole
descripteur, et se charge de fournir l'objet method encapsulant la
fonction et l'objet sur lequel elle est appelée, ce qui permet d'éviter
de faire des méthodes un cas particulier), mais il n'y a guère de
limites aux applications particulières qu'on peut trouver au mécanisme.
Là encore, regarde le code source de Django (et notament la partie ORM)
pour voir des exemples d'application pratique.

Quant à la syntaxe "décorateur", il ne s'agit jamais que de sucre
syntaxique pour des fonctions d'ordre supérieur (un autre mécanisme de
base de la programmation fonctionnelle, d'ailleurs usuellement utilisé
en combinaison avec les fermertures). En bref:

@un_decorateur
def une_fonction(...):
code_ici()

est un raccourci pour:

def une_fonction(...):
code_ici()

une_fonction = un_decorateur(une_fonction)


Les cas d'utilisation sont innombrables (à commencer par les classmethod
et staticmethod). Juste pour en citer un ou deux...

* Encore une fois dans le cadre d'un framework: la gestion des
permissions. Dans Zope2, par exemple, pour donner une permission sur une
méthode, il faut faire ceci:

class UneRessource(...):
# divers truc ici

security.declareProtected('une_methode', 'une_perm')
def une_methode(self, ...):
code_ici()

Une erreur courante consiste à se tromper dans le nom de la méthode dans
l'appel à security.declareProtected - soit parce qu'on fait une typo,
soit parce qu'on renomme la méthode mais qu'on oublie de reporter ça
dans la déclaration, soit (très souvent), parce qu'on a copié/collé la
déclaration et oublié de la mettre à jour.

En utilisant un décorateur à la place, on évite cette violation de
principe de non-répétition:

@security.declareProtected('une_perm')
def une_methode(self...):
code_ici()

* D'une manière plus générique : un bout de code qu'on veut exécuter
systématiquement avant / après / autour de certaines fonctions ou
méthodes. Par exemple, une journalisation. Sans les décorateurs, il faut
insérer manuellement le code en question dans toutes les fonctions
concernées, aux bons endroits - encore une violation du principe de non
répétition, et de celui d'ouverture / fermeture. Avec les décorateurs,
il suffit de décorer les fonctions concernées, et c'est tout - pas
besoin de toucher au code des fonctions elles-mêmes.

Accessoirement, on peut aussi combiner décorateurs et métaclasses pour
'instrumentaliser' un code existant sans même avoir à ajouter
manuellement les @decoration dans le code source - ce qui permet par
exemple de decider dynamiquement d'installer ou non certains décorateurs
(en fonction d'options de configuration par exemple).

D'une manière générale, Python expose (et permet de prendre la main sur)
l'essentiel de son modèle objet, et c'est clairement une des grandes
forces de ce langage. Ca permet de solutionner simplement des problèmes
complexes (et souvent récurrents sur certains types d'applications).
Sans ces mécanismes, on est souvent contraint au mieux d'écrire (et donc
de tester, maintenir etc) 10 à 20 fois plus de code - voire parfois de
forker une base de code complète (et complexe), ce qui est tout
simplement prohibitif en termes de coûts de maintenance.

HTH



OK ! Merci de ces pistes !
Avatar
Michel Claveau - MVP
Salut !


Perso, j'ai un avis légèrement différent de Bruno.
Si, effectivement, ces « concepts sophistiqués » ont le mérite d'exister, s'il est toujours intéressant de pouvoir en profiter avec Python, je suis moins enthousiaste sur leur intérêt au quotidien.

Exemples :
Les listes en intention sont légèrement plus rapides, et font économiser une ou deux lignes. Pas de quoi fouetter un chat.
Les décorateurs pourraient être, à peu près, remplacés par une première ligne dans la fonction. OK, on gagne un peu en lisibilité ou en généricité. Mais, pas de quoi en faire un fromage.
Les slots peuvent faire gagner, dans quelques cas pas très courants. Pas de quoi s'enrhumer.
Les fermetures, c'est amusant, mais il y a des alternatives. Et pourtant, je les utilise beaucoup dans NiouzArt (il n'y a pas de quoi rire).
Les métaclasses, on ne les utilise quand même pas souvent. Pas de quoi en faire un œuf sur le plat.

Attention, je n'ai pas dit que ces notions n'étaient pas bien. Mais que l'intérêt, le gain, est plutôt mesuré.


Par contre, le truc qui me semble fabuleux, c'est le protocole descripteur. Il permet une approche complètement différente de la programmation orientée objet à classes. Il m'a permis d'économiser de très nombreuses lignes de code.


@+
--
Michel Claveau
Avatar
Bruno Desthuilliers
Michel Claveau - MVP a écrit :
Salut !


Perso, j'ai un avis légèrement différent de Bruno. Si, effectivement,
ces « concepts sophistiqués » ont le mérite d'exister, s'il est
toujours intéressant de pouvoir en profiter avec Python, je suis
moins enthousiaste sur leur intérêt au quotidien.


>
Exemples : Les listes en intention sont légèrement plus rapides, et
font économiser une ou deux lignes. Pas de quoi fouetter un chat.



Outre le gain de lisibilité, dans la pratique, ça fait souvent gagner
plus de "une ou deux lignes" - parce que c'est une expression, alors
que for est une instruction.

Les
décorateurs pourraient être, à peu près, remplacés par une première
ligne dans la fonction.



???

OK, on gagne un peu en lisibilité ou en
généricité. Mais, pas de quoi en faire un fromage. Les slots peuvent
faire gagner, dans quelques cas pas très courants. Pas de quoi
s'enrhumer.



Sauf quand ça sauve purement et simplement la vie.

Les fermetures, c'est amusant, mais il y a des
alternatives.



Oui, les classes. Il y a *toujours* des alternatives. La question est
surtout de savoir ce que fait gagner une construction de haut-niveau
(par rapport à une alternative de bas-niveau).

Et pourtant, je les utilise beaucoup dans NiouzArt (il
n'y a pas de quoi rire). Les métaclasses, on ne les utilise quand
même pas souvent. Pas de quoi en faire un œuf sur le plat.



Tu n'en écris peut-être pas tous les jours, mais sans les métaclasses,
la plupart des frameworks Python ne seraient pas ce qu'ils sont, et
nécessiteraient tellement de boilerplate qu'on pourrait aussi bien faire
du Java ou du PHP.

Attention, je n'ai pas dit que ces notions n'étaient pas bien. Mais
que l'intérêt, le gain, est plutôt mesuré.



Peut-être parce tu vois ça par le petit bout de ta lorgnette. Je peux
t'assurer, en tant qu'utilisateur quotidien et auteur occasionnel de
frameworks que l'utilisation combinée de ces fonctionnalités apporte des
gains de productivité et de maintanabilité qui sont loin d'être "mesurés".


Par contre, le truc qui me semble fabuleux, c'est le protocole
descripteur. Il permet une approche complètement différente de la
programmation orientée objet à classes. Il m'a permis d'économiser de
très nombreuses lignes de code.



Je confirme: tu vois ça par le petit bout de ta lorgnette !-)

<mode="troll">
Je pourrais te répliquer que les attributs calculés sont inutiles
puisqu'on a le même résultat avec des getters et des setters explicites.
</mode>