ShadesOfPy

Traiter des fichiers suivant leurs extensions

Snippet 10 écrit le vendredi 04 avril 2008 a 18 h 30 min dans les catégories : Fichiers Répertoires

Je ne sais pas pour vous, mais j'ai souvent besoin de traiter des fichiers d' un type bien particulier.

Si par exemple on désire se débarasser de tous les fichier 'log' et 'pyw' du répertoire de notre script, on peut utiliser la méthode endswith() qui s'applique à une chaîne en créant une boucle sur chaque objet fichier trouvé dans ce meme répertoire.

On pourrait bien sûr écrire ceci ainsi :

for f in ma_liste_de_fichiers :
    if f.endswith(".pyw") or f.endswith(".log") or etc.

Mais ce n'est pas très beau, et imaginer si on veut traiter 10 extensions différentes.

Alors qu'au lieu de cela, on peu briller grâce à Python 2.5 et au mot-clef any, comme ceci:

import os
SCRIPTREP = os.getcwd()

for f in os.listdir(SCRIPTREP) :
    if any(f.endswith(x) for x in ('.pyw','.log')):
        # fais quelque chose avec f, par exemple ceci :
        print "Detecte %s"%f

Parsing techniques

Snippet 9 écrit le samedi 12 janvier 2008 a 15 h 40 min dans les catégories : Parsing

Je vais taĉher ici de vous faire découvrir une technique de parsing de texte élégante et bien cachée dans la documentation de Python.

Si l'on veut remplacer un motif recherché par autre chose à l'aide d' expressions rationnelles, il est de coutume d'employer la méthode sub du module re :

re.sub(le_motif, le_remplacement, le_texte_initial)
## ou bien
le_motif.sub(le_remplacement, le_texte_initial)

Ainsi, pour remplacer toute occurence de 'toto' par 'coincoin' dans la chaine

"Le papa de toto lui demande d'aller à la boulangerie, toto répond non."

on procèdera ainsi :

test = "Le papa de toto lui demande d'aller à la boulangerie, toto répond non."
print re.sub(re.compile(r"toto"), "coincoin", test)

Mais en fait, la méthode re.sub est bien plus puissante qu'il n'y paraît... Au lieu de lui fournir une chaîne de remplacement, vous pouvez lui donner une fonction de rappel à manger. Cette dernière se chargera alors de traiter la partie reconnue par votre expression régulière et de renvoyer une nouvelle chaîne.

Nous allons maintenant traiter un exemple un peu plus complexe qui consiste à remplacer dans une chaîne donnée tout texte de la forme **TEXT** par <bold>TEXT</bold>. A première vue, on se demande un peu quel est le rapport avec ce que nous venons de voir ci-dessus.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re

# La variable suivante va nous permettre de
# savoir si nous sommes a l'intérieur du texte
# délimité par les balises initiales.
is_in = 0

# La regexp qui permet de détecter la balise
# ouvrante et fermante '**'
rech_re = re.compile(r'(?P<bolded>[*]{2})')

# Le bout de texte qui va nous servir de test
test = """Je désire mettre **Ma balise qui s'étend
éventuelement sur plusieurs lignes** en gras, par
exemple dans un wiki."""

# notre fonction de rappel
# Notez l'élégante façon de retourner la balise
# ouvrante ou fermante
def replace(match):
    global is_in
    is_in = not is_in
    return [r"</bold>", r"<bold>"][is_in]

print re.sub(rech_re, replace, test)

Aucune difficulté dans ce code si ce n'est cette ligne :

return [r"</bold>", r"<bold>"][is_in]

On s'attend à ce que replace renvoie une chaîne, et c'est en fait le cas :) Vous vous souvenez des listes ? Ceci devrait maintenant éclairer vos lanternes :

>>> l = ['un','deux']
>>> print l[0]
un
>>> print l[1]
deux

Le script lancé, on obtient

Je désire mettre <bold>Ma balise qui s'étend
éventuelement sur plusieurs lignes</bold> en gras, par
exemple dans un wiki.

