import – Sam & Max http://sametmax.com Du code, du cul Wed, 30 Oct 2019 15:34:04 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Rendez votre package exécutable avec __main__.py http://sametmax.com/rendez-votre-module-executable-avec-__main__-py/ http://sametmax.com/rendez-votre-module-executable-avec-__main__-py/#comments Tue, 13 Jan 2015 06:47:42 +0000 http://sametmax.com/?p=15704 __init__.py, mais __main__.py est moins connu.]]> Tout le monde connait le fichier __init__.py, mais __main__.py est moins connu.

Il permet de lancer du code si on tente d’exécuter un package (c’est à dire un dossier qui contient un fichier __init__.py):

$ tree monpackage/
monpackakge/
├── __init__.py
└── __main__.py

0 directories, 2 files
$ cat monpackage/__main__.py
print('Hello :)')
$ python monpackage/ # ceci est un dossier 
Hello :)

Le __main__.py est aussi exécuté quand on fait python -m monpackage.

Notez que son exécution suppose l’import préalable du package, et que donc __init__.py sera toujours exécuté avant __main__.py. En revanche, faire juste import monpackage ne déclenche pas l’exécution de __main__.py.

Si vous zippez votre package et appelez la commande python, c’est aussi ce fichier qui sera exécuté. Pratique donc, pour faire un exécutable portable à peu de frais, tout en gardant la lib importable.

]]>
http://sametmax.com/rendez-votre-module-executable-avec-__main__-py/feed/ 5 15704
Importer des données, retour d’expérience http://sametmax.com/importer-des-donnees-retour-dexperience/ http://sametmax.com/importer-des-donnees-retour-dexperience/#comments Thu, 16 Jan 2014 15:49:21 +0000 http://sametmax.com/?p=8772 Dédicaçons la chanson de notre article au plus barbu de mes amis poneys.

J’ai importé des données un très grand nombre de fois dans ma vie. Depuis des APIs, des XML, des CSV, du filesystem, des formats binaires, des formats batards, etc

Pour tous les jobs d’import, Python est probablement le meilleur langage au monde. Autant j’aime Python, autant je suis lucide sur le fait qu’en dev Web, Ruby et Javascript sont d’excellentes alternatives. En programmation concurrente, Go dépasse Python. En IA, Lisp est le top de la concurrence.

Mais pour l’import de données, Python est simplement le meilleur langage au monde. Sa capacité à lire énormément de formats facilement, sa force de manipulation de données numériques et texte, sa philosophie d’itération, ses nombreuses libs en font un outil incroyablement souple et puissant.

Malgré cela, on retrouve toujours des grosses difficultés dans l’import de données. Elles sont les mêmes pour tous les langages.

Voici mes 2 centimes.

N’accordez aucune confiance à la donnée

Partez du principe que tout champ peut manquer. Que toute donnée peut être mal formée ou corrompue. Ou fausse.

Même si le service en face est sérieux. J’ai bossé avec des données du service de santé américain, de France Télécom, de startups, d’outils Open Source, de scrapping de sites, de mon pote Maurice, et de mes propres scripts

Vous savez ce qu’ils ont tous en commun ? Aucun ne sont fiables. Aucun.

Ils ont tous des données merdiques à un moment où à un autre.

Ayez donc une approche défensive. Pour CHAQUE champ, posez vous la question : que doit faire le script d’import si il manque ? Si la donnée contenue est foireuse ?

Les outils d’abstraction sont vos amis

Un import, c’est typiquement le genre de taff où les surcouches vont énormément vous aider. ORM, DSL, XML objectify et toute lib qui peut vous éviter de travailler trop proche du format va vous faire gagner un temps fou.

Prenez un peu de temps pour les mettre en place. Même si ils vous font perdre un peu en perf, un gros script d’import devient TRÈS VITE un sac de nœud. Et vous voulez que les problèmes soient facilement identifiables.

Pour cette même raison, virez toute la logique de l’insertion des données en dehors du script. Votre script doit avoir une logique découpée en 3 parties :

  1. Le script d’acquisition, qui charge les données et les passe sous forme brute à un importeur.
  2. Un importeur, qui est capable d’extraire des données raffinées à partir d’un format de données brutes et appeler le bon code d’insertion.
  3. Du code d’insertion, qui attend en paramètre une donnée toujours propre (sans aucun check), et qui se charge uniquement de prendre cette donnée et la mettre dans votre système de stockage (généralement la base de données).

Le script d’acquisition doit rester très simple. Une suite d’instructions logiques pour récupérer la donnée, l’énumérer et la passer à l’importeur. C’est lui qui fait les appels API, qui se connecte au FTP, qui ouvre le CSV, qui parse le XML, etc. Ainsi vous pouvez facilement interchanger les importeurs ou voir si il y a une couille dans la récupération des données.

L’importeur est généralement le code le plus crade. C’est une série de try / except, de logique métier, d’assainissement des données. Vous ne voulez pas de code d’insertion là dedans, car vous voulez que ce code, qui est difficile à débugger et va être celui qui va être modifié toutes les 5 minutes au fur et à mesure que vous découvrez toute les merdes, soit dédié à une seule logique : obtenir de la donnée saine. Ce code sera spaghetti, vous n’y pouvez rien. Mais vous pouvez l’isoler et le commenter à mort.

Le code d’insertion est un code réutilisable. Il se fout de savoir d’où vient la donnée. Il attend un format, et un seul, et toujours de données correctes, et propres. C’est le but des importeurs de lui filer une entrée normalisée et pertinente. Ce code est propre, et doit être très bien testé via des tests unittaires. Il va vous servir plusieurs fois, car c’est le même code qui sera utilisé que vous importiez d’un service X ou un service Y. Il représente VOTRE logique métier. N’insérez pas ce code dans le code d’une autre abstraction (type ORM), ainsi, si vous changez d’outils, vous changez simplement ce code, et l’interface reste la même pour vos importeurs.

