Quelques erreurs tordues et leurs solutions en Python 23


Quand vous débuggez, rappelez-vous que pdb est votre ami, et qu’il est souvent bon de supprimer tous les fichiers .pyc pour éviter la confusion. Mais parfois l’erreur semble n’avoir aucun sens. Bien que Python soit un langage dont l’une des grandes qualités soit la cohérence, voici une liste d’erreurs et leurs solutions qui ont tendance à énerver (les erreurs hein, pas les solutions).

NameError: name 'x' is not defined

Python plante en annonçant que la variable n’est pas définie. Vous allez à la ligne donnée, et elle est là. Vous vérifiez qu’il n’y a pas de faute de frappe (genre un zéro mélangé avec la lettre O), ni une majuscule ou une minuscule échangée quelque part (Python est sensible à la casse).

Et rien.

Tout est niquel.

Alors pourquoi ça plante bordel de merde ?

Et bien ce message qui n’aide absolument pas peut venir du fait que les closures sont en lecture seule en Python. En résumé, vous avez essayé de faire un truc comme ça:

chose = 'truc'
def fonction():
    chose = 'machin'
    # ou chose += machin ou une variante

La solution est simple: ne modifiez pas chose. Si vous avez besoin de modifier son contenu, utilisez une variable intermédiaire:

chose = 'truc'
def fonction():
    bidule = chose
    bidule += 'machin' # je sais c'est bidon, c'est pour l'exemple

En Python 3.0, vous pouvez utiliser le mot clé nonlocal pour y palier: vous modifierez alors la variable du scope du dessus.

chose = 'truc'
def fonction():
    nonlocal chose
    chose += 'machin' # je sais c'est bidon, c'est pour l'exemple

Évitez d’utiliser global, qui a un fort potentiel d’effet de bord.

ImportError: cannot import name bidule et ImportError: No module named truc

Une fois que vous avez vérifié qu’un module existe bien avec ce nom (regardez de près, parfois c’est subtile), voici 3 possibilités:

Pas de fichier __init__.py

Un dossier n’est pas un module importable si il ne contient pas de fichier __init__.py. Vérifiez qu’il y en a un, et dans le cas contraire, créez en un vide.

Erreur de Python Path

Quand vous faites import bidule, bidule ne peut être importé que si le dossier qui le contient est dans le Python Path. Le Python Path est une variable qui contient une liste de dossiers dans lesquels chercher les modules à importer.

Le dossier courrant, le dossier contenant la bibliothèque standard de Python et le dossier où sont installés les bibliotèques Python de votre système d’exploitation sont automatiquement présents dans le Python Path.

Première chose: assurez-vous d’être à la racine du projet que vous lancez (erreur typique quand on utilise la commande ./manage.py avec Django par exemple).

Deuxième chose: si vous utilisez une bibliothèque qui n’est pas dans le Python Path (ça arrive assez souvent avec les tests unitaires: on éxécute les tests depuis le dossier de test, et le projet est dans un dossier à côté, donc pas dans le Python Path), vous pouvez ajouter manuellement un chemin dans le Python Path.

Pour se faire, avant l’import qui va foirer:

import sys
sys.path.append('/chemin/vers/le/dossier/parent/du/module/a/importer')

On peut tout à fait spécifier un dossier relativement au dossier courant. Il n’est pas rare d’ajouter le dossier parent du dossier courrant au Python Path:

import sys
import os
 
DOSSIER_COURRANT = os.path.dirname(os.path.abspath(__file__))
DOSSIER_PARENT = os.path.dirname(DOSSIER_COURRANT)
sys.path.append(DOSSIER_PARENT)

Par exemple, souvent dans le dossier d’un projet Django je fais un sous-dossier ‘apps’, puis je rajoute ceci au fichier settings.py:

import sys
import os
 
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join(PROJECT_DIR, 'apps'))

Il y a deux avantages à cela:

  • Mes applications sont regroupées dans un dossier et pas en vrac à la racine du projet, mais je peux quand même les importer en faisant import nom et pas import apps.nom.
  • J’ai maintenant une variable PROJECT_DIR que je peux utiliser partout, notamment pour définir où sont certains dossiers comme le dossiers des fichiers statiques:
