Notions de Python avancées

Découvrez Python plus en profondeur

a marqué ce sujet comme résolu.

Bonjour les agrumes !

La bêta tutoriel « Notions de Python avancées » a été mise à jour et coule sa pulpe à l'adresse suivante :

Merci d'avance pour vos commentaires.


Je viens d'ajouter des conclusions pour chaque chapitre, ainsi qu'un logo. Après avoir survolé vite fait, je remarque que les tournures de phrase de ces conclusions ne sont pas toujours pertinentes, et parfois répétitives, donc je referai une passe.

Des illustrations ne me semblent pas indispensables. Du moins pas a priori. Je pense qu'au fil des retours sur ton tutoriel tu finiras peut-être par voir les points que les gens ne comprennent pas bien s'il y en a, et que ce sera l'occasion d'envisager rajouter des schémas ou des explications plus visuelles sur ces points de détail.

+0 -0

Merci pour la prise en compte de mes remarques. Je reprendrai depuis le début une fois la première passe de relecture terminée. Passons aux décorateurs.

Introduction

Un décorateur permet de se soustraire à un objet pour en modifier le comportement.

Je ne comprends pas tellement l'expression "se soustraire à".

D&CO, une semaine pour tout changer

afin de retourner un nouvel objet (généralement appelable). Un décorateur est donc une fonction prenant une fonction en paramètre et retournant une fonction.

Je chipote, mais le "et retournant une fonction" n'est pas tellement en phase avec la parenthèse qui précède.

Nous utilisons ici un décorateur très simple qui retourne la même fonction, mais il se pourrait très bien qu'il en retourne une autre, qui pourrait être créée à la volée.

Aurais-tu un exemple de décorateur retournant la fonction qu'il reçoit et plus utile que celui que tu donnes ?

Décorateurs paramétrés

En fait, @ ne doit pas nécessairement être suivi d'un nom d'objet, des arguments peuvent aussi s'y ajouter à l'aide de parenthèses.

La formulation est un peu étrange. Il me semble plus clair de dire que @ est suivi d'un décorateur et que make_dec(*args) en est un.

Envelopper une fonction

Mais vous pouvez choisir de leur donner un sens, nous le ferons dans le TP suivant. Une utilisation courante est de préciser dans les annotation les types des paramètres et le type de retour.

A noter que c'est devenu une convention depuis Python 3.5.

Tout ce qu'il y a à savoir pour le moment, c'est qu'une annotation peut-être n'importe quel objet python

Avec un objet valide quand même. :)

TP : divers décorateurs

Nous allons pour cela utiliser les signatures de fonctions.

Je chipote, mais la formulation ne convient pas tellement. Tu pourrais plutôt avoir : "Pour pallier cela".

Nous allons maintenant nous intéresser à singledispatch, une implémentation de fonctions génériques permettant de dispatcher l'appel en fonction du type du premier paramètre.

Sans avoir lu la suite, je ne comprends pas cela.

Un décorateur, singledispatch prend une fonction en paramètre (c'est la fonction qui sera appelée si aucune spécialisation n'est trouvée), et en retourne un nouvel objet. Cet objet possède une méthode register qui s'utilisera comme un décorateur paramétré en lui précisant le type pour lequel nous voulons spécialiser.

J'ai bloqué sur ce passage, parce que je ne comprenais pas ce que tu entendais par spécialisation/spécialiser.

Je vous propose pour cette fois de réaliser notre décorateur à l'aide d'une classe.

Le code permet de comprendre ce que tu dis plus haut, mais son utilité n'est pas très claire. Aurais-tu un exemple d'application ?

Notre décorateur va donc se charger d'analyser les paramètres lors de chaque appel à la fonction

Les arguments plutôt, non ?

Certains langages implémentent l'optimisation dite de récursivité terminale : si l'appel récursif est la dernière instruction exécutée dans la fonction, il est possible de supprimer de la pile le contexte courant avant d'appeler la fonction suivante, et ainsi ne pas surcharger la pile. Ce n'est pas le cas avec Python.

Ce passage risque d'être compliqué pour qui n'a pas de connaissances en programmation fonctionnelle. A toi de voir si tu veux dédier cette application aux personnes familières avec la notion de récursivité terminale. Sinon, tu pourrais illustrer tes propos par un exemple.