Notez simplement que cette technique possède aussi ses limitations :

  • placez un seul ** dans votre texte initial par exemple et c'est le début de la cata.
  • elle emploie ici une variable globale de façon à enregistrer si l'on est ou non à l'intérieur des balises. On s'en dispensera facilement si cette fonction se déguise en méthode à l'intérieur d'une classe.

Dispatcher

Snippet 8 écrit le jeudi 27 décembre 2007 a 16 h 05 min dans les catégories : POO

Supposons que nous ayons une classe qui éxécute différentes méthodes suivant la valeur d'un attribut command :

if command == 'get':
    get()
elif command == 'put':
    put()
else:
    error()

que nous pourrions aussi coder de cette manière :

dispatch_table = {
'get': get,
'put': put,
}

# Command dispatch:
if dispatch_table.has_key(command):
    func = dispatch_table[command]
    func()
else:
    error()

Au lieu de ceci, Guido van Rossum, créteur de Python a inventé un pattern spécifique à son language :

class Dispatcher:

    def do_get(self): ...

    def do_put(self): ...

    def error(self): ...

    def dispatch(self, command):
        mname = 'do_' + command
        if hasattr(self, mname):
            method = getattr(self, mname)
            method()
        else:
            self.error()

Ce code est beaucoup plus élégant, le design pattern est utilisé à outrance dans les librairies standard de Python comme BaseHTTPServer, cmd, pydoc, repr, sgmllib, SimpleXMLRPCServer, urllib, distutils, etc.

Conserver la trace des instances

Snippet 7 écrit le jeudi 20 décembre 2007 a 15 h 40 min dans les catégories : POO

Python ne permet pas nativement de connaître le nombre d'instances d'une classe. Cependant, ceci est facilement réalisable.

On mémorise celles-ci avec un membre de classe statique que l'on nomme ici compt :

class Truc(object):
    compt = 0

    def __init__(self):
        Truc.compt += 1

    def __del__(self):
        Truc.compt -= 1

    @classmethod
    def combien(self):
        return Truc.compt


t1 = Truc()
t2 = Truc()

print Truc.combien()

del t1

print Truc.combien()

Explications:

  • Dès qu'un objet Truc est instancié, sa méthode __init__ (qui n'est pas vraiment un constructeur en Python) est appelée et le compteur de l'objet Truc est incrémenté de 1.
  • De même, lorsqu'on détruit un objet, c'est cette fois-ci la méthode __del__ qui est appellée et le compteur de l'objet Truc est décrémenté de 1.
  • La méthode combien() retourne simplement la valeur du compteur d'objets crées. Elle est déclarée comme méthode de classe à l'aide d'un décorateur pour qu'on puisse l'appeler sous la forme Truc.combien() [je trouve ça plus logique qu'une méthode classique que l'on apellerait à partir d'un objet, et en outre cela permet même de savoir s'il existe déjà des instances de cette classe].

Un fois le script lancé, on obtiendra évidemment ceci à la sortie

2
1

Différentes façon de lire un fichier

Snippet 6 écrit le dimanche 16 décembre 2007 a 14 h 03 min dans les catégories : Fichiers Répertoires

Ce petit programme recessence bon nombre de façons de traiter la lecture de fichiers en Python : du classique en passant par StingIO, les zip, etc.

# a look at file handling in Python

# set up a test string
str1 = """There is no ham in hamburger.
Neither apple nor pine are in pineapple.
Boxing rings are square.
Writers write, but fingers don't fing.
If a vegetarian eats vegetables, what does a humanitarian eat?
Overlook and oversee are opposites.
Slim chance and fat chance are the same.
A house can burn up as it burns down.
"""

# more to test file appending
str2 = """An alarm goes off by going on.
Fill in a form by filling it out.
"""

# let's create our test file by writing the test string to the working folder/directory with write()
# modifier "w" is for writing text, use "wb" for binary data like images
fout = open("English101.txt", "w")
fout.write(str1)
fout.close()