STATIC = os.path.join(PROJECT_DIR, 'static')

Imports circulaires

Si vous importez poisson.rouge dans force.py, et force.bleu dans poisson.py, vous aurez aussi ce message d’erreur (qui n’aide pas beaucoup, on est d’accord).

Il n’y a pas vraiment de façon élégante de s’en sortir, c’est une des plus grosses couillasses en Python.

Solution 1: vous refactorez votre code pour avoir bleu et rouge dans un fichier couleur.py, lequel est importé dans poisson.py et force.py. C’est propre, mais parfois ça n’a aucun sens, et parfois ce n’est juste pas possible.
Solution 2: vous mettez l’import dans une fonctions ou une méthode dans un des deux modules (n’importe lequel):

def make_bouillabaisse():
    from poisson import rouge

C’est moche, mais c’est facile. Et je le répète, je n’ai jamais vu quelqu’un en 10 ans de Python proposer une solution élégante à ce problème. C’est un What The Fuck d’or.

UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 0: ordinal not in range(128)

Arf. L’erreur à la con. Parce que généralement elle vient du fait que l’on ne comprend pas vraiment ce qu’on fait. Or difficile de résoudre un problème quand on ne comprend pas de quoi il est question. Ne vous sentez pas mal, on s’est tous retrouvé comme un demeuré devant un problème d’encodage.

A noter que ce n’est pas une erreur spécifique à Python, mais si vous venez d’un langage comme PHP qui passe silencieusement ce genre d’erreur et affiche en prod des texts illisibles, voire une grosse erreur à l’écran peut surprendre.

Voici des causes très fréquentes:

Encodage du fichier.py

Comme il peut y avoir 1 million de possibilités, forcez vous à:

– TOUJOURS avoir votre éditeur de texte réglé pour utiliser UTF-8. Surtout sur Windows. Si votre chef vous l’interdit parce que “ça pose des problèmes d’encodage” (sic), quittez votre job (meilleur choix) ou faites vous former pour comprendre comment marchent les encodages et travailler dans cet environnement hostile.
– TOUJOURS avoir votre encodage (UTF-8 j’ai dis !) déclaré en haut du fichier.py: # -*- coding: utf-8 -*-

Vérifiez que les textes en entrée sont dans l’encodage prévu

Le contenu des bases de données ne sont parfois pas dans l’encodage déclaré de la table ou de la base. Le contenu d’une page HTML n’est parfois pas encodé dans l’encodage déclaré dans le HEAD. Le contenu d’un fichier n’est parfois pas encodé dans l’encodage par défaut de votre OS.

Il n’y a pas de secret. Pas de moyen infaillible de détection automatique. Il faut vérifier.

Vous confondez encodage et décodage (Python 2.7 et moins)

En Python, on DECODE pour passer d’un texte en encodé (UTF8, ISO-8859, CP1552, etc) et donc de type ‘str’ c’est à dire un flux de bits, à un texte unicode, une représentation interne, un objet non imprimable. Il est recommandé de décoder tout texte venant d’une source extérieur à votre programme, pour tout uniformiser.

A l’inverse, on ENCODE pour passer du type ‘unicode’ à un type ‘str’. Il obligatoire d’encoder un texte pour le communiquer au monde extérieur. Si vous ne le faites pas manuellement, Python le fera automatiquement, en essayant de deviner. Il n’est pas excellent à deviner.

En résumé:

In [7]: texte = open('/etc/fstab').read() # ou un téléchargement, ou une requete SQL...
In [8]: type(texte)
Out[8]: str
In [9]: texte = texte.decode('UTF8')
In [10]: type(texte)
Out[10]: unicode
In [11]: print texte # encode automatiquement le texte car votre terminal ne comprend qu'un text encodé
# /etc/fstab: static file system information.
#
[.............]
In [12]: type(texte.encode('UTF8')) # à faire avant de faire un write
Out[12]: str

Si ça continue de foirer, prenez tous les fichiers de votre application un par un: changez toutes les strings en unicode (les précéder d’un “u”), assurez vous que tout ce qui entre est converti en unicode (unicode() après urllib, open, etc) et tout ce qui sort est converti dans un encodage adapté (souvent UTF8) (encode(‘UTF-8′) avant send(), write() ou print).