Debugging

Votre script va planter. Beaucoup. Souvent.

Un champ absolument indispensable – que la spec papier notait comme toujours présent – va manquer. Un autre champ noté de type int dans le xld contient une lettre. L’encoding n’est pas le bon, alors qu’il l’a toujours été pendant 5 mois.

Ce n’est pas une question de si, c’est une question de quand.

Donc déjà, blindez votre script de log. Quand je dis blindez, je veux dire que chaque if, chaque résultat de check, doit être accompagné d’une ligne affichant l’action en cours, et son contexte (la donnée traitée, de préférence avec un truc pour l’identifier, genre un ID). Quand il plantera à 3 heure du matin sur un truc hubuesque et que le relancer pour obtenir le même état prendra une demi-journée, le log sera votre seul chance de réparer la panne sans engager un psy.

Mettez aussi un gros try / except générique qui loggue toute exception, pour pouvoir faire un debug post mortem. Idéalement, faites le dumper locals() et envoyez-vous un mail d’alerte. Vous ne voulez pas que le script ne tourne pas pendant une journée sans que vous le sachiez.

Mettez des options dans votre scripts pour pouvoir débugger plus facilement. Par exemple, si vous avez de code d’insertion qui est sous forme de tâche asynchrone (type celery), mettez une option --synchronous qui insère le code inline afin de pouvoir utiliser pdb sur tout le script en cas de besoin. Ou alors, si vous avez une grosse archive à décompresser, mettez une option --nozip pour pouvoir sauter cette étape.

Et si vous insérez un break point, mettez le dans une condition du genre :

if id_du_champ_ou_autre_moyen_identifiant == "valeur":
    send_mail('Alerte, on est peut être au bug. Bouge ton fion.')
    import ipdb; ipdp.set_trace()

Comme ça vous pouvez retourner à vos moutons le temps que le breakpoint s’active, ce qui, sur des gros jeux de données, peut prendre énormément de temps.

Enfin, je sais qu’on a tendance à être fainéant et vouloir toujours débugger en direct. Mais faites des mocks. Faites un faux XML, une API bidon, bref, un truc qui vous permet d’insérer des cas d’import avec une données dans le format attendu, et testez votre code avec ça. Pour les petits imports, c’est une perte de temps, mais pour les gros imports, ça va vous faire gagner des jours. Ainsi vous pouvez tester des cas isolé, rajouter des bugs rencontrés, etc. Et en plus ça sert de documentation.

Problèmes courants

Il y a mille et une manière d’avoir un import qui plante, mais il y a généralement 6 grosses foirades qu’on retrouve tout le temps.

Service défaillant

Le service (FTP, API, humain en face, NAS, etc) qui doit vous fournir les données est indisponible. Vous n’y pouvez rien. Envoyez-vous un SMS pour vous prévenir, aujourd’hui c’est facile et ça coûte presque rien. Ainsi vous pourrez dialoguer rapidement avec les personnes responsables de problème.

Champs manquants

Grand classique. Tout champ peut manquer. Tout. Même un ID unique sans lequel la donnée n’a aucune sens. Faites vous un wrapper du genre :

def get_data(champ):
    try:
        # extraire le champ
    except ChampAbsent, ChampMalFormé:
        return None

Et utilisez le partout. Et décidez ce que doit faire votre programme si il rencontre None. Pour TOUS les champs. Si None est une valeur possible, utilisez Ellipsis. Si Ellipsis est une valeur possible, faites vous une classe InvalidData.

Mauvais encoding

Super vicieux. Je vous renvoie à l’article sur l’encoding pour cela.

Donnée aberrante

Impossible à prévoir, très difficile à identifier. Donnée de mauvais type, date ou nombre hors limites, texte dans la mauvaise langue, etc. Vous ne pouvez pas tout prévoir. Pour ça, il faudra faire au fur et à mesure des plantages.

Données mal formatée et malicieuses

En plus de get_data, il vous faut un clean_data. Qui check si on peut processer la donnée sereinement. Pour tous les champs également. C’est con, mais si LEUR système n’escape pas les entrées utilisateurs, c’est VOTRE système dans lequel se retrouve les injections de code.

Performances

La vitesse de votre script sera généralement limitée par 3 facteurs :

  • Vitesse de lecture.
  • Vitesse d’écriture.
  • Vos plantages.

Ces 3 facteurs sont en général très liés.

La meilleur stratégie, c’est d’extrapoler un max de données, et de cacher tout ce qui est cacheable. Par exemple, copiez les données brutes sur vos serveurs (genre si c’est un fichier sur le leur). Copiez les références externes, même si vous n’en avez pas besoin afin d’éviter une query de plus, vous les supprimerez plus tard. Pré-calculez les champs, par exemple age, si vous avez la date de naissance, etc.

Si vous avez beaucoup de checks à faire pour l’assainissement des données, mettez vos données en cache (par exemple dans redis), pour que les looks up soient rapides, ou au moins, ajoutez les index qui vont bien dans la DB (on peut avoir des perfs X10 rien qu’avec ça).

Ensuite, partez du principe que ça va planter souvent, donc :

  • Faites des opérations idempotentes, c’est à dire qu’on peut les relancer autant de fois qu’on veut sans risque. Typiquement, vérifiez si une donnée existe avant l’insertion, et si oui, faites une mise à jour complète.
  • Mettez un historique. Sauvegardez quelque part l’avancement de votre import, afin de ne pas tout recommencer depuis le début après le plantage.

