Pourquoi Python modifie directement les listes

a marqué ce sujet comme résolu.

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.

+1 -0

Moi je n'ai aucun problème en tant que tel avec cette conception, c'était seulement une remarque !

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.

Kje

Je suis tout à fait d'accord, mon point était plutôt : pourquoi ne pas se tenir partout. Pourquoi ce qui est bon pour les strings ne l'est pas pour les listes et vice-versa ? Pourquoi a-t-on d'un côté des strings immuables et d'un autre côté des listes mutables ? Pourquoi s.capitalize() renvoit une copie alors que l.reverse() modifie directement l'objet. Je ne comprends simplement pas pourquoi on a les deux côtés d'une médaille dans un même langage.

+0 -0

D'abord pour garder des types simples et cohérents.

Restons sur l'exemple des chaînes de caractères et des listes.

Une chaîne est représentée en interne par un bloc d'octets contigus en mémoire. Ajouter ou supprimer des caractères en début/milieu/fin de chaîne ne peut se faire qu'en allouant un nouveau bloc plus grand/plus petit puis en recopiant entièrement la chaîne avec les modifications. C'est une opération qui fera de toute façon coexister à un instant T les deux versions de la chaîne, alors autant rendre ce fait transparent.

À l'inverse, les listes sont plus ou moins des listes chaînées de pointeurs (en réalité c'est un peu différent mais partons là-dessus pour simplifier le propos). Ajouter ou modifier un élément dans une liste chaînée peut se faire en place sans avoir à recopier entièrement la structure, alors autant le faire pour garder une opération performante.

À partir de là, Python fait le choix de ne proposer que des objets entièrement mutables ou entièrement immutables, tout simplement pour faciliter la vie des développeurs : savoir qu'une liste est mutable, c'est savoir que toutes les opérations sur cette liste se font en place. Pas besoin d'apprendre quelles méthodes des chaînes/listes opèrent en place ou par copie ni de savoir comment chaque type est représenté en mémoire, il n'y a qu'à retenir le caractère (im)mutable du type.

Par contre c'est vrai que ça ouvre le champ à des subtilités potentiellement déroutantes, comme le fait qu'un tuple, qui est en interne (en C) un simple tableau de PyObject*, est une structure immutable qui peut contenir des objets mutables, donc que rien n'interdit de modifier une liste contenue dans un tuple…

+0 -0

@vildric : parce qu'on a les deux. Une liste immuable comme une chaine ca existe et ça s'appelle un tuple. Donc en fait ce n'est pas un problème d'inconsistance mais que python te propose les deux formes d'agrégats.

On peut regretter cependant que les tuples proposent si peu de méthodes.

A mon avis, le débat strings imutables vs collections mutables est beaucoup plus profond que ça, et je ne crois pas que ce soit lié au simple fait que les listes soient implémentées en interne sous forme de listes chaînées ou en quoi que ce soit d'autre.

Pour moi, c'est avant tout une question d'histoire et d'influences qui se sont petit à petit muées en us et coutumes s'il en est ainsi. Bref, c'est rentré dans les usages, entres autres sous l'influence de Java, et ensuite tout le monde a continué.

Une autre raison à mon avis très profonde derrière l'immuabilité des strings, c'est qu'au départ on voulait économiser la mémoire. Tous les noms de classe, de méthodes, d'attributs, etc. sont mutualisés. Pour faire simple, si on rencontre 100 fois "print" dans un code, alors c'est 100 fois le même pointeur. Ca implique forcément que les chaînes soient immuables, et que ça fonctionne selon le principe du copy on write. Et la technique est utilisée au plus profond du compilateur et de l'interpréteur eux-mêmes, pour à la fois gagner du temps et de la mémoire, si bien que c'est extrêmement ancré dans le fonctionnement interne du langage.

En fouillant un peu, on s'aperçoit qu'on retrouve ce mécanisme dans beaucoup de langages interprétés ou semi-interprétés, notamment Java, C#, lua, python, javascript, très probablement aussi php et perl, et en fait aussi un peu en C++ (ou en tout cas avec certaines implémentations de std::string)

+0 -0

Un gros avantage de l’immuabilité aussi est que c’est intrinsèquement thread-safe. Si personne ne peut modifier une variable, la partager ne pose jamais de problème. C’est bien pour ça que beaucoup de gens prévoit que le paradigme fonctionnel finira par s’imposer. Les variables étant immuable par défaut, ça se parallélise très bien, et comme le but du jeux dans la fabrication de CPU modernes semble être de placer un maximum de cœur sur une puce, il va bien falloir paralléliser tout ce qu’on peut.