# read back the entire test string as a string with read()
# "r" is for reading text, use "rb" for binary data like images
fin = open("English101.txt", "r")
str3 = fin.read()
fin.close()
print "Contents of file English101.txt:"
print str3

# a similar read using try/except error handling
# tested this out by deliberately changing the filename
filename = "English102.txt"
try:
    fin = open(filename, "r")
    str3 = fin.read()
    fin.close()
    print "Contents of file %s:" % filename
    print str3
except IOError:
    print "File %s does not exist!" % filename

print

# append more text to an existing file with modifier "a"
fout = open("English101.txt", "a")
fout.write(str2)
fout.close()

# read the appended text file as a list of lines with readlines()
fin = open("English101.txt", "r")
lineList = fin.readlines()
fin.close()
print "Contents of appended file (first option):"
for line in lineList:
    print line,

print

# a short-form to do this, uses readlines() internally
# the comma at the end of print takes care of the extra newline character
# note: Python does clean up and closes the file for you
print "Contents of appended file (second option):"
for line in open("English101.txt", "r"):
    print line,

print

# similar to above, but creating a list with list comprehension
line_list = [line for line in open("English101.txt", "r")]
print "A list of the text lines:"
print line_list   # test

print

# read just one line of text at a time
print "The first two lines:"
fin = open("English101.txt", "r")
print "Line 1 =", fin.readline(),
print "Line 2 =", fin.readline()   # etc.
fin.close()

# show just the last line of text
fin = open("English101.txt", "r")
lineList = fin.readlines()
fin.close()
print "Last line =", lineList[-1],
print "Total lines =", len(lineList)

# the whole thing can be simplified (more cryptic though)
lastLine = file("English101.txt", "r").readlines()[-1]
print "Last line =", lastLine

# do some random access of the file
fin = open("English101.txt", "r")
# seek index is zero based, so the 10th character would be position 9
fin.seek(9)
print "From character 10 to end of line =", fin.readline()
print "End if this line is at character =", fin.tell()
print
num = 16
pos = 80
print "Read %d characters starting at position %d:" % (num, pos)
fin.seek(pos)
print fin.read(num)
fin.close()

print

# read a particular line, lineNumber is zero based
import linecache
lineNumber = 5
partLine = linecache.getline("English101.txt", lineNumber)
print "Line %d = %s" % (lineNumber, partLine)

print

# processing the lines as you read them in and forming a list
# using list comprehension
list2 = [line.replace(".", "!") for line in open("English101.txt", "r")]

# display the result
print "Processing the lines as you read them in ..."
print "Each period has been replaced with an exclamation mark:"
for line in list2:
    print line,

print; print

# print to a file (a different option of write)
fout = open( "test1.txt", "w" )
print >>fout, "I love Monte Python!"
fout.close()

# a file exists if you can open and close it
def exists(filename):
    try:
        f = open(filename)
        f.close()
        return True
    except:
        return False

# what does file object fin look like?
filename = 'test1.txt'
if exists(filename):
    fin = open(filename)
    print "file object =", fin
    print "file content =", fin.read()
    fin.close()
else:
    print "File %s does not exist!" % filename

print

# for large text files you can write and read a zipped file (PKZIP format)
# notice that the syntax is mildly different from normal file read/write
import zipfile
zfilename = "English101.zip"
zout = zipfile.ZipFile(zfilename, "w")
zout.writestr(zfilename, str1 + str2)
zout.close()
# read the zipped file back in
zin = zipfile.ZipFile(zfilename, "r")
strz = zin.read(zfilename)
zin.close()
print "Testing the contents of %s:" % zfilename
print strz

print

# read a binary image file, pick something you have ...
# (also shows exception handling)
filename = "Moo.jpg"
try:
    fin = open(filename, "rb")
    data = fin.read()
    fin.close()
    print "This is a hex-dumb of %s:" % filename
    for c in data:
        print "%02X" % ord(c),
    print
except IOError:
    print "Binary File %s not found" % filename
    #raise SystemExit  # optional exit