Il faut bien noter que le retour de la méthode call ne sera pas utilisable comme le résultat réel de notre fonction, nous ne pourrons que le retourner pour qu'il soit ensuite évalué par le wrapper.

Je ne comprends pas trop cela.

Maintenant nous allons réaliser notre décorateur tail_rec, j'ai opté pour une classe :

J'ai pas mal bloqué sur le code, notamment sur la différence entre call et __call__. En passant du temps dessus, ça va mieux, mais ce n'est pas évident. J'ignore par contre s'il est judicieux d'ajouter des explications, ou s'il est préférable de laisser le lecteur se débrouiller.

Merci !

+0 -0

Je ne comprends pas tellement l'expression "se soustraire à".

Vayel

C'est utilisé dans le sens de « remplacer ».

Je chipote, mais le "et retournant une fonction" n'est pas tellement en phase avec la parenthèse qui précède.

Vayel

D'où la note juste sous l'exemple.

Aurais-tu un exemple de décorateur retournant la fonction qu'il reçoit et plus utile que celui que tu donnes ?

Vayel

C'est compliqué dans le sens où ça ne vient généralement pas seul. Un tel décorateur serait utilisé pour ajouter des flags à la fonction, et ainsi y refaire référence dans un autre bout de code.

La formulation est un peu étrange. Il me semble plus clair de dire que @ est suivi d'un décorateur et que make_dec(*args) en est un.

Vayel

Je préférais être précis, (lambda: decorator)() est aussi un décorateur, et pourtant ça ne fonctionne pas.

A noter que c'est devenu une convention depuis Python 3.5.

Vayel

Oui, ça serait utile de le préciser.

Avec un objet valide quand même. :)

Vayel

Qu'est-ce qu'un objet invalide ?

Je chipote, mais la formulation ne convient pas tellement. Tu pourrais plutôt avoir : "Pour pallier cela".

Vayel

Moi elle me convient, je n'y vois pas de problème.

Sans avoir lu la suite, je ne comprends pas cela.

Vayel

Peut-être devrais-je alors ajouter un paragraphe sur la généricité.

J'ai bloqué sur ce passage, parce que je ne comprenais pas ce que tu entendais par spécialisation/spécialiser.

Vayel

Même remarque ce serait introduit avec la généricité.

Le code permet de comprendre ce que tu dis plus haut, mais son utilité n'est pas très claire. Aurais-tu un exemple d'application ?

Vayel

Son utilité est de retourner facilement un nouvel objet, qui contient par exemple ici la méthode register. Il faudrait d'ailleurs que j'ajoute un exemple d'utilisation de singledispatch.

Les arguments plutôt, non ?

Vayel

Non, les paramètres. Les paramètres que reçoit le décorateur.

Ce passage risque d'être compliqué pour qui n'a pas de connaissances en programmation fonctionnelle. A toi de voir si tu veux dédier cette application aux personnes familières avec la notion de récursivité terminale. Sinon, tu pourrais illustrer tes propos par un exemple.

Vayel

Ce n'est peut-être pas très clair, mais ce passage ne requiert pas de connaissances fonctionnelles.

Je ne comprends pas trop cela.

Vayel

call ne fait pas appel à notre fonction, mais retourne un objet intermédiaire qui servira à faire l'appel plus tard. Donc il n'est pas utilisable sans transformation préalable.

J'ai pas mal bloqué sur le code, notamment sur la différence entre call et __call__. En passant du temps dessus, ça va mieux, mais ce n'est pas évident. J'ignore par contre s'il est judicieux d'ajouter des explications, ou s'il est préférable de laisser le lecteur se débrouiller.

Vayel

L'un sert à appeler la fonction et l'autre à simuler un appel qui sera exécuté plus tard.

Merci àtoi.

Qu'est-ce qu'un objet invalide ?

D'après Sam :

L’annotation PEUT être n’importe quelle expression Python valide. En fait l’annotation DOIT être une expression valide. Et cette expression sera exécutée une, et une seule fois, à l’initialisation du module. Le résultat sera stocké et accessible, via l’attribut annotations :

S&M

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
>>> a = [1, 2, 3]
>>> def f(x: a[0]): pass
... 
>>> f.__annotations__
{'x': 1}
>>> def f(x: a[5]): pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

Moi elle me convient, je n'y vois pas de problème.

