Pourquoi Python modifie directement les listes

L'auteur de ce sujet a trouvé une solution à son problème.
Auteur du sujet

Bonjour !

J'ai arrêté la programmation depuis un bon moment (2-3 ans)et je m'y remet puisque je fais des études en informatique.

J'avais à l'époque touché pas mal à Python et je l'appréciais bien, mais maintenant que j'y retouche il y a quelque chose qui me titille avec le fait que les méthodes de la classe list modifient directement l'objet et retourne None.

Prenons un exemple :

Je veux mettre les mots d'une phrase dans une liste et ensuite les inverser (bon, exemple bidon me direz-vous puisqu'on peut faire ça directement avec le string, mais ce n'est qu'un exemple).

En Python, impossible d'y aller simplement avec une simple ligne, obligé d'y aller en plusieurs étapes :

1
2
3
4
def foo():
    r = "Euston saw I was not Sue".split()
    r.reverse()
    return r

Ou alors, si on ne veut pas utiliser reverse, il faut sortir des méthodes de la classe list et y aller avec le slicing ([::-1]), là où en Ruby par exemple (pour comparer des pommes avec des pommes) on peut facilement faire tenir le tout sur une ligne :

1
2
3
def foo()
    return "Euston saw I was not Sue".split().reverse()
end

Même chose si on prend un autre exemple : on a une liste d'éléments (par exemple [1,3,2,5,4]) et on veut trier cette liste et assigner cette liste triée à b sans modifier la liste d'origine. Avec ruby, rien de bien compliquer :

1
2
a = [1,3,2,5,4]
b = a.sort()

Par contre, encore une fois avec Python impossible d'y aller avec les méthodes de la classe list, on doit sortir de la classe et utiliser la fonction builtin sorted

1
2
a = [1,3,2,5,4]
b = sorted(a)

J'avoue que j'ai bien du mal à comprendre le pourquoi de cette conception. Qu'en pensez-vous ? Des choses sous-jacentes que je ne comprends pas ? Un choix comme un autre ? Je mets le doigt sur quelque chose qui revient souvent ? Je suis stupide et tout le monde est à l'aise avec cette façon de faire ?

PS: je ne fais pas de Ruby et je n'aime pas particulièrement ce langage, j'ai seulement pris le premier langage comparable qui m'est passé sous la main.

Édité par vildric

+1 -0
Staff

Tu peux aller regarder ici si ça t'amuse pour trouver d'autres faiblesses : http://sucre.syntaxique.fr/doku.php

Sinon je ne comprend pas ton premier exemple, tu peux écrire :

1
2
def foo()
    return "Euston saw I was not Sue".split().reverse()

en python.

Pour le reste je n'ai pas de bons arguments. Mais au moins ça laisse le choix à l'utilisateur s'il veut une modification en place ou non.

Edit : En effet, en l'écrivant, je me disais que c'était une connerie…

Édité par Saroupille

+0 -0
Staff

Non Saroupille, ton exemple ne fonctionne pas car les méthodes modificatrices de listes ne renvoient rien.

C'est vrai que ca peut sembler étrange dit comme ça, que ça semble alourdir le code mais il est important de noter une chose : les listes en python sont des objets mutables. Et donc le comportement est logique. Il existe une très grosse différence entre tes exemples python et ruby : les exemples python ne créé aucun nouvel objet. Fait ça des millions de fois par seconde et la différence va etre flagrante. C'est là la raison d'existence des listes en python. En fait une liste non modifiable en python ca existe et on appel ça les tuples.

Tu devrais donc plutôt te demander pourquoi les tuples ne proposent pas les mêmes méthodes renvoyant un nouvel objet…

Édité par Kje

+0 -0

Rien n'empêche de configurer ta classe str à ta guise

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> class myStr(str):
...     def rev(self):
...         words = self.split()
...         if len(words) >= 2:
...             return " ".join(words[::-1])
...         else:
...             return self[::-1]
... 
>>> s = str("Euston saw I was not Sue")
>>> s.rev()
'Sue not was I saw Euston'
>>> s = str("Sue")
>>> s.rev()
'euS'

Bref si on veut on peut le faire, mais la grammaire python a ses spécificités, ce qui le rend différent du langage ruby, comme l'anglais a des ressemblances avec la langue américaine, mais on des exceptions les rendant différentes.

Ta conception se rapproche de celle de ruby et donc est plus adaptée à ce langage, ce qui n'en fait pas une mauvaise grammaire concernant le langage python, c'est juste une autre vision des choses.

+0 -0

En résumé, le créateur de Python a jugé que maChaine.split().reverse() c'était illisible. Du coup, les gens préfèrent écrire " ".join(maChaine.split()[::-1]).

Ça se passe de commentaire.

Édité par anonyme

+0 -0
Staff

En résumé, le créateur de Python a jugé que maChaine.split().reverse() c'était illisible, alors que " ".join(maChaine.split()[::-1]) c'est parfait.