Ah oui, et faites des copies de sauvegarde des données brutes ET des données importées. Pour les premières, parce qu’on est pas à l’abri un “rm -fr” malencontreux. Ne rigolez pas, ça m’est arrivé la semaine dernière. Une semaine à tout DL à nouveau. Pour les secondes, parce que tant que le dernier import n’est pas terminé, on peut toujours corrompre toute sa base avec une couille de dernière minute. Comme un encoding qui change aléatoirement sur une donnée non datée.

Bon sens

Évidement, je parle ici d’un synthèse des problématiques rencontrées. Vous ne pouvez pas appliquer TOUT ça, ou en tout cas, pas au début, ou pas sur des petits scripts, etc. Selon le sérieux de votre source de données, il faudra plus ou moins être défensif. L’expérience et la douleur vous permettra de trouver la juste dose de morphine.

]]>
http://sametmax.com/importer-des-donnees-retour-dexperience/feed/ 13 8772
Pourquoi il faut éviter import * en Python http://sametmax.com/pourquoi-il-faut-eviter-import-en-python/ http://sametmax.com/pourquoi-il-faut-eviter-import-en-python/#comments Mon, 25 Nov 2013 07:00:41 +0000 http://sametmax.com/?p=8082 Ne pas utiliser l’opérateur splat dans un import, vous l’avez sans doute lu 100 fois, mais savez-vous pourquoi ?

Regardez la fonction open :

>>> help(open)
    open(name[, mode[, buffering]]) -> file object
    
    Open a file using the file() type, returns a file object.  This is the
    preferred way to open a file.  See file.__doc__ for further information.
(END)

Maintenant, si j’importe le module os, ça ne change rien :

>>> import os
>>> help(open)
    open(name[, mode[, buffering]]) -> file object
    
    Open a file using the file() type, returns a file object.  This is the
    preferred way to open a file.  See file.__doc__ for further information.
(END)

Si par contre j’importe tout le contenu du module os, sans namespace :

>>> from os import *
>>> help(open)
    open(filename, flag [, mode=0777]) -> fd
    
    Open a file (for low level IO).
(END)

La différence ?

Dans le premier cas, on a la fonction open() built-in de Python. Dans le second cas, la fonction os.open() a été importée et a remplacé la fonction open().

Ici le bug sera très difficile à trouver, car les deux fonctions ont presque la même signature :

open(name[, mode[, buffering]]) -> file object

VS

open(filename, flag [, mode=0777]) -> fd

Et en plus un usage très similaire.

Bottom line, import *, c’est pour les sessions shell. Dans vos fichiers de code, ne l’utilisez pas, vous ne savez pas ce que vous importez.

]]>
http://sametmax.com/pourquoi-il-faut-eviter-import-en-python/feed/ 11 8082
Vous pouvez mettre du code dans __init__.py http://sametmax.com/vous-pouvez-mettre-du-code-dans-__init__-py/ http://sametmax.com/vous-pouvez-mettre-du-code-dans-__init__-py/#comments Wed, 26 Jun 2013 11:20:04 +0000 http://sametmax.com/?p=3472 __init__.py ne sert pas qu'à déclarer un dossier comme un package importable. C'est aussi le code exécuté automatiquement, une seule fois, quand on importe le module.]]> Le fichier __init__.py ne sert pas qu’à déclarer un dossier comme un package importable. C’est aussi le code exécuté automatiquement, une seule fois, quand on importe le module.

Du coup vous pouvez mettre dedans tout code Python que vous souhaitez lancer à l’import. On y voit souvent :

  • Initialisation du cache.
  • Constantes comme __version__.
  • Import d’autres modules pour les mettre dans l’espace de nom courant.
  • Check de dépendances.
  • Aliasing.
  • Monkey patching and hacks tout moches qui sont là temporairement pour 6 ans.

Je vous déconseille de mettre trop de code dans le fichier __init__.py, notamment du code métier. C’est une mauvaise habitude que l’on peut voir dans le code de Django par exemple. Car ça veut dire que l’import du package déclenche ce code, qui lui-même importe d’autres modules, qui déclenche d’autres codes, etc. Cela donne des effets de bord à l’import, alors que l’import d’un simple package est quelque chose que l’on veut généralement ne pas avoir beaucoup d’effets.

Dans Django par exemple, c’est ce qui fait que beaucoup de modules lèvent une exception à l’import :

django.core.exceptions.ImproperlyConfigured: Requested setting X but settings are not configured.

Alors qu’ils n’ont pas du tout besoin des settings pour fonctionner.

Si vous avez besoin que votre classe Bidule soit directement importable dans votre package machin, faites dans machin/__init__.py :

from .module_qui_contient_bidule import Bidule

Du coup vous pourrez faire n’importe où ailleurs:

from machin import Bidule

C’est bien plus propre que de mettre tout le code de Bidule dans le __init__.py.

]]>
http://sametmax.com/vous-pouvez-mettre-du-code-dans-__init__-py/feed/ 12 3472
Les imports en Python http://sametmax.com/les-imports-en-python/ http://sametmax.com/les-imports-en-python/#comments Thu, 16 May 2013 09:13:27 +0000 http://sametmax.com/?p=6127 sys.path.append partout juste au cas où et c'était encore pire. Vous avez donc décidé de vous remettre à PHP, au moins le include utilise les chemins de fichiers, et ça, c'est facile.]]> Je suis fan de carmina burrana depuis l’age de 12 ans, alors pourquoi pas O Fortuna comme musique d’ambiance :

Les imports, c’était fastoche. Vous étiez dans votre petit programme, et pour importer un module de la lib standard, vous faisiez:

import module

Par exemple :

import os

Et pour importer une classe ou une fonction de cette lib, vous faisiez :

from module import fonction
from module import Classe

Par exemple :

from hashlib import md5
from xml.etree import Element

Parfois, c’était un peu plus compliqué, mais ça allait encore. Des fois il fallait importer un sous-module :