Autant pour moi, je n'avais pas compris que le "cela" faisait référence à "bénéficier du cache".

Non, les paramètres. Les paramètres que reçoit le décorateur.

Hum… je ne suis pas sûr de comprendre. Le décorateur prend une fonction en paramètre et à chaque appel de la fonction décorée, il compare les types des valeurs (les arguments) aux annotations, non ?

Ce n'est peut-être pas très clair, mais ce passage ne requiert pas de connaissances fonctionnelles.

Le cas échéant, tu pourrais expliquer plus en détail la notion de récursivité terminale. Quitte à donner un lien.

A plus.

+0 -0

Seulement un objet est déjà le résultat d'une expression valide, donc un objet invalide n'existe pas. Dire que l'annotation peut être n'importe quel objet n'est en rien une erreur ou une imprécision.

Hum… je ne suis pas sûr de comprendre. Le décorateur prend une fonction en paramètre et à chaque appel de la fonction décorée, il compare les types des valeurs (les arguments) aux annotations, non ?

Vayel

Le décorateur est appelé une seule fois, juste après la création de la fonction. Il la prend en paramètre et peut donc accéder à ses annotations. Il utilise ces annotations pour construire une nouvelle fonction : la fonction décorée (decorated). Cette fonction décorée remplacera notre fonction initiale, elle recevra ainsi les paramètres à sa place. Elle pourra alors vérifier leur types, avant de relayer l'appel à la fonction initiale, en lui passant les paramètres en arguments :)

Le cas échéant, tu pourrais expliquer plus en détail la notion de récursivité terminale. Quitte à donner un lien.

Vayel

C'est ce que j'envisageais suite à ton message précédent.

Maintenant, les accesseurs et descripteurs.

Introduction

En Python, tout est dynamique, vous pouvez donc interférer à peu près n'importe où.

Je ne comprends pas tellement ce que tu entends par là.

Ainsi, par exemple, quand on accède à l'attribut bar d'un objet toto via foo.bar

Ce serait plutôt un objet foo. :P

L'attribut de Dana

Si vous voulez ajouter des attributs dynamiques, il vous faut donc plutôt passer par getattr.

Le "Si vous voulez ajouter des attributs dynamiques" prête à confusion, puisque __getattr...__ permet de récupérer un attribut, pas d'en ajouter un, ce qui est le rôle de __setattr__. Je comprends ce que tu veux dire, mais peut-être pourrais-tu reformuler ?

Notez tout de même que si votre classe définit un slots, vous ne pourrez plus par défaut définir d'attributs autres sur l'objet que ceux décrits dans les slots.

Si j'ai bien compris, __slots__ contient les attributs de la classe, et le __dict__ de toutes les instances y est égal ?

En interne, la méthode mro fait appel à l'attribut mro de la classe.

Cet attribut est une liste (ou un tuple) ?

(<class 'toto.A'>, <class 'object'>)

Je me suis demandé ce qu'était le toto. Pourquoi avoir choisi de le mettre ?

On constate bien que les classes les plus à gauche sont proritaires lors d'un héritage, mais aussi que le mécanisme de MRO évite la présence de doublons dans la hiérarchie.

Comment élimine-t-il les doublons ("quelle classe" garde-t-il ?) ? Par exemple, pour G, A a été placée après D et non après B : y a-t-il une logique derrière ?

Les descripteurs

Aurais-tu un exemple d'application des descripteurs ? Avec le code que tu donnes, qui fait la même chose que celui de la section précédente, j'ai du mal à voir leur intérêt.

Merci.

+0 -0

Les gestionnaires de contexte :

with or without you

La syntaxe est assez simple à appréhender, on remplace simplement x = expr par with expr as x

Hum, d'après ce qui suit, il semblerait que expr doive être un gestionnaire de contexte instancié. Au début, je pensais que je pouvais mettre n'importe quoi.

et la désallocation de la ressource est gérée pour nous, dans tous les cas.

Peut-être pourrais-tu illustrer cela en regardant ce qu'est devenu x en sortie du bloc ? Ce serait l'occasion d'utiliser un outil pour analyser la mémoire.

La fonction open

L'un des gestionnaires de contexte les plus connus est probablement le fichier

Tu n'as pas vraiment défini ce qu'était un gestionnaire de contexte, et je ne suis pas sûr de comprendre cette phrase.