Ça se passe de commentaire.

katana

En fait, sans défendre l'argument de "lisibilité" (on va pas être de mauvaise foi), je vois les choses comme ça :

Le principe premier de python c'est "il doit y avoir un (si possible un seul) moyen évident de le faire".

Quand je dis "list.reverse()", je dis "inverse-moi la liste", la liste est donc belle et bien modifiée dans mon esprit, faire .reverse() est donc le moyen évident d'inverser l'ordre des éléments.

vient alors le moment de ton cas "afficher la représentation en chaîne de caractères des élééments de la liste du dernier au premier.". Le moyen qu'a décidé python pour traduire "du dernier au premier" c'est [::-1]. C'est pas ultra lisible mais c'est court. Du coup il existe bien un seul moyen de traduire la phrase initiale. C'est juste qu'il n'est pas "évident".

+0 -0

Du coup il existe bien un seul moyen de traduire la phrase initiale. C'est juste qu'il n'est pas "évident".

artragis

Unless you’re Dutch.

Édité par simbilou

La répétition est la base de l’enseignement. — ☮ ♡

+1 -0
Staff

En résumé, le créateur de Python a jugé que maChaine.split().reverse() c'était illisible. Du coup, les gens préfèrent écrire " ".join(maChaine.split()[::-1]).

Ça se passe de commentaire.

katana

Mouais, tu fais une interprétation de mauvaise fois là. Déjà tes deux bouts de codes ne sont pas équivalents (tu ne fais pas de join dans le premier). Ensuite si on lit son mail, il dirait plutot qu'il préfère :

1
2
3
ma_list_de_chaine = maChaine.split()
rev_list = reversed(ma_list_de_chaine)
ma_chaine_final = " ".join(rev_list)

et l'argument de lisibilité est clairement marqué. Après oui ça prend 3 lignes quand tu écris ça comme ça mais au moins tu sais clairement que ce sont des opérations qui ne modifies pas la liste sous jacente.

A noter que ce serait assez facile de refaire un tel type de liste, en se basant sur les tuples par exemple.

+0 -0

Je me suis effectivement rendu compte que mon premier message (cité par artragis) laissait entendre que Van Rossum préférait le code que je donne (à base de join). Effectivement, lui il préférerait sûrement le code que tu donnes (encore que reversed soit relativement récent). C'est pourquoi j'ai corrigé mon message (ce qui donne la version que tu cites).

Remarque qu'on passe quand même d'une composition toute bête (reverse . split) à 3 lignes de code, juste pour éliminer le [::-1] - que beaucoup de Pythonneux reconnaîtront sans doute pourtant comme un idiome fondamental.

La réponse d'artragis me semble intéressante, parce que je crois qu'elle décrit plus largement l'évolution de Python 2 à Python 3 : une différence de plus en plus grande entre la représentation interne des données (dans Python 3, des objets de classe map, reversed… là où on avait des listes en Python 2) et la façon d'interpréter/d'afficher ces données. Je ne connais pas bien Lisp (et je connais de moins en moins bien Python) mais j'ai l'impression que Python s'éloigne de plus en plus de ce langage, pour ressembler à autre chose. Personnellement je trouve ça contre-productif d'écrire 3 lignes quand on pourrait écrire 3 mots (ou 6 si tu veux rajouter le join), mais les goûts et les couleurs…

Quant à réimplémenter les listes à l'aide de tuples, oui on pourrait sans doute, mais c'est un autre débat.

Édité par anonyme

+0 -1
Staff

Note que je comprend cette réticence mais je comprend aussi le point de vue de Guido. Renvoyer l'objet quand tu le modifie apporte une certaine confusion. Je ne trouve pas ça super clair. Il faut bien connaitre le langage pour savoir ce qui ce passe. Ne pas renvoyer l'objet mais une copie est alors un autre problème. Les listes sont des objets modifiables. Ce sont surtout les tuples qui auraient ce rôle. Pourquoi les tuples n'ont pratiquement aucune méthodes est probablement la bonne fonction à se poser.

+0 -0

Remarque qu'on passe quand même d'une composition toute bête (reverse . split) à 3 lignes de code, juste pour éliminer le [::-1] - que beaucoup de Pythonneux reconnaîtront sans doute pourtant comme un idiome fondamental.

katana

Note que rien n'empêche de retranscrire sur une ligne le code de Kje:

1
' '.join(reversed(ma_chaine.split()))

Commentaire à part, je ne suis pas un expert de python mais c'est aussi la politique de java et de C# de ne pas renvoyer this pour les métodes qui modifient l'objet (en tout cas pas dans les API originelles, plus tard des exceptions sont apparues). Donc ça a aussi déjà été débattu plusieurs fois ailleurs pour d'autres langages.