from package.sous_package import module

Par exemple :

from xml.sax import saxutils

Mais ça allait encore.

Et puis un jour vous avez du écrire votre propre module. Vous n’aviez pas vraiment réfléchi à la question. C’était juste une petite lib pour regrouper des fonctions. Ou juste une app Django. Un truc tout simple. Mais les imports ont soudainement cessé de devenir clairs. Ça ne marchait pas. Rien ne marchait. Vous aviez des sys.path.append partout juste au cas où et c’était encore pire.

Vous avez donc décidé de vous remettre à PHP, au moins le include utilise les chemins de fichiers, et ça, c’est facile.

Sous le capot

Quand vous utilisez import, sous le capot Python utilise le fonction __import__. Malgré ses __ dans le nom, c’est une fonction ordinaire, et vous pouvez d’ailleurs l’utiliser vous-même :

>>> os = __import__('os')
>>> os.path.join('s', 'ton', 'mon', 'g')
u's/ton/mon/g'

En fait, importer un module, c’est créer un objet module qui est assigné à une variable tout à fait normale :

>>> type(os)

>>> os = "on peut ecraser un module"
>>> os.path
Traceback (most recent call last):
  File "", line 1, in 
    os.path
AttributeError: 'unicode' object has no attribute 'path'

>>> import sys
>>> type(sys)

>>> sys = "je t'ecrase la tronche"
>>> type(sys)

Le mécanisme de module Python n’est donc pas un truc à part, c’est un objet comme le reste, qui contient des attributs. Les attributs sont les variables et les fonctions du module.

Pour charger un module, la fonction __import__ passe par les étapes suivantes :

  1. Chercher si le module os existe.
  2. Chercher si le module a déjà été importé. Si oui, s’arrêter ici et renvoyer le module existant.
  3. Si non, chercher si il a été déjà compilé en .pyc.
  4. Si ce n’est pas le cas, compiler le fichier .py en .pyc.
  5. Charger le bytecode du fichier pyc.
  6. Créer un objet module vide.
  7. Éxecuter le bytecode dans le contexte de l’objet module et remplir ce dernier avec le résultat.
  8. Ajouter l’objet module dans sys.modules, un dictionnaire qui contient tous les modules déjà chargés.
  9. Retourner le module pour pouvoir l’assigner à une variable, par défaut la variable porte son nom.

La fonction __import__ est donc très complexe, et d’ailleurs si vous voulez l’utiliser pour des trucs plus compliqués qu’un simple import de module, vous allez galérer car sa signature est vraiment zarb.

Mais pour vous, seule l’étape 1 est importante à comprendre. C’est l’étape à laquelle tout se joue.

Comment Python définit quel module importer ?

C’est la partie vraiment difficile, en effet si un import ne marche pas, c’est très souvent parce que Python ne trouve pas le module que vous voulez. Et la raison pour laquelle il ne le trouve pas, c’est que vous ne comprenez pas comment il cherche.

Python utilise ce qu’on appelle le PYTHON PATH pour chercher les modules importables. C’est une variable système qui contient une liste de dossiers. Par exemple, sur ma machine, elle contient ceci :

['',
 '/usr/bin',
 '/usr/local/lib/python2.7/dist-packages/grin-1.2.1-py2.7.egg',
 '/usr/lib/python2.7',
 '/usr/lib/python2.7/plat-linux2',
 '/usr/lib/python2.7/lib-tk',
 '/usr/lib/python2.7/lib-old',
 '/usr/lib/python2.7/lib-dynload',
 '/home/sam/.local/lib/python2.7/site-packages',
 '/usr/local/lib/python2.7/dist-packages',
 '/usr/local/lib/python2.7/dist-packages/setuptools-0.6c11-py2.7.egg-info',
 '/usr/lib/python2.7/dist-packages',
 '/usr/lib/python2.7/dist-packages/PIL',
 '/usr/lib/python2.7/dist-packages/gst-0.10',
 '/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/ubuntuone-client',
 '/usr/lib/python2.7/dist-packages/ubuntuone-control-panel',
 '/usr/lib/python2.7/dist-packages/ubuntuone-couch',
 '/usr/lib/python2.7/dist-packages/ubuntuone-installer',
 '/usr/lib/python2.7/dist-packages/ubuntuone-storage-protocol',
 '/usr/lib/python2.7/dist-packages/wx-2.6-gtk2-unicode',
 '/usr/lib/python2.7/dist-packages/IPython/extensions']

Donc, quand vous faites import os, Python va faire une boucle for là dessus et chercher dans chaque dossier si un package (un dossier avec un fichier __init__.py) ou un module (un fichier avec l’extension .py) nommé os existe.

Dès qu’il en trouve un, il s’arrête de chercher et l’importe. Si il n’en trouve pas, il va lever une ImportError.

Ce qui signifie que si votre module n’est PAS dans le PYTHON PATH, vous ne pouvez PAS l’importer. C’est impossible.

La grande majorité des problèmes d’import vient du fait que le module que vous essayez d’importer n’est pas dans le PYTHON PATH.

Maintenant, la grande question, c’est :

Qu’est-ce qui est dans le PYTHON PATH ?

Par défault, les dossiers sites-packages et dist-packages dans le dossier d’installation Python sont dans le PYTHON PATH. Quelques autres sont ajoutés selon les systèmes, mais vous pouvez toujours compter sur sites-packages et dist-packages pour être dans le PYTHON PATH. Quand vous installez une lib, par exemple avec pip, c’est là dedans que la lib va s’installer, pour être sûre de pouvoir être importée.

Quand vous êtes dans un virtualenv, les dossiers sites-packages et dist-packages de l’environnement virtuel sont ajoutés au PYTHON PATH.

