Site original : Sam & Max: Python, Django, Git et du cul
Se faire plaisir et faire plaisir aux autres n’est pas incompatible.
Par exemple, quand Sublime Text est sorti j’ai pris un an avant d’acheter la licence. Là, j’ai sublime text 3 depuis à peu près la même période sur mon ordi, et je vais me payer la licence.
Pour moi, c’est une forme de don. En effet, l’auteur permet d’utiliser 100% de son logiciel pour toujours, et sans rien raquer. La seule chose, c’est que toutes les X sauvegardes, une pop up apparaît pour vous demander si vous voulez l’acheter. Vous pouvez ne jamais le faire. Beaucoup ne le font jamais.
Aujourd’hui, je vais faire sauter la pop up, et je remercie l’auteur pour me fournir un joujou que j’adore.
N’oubliez pas, si vous utilisez un IDE commercial (PyCharm, Komodo, etc), acheter la licence vous apporte des choses à vous, mais aussi, est un bon moyen de témoigner votre reconnaissance aux auteurs. Après tout, vous gagnez votre vie en partie grace à eux.
Si votre éditeur est gratuit, voire libre (je pense aux trolls de VI et Emacs qui frétillent dans les commentaires), pensez à faire un don à ces projets. Le fait qu’ils soient bien établis ne veut pas dire qu’il n’en ont pas besoin, bien au contraire.
Sublime n’est pas donné, et je paie de ma poche 70$, avec plaisir néanmoins, car cet outil les vaut largement.
Bref, la morale de ce mois, c’est que les donations ne sont pas uniquement pour les groupes à but non lucratif. Vous pouvez donnez à des boîtes, des administrations, des clodos dans la rue. Vous pouvez racheter un livre que vous avez déjà lu, offrir un morceau que vous avez déjà écouté. Le don n’a pas de forme propre, la seule condition étant l’intention.
En Python, il n’y a pas d’attributs privés au sens propre, uniquement une convention disant que tout ce qu’on préfixe d’un underscore ne fait pas partie de l’API publique.
Cette convention est tellement bien établie que les outils de génération de documentation et de complétion de code la prennent souvent en compte.
Ca fait partie de la philosophie du langage : tout est ouvert. Guido parle même d’open kimono, le petit coquin.
Pourtant il existe une fonctionnalité qui semble rendre un attribut privé, qui est d’utiliser un préfix de DEUX underscores :
class Yo(object): def __init__(self): self.__bitch = True >>> Yo().__bitch --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-5-225402ba2794> in <module>() ----> 1 Yo().__bitch AttributeError: 'Yo' object has no attribute '__bitch' |
En fait, l’attribut n’a pas été rendu privé, mais son mécanisme de résolution de nom a été changé :
>>> Yo()._Yo__bitch True |
C’est ce qu’on appelle le name mangling, et cette obscure fonctionnalité a été utilisée pour s’assurer qu’un attribut ne sera pas écrasé par erreur par une classe enfant.
L’attribut n’est pas privé, il est juste accessible autrement, c’est tout.
Après avoir bien galéré à créer un compteur à la main avec un dico, vous avez découvert les joies des méthodes dict.get
et dict.setdefault
. Puis évidemment quelqu’un vous a pointé vers collections.defaultdict
, et enfin, vous avez fini par découvrir collections.Counter
. Joie.
Le parcours est à peu près toujours le même quand on veut grouper ou compter des valeurs en Python.
Malgré cela, je vois encore des gens qui sous utilisent ces collections. Par exemple, Counter
peut compter automatiquement :
>>> from collections import Counter >>> Counter('jfsqmfjdklmqfjsdqklmfjdsqhfdqsjkhfdshjkl') Counter({'j': 6, 'f': 6, 'q': 5, 's': 5, 'd': 5, 'k': 4, 'l': 3, 'm': 3, 'h': 3}) |
Mais ce que ne réalisent pas beaucoup de développeurs, c’est que cet objet accepte n’importe quel itérable en paramètre. Nous sommes en Python, et rededjiou, je me tue à répéter que l’itération est la philosophie centrale du langage.
Donc le compteur peut prendre une expression génératrice en paramètre.
Par exemple, si vous voulez compter un truc un peu plus complexe que des éléments, comme mettons, le ratio de lignes commentées dans un fichier, vous n’avez pas besoin de faire ça :
count = Counter() for line in open('/etc/fstab', encoding='ascii'): count[line.startswith('#')] += 1 # out : Counter({True: 10, False: 3}) |
Ceci marchera parfaitement :
count = Counter(line.startswith('#') for line in open('/etc/fstab', encoding='ascii')) # out : Counter({True: 10, False: 3}) |
Vous pouvez également utiliser des générateurs plus complexes. Combien de fichiers par types d’extensions ?
import os import pathlib def get_extensions(path): for dirpath, dirnames, files in os.walk(path): for name in files: ext = pathlib.Path(name).suffix if ext: # on ignore les fichiers sans extension yield ext Counter(get_extensions('/etc')).most_common(9) # Out : # ('.conf', 632), # ('.0', 348), # ('.gz', 323), # ('.jhansonxi', 207), # ('.pem', 177), # ('.load', 127), # ('.ttb', 86), # ('.ktb', 80), # ('.kti', 55)] |
Notez que le Counter
peut faire plus que compter. Ici il nous donne les 9 plus grandes valeurs du classement, mais en prime, il peut aussi nous faire des opérations ensemblistes :
>>> c = Counter("aabbbbbbbbbbbbcccc") >>> c & Counter('aaaaaaaaaaaaaaabbcddddddd') # valeurs min Counter({'b': 2, 'a': 2, 'c': 1}) >>> c | Counter('aaaaaaaaaaaaaaabbcddddddd') # valeurs max Counter({'a': 15, 'b': 12, 'd': 7, 'c': 4}) |
Le compteur fournit par Python est donc naturellement très, très puissant.
Une autre chose qui est rarement faite : sous-classer ces types.
Par exemple, si vous avez souvent des opérations où il faut grouper des valeurs :
from collections import defaultdict class Grouper(defaultdict): def __init__(self, iterable): super(Grouper, self).__init__(list) self.update(iterable) def update(self, iterable): try: iterable = iterable.items() except AttributeError: iterable = iterable for k, v in iterable: self[k].append(v) |
On prend un default dict, on lui dit qu’un update ajoute les éléments à la liste en valeur plutôt que de la remplacer, et zou, vous avez un dictionnaire qui va grouper toutes les valeurs automatiquement.
Liste des fichiers par extensions ? Fastoche !
def get_extensions(path): for dirpath, dirnames, files in os.walk(path): for name in files: ext = pathlib.Path(name).suffix if ext: yield ext, name # on rajoute le name ici >>>files = Grouper(get_extensions('/etc')) >>> files['.tti'] ['en-na-ascii.tti', 'numbers-french.tti', 'devanagari.tti', 'letters-cyrillic.tti', 'punctuation-basic.tti', 'malayalam.tti', 'ascii-basic.tti', 'spaces.tti', 'letters-latin.tti', 'letters-latin-dot8.tti', 'en-chess.tti', 'numbers-dot8.tti', 'punctuation-tibetan.tti', 'boxes.tti', 'gujarati.tti', 'numbers-nemeth.tti', 'punctuation-alternate.tti', 'common.tti', 'blocks.tti', 'gurmukhi.tti', 'kannada.tti', 'telugu.tti', 'tamil.tti', 'numbers-dot6.tti', 'de-chess.tti', 'control-latin.tti', 'letters-tibetan.tti', 'oriya.tti', 'bengali.tti'] |
Bref, compter et grouper sont des opérations si communes : ne vous faites par chier à refaire tout ça à la main.
Dans beaucoup de langages populaires, and
et or
sont écrits &&
et ||
. Ces symboles existent en Python, mais ils sont là pour appliquer des opérations binaires :
>>> bin(0b010 & 0b111) '0b10' >>> bin(0b010 | 0b111) '0b111' |
Ce n’est néanmoins pas la seule bizarrerie de Python dans le domaine.
Les opérateurs and
et or
court-circuitent les conditions dès que possible, c’est à dire qu’ils retournent la valeur au plus tôt, même si ça signifie ne pas exécuter tout le code.
Par exemple, prenons deux fonctions:
def vrai(): print('Yeah !') return True def faux(): print('Errrr...') return False |
Si je fais un or
dessus, ça va me retourner True
, et afficher deux messages :
>>> faux() or vrai() Errrr... Yeah ! True |
Mais si j’INVERSE les deux fonctions, alors je n’aurais qu’un seul message qui va s’afficher :
>>> vrai() or faux() Yeah ! True |
La raison est que or
sait qu’il peut retourner True
dès qu’il obtient au moins une valeur True
. vrai()
retourne True
, donc or
sait que tout la condition sera forcément vraie, et il n’exécute pas le code du reste de la condition. Ainsi, faux()
n’est jamais appelée.
and
fait pareil :
>>> vrai() and faux() Yeah ! Errrr... False |
Et à l’envers :
>>> faux() and vrai() Errrr... False |
Car dans le second cas, and
sait qu’il doit avoir toutes les valeurs à True
pour renvoyer True
. Comme il reçoit False
dès le premier test, il ne va pas plus loin, et vrai()
n’est jamais appelée.
Le but de cette fonctionnalité est d’autoriser le développeur à mettre les fonctions qui sont les plus gourmandes en ressource tout à droite de la condition, ainsi elle ne seront pas toujours appelées, ce qui améliore les perfs.
Si vous avez besoin que les fonctions soient toujours appelées car elles ont des effets de bord (c’est mal, boooouh !), il suffit de mettre leurs résultats dans des variables :
>>> a = vrai() Yeah ! >>> b = faux() Errrr... >>> b and a False |
La plupart des opérateurs utilisés pour faire des tests retournent des booléans :
>>> 1 > 2 False >>> "a" in "chat" True |
Mais and
et or
ne retournent pas des booléans. Dès qu’ils sont certains du résultats de la condition, ils retournent la valeurs qu’ils ont sous la main.
Cela est du au fait qu’en Python, tout a une valeur True
ou False
dans un contexte booléen. Pour faire simple, n’importe quel objet mis dans une condition vaut soit True
, soit False
.
Par exemple, une liste vide vaut False
dans une condition, une liste non vide vaut True
:
>>> couleurs = [] >>> if couleurs: ...: print("J'ai une couleur !") >>> couleurs.append('rouge') >>> if couleurs: print("J'ai une couleur !") J'ai une couleur ! |
On peut le vérifier facilement :
>>> bool([]) False >>> bool(['rouge']) True |
Il est facile de se souvenir de ce qui est faux ou vrai en Python. False
, None
, 0
et tout ce qui est vide est faux :
>>> for x in (False, None, 0, "", [], set(), {}, ()): ...: print(type(x), bool(x)) ...: <class 'bool'>, False <class 'NoneType'>, False <class 'int'>, False <class 'str'>, False <class 'list'>, False <class 'set'>, False <class 'dict'>, False <class 'tuple'>, False |
Tout le reste est vrai :
>>> for x in (Ellipsis, True, 432, "foo", ["bar"], set("ba"), {"pa": "pa"}, ("doh",), lambda : None, len): print(type(x), bool(x)) <class 'ellipsis'> True <class 'bool'> True <class 'int'> True <class 'str'> True <class 'list'> True <class 'set'> True <class 'dict'> True <class 'tuple'> True <class 'function'> True <class 'builtin_function_or_method'> True |
Du coup, and
et or
vont vérifier la valeur de chaque objet de la condition, et retourner le premier à partir duquel ils sont certains du résultat de la condition entière.
Par exemple, si je fais :
>>> True and True and False and False False |
and
n’est certain que la condition est fausse qu’au moment où on attend le premier False
. C’est donc ce False
qu’il retourne.
Cela est beaucoup plus clair quand on le fait avec des objets plus complexes :
>>> "a" and 1 and [] and {} [] |
Puisque :
>>> bool('a') True >>> bool(1) True >>> bool([]) False >>> bool({}) False |
and
n’est certain du résultat de la condition qu’en arrivant sur []
, qu’il retourne.
Si tous les éléments sont vrais, il va donc prendre le dernier :
>>> "a" and 1 and True and [1, 2, 3] [1, 2, 3] |
C’est la même chose pour or
:
>>> "" or None or False or 0 0 |
Là, or
ne peut pas savoir si la condition est fausse avant d’arriver au tout dernier élément, qu’il retourne.
Mais si je glisse un truc vrai dans le lot :
>>> "" or {1: 2} or False or 0 {1: 2} |
Comme il n’a besoin que d’un élément vrai pour que toute la condition soit vraie, dès qu’il en rencontre un, il le retourne.
Le “ou” exclusif, opération qui retourne vrai seulement si un élément est vrai mais pas l’autre, n’existe pas sous la forme d’un opérateur en Python. Évidement on peut l’émuler manuellement :
def xor(a, b): return (a and not b) or (not a and b) |
Mais une astuce de sioux permet un résultat plus court avec une syntaxe un poil plus proche des langages qui possèdent cet opérateur :
bool(a) ^ bool(b) |
Exemple :
>>> bool(['pomme']) ^ bool([]) True >>> bool(['pomme']) ^ bool(['banane']) False |
^ est en effet l’opérateur XOR pour les opérations binaires. La partie marrante, c’est qu’en Python :
>>> True == 1 True >>> False == 0 True |
Et comme :
>>> 1 ^ 1 0 >>> 1 ^ 0 1 |
Alors:
>>> True ^ True False >>> True ^ False True |
On obtient le résultat voulu.
Oui, c’est un peu tordu, je vous l’accorde.
Une suite de valeurs ne veut rien dire en soi, et même le sacro-saint binaire supposé être le socle de toute l’informatique n’a aucun sens si on ne connaît pas le format utilisé pour ce qu’il doit représenter.
Toujours la même opposition entre données et représentation.
Par exemple, le binaire peut représenter un chiffre en base 2 ou un texte encodé.
Pour autant, cela ne veut pas dire qu’il n’existe pas des formats prépondérant. En informatique, beaucoup de données binaires sont organisées pour correspondre aux structures de données du langage C, ces dernières étant une implémentation du standard IEEE 754 (en effet les strings sont des arrays d’int en C, donc le texte et les nombres sont des suites de chiffres).
Par exemple, si vous créez un array numpy contenant des nombres de 0 à 1000 stockés en int32 et sauvegardez son contenu dans un fichier :
>>> import numpy >>> numpy.arange(0, 1000, dtype=np.int32).tofile('/tmp/data') |
Le fichier va ici contenir une suite de 1 et de 0 représentant 1000 entiers, chacun comme un paquet de 4 octets organisés selon la sémantique que comprend le langage C.
Pour avoir une idée de l’organisation du contenu, on peut prendre un éditeur hexa qui vous affichera :
0000 0000 0100 0000 0200 0000 0300 0000 0400 0000 0500 0000 0600 0000 0700 0000 0800 0000 0900 0000 0a00 0000 0b00 0000 0c00 0000 0d00 0000 0e00 0000 0f00 0000 1000 0000 1100 0000 1200 0000 1300 0000
Ça se lit ainsi :
0000 0000 => 0 0100 0000 => 1 0200 0000 => 2 0300 0000 => 3 0400 0000 => 4 0500 0000 => 5 0600 0000 => 6 0700 0000 => 7 0800 0000 => 8 0900 0000 => 9 0a00 0000 => 10 0b00 0000 => 11 0c00 0000 => 12 0d00 0000 => 13 0e00 0000 => 14 0f00 0000 => 15 1000 0000 => 16 1100 0000 => 17 1200 0000 => 18 1300 0000 => 19 ...
Numpy étant codé en C, cela semble plutôt logique qu’il dump tout ça dans ce format.
Mais c’est une représentation tellement courante que de nombreux formats standards l’utilisent. Par exemple, les archives et les images stockent souvent leurs données ainsi.
Prenez le format d’image PNG, la RFC indique que la taille de l’image est stockée dans le fichier sous la forme de deux entiers représentés par 4 octets chacun, ordonnés en big-endian, entre l’octet 16 et l’octet 24.
On peut donc récupérer ces informations en lisant son fichier image :
with open('image.png', 'rb') as f: taille = f.read(24)[16:24] |
Le problème étant : comment lire cette info ? C’est un blob binaire qui ne veut rien dire pour Python :
print(taille) b'\x00\x00\x07\x80\x00\x00\x048' |
Le module struct est fait pour ça, on lui passe une donnée au format structure C, et il la convertit en type Python. Cela marche ansi, pardon, ainsi :
struct.unpack('motif_du_format_a_convertir', donnee) |
Le format à convertir est une chaîne de caractères qui contient des symboles décrivant la structure de la donnée qu’on souhaite récupérer. Little-endian ou big-endian ? String, Int, Bool ?
Pour la taille de la photo, on sait qu’il y a deux entiers, non signés (une taille ne va pas être négative), en big-endian. D’après la doc de struct
, on peut lui désigner un entier non signé avec ‘I’, et il faut les qualifier avec ‘>’ pour l’ordre big-endian. Du coup:
taille = struct.unpack('>II', taille) print(taille) (1920, 1080) |
Il se trouve que mon image de test est un screenshot et que mon écran a une résolution de 1920×1080 :)
On peut faire l’opération inverse avec struct.pack
, et bien entendu manipuler des formats plus complexes : il suffit de changer le motif qui représente le format à convertir.