encodage – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Le type bytes n’est pas du texte http://sametmax.com/le-type-bytes-nest-pas-du-texte/ Fri, 11 Jan 2019 11:30:10 +0000 http://sametmax.com/?p=25125 je craque. Mais je me soigne, globalement j'ai récupéré plein de temps, et ça se voit sur mon quotidien. Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.]]> J’ai beau essayer très fort de ne pas répondre en ligne, des fois je craque. Mais je me soigne, globalement j’ai récupéré plein de temps, et ça se voit sur mon quotidien.

Et ce craquage, et bien il est cette fois dû à une totale mécompréhension des types de texte en Python 3.

Mais c’est bien normal: Python 3 ne gère pas le texte de la même manière que la grande majorité des langages de programmation, justement à cause de la débâcle qu’on a eue en Python 2. Du coup, de nombreux programmeurs arrivent avec leur expérience d’ailleurs, et tentent de l’appliquer tel un utilisateur de SVN migrant sur git. En surface ça semble coller, malheuseuement à l’usage, ça fait faire des erreurs.

Donc un peu d’explications.

En informatique, tout est une histoire de convention. On dit que tel mot clé a tel effet. Que tel nom suppose telle chose. Que tel code de retour implique telle erreur. Que tel schéma XML représente tel type de document.

Essentiellement, tout cela est arbitraire: des gens ont décidé qu’il en serait ainsi. Impossible de deviner que ce que fait yield ou with si vous n’avez pas d’expérience similaire avant. Impossible de savoir que le code 0 en bash ou 200 en HTTP signifie tout va bien sans qu’on vous transmette l’information, ou faire de nombreux tests.

Quand je dis arbitrairement, évidemment je ne veux pas dire complètement sans raison. Il y a des raisons techniques, politiques, économiques, et parfois esthétiques à ces conventions. Cela n’en retire en rien l’aspect parfaitement artificiel de ces choix.

La convention la plus omniprésente, et pourtant aujourd’hui la plus masquée dans un monde où on utilise massivement des langages de haut niveau comme Javascript, Ruby, PHP et Python, est celle de l’organisation des octets.

Musique !

…je vois même plus le code : tout ce que je vois, c’est des blondes, des brunes, des rousses.

Tout ce qui passe par nos ordinateurs n’est qu’une suite de zéros et de uns, que nous avons groupés par paquets de 8:

Seulement la grande révélation, le “aaaaaaahhhhh okayyyyyyy” qui arrive un jour dans toute vie de dev, c’est que ces paquets de 8 ne veulent rien dire. Rien. C’est nous qui avons décidé, arbitrairement encore une fois, de leur signification.

Vous voyez ce moment dans les films et séries où un personnage arrive à “lire du binaire” ?

Evidement, "c'est une representation binaire ASCII de coordonnées WGS 84 Web Mercator" est plus dur à caser dans un dialogue

Evidement, “c’est une representation binaire ASCII de coordonnées WGS 84 Web Mercator” est plus dur à caser dans un dialogue

C’est de l’enculage de dauphin.

Le binaire n’est pas un langage, pas plus que les lettres “abcdefghijklmnopqrstuvwxyz”. Vous pouvez utiliser ces lettres pour représenter certains mots italiens, français, anglais, un nom propre (sans langue), le label d’un immeuble (sans langue encore) ou un chiffre latin.

Que veut dire “les gosses” ? Pour la même combinaisons de lettres, cela signifie “les enfants” avec la convention française européenne, et “les couilles” avec la convention québéquoise.

Pour le binaire c’est pareil, ce que veut dire un octet dépend de la convention que vous avez choisie.

Par exemple, que signifie cette suite d’octets ?

1100001 1100010 1100011 1100100

Bah rien. Mais on peut lui donner un sens en lui appliquant une convention.

Je peux lui appliquer la convention ASCII, et donc supposer que c’est un texte dans un certain format. Voici ce que ça donne en Python:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> print(data.decode('ascii'))     
abcd 
Les processeurs modernes ne comprenent pas nativement l'american apparel

Les processeurs modernes ne comprenent pas nativement l’american apparel