Je me demande s'il ne serait pas préférable d'utiliser un titre de niveau 1 et de placer cette partie dans la première section, comme exemple d'utilisation de with.

Fonctionnement interne

Nous pouvons maintenant créer notre propre type de contexte, contentons-nous pour le moment de quelque chose d'assez simple qui afficherait un message à l'entrée et à la sortie.

Peut-être pourrais-tu revenir sur la mémoire, en montrant que ctx a été détruit à la sortie du bloc with.

Simplifions-nous la vie avec la contextlib

Cela peut s'avérer utile pour créer un module qui mesurerait le temps d'exécution d'un ensemble d'instructions : on peut vouloir sans servir via with, ou via un décorateur autour de notre fonction à mesurer.

Je comprends ce que tu veux dire, parce que j'ai déjà utilisé with, mais le lecteur pourrait ne pas voir le rapport avec la désallocation des ressources, à partir de laquelle tu as introduit les context managers.

TP : changement de répertoire

Là encore, on ne comprend plus trop le rapport avec la gestion de la mémoire.

A plus. :)

+0 -0

Je ne comprends pas tellement ce que tu entends par là.

Vayel

Qu'à la manière des décorateurs, il est possible de changer le comportement d'objets de façon quasi-transparente.

Le "Si vous voulez ajouter des attributs dynamiques" prête à confusion, puisque __getattr...__ permet de récupérer un attribut, pas d'en ajouter un, ce qui est le rôle de __setattr__. Je comprends ce que tu veux dire, mais peut-être pourrais-tu reformuler ?

Vayel

Je verrai pour expliciter ça mieux.

Si j'ai bien compris, __slots__ contient les attributs de la classe, et le __dict__ de toutes les instances y est égal ?

Vayel

Non, __dict__ n'existe pas pour les objets dont la classe utilise __slots__.

Cet attribut est une liste (ou un tuple) ?

Vayel

Oui.

Je me suis demandé ce qu'était le toto. Pourquoi avoir choisi de le mettre ?

Vayel

Une réminiscence du fichier dans lequel je travaillais pour tester mes exemples, je corrigerai ça.

Comment élimine-t-il les doublons ("quelle classe" garde-t-il ?) ? Par exemple, pour G, A a été placée après D et non après B : y a-t-il une logique derrière ?

Vayel

Il me semble que lors d'un héritage en diamant, la classe est placée dans la liste à sa dernière position possible (ce qui expliquerait que A apparaisse après B pour F et après D pour G, ou encore qu'il soit impossible de faire class H(A, B)). Mais je n'ai pas étayé ce sujet et n'ai pas plus d'informations, il faudrait que je regarde si j'ai un document qui en parle.

Aurais-tu un exemple d'application des descripteurs ? Avec le code que tu donnes, qui fait la même chose que celui de la section précédente, j'ai du mal à voir leur intérêt.

Vayel

Plusieurs même : property, classmethod, staticmethod.

Hum, d'après ce qui suit, il semblerait que expr doive être un gestionnaire de contexte instancié. Au début, je pensais que je pouvais mettre n'importe quoi.

Vayel

Tu peux y mettre tout objet qui respecte l'interface des gestionnaires de contexte, oui, mais tu n'as pas besoin de l'instancier au moment du with :

1
2
3
4
f = open('/dev/stdout', 'w')
with f:
    print('Salut', file=f)
assert(f.closed)

Peut-être pourrais-tu illustrer cela en regardant ce qu'est devenu x en sortie du bloc ? Ce serait l'occasion d'utiliser un outil pour analyser la mémoire.

Vayel

Je ne comprends pas ce que tu entends par là. x n'est rien devenu de spécial, on est juste sorti du contexte, et donc une méthode de sortie a été appelée sur le contexte.

Tu n'as pas vraiment défini ce qu'était un gestionnaire de contexte, et je ne suis pas sûr de comprendre cette phrase.

Vayel

Je vais revoir ça. Il faut juste comprendre que file est un gestionnaire de contexte : un objet file possède une méthode __enter__ et une méthode __exit__.

Peut-être pourrais-tu revenir sur la mémoire, en montrant que ctx a été détruit à la sortie du bloc with.

Vayel

Il n'a pas été détruit.