print

# below is a typical Python dictionary object of roman numerals
romanD1 = {'I':1,'II':2,'III':3,'IV':4,'V':5,'VI':6,'VII':7,'VIII':8,'IX':9,'X':10}

# to save a Python object like a dictionary to a file
# and load it back intact you have to use the pickle module
import pickle
print "The original dictionary:"
print romanD1
file = open("roman1.dat", "w")
pickle.dump(romanD1, file)
file.close()
# now load the dictionay object back from the file ...
file = open("roman1.dat", "r")
romanD2 = pickle.load(file)
file.close()
print "Dictionary after pickle.dump() and pickle.load():"
print romanD2

print

# module StringIO allows you to treat a data stream like a file
# if you do a lot of processing, memory streams are much faster then file streams
# (StringIO.StringIO is a class that can be inherited in a class of your own)
print "You can stream text/data to memory ..."
import StringIO
stream1 = StringIO.StringIO(str2)  # use the string str2 here, or read one in from a file
print str2

# show the memory where the object is located
print stream1

print

print "... and use stream like a file:"
print stream1.readline()

# reset the stream to zero (beginning) and read all lines
stream1.seek(0)
list1 = stream1.readlines()
print "All lines from beginning:"
for item in list1:
    print item,
print
# reset the stream to position 9 and read the next 20 characters
stream1.seek(9)
print "Read 20 characters starting at position 9:"
print stream1.read(20)

# finally close the stream
stream1.close()

Comment pluger son application

Snippet 5 écrit le dimanche 16 décembre 2007 a 02 h 17 min dans les catégories : Plugins

Un système de plugins avec Python

Ayant été confronté à la délicate question Comment fait-on pour étendre dynamiquement les capacités d'une application ?, je vais ici tenter de répondre à la question de façon simple.

Ce billet est une traduction de l'article d'Armin situé sur cette page : Article en Anglais lequel j'ai modifié certaines parties.

Depuis sa version 2.2, Python possède ce que l'on nomme les New Style Classes. Parmi celles-ci la classe object qui va nous servir de modèle, permet diverses manipulations, parfois très complexes, à leur création.

Pourtant, certaines d'entre elles sont très utiles pour la création d'un système de plugins.

La partie la plus difficile est le chargement de ceux-ci. Il existe plusieurs méthodes, mais je n'en étudierai ici qu'une seule : celle qui utilise un répertoire secondaire nommé plugins/ que l'on va ajouter au celèbre PYTHONPATH (sys.path).

Chaque plugin doit hériter d'une classe spéciale qui va tenir un registre de ses sous-classes.

class Plugin(object):
    pass

Maintenant, nous avons une structure de plugin. Pardon ? Mais qui y-a-t-il de si spécial à propos de cette classe ?

Rien si ce n'est que cette classe hérite de object (une des New Style Classes, vous-vous rapellez ?). Si vous n'héritez pas de celle-ci, en écrivant juste :

class Plugin():
    pass

et bien Python interprêtera ceci comme suit, en héritant de types.ClassType :

class Plugin(types.ClassType):
    pass

L'intérêt, c'est que chaque classe dérivant de object tient un registre de ses descendants:

>>> class MyPlugin(Plugin): pass
...
>>> class OtherPlugin(Plugin): pass
...
>>> Plugin.__subclasses__()
[<class '__main__.MyPlugin'>, <class '__main__.OtherPlugin'>]

Intéressant pour implémenter le concept de plugins, à partir du moment où ceux- ci sont chargés en mémoire, évidemment. Voyons un peu comment faire. Nous avons deux possibilités naturelles qui nous viennent à l'esprit :

  • utiliser un fichier de configuration dans lequel nous entrons chaque plugin;
  • faire un scan du répertoire au moment du lancement de notre application, et importer tout ce qui se termine par ".py" ou tout dossier contenant un fichier "__init__.py";

Nous allons ici utiliser la première possibilité.

import sys
import os

class Plugin(object):
    pass