C'est aussi la politique des méthodes génériques de la STL en C++ me semble-t-il (si on veut une copie, on a parfois des variantes suffixées _copy).

Le but c'est je pense surtout de bien pouvoir différencier rapidement d'un coup d'œil qu'Est-ce qui modifie et qu'est-ce qui renvoie une copie, sans pour autant être un expert du langage. De ce que je connais de ruby, de leur côté ils ont choisi de faire la différence avec des méthodes suffixés de '!' pour les variantes modifiantes. A chacun son choix…

Édité par QuentinC

Ma plateforme avec 23 jeux de société classiques en 6 langues et 13000 joueurs: http://qcsalon.net/ | Apprenez à faire des sites web accessibles http://www.openweb.eu.org/

+0 -0

Remarque qu'on passe quand même d'une composition toute bête (reverse . split) à 3 lignes de code, juste pour éliminer le [::-1] - que beaucoup de Pythonneux reconnaîtront sans doute pourtant comme un idiome fondamental.

katana

Note que rien n'empêche de retranscrire sur une ligne le code de Kje:

1
' '.join(reversed(ma_chaine.split()))

entwanne

C'est vrai, mais c'est indépendant de ma remarque puisque reversed n'a pas dû être introduit avant Python 2.4.

+0 -0
Staff

Remarquez cependant que le PO ne semblait pas contre cette idée et il demandait pourquoi les listes étaient mutables justement. En python il y a les deux, ce sont les tuples qui sont immuables. Il ne serait pas choquant qu'un tuple est une méthode append qui forme un nouveau tuple avec la nouvelle valeur rajouté permettant ainsi de les chaîner.

NB pour katana : python 2.4 c'est du legacy, il n'est largement plus supporté. C'est pas une bonne raison pour ne pas l'utiliser, je pense.

+0 -0

python 2.4 c'est du legacy, il n'est largement plus supporté. C'est pas une bonne raison pour ne pas l'utiliser, je pense.

Kje

Ça n'est pas mon propos. Je suis toujours là-dessus. La question initiale est "pourquoi c'est pas comme ça". La réponse est "parce que Van Rossum a décidé que ça n'était pas lisible". D'où ma remarque : à l'époque où il l'a décidé, reversed n'existait pas et les gens ont adopté un idiome complètement illisible (ce qui se voit chez les gens qui ont appris Python avant 2.4, comme fred1599 ou moi-même).

J'ai bien compris que maintenant les gens faisaient autrement, et préféraient chainer(par(la(gauche))) plutôt que par.la().droite().

Édité par anonyme

+0 -0
Staff

Je suis pas surs. Encore une fois le chaînages n'a de sens que quand tu joue avec des tuples ou des générateurs, pas les listes qui sont des objets mutables et ont donc un autre comportement

+0 -0
Auteur du sujet

Du coup, je me demande quand même pourquoi d'un côté on a des listes mutables qui ne renvoit rien, et de l'autre côté des strings immuables qui renvoit une copie sur des méthodes comme upper et qui permet donc une composition et avec le comportement "normal" qu'on attend normalement.

Encore là il doit y avoir une bonne explication qui dépasse (grandement) mes compétences, mais je trouve ça un peu incohérent quand-même.

+0 -0
Staff

Non justement c'est logique : une méthode qui modifie l'objet ne renvoit jamais de reference a elle meme pour justement faire la distinction avec des méthodes qui renvoie des copies.

+0 -0
Staff

Remarque qu'on passe quand même d'une composition toute bête (reverse . split) à 3 lignes de code, juste pour éliminer le [::-1] - que beaucoup de Pythonneux reconnaîtront sans doute pourtant comme un idiome fondamental.

katana

Tout le problème est là : quand on travaille avec des objets mutables, sur lesquels toutes les méthodes ont des effets de bord, la composition est contre-intuitive.

D'une façon générale j'adhère à l'argument de Guido. Sans parler de liste, si je vois que dans un code, un objet est manipulé de cette façon :

1
2
3
spam.foo()
spam.bar(eggs, bacon)
spam.baz(ham)

Je sais tout de suite que ces opérations vont avoir des effets de bord modifiant l'état interne de spam.

À l'inverse :

1
polly = parrot.with_color("blue").with_status("dead")

Ici, intuitivement, je m'attends à ce que l'objet parrot n'ait pas été modifié par ce code. Et s'il l'a été, alors je suis tombé dans le panneau et je m'expose à des bugs bien mystiques.

Ce n'est pas tellement de lisibilité qu'il est question, mais plutôt d'intelligibilité : la forme du code donne des indications sur la nature des objets qu'il manipule.

PS : Je ne comprends pas qu'on veuille aller absolument à l'économie de lignes sur ce genre de cas. D'autant que lorsque l'on respecte les 79 caractères de la pep-8, un chaînage de 3km de long devient rapidement inesthétique.

Édité par nohar

I was a llama before it was cool

+1 -0
Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

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