Mais tout ça ne change pas grand chose pour vous. En effet, vous n’allez pas mettre VOTRE code dans les dossiers sites-packages et dist-packages.

C’est pour cela que Python possède un mécanisme supplémentaire : le dossier qui contient le module sur lequel vous lancez la commande python est automatiquement ajouté au PYTHON PATH.

Le PYTHON PATH, en pratique

Supposons que je sois dans le dossier /home/sam/Bureau et que j’aie dedans ce package. Voici à quoi ressemble mon arbo (téléchargez l’arbo vierge pour vos tests):

/home/sam/Bureau # <-- je suis ici
.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   |-- autre_sous_package
    |   |   |-- __init__.py
    |   |   `-- autre_module_en_bas.py
    |   |-- sous_module.py
    |   `-- sous_package
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       |-- autre_sous_package
    |       |   |-- __init__.py
    |       |   `-- autre_module_en_bas.py
    |       `-- module_tout_en_bas.py
    `-- top_module.py

Si je lance un shell Python depuis ce dossier ou un script Python contenu dans ce dossier, je peux faire import test_imports, car /home/sam/Bureau est automatiquement ajouté au PYTHON PATH.

Je peux donc faire :

>>> import test_imports
>>> from test_imports import package_tout_en_haut
>>> from test_imports import top_module
test_imports.top_module
>>> from test_imports.package_tout_en_haut import sous_module
test_imports.package_tout_en_haut.sous_module

Mais si je me mets ici dans ./package_tout_en_haut/sous_package :

/home/sam/Bureau
.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   |-- autre_sous_package
    |   |   |-- __init__.py
    |   |   `-- autre_module_en_bas.py
    |   |-- sous_module.py
    |   `-- sous_package     # <-- je suis ici
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       |-- autre_sous_package
    |       |   |-- __init__.py
    |       |   `-- autre_module_en_bas.py
    |       |-- module_tout_en_bas.py
    `-- top_module.py

Je ne peux PAS importer test_imports, ni dans un shell, ni depuis un module de ce dossier :

>>> import test_imports
Traceback (most recent call last):
  File "", line 1, in 
ImportError: No module named test_imports

En effet, comme je lance la commande Python depuis

./package_tout_en_haut/sous_package

alors

./package_tout_en_haut/sous_package

EST ajouté au PYTHON PATH, mais

/home/sam/Bureau/

n’est PAS ajouté au PYTHON PATH.

Je ne peux donc PAS faire

from test_imports import top_module 

depuis un fichier comme

.test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py

et exécuter directement

python autre_module_en_bas.py

ni même

python ./test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py

Je peux faire

from test_imports import top_module

depuis

autre_module_en_bas.py

uniquement si je lance un script Python tout en haut de mon arbo qui importe

autre_module_en_bas.py.

Mais alors, comment on fait ?

Il faut s’assurer que le dossier qui contient test_imports, notre module racine, soit TOUJOURS dans le PYTHON PATH.

Il y a plusieurs possibilités pour cela.

La première, c’est que notre lib va être utilisée une fois installée avec pip. Dans ce cas, on s’en branle, test_imports sera dans sites-packages automatiquement, et on pourra faire from test_imports import top_module de partout joyeusement.

Mais souvent, ce n’est pas le cas, votre code n’est pas fait pour être installé.

La seconde technique consiste à s’assurer que l’on appelle TOUJOURS la commande Python depuis le dossier qui est tout au dessus. C’est ce que fait django avec sa commande ./manage.py par exemple.

Vous avez votre projet :

./manage.py
projet

Et tout passe par python manage.py, qui est au dessus de projet, donc le dossier est bien ajouté au PYTHON PATH, et tout va bien.

Dans votre cas ça veut dire vous assurer qu’on lance toujours votre programme depuis un script d’entrée qui est tout en haut de votre arborescence.

Ca veut dire que vous devez avoir un point d’entrée UNIQUE pour votre package.

Mais parfois ça ne convient pas. Dans le cas des tests unitaires par exemple, il vous faut un point d’entrée spécialement pour les tests.

Pour ce genre de scénario, il faut donc avoir le dossier qui les contient à côté de votre package. Ainsi, si j’avais des tests unitaires, je devrais faire un dossier tests à côté du dossier test_imports. Par exemple, transformer mon arbo en un truc comme ça :

src
   |_ test_imports
   |_ tests

Afin que je lance les tests en faisant python tests depuis src. Et dans mes fichiers de tests, je pourrai faire des from test_imports import truc.

La manière dont vous organisez votre projet est donc très importante en Python, et si vous avez des problèmes d’import, la première chose à faire est de changer sa structure. Il n’y a pas de magie.

La dernière possibilité, quand tout a échoué, c’est de rajouter à la main le dossier dans le PYTHON PATH. sys.path est une simple liste, on peut donc faire un append() dessus.

Par exemple, si je veux absolument (mais je ne devrais pas :-)) pouvoir faire :

python .test_imports/package_tout_en_haut/sous_package/autre_module_en_bas.py et importer test_imports dans autre_module_en_bas.py, je peux faire un truc du genre :

import os

dossier = os.path.dirname(os.path.abspath(__file__))

while not dossier.endswith('test_imports'):
    dossier = os.path.dirname(dossier)

dossier = os.path.dirname(dossier)

if dossier not in sys.path:
    sys.path.append(dossier)

Ce code va remonter dans l’arbo jusqu’à tomber sur le chemin du dossier test_imports et ajouter son dossier parent au PYTHON PATH.

Ce n’est pas le truc le plus propre du monde, mais ça peut dépanner.

Imports absolus et relatifs

Si vous êtes dans ./package_tout_en_haut/sous_package :

/home/sam/Bureau
.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   |-- autre_sous_package
    |   |   |-- __init__.py
    |   |   `-- autre_module_en_bas.py
    |   |-- sous_module.py
    |   `-- sous_package     # <-- je suis ici
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       |-- autre_sous_package
    |       |   |-- __init__.py
    |       |   `-- autre_module_en_bas.py
    |       |-- module_tout_en_bas.py
    |       `-- test_imports  # <-- autre package nommé test_imports
    |           `-- sous_module.py
    `-- top_module.py

Vous voyez que vous avez deux packages nommés test_imports.

Si vous écrivez import test_imports dans autre_module_en_bas.py, que va-t-il se passer ?

C'est le module tout en bas qui va être importé.

Ce n'est pas forcément ce que vous voulez. Python 3 corrige cela en permettant des imports relatifs, et Python 2.7 peut en bénéficier en important tout en haut du module :

from __future__ import absolute_import

En faisant cela, vous obtenez le comportement de Python 3 dans Python 2.7, et vous pourrez alors choisir entre faire :

import test_imports # importe le module tout en haut
from . import test_imports # import le module dans le même dossier
from .test_imports import sous_module
from test_imports import top_module

Je vous recommande de toujours utiliser from __future__ import absolute_import. Ca ne coûte rien, et c'est plus cohérent. Par contre, vous ne pourrez pas tester from __future__ import absolute_import dans le shell, donc cet exemple ne marche pas dans ipython, mais il fonctionne parfaitement dans vos modules.

On peut aussi faire des imports relatifs du package contenant avec :

from .. import truc
from ..package import machin

N'oubliez pas que ceci ne marche que :

  • Si from __future__ import absolute_import est activé.
  • Le package tout en haut (celui qui contient tous les autres) est dans le PYTHON PATH

Sinon, ça ne sert A RIEN. Ce n'est pas comme un ../ dans un bash. Ça ne remonte pas d'un dossier. C'est juste une notation pour dire j'utilise celui la plutôt que l'autre, quand il y a ambiguité.

Pièges des imports

Package sans init

Si vous avez :

.
`-- test_imports
    |-- __init__.py
    |-- package_sans_init
    |   `-- nada.py

nada.py n'est pas importable, car package_sans_init ne contient pas de fichier __init__.py, même si test_imports est dans le PYTHON PATH. Ce comportement est corrigé en Python 3, et tout sous-dossier d'un package importable est automatiquement importable, qu'il contienne un __init__.py ou non.

Imports circulaires

J'en ai déjà parlé ici.

Vous avez :

.
`-- test_imports
    |-- __init__.py
    |-- package_tout_en_haut
    |   |-- __init__.py
    |   `-- sous_package
    |       |-- __init__.py
    |       |-- autre_module_en_bas.py
    |       `-- module_tout_en_bas.py