def load_plugins(plugins):
    for plugin in plugins:
        __import__(plugin, None, None, [''])

def init_plugin_system(cfg):
    if not cfg['plugin_path'] in sys.path:
        sys.path.insert(0, cfg['plugin_path'])
    load_plugins(cfg['plugins'])

def find_plugins():
    return Plugin.__subclasses__()

Sauvegardons ce fichier sous le nom plugins.py. De façon à le rendre utilisable, on va maintenant utiliser les méthodes init_plugin_system et find_plugins :

>>> from plugins import init_plugin_system, find_plugins
>>> init_plugin_system({'plugin_path': 'plugins/', 'plugins': ['testplugin']})
>>> find_plugins()
[<class 'testplugin.SpecialPlugin'>, <class 'testplugin.AnotherPlugin'>]

testplugin.py était simplement un simple fichier test contenant uniquement deux classes héritant de plugins.Plugin.

Nos plugins auraient aussi besoin de nous renseigner un minimum sur leur capacités. Pour cela, on peut utiliser des Interfaces, le duck typing, l' héritage ou bien une liste fournissant les capacités. Nous allons traiter ici cette dernière possibilité, elle se veut basique mais fonctionnelle :

class Plugin(object):

    capabilities = []

    def __repr__(self):
        return '<%s %r>' % (self.__class__.__name__, self.capabilities)

    def get_plugins_by_capability(capability):
        result = []
        for plugin in Plugin.__subclasses__():
            if capability in plugin.capabilities:
                result.append(plugin)
    return result

Maintenant, un plugin va définir une, voire plusieurs capacités. Leurs noms doit par conséquent être expliqué quelque part, ainsi que celui des méthodes qu'il va faire partager.

from plugins import Plugin

class ExamplePlugin(Plugin):
    capabilities = ['foo']

    def do_foo(self, name):
        return 'Hello %s!' % name

Sauvegardez ceci sous le nom testplugin.py; vous devriez mainteant pouvoir le charger :

>>> from plugins import init_plugin_system, get_plugins_by_capability
>>> init_plugin_system({'plugin_path': '.', 'plugins': ['testplugin']})
>>> for plugin in get_plugins_by_capability('foo'):
...     plg = plugin()
...     print plg.do_foo('Huhu')
...
Hello Huhu!

Ce petit exemple montre aussitôt ses limites. Vous avez besoin de créer une nouvelle instance après chaque itération, parce que la méthode get_plugins_by_capability renvoie une classe, pas une instance de celle-ci.

On peut cependant contourner le problème facilement :

_instances = {}

def get_plugins_by_capability(capability):
    result = []
    for plugin in Plugin.__subclasses__():
        if capability in plugin.capabilities:
            if not plugin in _instances:
                _instances[plugin] = plugin()
            result.append(_instances[plugin])
    return result

Maintenant, get_plugins_by_capability renvoie une seule instance, plus une classe.

>> from plugins import init_plugin_system, get_plugins_by_capability
>>> init_plugin_system({'plugin_path': '.', 'plugins': ['testplugin']})
>>> for plugin in get_plugins_by_capability('foo'):
...     print plugin.do_foo('Huhu')
...
Hello Huhu!
>>> from plugins import _instances
>>> _instances
{<class 'testplugin.ExamplePlugin'>: <ExamplePlugin ['foo']>}

Voilà, vous disposez maintenant de votre propre système de plugins en 38 lignes de code. Bien sûr il est très basique et on peut l'améliorer à souhait, mais vous aurez au moins l'idée sous-jacente.

Méthodes dynamiques

Snippet 4 écrit le samedi 15 décembre 2007 a 17 h 20 min dans les catégories : Dynamisme

Une question a été posée sur irc

je fais varier le nom d'une variable, exemple : a="pouet1" puis
self.a.methodequitue() et j'aimerais que python comprenne :
self.pouet1.methodquitue() , ça se définit ou ça ?

Réponse :

getattr(self,a).methodequitue()

ConfigParser