Je comprends ce que tu veux dire, parce que j'ai déjà utilisé with, mais le lecteur pourrait ne pas voir le rapport avec la désallocation des ressources, à partir de laquelle tu as introduit les context managers.

Vayel

C'est peut-être mal formulé, mais au début de la section j'élargis le scope en indiquant que les contextes permettent d'exécuter un bout de code en entrée et en sortie d'un bloc.

Là encore, on ne comprend plus trop le rapport avec la gestion de la mémoire.

Vayel

Je parle plus généralement de ressources que de mémoire, et l'environnement courant en fait pour moi partie.

Pour le reste, quelques erreurs inattention, j'ai déjà pas mal de modifications localement, je pousserai ça bientôt. Merci.

Je pense que ce qui m'a perturbé, sur les gestionnaires de contexte, c'est :

Pourtant, une gestion déterministe des ressources peut s'avérer utile, et c'est là qu'interviennent les gestionnaires de contexte (context managers) et leur mot-clef with.

Même si j'avais déjà utilisé des gestionnaires de contexte, j'ai interprété cette phrase comme "Un with est équivalent à un declare en Ada (blocs de déclaration)".

D'ailleurs, peut-être pourrais-tu expliquer en quoi un context manager diffère d'un décorateur ? Est-ce juste du sucre syntaxique, ou les premiers permettent-ils de faire des choses inacessibles aux seconds ?

Merci.

+0 -0

D'ailleurs, peut-être pourrais-tu expliquer en quoi un context manager diffère d'un décorateur ? Est-ce juste du sucre syntaxique, ou les premiers permettent-ils de faire des choses inacessibles aux seconds ?

??

Un décorateur permet de modifier ou manipuler statiquement une fonction ou une classe juste après sa définition. Un context manager est un bloc try/except/finally intelligent.

Je vois pas bien comment on peut confondre les deux, ou même hésiter entre les deux pour résoudre un problème donné.

PS : Vous parlez pas mal de "ressources" à propos des context managers, je pense que le mot-clé est plutôt "contexte". Le rôle d'un context-manager est plutôt de garantir quelquechose à l'intérieur de son contexte, et autre chose à l'extérieur.