Et vous importez autre_module_en_bas dans module_tout_en_bas et inversement. Non seulement ça ne marchera pas, mais en plus l'erreur est déroutante :

ImportError: No module named module_tout_en_bas

Oui vous avez bien lu, il va vous dire que le module n'existe pas !

Il n'y a pas non plus de solution propre à ce problème : soit vous fusionnez vos deux fichiers, soit vous faites un 3eme module qui utilise ces deux modules (et ces deux modules n'importent pas ce 3eme module).

Sinon il y a la solution crade : mettre un des imports dans un appel de fonction ou de méthode comme ça:

def truc():
    import module_tout_en_bas
    module_tout_en_bas.bidule()

Parfois, ça dépanne :-) On ne tue pas des chatons non plus, donc si ça ne devient pas une habitude, ça peut passer.

]]>
http://sametmax.com/les-imports-en-python/feed/ 37 6127
Include / require / import en javascript http://sametmax.com/include-require-import-en-javascript/ http://sametmax.com/include-require-import-en-javascript/#comments Thu, 03 Jan 2013 15:04:33 +0000 http://sametmax.com/?p=3977 import, include ou require. On peut néanmoins trouve un moyen d'inclure du code en le téléchargeant et en l'incluant dans la page.]]> On ne peut pas inclure un script dans un script en JS dans un navigateur Web. Il n’y a pas de mot clé import, include ou require.

On peut néanmoins trouver un moyen d’inclure du code en le téléchargeant et en l’incluant dans la page.

Solution 1, la bourrine

On peut faire une requête GET et un eval sur. C’est dangereux. C’est bancal. Ca marche que pour le domaine en cours. C’est pas une bonne idée. Je ne vous le montre donc pas.

Solution 2, la maline

On va créer une balise <script></script> et la faire pointer sur le fichier à télécharger. Ainsi on utilise le mécanisme naturel pour le navigateur d’include du code.

var include = function(url, callback){

    /* on crée une balise */
    var script = document.createElement('script');
    script.type = 'text/javascript';

    /* On fait pointer la balise sur le script qu'on veut charger
       avec en prime un timestamp pour éviter les problèmes de cache
    */

    script.src = url + '?' + (new Date().getTime());

    /* On dit d'exécuter cette fonction une fois que le script est chargé */
    if (callback) {
        script.onreadystatechange = callback;
        script.onload = script.onreadystatechange;
    }

    /* On rajoute la balise script dans le head, ce qui démarre le téléchargement */
    document.getElementsByTagName('head')[0].appendChild(script);
}

Ca s’utilise comme ça:

include('http://adressedemonscript.com/fichier.js', function() {
    code à exécuter une fois que le script est chargé
})

La partie callback est très importante. En effet, si vous essayez d’exécuter du code après include() qui dépend du code chargé par include(), ça va foirer : le code n’est pas encore téléchargé. En effet, les navigateurs téléchargent les balises scripts en arrière plan et en parallèle :

include('http://adressedemonscript.com/fichier.js');
code à exécyter une fois que le script est chargé

Il faut donc mettre ce code dans un callback, pour garantir qu’il soit lancé quand le script a terminé de chargé.