Si ça ne marche toujours pas, embauchez un mec comme moi qui est payé cher pour se taper la tête contre les murs à la place des autres.

TypeError: ‘int’ object has no attribute ‘__getitem__’ et autres erreurs sur les tuples

Tuples d’un seul élément

CECI N’EST PAS UN TUPLE: (1)

Ceci est un tuple: (1,)

>>> type((1))
<type 'int'>
>>> type((1,))
<type 'tuple'>
>>> t = (1,)
>>> t[0]
1
>>> t = (1)
>>> t[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object has no attribute '__getitem__'

Et il y a plus vicieux:

>>> a = ("12345")
>>> b = ("12345",)
>>> a[0]
'1'
>>> b[0]
'12345'

C’est très dur à débugguer car on dans les deux cas il n’y a pas d’erreur étant donné que c’est une opération tout à fait légitime.

Concaténation automatique

Python vient avec une fonctionnalité qui concatène automatiquement les descriptions littérales de chaînes de caractères:

>>> "Ceci est un"                                  " test"
'Ceci est un test'

C’est très pratique pour les chaînes longues:

>>> print ("Ceci est une chaîne longue "
... "et je peux la diviser sur plusieurs lignes"
... " sans me fouler")
'Ceci est une chaîne longue et je peux la diviser sur plusieurs lignes sans me fouler'

Mais si vous oubliez une virgule dans un tuple (par exemple dans INSTALLED_APPS dans le fichier de settings.py de Django):

>>> REGLES = (
...     "Ne jamais parler du fight club",
...     "Ne jamais croiser les effluves",
...     "Ne jamais appuyer sur le petit bouton rouge" # <===== virgule oubliée !
...     "Ne jamais goûter"
... )
>>> print REGLES[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: tuple index out of range
>>> print REGLES[-1]
Ne jamais appuyer sur le petit bouton rougeNe jamais goûter

Le fichier/la liste est vide

On ne peut lire qu’une seule fois les générateurs en Python.

Si vous faites:

toto = (blague.title() for blague in histoire)

ou

toto = open('histoire.txt')

Et ensuite:

for blague in toto:
    print toto
 
len(list(toto))

La dernière ligne ne marchera pas. Toto aura été vidé par la première boucle for. Si vous souhaitez utiliser plusieurs fois le résultat de votre générateur, il faut le transformer en liste:

toto = list(toto)
for blague in toto:
    print toto
 
len(list(toto))

Attention, car vous avez maintenant l’intégralité des données chargées en RAM.

TypeError: ma_function() takes exactly x argument (y given)

Cette erreur est très explicite, et la plupart du temps ne pose aucun problème: vérifiez que vous passez le bon nombre d’arguments à la fonction. Faites particulièrement attention si vous utilisez l’opérateur splat.

Il existe néanmoins un cas particulier un peu taquin:

>>> class Americaine(object):
...     def dernier_mot(mot):
...         print mot
... 
>>> homme_le_plus_classe_du_monde = Americaine()
>>> homme_le_plus_classe_du_monde.dernier_mot("Monde de merde !")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: dernier_mot() takes exactly 1 argument (2 given)

On définie une seul argument (mot) et on en passe un seul ("Monde de merdes !"), alors pourquoi Python n’est pas d’accord ?

C’est parce que l’on déclare une méthode sans self dans la signature. Or Python va passer automatiquement (et de manière invisible) la référence à l’objet courrant en premier argument, du coup la méthode reçoit deux arguments: la référence à homme_le_plus_classe_du_monde et "Monde de merde !". Ca ne marche pas puisque la méthode est déclarée pour n’accepter qu’un seul argument.

Il y a deux solutions. La plus simple, ajoutez self:

>>> class Americaine(object):
...     def dernier_mot(self, mot):
...         print mot
... 
>>> homme_le_plus_classe_du_monde = Americaine()
>>> homme_le_plus_classe_du_monde.dernier_mot("Monde de merde !")
Monde de merde !

Une seconde solution consiste à déclarer une méthode statique. Du coup on a plus besoin d’instance:

>>> class Americaine(object):
...     @staticmethod
...     def dernier_mot(mot):
...         print mot
... 
>>> Americaine.dernier_mot("Monde de merde !")
Monde de merde !

Ma structure de données par défaut n’est pas la bonne

Piège classique en Python, qu’il est important de répéter encore et encore tant il est la source de frustration chez les personnes qui ne le connaissent pas.

>>> from random import choice
>>> def bioman(forces=['rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere'], invite=None):
...     if invite is not None:
...         forces.append(invite)
...     return choice(forces)
... 
>>> bioman()
'rose'
>>> bioman()
'rouge'
>>> bioman(invite='magenta a pois gris')
'vert'
>>> bioman()
'jaune devant, marron derriere'
>>> bioman() # WTF ??????????
'magenta a pois gris'

Dans le dernier appel ‘magenta a pois gris’ est tiré au sort alors qu’on ne l’a pas passé en paramètre. Comment cela est-il possible ?

Cela vient du fait que les paramètres par défaut sont initialisés une seule fois pour tout le programme: dès que le module est chargé.

Si vous utilisez un objet mutable (liste, set, dico) et que vous le modifiez (ici avec append), le prochain appel de la fonction utilisera toujours la référence de cet objet, et donc de sa versio modifiée.

La solution est soit de ne pas utiliser d’objet mutable (tuple, strings, int, etc), soit de ne pas modifier l’objet:

>>> def bioman(forces=('rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere'), invite=None):
...     if invite is not None:
...         forces += (invite,) # ne modifie pas l'ancien objet
...     return choice(forces)

Ou alors (et ceci est souvent utilisé même si c’est moche):

>>> def bioman(forces=None, invite=None):
...     if forces is None:
...        forces = ['rouge', 'bleu', 'vert', 'rose', 'jaune devant, marron derriere']
...     if invite is not None:
...         forces.append(invite)
...     return choice(forces)

Toutes les parties qui sont éxécutées à l’inialisation du code (en opposition à celles qui le sont à l’appel du code) sont concernées par ce problème: les paramètres par défaut, les variables à la racine des modules, les attributs de classe déclarés en dehors d’une méthode, etc.

ItZ naute a beuhgue, Itse fitiure

Néanmoins cela a aussi son utilité. On peut en effet l’utiliser pour partager des états:

class Model(object):
    _pool = {
        'mysql': MySQL().connect('test'),
        'sqlite': Sqlite.open('test.db')
    }
    default_connection = 'mysql'
 
    def query(self, connection=default_connection, *params):
        connection.super_query(*params)

Et vous avez maintenant une classe de modèle qui gère plusieurs connections. Si vous l’étendez, les enfants de la classe et toutes les instances partageront le même objet connection, mais tout le reste sera unique à chacun d’eux. Cela évite un effet de bord du singleton qui oblige à partager un état et une identité. Ici on ne partage que la partie de l’état que l’on souhaite, et pas l’identité.

On gagne sur les deux tableaux: si on update la connection MySQL (par exemple parcequ’on a détecté qu’elle était stale), toutes les instances ont accès à l’objet modifé. Mais si on veut overrider la connection pour une seule classe, on peut le faire sans affecter les autres simplement en remplaçant l’objet à la déclaration de la classe.

On peut aussi utiliser cette fonctionnalité pour créer un cache. On appelle ça “mémoiser”:

def fonction_lente(param1, param2, _cache={}):
    # les tuples peuvent être des clés de dico \o/
    key = (param1, param2)
    if key not in _cache:
        _cache[key] = process_lent(param1, param2)
    return _cache[key]

Tous les résultats sont alors stockés en mémoire vive.

23 thoughts on “Quelques erreurs tordues et leurs solutions en Python

  • chris

    Ah merci je reconnais 1 ou 2 trucs que j’ai pas aimer en
    python.

    PS: j’aime bien l’expression “what the fuck d’or”

    très classe, simple concise on comprend tout de suite de quoi on parle.

    Python c’est un langage burné sévère après tout …

  • Kontre

    Pour les imports circulaires, je crois que ça marche en n’utilisant pas “from machin import truc” mais plutôt “import machin as m” et en utilisant dans le code “m.truc”.
    Quand on demande d’importer quelque chose en particulier d’un module, il exécute le module à ce moment là et a donc besoin de pouvoir tout inclure, d’où le coincage avec les import circulaires. Au contraire si on importe le module lui-même, il peut le faire plus tard.

  • JM

    Cool, merci pour tout ça. Un peu de grammar-nazisme pour garder la forme :

    vous avez essayer (é)
    dans un des deux module (+s)
    Parceque (+espace)
    1 millions (-s)
    comment marche(+nt) les encodages

  • Sam Post author

    J’ai emprunté l’expression “WTF d’or” au joueur du grenier:

    Cet épisode est particulièrement marrant.

  • Réchèr

    Je viens de faire un combo d’erreur “concaténation de chaîne” + “encodage”

    # -*- coding: utf-8 -*-

    a = u"vive lé gros" "nénés"

    Ça renvoie l’erreur bien connue : UnicodeDecodeError: ‘ascii’ codec can’t decode byte 0xc3 in position 1: ordinal not in range(128)

    Sans préciser le numéro de ligne, parce que sinon c’est trop facile. L’indication de position à 1 correspond au premier “é” de “nénés”. Mais si on le sait pas, ça avance pas à grand chose.

    # -*- coding: utf-8 -*-

    a = u"vive lé gros" u"nénés"

    Et là ça marche. Je m’étais dit qu’avec la première déclaration de chaîne unicode, l’interpréteur allait en déduire que le reste le serait aussi. Eh bien non. Faut faire gaffe.

  • Etienne

    Et bien je me suis fait eu par un mutable en paramètre par défaut.

    J’instancie un classe avec un dictionnaire, puis j’instancie avec un autre et, quoi, ??…: je retrouve le premier dict dans la seconde instance… Au début je me suis dit: “y’a un fantôme dans ma machine!!, c’est pas possible”.

    J’ai bien soupçonné le mutable, mais rien n’y fit jusqu’à ce que je me rappelle ce post qui m’a finalement permis de débloquer la situation.

    Une méthode prenait un dictionnaire vide comme argument par défaut:

    def methode(self, machin, dico = {}):
        fait qqch avec le dico
        return dico

    Je l’ai finalement remplacé par:

    def methode(self, machin, dico):
        ...

    … et je passe un dictionnaire vide lors de l’appel :

    self.truc = self.methode(var_machin, {})

    Merci!

    PS:
    En fouillant pour résoudre le problème, je me suis rendu compte que la méthode en question a toujours la même adresse mémoire (c’est vrais pour toutes les méthodes j’imagine), quelque soit l’instance (qui ont chaque fois une nouvelle adresse).

  • Etienne

    .. le lien vers le SDZ est cassé…

    … C’est là que je vois qu’ils ont changé les meubles …
    … jamais aimé l’organisation des tutos là-bas. Trop de sous-titre sur lesquels cliquer, trop de “page suivante”…

  • Sam Post author

    Ca a pas toujours été comme ça. A une époque c’était simple et pas chargé.

  • kontre

    un petit WTF : j’ai fait un import circulaire dans un moment d’inattention, c’est mal. Mais le pire, c’est que les tests unitaires sont passés tranquilillou, il n’y a qu’en utilisant mes modules de manière normale que ça m’a pété à la gueule ! Comment ça se fait que ni nosetests ni py.test n’ait bloqué là-dessus ? Ils importent bien les fichiers pourtant !
    Je précise que j’utilise exclusivement des doctests dans les docstring de mes classes et méthodes.

  • Sam Post author

    Aucune idée. Peut être que les doctests n’importent pas les modules qui contiennent les doctests quand on utilise nosetests et py.test et que dans les doctests, il n’y a aucun import qui entraine un import circulaire.

  • Sorcier_FXK

    >>> REGLES = (
    … “Ne jamais parler du fight club”,
    … “Ne jamais croiser les effluves”,
    … “Ne jamais appuyer sur le petit bouton rouge” # <===== virgule oubliée !
    … "Ne jamais goûter"
    … )

    hahaha, énorme, le clin d'oeil à Ghostbuster m'a beaucoup faire rire. J'ai encore la réplique en RAM. :D
    Merci pour se pense-bête qui me dépanne au beau milieu de la nuit.

  • Sam Post author

    On a aussi une tag line avec cette référence, mais elle apparait aléatoirement :-)

  • Jérôme Plumecoq

    Bonjour,

    il me semble que juste avant la ligne :

    Si ça ne marche toujours pas, embauchez un mec comme moi qui est payé cher (…)

    il faut lire encode et pas decode dans la phrase :

    decode(‘UTF-8′) avant send(), write() ou print

    Si j’ai bien compris la partie sur les encodages :-)

  • francis

    petites gourmandises exquises

    Je viens de me remettre à Ubuntu après avoir installer la version 14

    j’ai commencé un portage d’une bidouille sous python 2.7 avant de faire le grand saut sous gtk3 pardon Gobject.

    Mon affaire plante et après investigation quelle n’est pas ma surprise après cette découverte

    l’histoire part d’un module non reconnu que je stocke dans site-packages

    je creuse et je décortique ma variable sys.path……………surprise

    for element in sys.path :

    print element

    me donne

    /home/francis/Documents/Python-linux/appligenerique

    /usr/lib/python2.7

    /usr/lib/python2.7/plat-x86_64-linux-gnu

    /usr/lib/python2.7/lib-tk

    /usr/lib/python2.7/lib-old

    /usr/lib/python2.7/lib-dynload

    /usr/local/lib/python2.7/dist-packages

    /usr/lib/python2.7/dist-packages

    /usr/lib/python2.7/dist-packages/PILcompat

    /usr/lib/python2.7/dist-packages/gtk-2.0

    /usr/lib/pymodules/python2.7

    /usr/lib/python2.7/dist-packages/ubuntu-sso-client

    /usr/lib/python2.7/dist-packages/wx-2.8-gtk2-unicode

    sous python 2.7.6 c’est plus site-packages qui est scruté mais dist-packages

    alors que sous vista (oui je sais je suis du côté nul de la force :-) et sous ubuntu 12 ou 13 et python 2.6 c’est sous site-packages

    du coup je vais déménager mes bibliothèques sous dist-packages sous python 2.7.6 sous ubuntu 14( je suis en train de passer du coté light ou dark no say de la force :-))

    Et vous le saviez-vous ?

    Site ultra intéressant il y a pas à dire

  • Sam Post author

    Il ne faut pas choisir où stocker son module soit-même. Si tu as besoin de mettre un module dans site/dist package, fait un setup.py et installe le avec python setup.py develop. Ca te permet de l’avoir à la fois installé, et à la fois modifiable en local.

  • francis

    ok je découvre cet aspect toute juste.

    En fait je ne sais pas encore fabriquer un paquet sous window ou sous linux d’ailleurs pour l’exporter et ensuite l’installer sous ubuntu. Du coup j’ai cela à la mano.

    fait un setup.py et installe le avec python setup.py develop ⇒ peux-tu développer ?

    Ou donner un lien vers un tuto stp?

  • ast2

    je ne vois pas de soucis avec le code:

    chose = ‘truc’

    def fonction():

    ….chose = ‘machin’

    ….# ou chose += machin ou une variante

    la variable “chose” dans fonction est une variable locale distincte de “chose” du niveau supérieur. Exécuter “fonction” ne change pas cette variable.

    Quant à ce code:

    chose = ‘truc’

    def fonction():

    ….nonlocal chose

    ….chose += ‘machin’

    il plante avant même qu’on exécute la fonction. Une variable nolocal n’est ni locale ni globale or ci elle est globale. (Je suis en python 3.4)

  • Ced

    Comme toujours excellent. Dommage qu’il n’y ait pas d’id dans le code pour faire des liens directement aux bons § !

  • Landry

    Pour ma part, je fais régulièrement face au problème des méthodes de la librairie standard qui renvoient d’autres objets.

    Genre (dernière connerie en date) :

    import datetime as dt

    datetemps = dt.datetime.now()

    datetemps.year

    2016

    datetemps.replace(year=2018)

    datetemps.year

    2016

    Hé ben ça a beau être bien documenté toussa-toussa, je me fais avoir à chaque fois. Je ne sais pas, ça ne me parait pas naturel… Mais ça me semble une erreur plutôt spécifique à Python et à son besoin (maladif ?) de ne pas modifier un objet mais plutôt en créer un nouveau lorsqu’on veut faire une modification.

    J’en profite : putain, merci pour ce blog !!!

Leave a comment

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.