Ou je peux lui appliquer une autre convention, et decider de lire ces octets comme si ils étaient le dump d’une structure C. Interprettons en Python ces octets comme un entier non signé en big-endian:

     
>>> data = bytearray([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
>>> import struct     
>>> struct.unpack('>I', data)     
(1633837924,)

Même suite de bits, mais selon la convention choisie, elle veut dire les lettres “abcd” ou le nombre “1633837924”. Et oui, comme il n’y a pas une infinité de combinaisons de 0 et de 1 qui tiennent dans un espace mémoire limité, différentes conventions vont utiliser les mêmes octets mais décider que ça veut dire quelque chose de différent.

En fait, même des conventions pour le même type usage ne veulent pas forcément dire la même chose. Par exemple, prenez l’octet:

11101001

Un octet somme toute sympathique, de bonne famille. Il ne paie pas de mine, mais c’est un membre utile de la société.

Et maintenant, quelqu’un vous donne un indice, il vous dit que cet octet représente… du texte.

Super !

Oui, mais du texte avec quelle convention ? Car les pays du monde entier ont créé leur propre convention pour représenter du texte.

Avec la convention “latin-1”, utilisé par 0.7% de tous les sites Web du monde ?

 
>>> bytearray([0b11101001]).decode('latin-1') 
'é' 

Avec la convention “cp850”, utilisé par la console DOS ?

 
>>> bytearray([0b11101001]).decode('cp850')
'Ú'

Vous voulez rire ? Le premier à remplacé presque partout le second parce qu’ils contiennent les mêmes lettres. Elles ne sont juste pas représentées par la même combinaison d’octets.

Et cet octet, que veut-il dire avec la convention “utf8”, qui est aujourd’hui le standard international recommandé pour représenter du texte ?

 
>>> bytearray([0b11101001]).decode('utf8')
Traceback (most recent call last):
File "", line 1, in 
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 0: unexpected end of data 

Il n’a pas de correspondance. Cet octet n’est pas de l’utf8 valide.

Si vous voulez représenter ces lettres en utf8, il faut utiliser une convention différente, en utilisant non pas un seul octet, mais une séquence d’octets:

 
>>> list(map(bin, 'é'.encode('utf8')))
['0b11000011', '0b10101001']
>>> list(map(bin, 'Ú'.encode('utf8')))
['0b11000011', '0b10011010']

Vous pourriez croire que puisque le texte est particulièrement compliqué, c’est normal d’avoir des conventions qui divergent. Mais non, c’est juste la nature des conventions. Puisqu’elles sont arbitraires, l’une n’est pas plus “la vérité” qu’une autre. On retrouve la même chose avec les nombres:

>>> struct.unpack("h", bytearray([0b11101001, 0b11101001]))
(-5655,)
>>> struct.unpack("H", bytearray([0b11101001, 0b11101001])) 
(59881,)

La même suite d’octets peut représenter deux nombres totalement différents, selon que je décide de les lire comme des “short”, ou des “unsigned short”.

Et l’inverse est aussi vrai.

Ben oui, si quelque chose peut être interprété de plusieurs façons, on a aussi le fait que deux représentations différentes peuvent être interprétées … pour aboutir au même résultat.

Par exemple, le nombre des doigts de ma main peut être représenté de plein de façons différentes:

  • décimal: 5
  • français écrit: cinq
  • chiffre latin: V
  • anglais écrit: five
  • espagnol écrit: cinco
  • base deux: 101
  • structure C d’un signed short en little-endian avec Python: bytearray([0b101, 0b0])

Que de manières différentes, pour le même concept ! En plus, il y a confusion possible: V est une lettre également. cinq, five et cinco utilisent le même alphabet, mais pas les mêmes symboles spécifiques, pour représenter la même chose. Et le plus confusionant, 101 est une représentation binaire, mais bytearray([0b101, 0b0]) aussi.

Bref, voilà toute la complexité de la différence entre la donnée, un concept abstrait qui n’existe pas, et sa représentation, une convention humaine concrète qui nous permet de communiquer entre nous.

Donc, pour lire “du binaire”, ou faire n’importe quoi en informatique, il faut connaitre la convention utilisée. Mais pas juste en informatique: pour lire le journal, il faut connaitre la convention des symboles imprimés sur les pages, pour conduire sans se faire tuer, il faut connaitre la convention des panneaux, et pour parler, il faut connaitre la convention de la compression des molécules d’air émise par l’appareil buccal et respiratoire d’un individu qui vient rencontrer votre système auditif.

Vous êtes un être très conventionnel au fond.

Évidemment on trouve la même chose en Python. Par exemple vous pouvez utiliser plusieurs conventions pour demander à Python de créer le même nombre en mémoire:

>>> 245 # base 10
245
>>> 0xF5 # hexadecimal
245
>>> 0b11110101 # binaire
245
>>> 245 == 0xF5 == 0b11110101
True     
>>> type(245)     
     
>>> type(0xF5)     
     
>>> type(0b11110101)     
 

Inversement, "1" et 1 paraissent similaire, mais ils ont différents buts. Le premier est un outil destiné à l’affichage, qui matérialise le caractère représentant le chiffre arabe après le zéro. Il est stocké en interne avec une séquence d’octets similaire à:

>>> bin(ord("1"))
'0b110001'

Tandis que que le second est un outil fait pour faire des calculs avec la plus petite valeur positive entière non nulle. Il est stocké en interne avec une séquence d’octets similaire à:

>>> list(map(bin, struct.pack('l', 1)))
['0b1', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0', '0b0']

Je simplifie bien entendu, en vérité la representation interne des nombres et du texte en Python est plus complexe que cela, et dépend de l’implémentation choisie, du type de processeur, de la taille de la donnée et de votre configuration.

Retour sur le type bytes

J’ai soigneusement évité d’utiliser le type bytes durant cette démonstration, le remplaçant techniquement inutilement (mais pédagogiquement brillamment, car je suis génial) par bytearray.

En effet, toute cette leçon est là pour arriver à la conclusion que bytes ne représente pas du texte, mais si je vous avais montré tout ça avec lui, voilà qui vous aurait interloqué:

     
>>> bytes([0b1100001, 0b1100010, 0b1100011, 0b1100100])     
b'abcd' 

“Heu, mais c’est du texte !” me dirait alors un lecteur ayant diagonalisé l’article.

Mais bien entendu que non.

bytes ne présente pas du texte, c’est une structure de données dont le but est de permettre de manipuler une séquence d’octets ordonnée, et ce manuellement. N’importe laquelle.

Or, il se trouve que beaucoup de langages de programmation représentent le texte comme un array d’octets, et y attachent quelques opérations de manipulation. C’est le cas du C, ou de Python 2 par exemple. Les gens ayant eu cette expérience pensent donc que b'abcd' représente du texte, allant parfois jusqu’à aller lui donner l’appellation de “byte string”.

Il n’existe rien de tel en Python 3.

En Python 3, vous avez deux types pour manipuler des séquences d’octets: bytes et bytearray. Ils sont équivalents, à ceci près que bytes est non mutable (non modifiable) alors que bytearray est mutable (modifiable).

Ces types peuvent contenir n’importe quels octets, et nous avons vu ensemble qu’une même séquence d’octets pouvait être interprétée différemment selon la convention choisie pour la lire. Évidemment il est préférable de la lire avec la même convention qui a été utilisée pour la produire, sans quoi on ne comprendra pas ce que le producteur de la donnée à voulu dire.

Sauf que…

Beaucoup d’outils en informatique utilisent les conventions ASCII et hexadécimale pour symboliser les valeurs des octets. Si vous lancez Wireshark pour regarder les paquets d’un protocole réseau ou si vous ouvrez un PNG avec xxd, on va vous représenter le contenu avec un mélange de ces conventions.

Pour des raisons pratiques, Python fait donc la même chose, et permet ainsi de visualiser (ou produire) le type bytes à l’aide d’une notation ASCII:

    
>>> print(b'abcd'.decode('ascii'))     
abcd     
>>> struct.unpack('>I', b'abcd')     
(1633837924,)

Ou d’une notation héxa (ironiquement, l’héxa est representé par une combinaison de caractères ASCII \o/) si les valeurs ne tiennent pas dans la table ASCII:

     
>>> "é".encode('utf8')  # hexa C3 A9   
b'\xc3\xa9'     
>>> struct.unpack('h', b'\xc3\xa9')    
(-22077,)

Donc bytes, bien qu’il puisse contenir des octets interprétables comme du texte, n’est pas particulièrement fait pour manipuler du texte. Il peut contenir n’importe quoi. Mais pour des raisons pratiques, sa représentation dans le terminal est faite avec une convention familière. Après tout, il faut bien l’écrire en quelque chose pour l’affiquer à l’écran.

Si on veut manipuler du texte en Python 3, il faut utiliser le type str, qui est l’outil spécialisé dans la representation et la manipulation textuelle. Si vous savez qu’un type bytes contient des octets qui representent du texte, alors utilisez la méthode décode() avec la bonne convention (appelée “charset”), pour récupérer un str:

     
>>> print(b'P\xc3\xa8re No\xc3\xabl'.decode('utf8'))
Père Noël 

On a un très bon article sur l’encoding en Python sur le blog, d’ailleurs.

Toute cela n’était bien entendu pas vrai en Python 2. En Python 2, le type str était un array d’octets, rendant tout cela bien confus, et amenant à plein d’erreurs. L’introduction lors de la version 2.0 de l’objet unicode pour pallier le problème, bien que très utile, n’a fait que rajouter à l’incomprehension des nouveaux venus.

Or le monde extérieur, lui, n’a pas d’abstraction pour le texte. Faire des abstractions, c’est le rôle du langage de programmation. Si vous écrivez dans un terminal, ou lisez depuis un terminal, un nom de fichier, le contenu d’une base de données, une requête AJAX, etc., ce sont évidemment des octets qui sont échangés, et il vous faut la bonne convention pour faire partie de la discussion.

Le type bas niveau bytes est un outil qui sert donc à communiquer avec le monde extérieur, tandis que les types haut niveau (str, int, list, etc.) sont des outils qui font l’abstraction de ces conventions, pour vous permettre de manipuler confortablement un concept général (du texte, un nombre, une collection ordonnée) à l’interieur des murs de votre programme.

]]>
25125
Quelques erreurs tordues et leurs solutions en Python http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/ http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/#comments Sun, 24 Jun 2012 02:29:56 +0000 http://sametmax.com/?p=995 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((1,))

>>> t = (1,)
>>> t[0]
1
>>> t = (1)
>>> t[0]
Traceback (most recent call last):
  File "", line 1, in 
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 "", line 1, in 
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 "", line 1, in 
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.

]]>
http://sametmax.com/quelques-erreurs-tordues-et-leurs-solutions-en-python/feed/ 23 995