Solution 3, la coquine

jQuery, encore et toujours, possède un raccourcis pour faire tout ça pour vous:

$('http://adressedemonscript.com/fichier.js', function() {
    code à exécuter une fois que le script est chargé
})
]]>
http://sametmax.com/include-require-import-en-javascript/feed/ 24 3977
Se faciliter les imports avec les fichiers *.pth http://sametmax.com/se-faciliter-les-imports-avec-les-fichiers-pth/ http://sametmax.com/se-faciliter-les-imports-avec-les-fichiers-pth/#comments Mon, 10 Sep 2012 16:49:24 +0000 http://sametmax.com/?p=2082 *.pth. Néanmoins cette extension est souvent mal comprise, et voici un comment en profiter au maximum.]]> Dans un article, nous parlions des extensions alternatives en Python, et notamment de l’usage des fichiers *.pth. Néanmoins cette extension est souvent mal comprise, et voici un comment en profiter au maximum.

En Python, on a souvent des problèmes d’import: la lib est dans un dossier au dessous, ou à côté, ou à l’autre bout du disque dur, et ça plantouille parceque Python ne trouve pas le module.

Il existe plein de moyens de jouer avec le fameux PYTHON_PATH qui contient la liste des dossiers dans lesquelles chercher les libs, et la plupart sont fort verbeuses et répétitives, du genre:

import os
import sys

CUR_DIR = os.path.dirname(os.path.realpath(__file__))
sys.path.extend([
    os.path.join(CUR_DIR, 'apps'),
    os.path.join(os.path.dirname(CUR_DIR), 'libs'),
    os.path.join(os.path.realpath('~'), '.local_libs')
]]}

Une manière simple est d’utiliser un fichier *.pth: on créé un fichier texte, on le nomme comme-on-veut.pth (moi je me fais pas chier, je le nomme .pth comme ça c’est court et c’est caché sous nunux), et on liste tous les dossiers qu’on veut ajouter au PYTHON_PATH, y compris avec des chemins relatifs.

Oui mais, argh parmi les argh, la première fois qu’on le fait, ça ne marche pas. Alors on cherche dans la doc, et là, fustration, on apprend que les fichiers *.pth ne sont parsés que dans les sites directories, c’est à dire les dossiers officiels du système dans lesquels sont censés être les libs de la bibliothèque standard. Donc pas le dossier courant.

Eh oui, ces fichiers ont été conçus pour faciliter les déploiements, du genre quand on fait un setup.py pour son app, pas pour nous, pauvres mortels.

Heureusement il y a une solution Sam et Max à tout, et ici elle consiste à faire:

import os
import site

CUR_DIR = os.path.dirname(os.path.realpath(__file__))
site.addsitedir(CUR_DIR)

Quelque part dans son code n’importe où qui est sûr d’être éxécuté tôt, par exemple dans Django, dans le settings.py. Ca ajoute le dossier du projet en tant que site directory, et donc le fichier .pth est parsé. On peut alors dumper son listing dedans sans avoir à le répéter dans tous les scripts et fichiers qui doivent l’utiliser (et ainsi modifier cette liste facilement).

Attention cependant, celà rajoute votre dossier de projet dans le PYTHON_PATH, rendant tout ce qu’il contient importable. Normalement, c’est ce que vous voulez, mais si ce n’est pas le cas, vous pouvez avoir une gestion plus fine en ajoutant juste le fichier .pth avec site.addpackage(CUR_DIR, 'fichier.pth', set())

La feature de magie noire

Dans les fichiers *.pth, il y a un support limité de la syntaxe Python: on peut commenter des lignes avec # et faire des import truc.

Sauf que si vous êtes curieux, vous verrez le détail qui tue dans l’implémentation:

if line.startswith(("import ", "import\t")):
    exec line

On peut donc virtuellement mettre n’importe quel code Python dans les fichiers *.pth. Par exemple:

../foo
import sys; print "Foo a été ajouté au PYTHON PATH"

Va afficher “Foo a été ajouté au PYTHON PATH” au démarrage du programme. Amusant. Et tellement de potentiels pour faire des trucs tordus !

]]>
http://sametmax.com/se-faciliter-les-imports-avec-les-fichiers-pth/feed/ 1 2082
Astuces Python en vrac http://sametmax.com/astuces-python-en-vrac/ http://sametmax.com/astuces-python-en-vrac/#comments Fri, 03 Aug 2012 13:15:35 +0000 http://sametmax.com/?p=1463 Je n’arrive pas à trouver un lien entre tous ces trucs, alors un bon vrac fera l’affaire.

Float accepte de parser l’infini

>>> infini = float('inf')
>>> infini
inf
>>> infini + 1
inf
>>> infini - 1
inf
>>> float('-inf')
-inf
>>> float('-inf') + 1
-inf
>>> float('inf') + float('-inf')
nan

Attention, il est très probable que ce soit dépendant de l’implémentation CPython.

Iter() peut prendre un callable en argument

iter(), c’est la fonction qui créé un générateur à partir d’un itérable:

>>> generateur = iter([1, 2, 3])
>>> generateur.next()
1
>>> generateur.next()
2
>>> generateur.next()
3

Il se trouve qu’il a aussi une autre forme: iter(callable, sentinel).

Sous cette forme, il va créer une générateur qui appelle callable jusqu’à ce que la valeur sentinel apparaisse.

>>> import datetime
>>> datetime.datetime.now().strftime("%S")
'45'
>>> generateur = iter(lambda: datetime.datetime.now().strftime("%S"), "59")
>>> generateur.next()
'56'
>>> generateur.next()
'57'
>>> generateur.next()
'58'
>>> generateur.next()
Traceback (most recent call last):
  File "", line 1, in 
    generateur.next()