Par exemple :

  • un CM sur une connexion MySQL peut définir visuellement que toute la transaction en cours se trouve dans le bloc with. Une fois sorti de celui-ci, soit la transaction est commise, soit elle est annulée, mais en tout cas la connexion à la base est dans un état cohérent (hors du with, on ne se trouve pas au milieu d'une transaction).

  • un CM sur un objet "Directory" pourrait garantir que qu'un répertoire est ouvert dans le processus (associé à un FD) et que tout fichier manipulé à l'intérieur du with l'est à partir de ce repertoire. Dans un contexte de programmation système ça permet de résoudre beaucoup plus rapidement les chemins des fichiers en faisant partir les chemins de l'inode du répertoire au lieu du CWD qui se trouve dans l'environnement du processus (et dont la résolution, sans ça, est systématique et un peu plus laborieuse pour le noyau).

  • with suppress(...) garantit la non-propagation des exceptions levées à l'intérieur du bloc.

  • with lock: garantit qu'un seul fil d'exécution à la fois (thread, processus ou coroutine, suivant le contexte et l'implémentation), exécute le contenu du bloc.

  • En Cython, with nogil: garantit qu'on ne touche à aucun objet Python (sous la responsabilité de l'interpréteur) à l'intérieur du bloc (ça ne compile pas sinon). Donc ça permet de définir un bloc qui peut s'exécuter complètement en parallèle de CPython et ainsi dépasser temporairement le pinacle des 100% CPU dans une application multithreadée…

  • Avec asyncio on peut définir un contexte qui nous garantisse que les opérations que l'on s'apprête à effectuer faire ne suspendront pas la coroutine (donc que le contenu du with est atomique).

En bref : la gestion déterministe des ressources n'est qu'une application possible des CM. Ou bien il faut utiliser le terme "ressource" dans son acception la plus vague possible.

+0 -0

Un décorateur permet de modifier ou manipuler statiquement une fonction ou une classe juste après sa définition. Un context manager est un bloc try/except/finally intelligent.

Je vois pas bien comment on peut confondre les deux, ou même hésiter entre les deux pour résoudre un problème donné.

Effectivement, je sais pas ce que j'ai fumé.

+0 -0

En effet, je pense que je vais revoir l'introduction des gestionnaires de contexte pour éviter de parler de ressources, et éclaircir un peu le tout.

Et à propos du MRO, c'est explicité ici: https://www.python.org/download/releases/2.3/mro/

Finissons-en, avec les métaclasses.

Introduction

L'intérêt principal des métaclasses est de pouvoir modifier les classes lors de leur création, en ajoutant de nouvelles méthodes ou attributs par exemple.

Peut-être pourrais-tu ajouter le mot "dynamiquement" dans ta phrase. Là, le lecteur pourrait se demander pourquoi ne pas simplement ajouter à la main les attributs et méthodes dans la déclaration de notre classe.

Aurais-tu un exemple concret ?

>>> type(int) # int est une instance de type

Je ne comprends pas pourquoi int n'est pas de type object vu qu'il en hérite. En fait, je me rend compte que j'ignore ce qu'est un type exactement. L'article que tu fournis semble répondre à la question, mais dans ce cas, il serait- peut-être judicieux de le placer en haut de l'introduction, comme pré-requis.

Je vais m'arrêter ici pour l'instant, parce que je ne suis pas sûr de satisfaire les pré-requis. J'ai commencé la suite, et ça me semble plutôt abstrait. Peut-être qu'un exemple d'utilisation des métaclasses en introduction aiderait ? Autrement dit, partir d'un problème, et introduire les métaclasses pour le résoudre.

Merci.

+0 -0

Là, le lecteur pourrait se demander pourquoi ne pas simplement ajouter à la main les attributs et méthodes dans la déclaration de notre classe

Il a le droit de se le demander. D'ailleurs la réponse à cette question est complexe et mérite de toute façon une réflexion de sa part.

+0 -0

Peut-être pourrais-tu ajouter le mot "dynamiquement" dans ta phrase. Là, le lecteur pourrait se demander pourquoi ne pas simplement ajouter à la main les attributs et méthodes dans la déclaration de notre classe.

Vayel

Ça me semble redondant, je ne vois pas trop à quoi correspondrait l'action d'ajouter statiquement de nouvelles méthodes.

Aurais-tu un exemple concret ?

Vayel

Les exemples viennent plus tard. Mais il faut bien noter que c'est une notion avancée, et que les exemples ne sont pas évidents. Celui qui m'a paru le plus simple étant l'Enum, présenté plus loin.

Je ne comprends pas pourquoi int n'est pas de type object vu qu'il en hérite. En fait, je me rend compte que j'ignore ce qu'est un type exactement. L'article que tu fournis semble répondre à la question, mais dans ce cas, il serait- peut-être judicieux de le placer en haut de l'introduction, comme pré-requis.

Vayel

Je pense que tu confonds instanciation et héritage. Pour créer une classe héritant de object, tu n'instancies pas ce dernier (tu ne fais pas appel à son constructeur), par contre, tu fais appel à celui de la métaclasse. Les classes parentes n'interviennent que plus tard, à l'utilisation des objets, lors de la résolution du MRO.

Je veux bien comprendre ce qu'il manque dans le tutoriel et qui serait présent dans le lien donné permettant de bien saisir la différence entre les deux.

Je vais m'arrêter ici pour l'instant, parce que je ne suis pas sûr de satisfaire les pré-requis. J'ai commencé la suite, et ça me semble plutôt abstrait. Peut-être qu'un exemple d'utilisation des métaclasses en introduction aiderait ? Autrement dit, partir d'un problème, et introduire les métaclasses pour le résoudre.

Vayel

Comme dit, c'est abstrait, et les exemples sont assez complexes. Je peux difficilement donner un exemple quand le lecteur ne connaît pas type, __new__ ou metaclass=.

>>> type(int) # int est une instance de type

Je ne comprends pas pourquoi int n'est pas de type object vu qu'il en hérite. En fait, je me rend compte que j'ignore ce qu'est un type exactement.

Vayel

Là tu touches du doigt l'essence même de Python, son "mystère fondamental" qui, à l'instar de l'oeuf et de la poule, donne le vertige en bouclant à l'infini :

  • int est une instance de type.
  • type est une sous-classe de object.
  • object est une instance de type.
  • type est également une instance d'object.
  • Donc object est une instance… d'object.
  • Et type est une instance de lui-même également.

Quand on dit que tout est un objet en Python, c'est pour de vrai : le type "objet" est une instance de lui-même.

+1 -0

Je suis toujours sur les métaclasses. J'attends d'avoir les idées plus claires dessus pour te faire mes retours. J'essayerai de refaire une relecture de l'ensemble du tutoriel pendant ces vacs.

Encore merci pour ton travail. :)