En fouillant un peu, on s'aperçoit qu'on retrouve ce mécanisme dans beaucoup de langages interprétés ou semi-interprétés

C’est aussi le cas en D qui est un langage compilé. En Eiffel aussi, Delphi à prit le pli récemment (avant c’était du COW). C’est dans l’air du temps on va dire.

+0 -0

de langages interprétés ou semi-interprétés

Je n'arrive pas à faire la différence entre un langage "interprété", "semi interprété" ou "compilé".

Dans ton message, tu sembles inclure Java dans les non compilé (ou semi interprété) alors qu'il est statiquement compilé et qu'il a même des élément de compilation à la volée en code natif.

PHP est compilé à chaque fois sauf si tu active ZO+ (inclus par défaut), et possède de plus en plus d'éléments de compilation à la volée.

pour python, tu as cython qui compile python de la même manière il me semble.

Bref, je ne comprends pas.

php fait du C.O.W sur les string et ses tableaux (php array = une mapped hash table).

Merci d'avoir confirmé. Par contre pour les tableaux, ça doit plutôt être une ahsh map + une liste chaînée, sinon les éléments ne resteraient pas en ordre.

Un gros avantage de l’immuabilité aussi est que c’est intrinsèquement thread-safe. Si personne ne peut modifier une variable, la partager ne pose jamais de problème. C’est bien pour ça que beaucoup de gens prévoit que le paradigme fonctionnel finira par s’imposer.

Très juste, bien vu pour le thread safe.

Par contre, limmuabilité a des limites à mon avis. Devoir recopier entièrement un tableau/liste/vector/ArrayList à chaque fois qu'on ajoute ou suppprime un élément, ça peut finir par coûter cher en performances. C'est pour ça qu'en général les implémentations de tableaux dynamiques font des allocations à l'avance. Et c'est aussi pour ça que pour des manip caractère par caractère sur les strings, on a une autre classe genre StringBuffer. Et j'ose imaginer que recopier un set ou une map c'est encore plus coûteux.

Le but du jeu pour l'avenir, ça va être de réussir à déterminer ce qui peut être muable, et ce qui vaudrait mieux ne pas l'être, selon comment on utilise tel ou tel objet dans le programme, si c'est destiné à être partagé/parralélisé ou non… les langages fonctionnels sont-ils capables de choisir la meilleure stratégie à chaque fois ? ou bien il font systématiquement de la copie ?

Je n'arrive pas à faire la différence entre un langage "interprété", "semi interprété" ou "compilé".

Dans ton message, tu sembles inclure Java dans les non compilé (ou semi interprété) alors qu'il est statiquement compilé et qu'il a même des élément de compilation à la volée en code natif.

PHP est compilé à chaque fois sauf si tu active ZO+ (inclus par défaut), et possède de plus en plus d'éléments de compilation à la volée.

pour python, tu as cython qui compile python de la même manière il me semble.

Bref, je ne comprends pas.

Ce genre de truc est souvent sujet à débat et les frontières se font de plus en plus floues, mais mon classement est le suivant :

  • Compilé pour moi ça veut dire convertir directement du code source en langage machine ou très proche. Ici je mets évidemment C/C++.
  • Semi-compilé ou semi-interprété pour moi ça veut dire compiler depuis un code source vers un code intermédiaire (bytecode), visible/sauvegardable par l'utilisateur ou non d'ailleurs, puis, à l'exécution, interprétation du bytecode. Le JIT c'est du bonus à part. Ici je classe en fait à peu près tous les autres langages: Java, C#, php, python, lua, ruby, javascript…
  • Interprété pour moi c'est pas de conversion en bytecode du tout, le code source est simultanément lu et exécuté. Aujourd'hui, je ne connais plus beaucoup de langages qui ne passent pas par l'étape bytecode…
+0 -0

Aujourd'hui, je ne connais plus beaucoup de langages qui ne passent pas par l'étape bytecode…

Bash? Dans une certaine mesure PowerShell (bien qu'il s'appuie sur .NET)?

Sinon, OK. En fait ta classe "semi" c'est juste ce que j'appelle "les langages à machine virtuelle".

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