Snippet 3 écrit le vendredi 14 décembre 2007 a 11 h 29 min dans les catégories : Configuration Parsing

On a très souvent besoin dans une application d'un fichier de configuration, ne serait-ce que pour donner par défaut à certaines variables, et enregistrer les changements effectués par l'utilisateur le cas échéant.

C'est le module ConfigParser qui se charge de cette tâche [Il en existe d' autres, bien entendu].

Pour importer ce module, on écrira :

import ConfigParser

Un fichier de configuration est composé de sections. Dans chacune, on peut associer un nom à :

  • une chaîne
  • un entier
  • un décimal
  • booléen

Exemple d'un fichier simple maConfig.conf:

[MonBlog]
BlogHomePage : http://moi.free.fr/MonSite/
Host : ftpperso.free.fr
User : moi
Password : tr8zb9
RemoteDir : MonSite

Ce fichier est composé d'une section MonBlog encadrée par les balises [ et ] .

Attention!

Notez un point très important (je me suis fais prendre au piège) : les valeurs sont telles quelles, pas besoin de les encadrer par des guillemets, même si se sont des chaînes.

Lire le fichier de config

Une fois importé le module, on créé une instance de ConfigParser et il suffit alors de lire le fichier par la méthode 'read'. Ensuite, on capture les valeurs de la config par la méthode 'get', comme ceci:

config = ConfigParser.ConfigParser()
config.read(os.path.join(configdir,'maConfig.conf'))

away = config.get('MonBlog', 'BlogHomePage')
host = config.get('MonBlog', 'Host')
user = config.get('MonBlog', 'User')
passw = config.get('MonBlog', 'Password')
remote = config.get('MonBlog', 'RemoteDir')

A suivre.

Un correcteur syntaxique en 25 lignes de code

Snippet 2 écrit le jeudi 13 décembre 2007 a 23 h 59 min dans les catégories : Parsing Texte

Le script n'est évidemment pas le mien, mais tiré d'un article de Peter Norvig [Directeur de Recherche chez Google] .

import re, collections

def words(text): return re.findall('[a-z]+', text.lower())

def train(features):
    model = collections.defaultdict(lambda: 1)
    for f in features:
        model[f] += 1
    return model

NWORDS = train(words(file('big.txt').read()))

alphabet = 'abcdefghijklmnopqrstuvwxyz'

def edits1(word):
    n = len(word)
    return set([word[0:i]+word[i+1:] for i in range(n)] +                     # deletion
               [word[0:i]+word[i+1]+word[i]+word[i+2:] for i in range(n-1)] + # transposition
               [word[0:i]+c+word[i+1:] for i in range(n) for c in alphabet] + # alteration
               [word[0:i]+c+word[i:] for i in range(n+1) for c in alphabet])  # insertion

def known_edits2(word):
    return set(e2 for e1 in edits1(word) for e2 in edits1(e1) if e2 in NWORDS)

def known(words): return set(w for w in words if w in NWORDS)

def correct(word):
    candidates = known([word]) or known(edits1(word)) or known_edits2(word) or [word]
return max(candidates, key=lambda w: NWORDS[w])

En action !

>>> correct('speling')
'spelling'
>>> correct('korrecter')
'corrector'

Hallucinant non ?

Pour davantage de théorie, notamment sur les probas, je vous laisse regarder l' article en question qui se trouve être très intéressant. Du reste, l'algo n'est pas implémenté qu'en Python, mais aussi avec beaucoup d'autres languages...mais c'est Python qui remporte le nombre de lignes minimal :)

Fichier ou répertoire

Snippet 1 écrit le jeudi 13 décembre 2007 a 23 h 09 min dans les catégories : Fichiers Répertoires

La méthode isfile de os.path est utilisée ici. Sa ginature est la suivante :

os.path.isfile(path)

Elle renvoie True si path désigne un fichier existant.

Attention à une chose : path doit être évidemment le nom complet (sinon, comment voulez-vous que Python le devine à votre place, hein ?), çàd avec le chemin.