+0 -0

Il y a eu quelques reformulations sur les métaclasses, mais n'ayant pas compris ce qui te posait problème, je ne sais pas si ça convient.

Je voulais aussi signaler que j'avais envoyé la version actuelle de la beta en validation.

Ce qui suit devrait être plus explicite. :)

Instance, classe et métaclasse

Une classe est ainsi une instance de la classe type.

Tu pourrais explicité cela. En effet, si la notion de type est assez intuitive (un nombre n'est pas la même chose qu'une chaîne), le rapport avec les classes (5 est une instance de int, par exemple) est, je pense, moins connu.

Peut-être d'ailleurs pourrais-tu commencer par là, ayant ainsi le raisonnement suivant :

  • En Python, tous les objets ont un type
  • Exemples simples (entiers, chaînes, etc.), en utilisant juste la fonction type
  • Mais, pour un objet, être du type T revient à être une instance de cette classe T
  • Or une classe est un objet comme un autre
  • Donc quelle est la classe d'une classe (son type) ?

D'ailleurs, y a-t-il une différence entre "être du type T" et "hériter de la classe T" ? Quid des héritages multiples ?

Quel est donc ce type ?

Une chaîne de caractères représentant le nom de la classe ;

Je ne comprends pas tellement l'intérêt vu qu'on stocke la classe dans une variable : A = type(...).

Le dictionnaire des attributs et méthodes de la classe.

Peut-on renseigner des attributs et méthodes de classes ou statiques ?

Le vrai constructeur

Nous choisissons ici de faire appel à object.new dans notre constructeur (via super), mais nous n'y sommes pas obligés.

Peut-être cela a-t-il plus sa place dans un tutoriel sur la POO, mais aurais-tu un exemple concret où MaClasse.__new__ ne fait pas appel à object.__new__ ?

Les métaclasses

Puisque nous voulons altérer la création et non l'initialisation, c'est dans le constructeur que le tout va s'opérer.

J'ai un peu bloqué là-dessus, puisque je me suis dit que ce n'est pas la création de la métaclasse qu'on souhaite altérer, mais celle de la classe. Du coup, je ne comprends pas tellement pourquoi on modifie le constructeur de la métaclasse.

>>> class M(type):

Tu pourrais faire autre chose dans le __new__, même si c'est bidon. Là, M ne sert à rien d'autre que montrer la syntaxe. Par exemple, Sam illustre ça en créant une métaclasse qui préfixe tous les noms d'attributs et de méthodes.

Cette méthode doit toutefois retourner un dictionnaire ou objet similaire.

Par "objet similaire", tu veux dire "objet indexable (par des chaînes ?)" ?

Les énumérations en Python sont implémentées à l'aide de métaclasses.

Peut-être pourrais-tu redonner le lien fourni dans la première section ?

Une implémentation simplifiée possible d'Enum est la suivante :

J'ai vraiment du mal avec cet exemple. N'y aurait-il pas moyen de le simplifier en enlevant le cache ?

Utiliser une fonction comme métaclasse

Ce qui fait qu'à l'héritage, l'appel à la métaclasse serait perdu

J'ai un peu bloqué là-dessus parce que tu n'as pas vraiment mis en relation l'héritage et les métaclasses.

TP : Évaluation paresseuse

Et dans notre exemple, l'objet retourné devra posséder les méthodes add et eq. Méthodes qui se chargeront d'effectuer le calcul du carré.

Ce ne sont ici que deux opérateurs, mais il en existe beaucoup d'autres, dont l'énumération serait inutile et fastidieuse, et il va nous falloir tous les gérer.

A la première lecture, j'ai eu un peu de mal là-dessus. Peut-être pourrais-tu clarifier en disant explicitement qu'on souhaite exécuter la fonction (calculer le carré) lorsqu'on applique un opérateur sur notre objet, ou plus généralement, qu'on souhaite exécuter l'expression lorsqu'on appelle une méthode de l'objet.

Merci.

+0 -0
Ce sujet est verrouillé.