StopIteration

Chainer les comparateurs

Histoire de diminuer le nombre de if:

>>> a, b, c, d = 1, 2, 3, 4
>>> a < b < c < d
True
>>> a == 1 > b -2
True

Le mot clé else, en dehors de if

Else ne s’applique pas qu’aux conditions, mais aussi aux exceptions et aux boucles.

Dans une boucle for, la clause else est exécutée à la fin de l’itération si il n’y a pas eu de break.

>>> import random
>>> lst = [random.randint(1, 5) for x in xrange(5)]
... for x in lst:
...     if x == 5:
...         break
... else:
...     print "5 n'a jamais été trouvé"

Dans la gestion des exceptions, else est éxécuté si catch n’est jamais appelé:

try:
    open('fichier')
except IOError:
    print "Une IO Error est arrivée"
else:
    print "Tout s'est bien passé"
finally:
    print "Toujours éxécuté"

print "Exécuté si on est rentré dans except ou else"

Continuation de lignes avec les parenthèses

\ sur les longues lignes, ça va 5 minutes.

from force import (rouge, bleu, jaune, vert, noir, blanc, rose, fushia,
                   vermeille, vers_a_pois_jaune, call)

# ici le multi line édit de sublime text m'a vachement aidé
if (rouge.transformation and bleu.transformation and jaune.transformation
        and vert.transformation and noir.transformation and blanc.transformation
        and rose.transformation and fushia.transformation
        and  vermeille.transformation and vers_a_pois_jaune.transformation):
    print ("C'est bon là je crois qu'on a tout le monde sauf erreur de ma part"
           " dans le comptage... Ah non merde il manque jaune devant marron "
           "derrière")
    call(force='jaune_devant_marron_derriere', message="Ramène ton cul tout"
                                                       "de suite !")

Les extensions .pyw, .pyo et .pth

Vous croyiez qu’il n’y avait que .py et .pyc dans la vie ? Ah, jeunes padawans…

.pyw est juste un renommage, il permet, sous Windows, de ne pas ouvrir de terminal quand on lance le script (ce qui est préférable quand on a déjà une UI)

.pyo est l’extension générée quand on lance la commande python avec l’option -o (optimize). Pour le moment il retire juste les assert.

.pth est l’extension qu’on donne à un simple fichier texte qui contient une liste de chemins de dossiers. Posé à la racine d’un site directory, il dit à Python de rajouter automatiquement ces dossiers au Python Path, ce qui évite de manipuler sys.path.

Lever une exception à nouveau

Il suffit d’utiliser raise sans paramètre. Pratique quand on ne veut pas interrompre la remontée d’exception, et insérer un traitement juste avant que l’exception se déclenche (en opposition à finally qui garanti le traitement, pas le moment de celui-ci.)

try:
    open('fichier')
except IOError:
    print "pouet"
    raise

Passer une valeur à un générateur

Vous aimez yield ? Vous en abusez ? Sachez qu’on peut faire plus vicieux encore:

def je_te_yield_tu_me_yield(lst):
    for x in lst:
        nouvelle_liste = yield x
        if nouvelle_liste is not None:
            for z in nouvelle_liste:
                print z

Le truc tordu est qu’ici yield est dans un assignement. Non seulement il retourne une valeur, mais en plus il en récupère une. send() permet de passer une valeur à yield à son prochain retour (sinon la valeur reçue est toujours None):

>>> generateur = je_te_yield_tu_me_yield([1, 2, 3])
>>> generateur.next()
1
>>> generateur.next()
2    
>>> generateur.send(('a', 'b', 'c'))
a
b
c
3

On peut inverser les booléens avec 1/0 et vice-versa

Les booléens ne sont qu’une surcouche des entiers, et même si:

>>> type(1) 

>>> type(bool)

Dans la pratique:

>>> 1 == True
True
>>> 0 == False
True
>>> 2 == True
False

On peut donc les interchanger:

>>> True + 1 # pas sur que ce soit utile
2
>>> lst = ['a', 'b'] # par contre ça c'est cool pour les binary trees
>>> lst[False]
'a'
>>> lst[True]
'b'
>>> fin_de_phrase = True # et ça c'est TRES pratique pour le formating
>>> print "... d'une longue histoire" + ("." * fin_de_phrase)
... d'une longue histoire.
>>> fin_de_phrase = False
>>> print "... d'une longue histoire" + ("." * fin_de_phrase)
... d'une longue histoire

_ contient la dernière sortie sur le shell

>>> _
'b'
>>> 1 + 1
2
>>> _
2
>>> _ + 1
3
>>> a = [1, 2, 3]
[1, 2, 3]
>>> _.append(4)
>>> a
[1, 2, 3, 4]

Ca ne marche que dans le shell, et uniquement sur ce qui est affiché à l’écran. Si vous n’affichez pas la valeur, _ ne change pas. Attention à ne pas trop compter sur _ car il est très volatile.

Enumerate() accepte un index de départ

>>> for i, elem in enumerate('azerty'):
...     print i, elem
...     
0 a
1 z
2 e
3 r
4 t
5 y
>>> for i, elem in enumerate('azerty', 10):
    print i, elem
...     
10 a
11 z
12 e
13 r
14 t
15 y

On peut assigner et supprimer des slices

>>> a = range(10)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> a[:5] = [42]
>>> a
[42, 5, 6, 7, 8, 9]
>>> a[:1] = range(5)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> del a[::2]
>>> a
[1, 3, 5, 7, 9]
>>> a[::2] = a[::-2]
>>> a
[9, 3, 5, 7, 1]

import braces

Si vous n’aimez pas les espaces pour l’indentation, vous pouvez faire:

from __future__ import braces
]]>
http://sametmax.com/astuces-python-en-vrac/feed/ 11 1463
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