Sam & Max: Python, Django, Git et du cul » lambda http://sametmax.com Deux développeurs en vadrouille qui se sortent les doigts du code Wed, 05 Feb 2014 14:20:37 +0000 en hourly 1 http://wordpress.org/?v=3.3.1 map(), filter() et reduce () ? http://sametmax.com/map-filter-et-reduce/ http://sametmax.com/map-filter-et-reduce/#comments Thu, 14 Nov 2013 09:44:58 +0000 Sam http://sametmax.com/?p=7791 map(), filter() et reduce() sont des fonctions de traitement d'itérables typiques de la programmation fonctionnelle, qui ont été marquées comme à retirer des builtins pour Python 3. Finalement, seule reduce() sera déplacée dans le module functools pour Python 3.]]> map(), filter() et reduce() sont des fonctions de traitement d’itérables typiques de la programmation fonctionnelle, qui ont été marquées comme à retirer des builtins pour Python 3. Finalement, seule reduce() sera déplacée dans le module functools pour Python 3.

Les opérations que font ces fonctions sont typiquement quelque chose que l’ont peut faire sans elles, et nous allons les passer en revue pour voir dans quels cas elles sont pertinentes, dans quel cas une alternative est meilleure. L’alternative étant, dans 90% des cas, une liste en intention.

filter()

filter() prend une fonction en paramètre, souvent une lambda, comme ses deux soeurs puisqu’on est dans le paradigme fonctionnel. Elle doit renvoyer True si on garde un élément, et False sinon.

L’usage typique est celui-ci :

ages = range(30)
majeurs = filter(lambda x: x > 18, ages)
print(majeurs)
## [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29]

Typiquement, ce code peut être remplacé par une liste en intention dans le pur style Python :

majeurs = [a for a in ages if a > 18]

Le code est plus court, et fait usage d’une des fonctionalités les plus importantes du langage. On peut en plus ajouter une transformation à a facilement si on le désire, au lieu de devoir coller un map() derrière.

filter() est vraiment la moins utile des 3, et sera une question de style, surtout pour les nostalgiques de Lisp. Je répète souvent que quand on code avec un autre langage, on doit essayer de se tenir au style du nouveau et pas faire un mix avec ses anciennes habitudes. Quand je code en Java, je fais des getter et setter, même si j’ai horreur de ça.

Il existe quand même UNE astuce pour laquelle filter est utile : garder uniquement les éléments vrais d’une liste.

Typiquement :

l = [1, 0, None, [], True]
print filter(bool, l)
[1, True]

Ca marche si la fonction pre-existe et qu’on a pas à faire une lambda, mais c’est vraiment le seul usage potable. Un peu plus court qu’une liste en intention.

map()

Si filter() est l’équivalent de la partie de droite d’une liste en intention, map() est l’équivalent de la partie de gauche. La fonction passée retourne un résultat qui permet de transformer la liste.

Typiquement :

memes = ["It's over 9000 !", "All your base are belong to us."]
print(map(unicode.upper, memes))

Ce qui peut se traduire par :

print(s.upper() for s in memes)

map() est un peu plus utile, dans le sens où sa syntaxe peut être plus concise dans certains cas, comme le casting de types. Par exemple si je reçois une heure sous forme de string :

h, m, s = map(int, '8:19:22'.split(':'))

sera plus court et plus concis, et plus clair que :

h, m, s = (int(i) for i in '8:19:22'.split(':'))

Mais bon, la différence n’est pas non plus incroyable au point d’en faire une fonctionnalitéé clé. Je l’utilise de temps à autre par soucis de brièveté, mais vraiment c’est tout.

reduce()

reduce() est plus tordu. La fonction doit prendre deux paramètres en entrée, et retourner une valeur. Au premier appel, les deux premiers éléments de l’itérable sont passés en paramètres. Ensuite, le résultat de cet appel et l’élément suivant sont passés en paramètre, et ainsi de suite.

Vous n’avez rien pigé ? C’est normal. reduce() est parfaitement cryptique. Voici ce que ça donne en pratique :

def afficher(a, b):
    print("Entrée :", a, b)
    print("Sortie :", a + b)
    return a + b
 
res = reduce(afficher, range(10))
print("Résultat final", res)
 
## Entrée : 0 1
## Sortie : 1
## Entrée : 1 2
## Sortie : 3
## Entrée : 3 3
## Sortie : 6
## Entrée : 6 4
## Sortie : 10
## Entrée : 10 5
## Sortie : 15
## Entrée : 15 6
## Sortie : 21
## Entrée : 21 7
## Sortie : 28
## Entrée : 28 8
## Sortie : 36
## Entrée : 36 9
## Sortie : 45
## Résultat final 45

Vous allez me dire, à quoi ça sert ? Et bien par exemple à appliquer des opérateurs commutatifs, ici nous l’avons fait avec +, nous avons fait la somme de tous les éléments retournés par range(10). La preuve :

print(sum(range(10)))
## 45

Il n’y a pas, en Python, de fonction équivalent à sum() pour la multiplication. Donc on ferait :

print(reduce(lambda a, b: a * b, range(1, 11)))
## 3628800

Ce qui multiplie tous les éléments entre eux. Comme l’ordre dans lequel les éléments sont multipliés n’a pas d’important (d’où le ‘commutatif’), ça fonctionne.

reduce() peut prendre un troisième paramètre, initial, qui sera la valeur passée en premier au premier appel de la fonction. Cela permet de travailler sur des calculs en cascade qui ne fonctionneraient sinon pas. Revenons à notre exemple de temps :

temps = map(int, '8:19:22'.split(':'))
print(reduce(lambda a, b: a * 60 + b, temps, 0))
## 29962

Ce qui peut se traduire par :

h, m, s = map(int, '8:19:22'.split(':'))
print(h * 3600 + m * 60 + s)
## 29962

Bien sûr, cette conversion ne fonctionnerait pas si le calcul était sur un itérable plus long. Mais une version itérative est facile à faire :

res = 0
for i in map(int, '8:19:22'.split(':')):
    res = res * 60 + i
print(res)
## 29962

Maintenant, autant les deux dernières versions sont faciles à comprendre, autant la première prend quelques secondes. Et c’est la raison pour laquelle reduce() a été retirée des builtins, pour encourager l’usage des alternatives. En effet, cette fonction donne toujours un résultat très peu lisible. Je cite et approuve Guido là dessus:

C’est en fait celle que je déteste le plus, car, à part pour quelques exemples impliquant + ou *, presque chaque fois que je vois un appel à reduce() avec une fonction non-triviale passée en argument, j’ai besoin de prendre un crayon et un papier pour faire le diagramme de ce qui est effectivement entrée dans la fonction avant que je comprenne ce qu’est supposé faire reduce(). Donc à mes yeux, l’application de reduce() est plutôt limitée à des opérateurs associatifs, et dans d’autres cas il est mieux d’écrire une boucle d’accumulation explicitement.

Graissage maison.

Bref, reduce() est dur à lire, et une boucle ne l’est pas. Écrivez 3 lignes de plus, ça ne va pas vous tuer. Relire votre one-liner dans un mois par contre…

Cette fonction a été beaucoup utilisée avec les opérateurs or et and pour savoir si tous les éléments étaient vrais au moins un élément vrai dans une liste :

tout_est_vrai = [1, 1, 1, 1]
certains_sont_vrais = [1, 0, 1, 0]
tout_est_faux = [0, 0, 0, 0]
 
# Retourne True si tout est vrai
print(bool(reduce(lambda a, b: a and b, tout_est_vrai)))
## True
print(bool(reduce(lambda a, b: a and b, certains_sont_vrais)))
## False
print(bool(reduce(lambda a, b: a and b, tout_est_faux)))
## False
 
# Retourne True si au moins un élément est vrai
print(bool(reduce(lambda a, b: a or b, tout_est_vrai)))
## True
print(bool(reduce(lambda a, b: a or b, certains_sont_vrais)))
## True
print(bool(reduce(lambda a, b: a or b, tout_est_faux)))
## False

Mais aujourd’hui, c’est parfaitement inutile puisque nous avons les fonctions built-in all() et any(), qui font ça en plus court et plus rapide :

# Retourne True si tout est vrai
print(all(tout_est_vrai))
## True
print(all(certains_sont_vrais))
## False
print(all(tout_est_faux))
## False
 
# Retourne True si au moins un élément est vrai
print(any(tout_est_vrai))
## True
print(any(certains_sont_vrais))
## True
print(any(tout_est_faux))
## False

Petite astuce finale

Souvenez-vous également que les fonctions Python peuvent être déclarées n’importe où à la volée, même dans une autre fonction, une classe, une méthode, un context manager, etc. Or une fonction peut retourner un générateur grâce à yield, ce qui vous permet de déclarer des gros bouts de logique, et de les plugger dans votre process itérative a posteriori :

def traitement_complexe(iterable):
    for x in iterable:
        if x not in (1, 3, 7) and x % 2 != 0:
            if x + x < 13 :
                yield x
            else: 
                yield x - 2
 
print("-".join(map(str, traitement_complexe(range(20)))))
## 5-7-9-11-13-15-17

flattr this!

]]>
http://sametmax.com/map-filter-et-reduce/feed/ 1
S’affranchir des doublons d’un itérable en Python http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/ http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/#comments Tue, 20 Aug 2013 10:10:32 +0000 Sam http://sametmax.com/?p=7143 Supprimer ou ignorer les doublons d’un itérable tel qu’une liste ou un array est un challenge dans tous les langages. Il faut se poser les questions suivantes :

  • Qu’est-ce qu’un doublon ?
  • Quels types d’itérables traite-t-on ?
  • Quel est la taille de l’itérable ?
  • Et niveau perfs ?

En Python, on a des structures de données qui suppriment automatiquement les doublons : les sets et les dictionnaires. Mais elles ne conservent pas l’ordre des élements.

Il y a aussi le fait qu’un itérable en Python peut avoir une taille inconnue, ou infinie.

Le post est long, donc…

Solution 1 : générateur et hashing

En utilisant conjointement les générateurs, les sets et une petite injection de dépendance, on peut trouver un compromis entre flexibilité et performances :

def skip_duplicates(iterable, key=lambda x: x):
 
    # on va mettre l’empreinte unique de chaque élément dans ce set
    fingerprints = set()
 
    for x in iterable:
        # chaque élement voit son emprunte calculée. Par défaut l’empreinte
        # est l'élément lui même, ce qui fait qu'il n'y a pas besoin de
        # spécifier 'key' pour des primitives comme les ints ou les strings.
        fingerprint = key(x)
 
        # On vérifie que l'empreinte est dans la liste des empreintes  des
        # éléments précédents. Si ce n'est pas le cas, on yield l'élément, et on
        # rajoute sont empreinte ans la liste de ceux trouvés, donc il ne sera
        # pas yieldé si on ne le yieldera pas une seconde fois si on le
        # rencontre à nouveau
        if fingerprint not in fingerprints:
            yield x
            fingerprints.add(fingerprint)

La fonction s’appelle skip_duplicates car c’est ce qu’elle fait. Elle ne retire pas vraiment les doublons, elle produit un flux de d’éléments qui ne comporte pas de doublons en ignorant tout doublons présent dans l’itérable initial.

Cette approche a plusieurs avantages :

  • Les doublons sont bien retirés, et l’ordre est conservé.
  • La complexité est de 0(n).
  • L’utilisateur peut choisir ce qui fait qu’un élément est unique : un attribut, un sous-élément, l’affichage sous forme de string…
  • C’est un générateur, est cela fonctionne donc avec des itérables de toute taille, même inconnue ou infinie.

Il faut néanmoins que l’ensemble des éléments uniques tiennent au moins une fois en mémoire en plus de l’itérable initial, et potentiellement d’un stockage à la sortie du générateur. On fait donc un trade-off sur la mémoire.

Comme la valeur de key par défaut est une valeur saine, ça fonctionne comme on s’y attend pour les cas simples :

>>> list(skip_duplicates([1, 2, 3, 4, 4, 2, 1, 3 , 4]))
[1, 2, 3, 4]
>>> list(skip_duplicates('fjsqlkdmfjsklqmdfjdmsl'))
[u'f', u'j', u's', u'q', u'l', u'k', u'd', u'm']
>>> list(skip_duplicates(((1, 2), (2, 1), (1, 2), (1, 1))))
[(1, 2), (2, 1), (1, 1)]

Pourvoir spécifier ‘key’ permet de faire des choix dans ce qu’est un doublon :

>>> list(skip_duplicates((1, 2, '1', '1', 2, 3, '3')))
[1, 2, u'1', 3, u'3']
>>> list(skip_duplicates((1, 2, '1', '1', 2, 3, '3'), key=lambda x: str(x)))
[1, 2, 3]

Et si on s’attaque à des cas plus complexes, le fonction vous force à préciser votre pensée :

>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)))
... )
Traceback (most recent call last):
  File "<ipython-input-20-ed44f170c634>", line 1, in <module>
    list(skip_duplicates(([], [], (), [1, 2], (1, 2)))
  File "<ipython-input-18-42dbb94f03f8>", line 7, in skip_duplicates
    if fingerprint not in fingerprints:
TypeError: unhashable type: 'list'

En effet les listes ne sont pas des types hashables en Python, on ne peut donc pas les stocker dans un set.

Mais on peut caster la liste, et faire ainsi le choix de savoir sur quel critère on base notre égalité. Par exemle, considère-t-on que deux itérables ayant le même contenu sont égaux, où alors doivent-ils avoir le même type ?

>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x: tuple(x)))
[[], [1, 2]]
>>> list(skip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x: (type(x), tuple(x))))
[[], (), [1, 2], (1, 2)]

Nous utilisons le fait que :

>>> tuple([1, 2]) == (1, 2)
True
>>> (type([1, 2]), tuple([1, 2])) == (type((1, 2)), (1, 2))
False

Puisque :

>>> (type([1, 2]), tuple([1, 2]))
(<type 'list'>, (1, 2))
>>> (type((1, 2)), (1, 2))
(<type 'tuple'>, (1, 2))

Dans le cas où nous ne sommes pas capables de déterminer ce qu’est un doublon, la fonction ne retire simplement rien :

class Test(object):
    def __init__(self, foo='bar'):
        self.foo = foo
    def __repr__(self):
        return "Test('%s')" % self.foo
 
>>> list(skip_duplicates([Test(), Test(), Test('other')]))
[Test('bar'), Test('bar'), Test('other')]

Mais permet encore une fois de faire le choix de quoi retirer :

>>> list(skip_duplicates([Test(), Test(), Test('other')], key=lambda x: x.foo))
[Test('bar'), Test('other')]

Ce principe de la fonction key, on le retrouve dans sorted(), donc les habitués seront déjà à l’aise. Et j’aime beaucoup ce pattern, car il est très puissant. On peut avoir la fonction key qui renvoit des choses très simples :

  • Un attribut.
  • Un element (x[2], x['cle']…)
  • Une version castée avec int(), str(), tuple(), etc

Mais on peut aussi faire des choses très complexes. En effet, rien ne nous oblige à utiliser une lambda, on peut mettre une fonction complète et lui faire retourner :

  • Un hash md5.
  • Une entrée en base de données.
  • Un nouvel objet customisé.
  • Un tuple de tuples d’objets custos avec des dictionnaires en attributs…
  • Le contenu d’un fichier.

Python sait naturellement comparer tout ça.

Notez que nous trichons un peu, puisque nous retirons les doublons en nous basant sur un set qui va calculer un hash de l’objet, et pas véritablement vérifier l’égalité. La fonction en fonctionnera donc pas si l’utilisateur définie __eq__ et s’attend à ce que les doublons soient retirés. Ce qui nous amène à …

Solution 2 : iterateur et comparaison

Pour ce genre de chose, un autre algo, qui ne fontionerait que sur les itérables de taille finie, et qui serait bien plus lent (complexité n log(n)), peut être utilisé :

def strip_duplicates(iterable, equals=lambda x, y: x == y):
 
    # On transforme l'itérable en iterateur sur lui même, cela va nous
    # permettre d'appeler next() dessus et récupérer le premier élément,
    # même sur un objet non indexable (sur lequel on ne peut utiliser [0])
    iterable = iter(iterable)
 
    res = []
    # Une petite boucle infinie est nécessaire car la boucle 'for' ne nous
    # permet pas de récupérer le premier élément indépendamment des autres,
    # et la boucle 'while' attend une condition de sortie, ce que nous n'avons
    # pas forcément (il n'est pas possible de vérifier le nombre d'éléments
    # restant dans un générateur).
    while True:
 
        # on récupère le premier élément de l'iterable restant, si il n'y en
        # a plus, on sort de la boucle.
        try:
            elem = next(iterable)
        except StopIteration:
            break
 
        # Le premier élément est ajouté au résultat sans doublons. Maintenant
        # on va recréer l'itérable, mais en retirant tout ce qui était égal
        # au premier élément. Notez que 'être égal' est une condition modifiable
        # en passant une fonction en paramètre, comme l'était 'key' précédemment.
        res.append(elem)
 
        iterable = iter([x for x in iterable if not equals(elem, x)])
 
    return res

La fonction s’appelle strip_duplicates car elle produit une nouvelle liste, mais sans les éléments indésirables, comme le fait strip() sur une chaîne (produit une nouvelle chaîne, sans les éléments indésirables).

Ce type de fonction peut être utile dans plusieurs cas :

  • On a pas envie de se poser la question de savoir si nos types à comparer sont hashable ou pas, et on est prêt à payer un peu de CPU pour cela.
  • On a besoin de retirer les doublons sur la base d’une égalité, par exemple sur l’existence de la méthode __eq__.
  • On a besoin de retirer les doublons sur la base d’une logique complexe qui dépend du contexte.

A première vu cela fonctionne presque de la même manière que skip_duplicates, mais en retournant une liste plutôt qu’un générateur :

>>> strip_duplicates('fdjqkslfjdmkfdsqjkfmjqsdmlkfjqslkmfjsdklfl')
['f', 'd', 'j', 'q', 'k', 's', 'l', 'm']

Mais déjà il n’y a pas à se soucier de savoir si une structure de données est hashable :

>>> strip_duplicates(([], [], (), [1, 2], (1, 2)))
[[], (), [1, 2], (1, 2)]

Même si on garde la même flexibilité, bien que la fonction à passer ait une signature légèrement différente :

>>> strip_duplicates(([], [], (), [1, 2], (1, 2)), lambda x, y: tuple(x) == tuple(y))
[[], [1, 2]]

Le plus interessant reste que cela fonctionne sur l’égalité, et donc cela marche d’office avec les objets qui déclarent __eq__ ce qui est le cas dans de nombreuses libs, comme les ORM :

class Test(object):
    def __init__(self, foo='bar'):
        self.foo = foo
    def __repr__(self):
        return "Test('%s')" % self.foo
    def __eq__(self, other):
        return self.foo == other.foo
 
>>> strip_duplicates([Test(), Test(), Test('other')])
[Test('bar'), Test('other')]

Dans certains cas, notamment dans le cas où le point de comparaison est un object non hashable de très grosse taille (par exemple un dico très long), on peut espérer aussi pas mal économiser en mémoire. Mais on est qu’en est-il des besoins en mémoire et en CPU ?

Solution 3 : retirer les doublons, in place

Enfin, pour ceux qui ont de grosses contraintes de mémoire et qui veulent un algo rapide au prix de la flexibilité du code, voici une solution qui oblige à travailler sur des listes et à modifier la liste sur place :

def remove_duplicates(lst, equals=lambda x, y: x == y):
 
    # Normalement en Python on adore le duck typing, mais là cet algo suppose
    # l'usage d'une liste, donc on met un gardefou.
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
 
    # là on est sur quelque chose qui ressemble vachement plus à du code C ^^
    i1 = 0
    l = (len(lst) - 1)
 
    # on itère mécaniquement sur la liste, à l'ancienne, avec nos petites
    # mains potelées.
    while i1 < l:
 
        # on récupère chaque élément de la liste, sauf le dernier
        elem = lst[i1]
 
        # on le compare à l'élément suivant, et chaque élément après
        # l'élément suivant
        i2 = i1 + 1
        while i2 <= l:
            # en cas d'égalité, on retire l'élément de la liste, et on
            # décrément la longueur de la liste ainsi amputée
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
 
        i1 += 1
 
    return lst

Et là on est bien dans de la modification sur place :

>>> lst = list('fjdsklmqfjskdfjmld')
>>> lst
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q', u'f', u'j', u's', u'k', u'd', u'f', u'j', u'm', u'l', u'd']
>>> remove_duplicates(lst)
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q']
>>> lst
[u'f', u'j', u'd', u's', u'k', u'l', u'm', u'q']

La fonction s’appelle cette fois bien remove_duplicates puisque c’est ce qu’elle fait : retirer les doublons de la liste originale.

Et maintenant, c’est l’heure du benchmark à deux balles !

skip_duplicates :

setup = """
def skip_duplicates(iterable, key=lambda x: x):
        fingerprints = set()
        for x in iterable:
                fingerprint = key(x)
                if fingerprint not in fingerprints:
                        yield x
                        fingerprints.add(fingerprint)
import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(skip_duplicates(lst))', setup=setup, number=1000)
0.9810519218444824

strip_duplicates :

>>> setup = """
def strip_duplicates(iterable, equals=lambda x, y: x == y):
    iterable = iter(iterable)
    res = []
    while True:
        try:
            elem = next(iterable)
        except StopIteration:
            break
        res.append(elem)
 
        iterable = iter([x for x in iterable if not equals(elem, x)])
 
    return res
 
import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(strip_duplicates(lst))', setup=setup, number=1000)
41.462974071502686

remove_duplicates :

setup = """
def remove_duplicates(lst, equals=lambda x, y: x == y):
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
    i1 = 0
    l = (len(lst) - 1)
    while i1 < l:
        elem = lst[i1]
        i2 = i1 + 1
        while i2 <= l:
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
        i1 += 1
    return lst
 
import string
lst = list(string.ascii_letters * 100)"""
>>> timeit.timeit('list(remove_duplicates(lst))', setup=setup, number=1000)
0.37493896484375

Sans surprise, la version inplace est la plus rapide puisque la plus restrictive. En second vient notre strip_duplicates, beaucoup fois plus lente. Et en dernier, 50 fois plus lente, le compromis entre les deux : souple, consomme moins de mémoire que skip, mais plus que remove.

Mais ce n’est pas très juste pour strip, puisque que skip n’a pas à faire un gros travail de conversion. Essayons avec des clés plus grosses :

skip_duplicates :

setup = """
def skip_duplicates(iterable, key=lambda x: x):
        fingerprints = set()
        for x in iterable:
                fingerprint = key(x)
                if fingerprint not in fingerprints:
                        yield x
                        fingerprints.add(fingerprint)
import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(skip_duplicates(lst, lambda x: tuple(x)))', setup=setup, number=1000)
15.516181945800781

strip_duplicates :

>>> setup = """
def strip_duplicates(iterable, equals=lambda x, y: x == y):
    iterable = iter(iterable)
    res = []
    while True:
        try:
            elem = next(iterable)
        except StopIteration:
            break
        res.append(elem)
 
        iterable = iter([x for x in iterable if not equals(elem, x)])
 
    return res
 
import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(strip_duplicates(lst))', setup=setup, number=1000)
22.047110080718994

remove_duplicates :

setup = """
def remove_duplicates(lst, equals=lambda x, y: x == y):
    if not isinstance(lst, list):
        raise TypeError('This function works only with lists.')
    i1 = 0
    l = (len(lst) - 1)
    while i1 < l:
        elem = lst[i1]
        i2 = i1 + 1
        while i2 <= l:
            if equals(elem, lst[i2]):
                del lst[i2]
                l -= 1
            i2 += 1
        i1 += 1
    return lst
 
import string, random
lst = [list(string.ascii_letters * 100) for x in xrange(100)]
for x in lst:
    x.pop(random.randint(0, len(x) - 1))"""
>>> timeit.timeit('list(remove_duplicates(lst))', setup=setup, number=1000)
14.763166904449463

Comme souvent les résultats sont contre untuitifs, car bien que remove garde son avance, elle s’est largement réduite. A l’inverse, skip n’est pas tant à la ramasse que ça, et strip reste le plus lent.

Il faudrait aussi mesurer la consommation mémoire, je suis certain que ce serait interessant.

Bon, il est temps de mettre tout ça dans batbelt.

flattr this!

]]>
http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/feed/ 7
Ordonner en Python http://sametmax.com/ordonner-en-python/ http://sametmax.com/ordonner-en-python/#comments Mon, 21 Jan 2013 13:37:37 +0000 Sam http://sametmax.com/?p=4107 Python possède une manière de mettre les choses dans l’ordre qui est à la fois simple et puissante.

Tout est ordonnable

Pour comprendre comment marche le tri en Python, il faut comprendre que presque tout en Python est ordonnable car comparable :

>>> 1 > 0 # les nombres sont comparables
True
>>> 1 < 0
False
>>> "a" < "b" # les lettres, par ordre alphabetique
True
>>> True > False # les booléan (!)
True
>>> (1, 2) > (2, 1) # les iterables comparés, élément par élément dans l'ordre
False
>>> (1, 2) > [2, 1] # mais ils doivent être du même type
True
>>> {1:1} < {1:1, 0:0} # les dictionnaires, par nombre d'éléments
True
>>> "a" > 2 # si on mélange des types ça peut vide devenir bizarre
True
>>> 1j > 1 # j'ai dis PRESQUE tout est ordonnable
Traceback (most recent call last):
  File "<ipython-input-11-ed3c013d3df8>", line 1, in <module>
    1j > 1
TypeError: no ordering relation is defined for complex numbers

C’est ce qu’on appelle l’ordre naturel des éléments. Quand il n’y a pas d’ordre naturel évident (et que ce n’est pas une opération explicitement interdite comme avec les complexes), Python va comparer l’id (imaginez l’adresse en mémoire):

>>> class PersonnageDeLost(object):
...     pass
... 
>>> mort1 = PersonnageDeLost()
>>> mort2 = PersonnageDeLost()
>>> mort1 < mort2
True
>>> id(mort1) # son id est plus petit, donc il est inférieur
39611408
>>> id(mort2)
41720976

Mais on peut définir un ordre naturel pour un élément personnalisé en implémentant les méthodes suivantes:

  • object.__lt__(self, other)
  • object.__le__(self, other)
  • object.__eq__(self, other)
  • object.__ne__(self, other)
  • object.__gt__(self, other)
  • object.__ge__(self, other)

N’implémentez plus __cmp__, elle est dépréciée.

Par exemple:

class PersonnageDeCityHunter(object):
 
    def __init__(self, nom, erectopouvoir):
        self.nom = nom
        self.erectopouvoir = erectopouvoir
 
 
    def __lt__(self, other):
        # on doit retourner un booléan qui confirme ou infirme 
        # l'opération "lt" ("lower than", c'est à dire "plus petit que")
        return self.erectopouvoir < other.erectopouvoir
 
    def __gt__(self, other):
        # on doit retourner un booléan qui confirme ou infirme 
        # l'opération "gt" ("greater than", c'est à dire "plus grand que")
        return self.erectopouvoir > other.erectopouvoir
 
    # on peut faire la même chose pour les autres méthodes qui 
    # concernent les opérateurs <=, >=, == et !=
 
>>> PersonnageDeCityHunter('Ryo Saeba', 99999) > PersonnageDeCityHunter('Mamouth', 1)
True
>>> PersonnageDeCityHunter('Ryo Saeba', 99999) < PersonnageDeCityHunter('Mamouth', 1)
False

Ordonner une liste

Une liste est ce qu’on ordonne le plus souvent. Elle possède pour cela une méthode sort() qui est la méthode de tri la plus rapide qui existe avec la lib standard.

sort() trie une liste sur place (elle modifie donc la liste), et retourne None.

>>> l = [1, 7, 3, 8]
>>> res = l.sort()
>>> print res # n'assignez jamais le résultat de sort(), ça ne sert à rien !
None
>>> l
[1, 3, 7, 8]

Les éléments sont triés dans leur ordre naturel automatiquement, du plus petit au plus grand:

>>> l = ['b', 'a', 'c']
>>> l.sort()
>>> l
['a', 'b', 'c']
>>> l = [(2, 1), (1, 2), (7, 8), (2, 2), (2, 0), (2, 3)]
>>> l.sort()
>>> l # ordonne sur le premier élément, puis le second si il y a égalité
[(1, 2), (2, 0), (2, 1), (2, 2), (2, 3), (7, 8)]
>>> persos = [PersonnageDeCityHunter('Ryo Saeba', 99999), PersonnageDeCityHunter('Mamouth', 1), PersonnageDeCityHunter('Kaori', 0)]
>>> persos.sort()
>>> for perso in persos:
...     print perso.nom
...     
Kaori
Mamouth
Ryo Saeba

On peut demander à inverser l’odre de tri en passant l’argument reverse:

>>> l = [1, 7, 3, 8]
>>> l.sort(reverse=True)
>>> l
[8, 7, 3, 1]

Il n’y a pas que les listes dans la vie

Tout itérable est ordonnable, et la fonction sorted() permet justement d’ordonner n’importe quel itérable de taille finie.

>>> sorted([1, 7, 3, 8]) # une liste
[1, 3, 7, 8]
>>> sorted(set([1, 7, 3, 8])) # un set
[1, 3, 7, 8]
>>> sorted((1, 7, 3, 8)) # un tuple
[1, 3, 7, 8]
>>> sorted('Un clavier azerty en vaut deux') # une chaîne
[' ', ' ', ' ', ' ', ' ', 'U', 'a', 'a', 'a', 'c', 'd', 'e', 'e', 'e', 'e', 'i', 'l', 'n', 'n', 'r', 'r', 't', 't, 'u', 'u', 'v', 'v', 'x', 'y', 'z']
>>> sorted(open('/etc/fstab'))[:3] # un fichier
['#\n', '#\n', '# / was on /dev/sda5 during installation\n']
>>> import random # plus ou moins n'importe quoi
>>> sorted([random.randint(0, 100) for x in range(random.randint(0, 20)) if x * 2 not in (42, 69)])
[2, 4, 21, 35, 37, 41, 48, 48, 54, 58, 58, 59, 62, 76, 82, 94]

sort() et sorted() acceptent toutes les deux les mêmes arguments. Ce que vous apprenez pour l’un vaut pour l’autre. La seul différence est que sort() retourne None et modifie sur place, tandis que sorted() retourne une nouvelle liste. sorted() est un peu moins performant.

>>> sorted((1, 7, 3, 8), reverse=True)
[8, 7, 3, 1]

Tri personnalisé avec key

Parfois on a besoin de trier sur quelque chose de plus complexe qu’une lettre ou un chiffre. Par exemple, vous avez des scores dans un dictionnaires. Un dictionnaire n’est pas ordonné. Si vous imprimez un classement, il faut en faire une liste de tuples :

>>> scores = {'Robert': 2, 'Roger': 1, 'Gertrude': 4, 'Cunegonde': 3}
>>> scores.items()
[('Gertrude', 4), ('Cunegonde', 3), ('Robert', 2), ('Roger', 1)]

Et il faut maintenant ordonner ça sur le deuxième élément de chaque tuple : le score. Seulement si on appelle sorted(), il va trier sur l’ordre naturel, donc il va commencer par le premier élément, ça ne marchera pas :

>>> sorted(scores.items())
[('Cunegonde', 3), ('Gertrude', 4), ('Robert', 2), ('Roger', 1)]

C’est là qu’intervient le paramètre key. key est très particulier, c’est un paramètre qui attend qu’on lui passe un callback, donc key attend qu’on lui passe une fonction.

Pas le résultat d’une fonction. La fonction elle même.

Cette fonction doit attendre en paramètre un élément, et en extraire la partie sur laquelle on veut trier.

Par exemple dans le cas des scores, on doit passer à key une fonction qui attend un tuple en paramètre, et retourne le score :

>>> def get_score(nom_et_score):
...     return nom_et_score[1] # retourne le 2nd element du tuple
... 
>>> sorted(scores.items(), key=get_score) # on passe la fonction a key
[('Roger', 1), ('Robert', 2), ('Cunegonde', 3), ('Gertrude', 4)]

Et voilà, c’est trié dans le bon ordre car sorted() va utiliser get_score() pour récupérer la valeur de chaque élément un par un, sur laquelle on va trier la liste (selon l’ordre naturel de cette valeur).

On peut utiliser key pour faire des choses beaucoup plus complexes, comme comparer sur des valeurs qui n’existent pas, mais que l’on calcule :

>>> def carre(val): # on va ordonner par valeur de carré
...     return val * val 
... 
>>> sorted([-1, -2, 0, 3], key=carre)
[0, -1, -2, 3]

Ici, un carré est toujours positif, du coup on retrouve -1 et -2 devant 0, car leurs carrés 1 et 4 sont plus grands que le carré 0 de 0.

Ne vous embrouillez pas : la valeur retournée par carre() n’est pas MISE dans la liste, elle est juste utilisée pour choisir l’ordre d’un élément dans la liste.

On peut même faire des comparaisons très avancées, comme par exemple comparer sur plusieurs attributs d’un objet. Imaginez, je veux ordonner des objets voitures selon leur coût d’entretien d’abord, et ensuite par coût d’achat.

class Voiture(object):
 
    def __init__(self, cout_entretien, cout_achat):
        self.cout_entretien = cout_entretien
        self.cout_achat = cout_achat
 
    def __repr__(self):
        return "<Voiture E-{} / A-{}>".format(self.cout_entretien, self.cout_achat)
 
>>> voitures = [Voiture(10000, 10000), Voiture(50000, 10000), Voiture(10000, 60000)]
>>> voitures
[<Voiture E-10000 / A-10000>, <Voiture E-50000 / A-10000>, <Voiture E-10000 / A-60000>]
>>> def get_entretien_achat(voiture): 
...         return (voiture.cout_entretien, voiture.cout_achat)
... 
>>> sorted(voitures, key=get_entretien_achat)
[<Voiture E-10000 / A-10000>, <Voiture E-10000 / A-60000>, <Voiture E-50000 / A-10000>]

get_entretien_achat() retourne un tuple de deux valeurs. Python va donc ordonner sur ce tuple, dans l’odre naturel du tuple en prenant la première valeur comme point de comparaison (coût d’entretien), puis la seconde (coût d’achat) si les valeurs sont égales.

On peut donc comparer des objets arbitraires sur des critères arbitraires.

La plupart du temps, vous voudrez juste comparer sur un élément ou un attribut, donc dans ce cas le module operator (ou une lambda) est votre ami :

>>> from operator import attrgetter, itemgetter
>>> sorted(voitures, key=attrgetter('cout_achat')) # ordonner par cout d'achat
[<Voiture E-10000 / A-10000>, <Voiture E-50000 / A-10000>, <Voiture E-10000 / A-60000>]
>>> sorted(scores.items(), key=itemgetter(1)) # ordonner par score
[('Roger', 1), ('Robert', 2), ('Cunegonde', 3), ('Gertrude', 4)]
>>> sorted(scores.items(), key=itemgetter(1), reverse=True) 
[('Gertrude', 4), ('Cunegonde', 3), ('Robert', 2), ('Roger', 1)]

Pour finir, je vous invite à lire l’article sur le module heapq qui lui permet de faire des tris sur des flux de données sans taille définie.

flattr this!

]]>
http://sametmax.com/ordonner-en-python/feed/ 10
La fonction anonyme appelée immédiatement en Javascript: (function())() http://sametmax.com/la-fonction-anonyme-appelee-immediatement-en-javascript-function/ http://sametmax.com/la-fonction-anonyme-appelee-immediatement-en-javascript-function/#comments Wed, 02 Jan 2013 17:09:13 +0000 Sam http://sametmax.com/?p=3979 Javascript est un langage qui a plein d’idiomes bien à lui. En effet, c’est un langage très puissant, et ausi plein de couilles velues planquées ici et là. Les ninjas JS ont donc créée des astuces pour pallier à ces problèmes, en utilisant la force de leur outil.

Un des gros soucis en JS, c’est qu’il dépend beaucoup des variables globales, qui sont une grosse source d’ennuis en tout genre. Et il est très facile de déclarer ou d’utiliser une variable globale par erreur.

Pour limiter ce problème, on utilise la technique de la fonction anonyme immédiatement appelée. Cela fonctionne comme ceci, au lieu de faire:
pre lang=”javascript”>
alert(‘rouge’);

On fait:

(function(){
    alert('rouge');
})()

Explication pas à pas

En effet, en Javascript on peut créer une fonction sans lui donner de nom. C'est ce qu'on appelle une fonction anonyme:

function(){
    alert('rouge');
}

On peut attribuer cette fonction à une variable.

var une_variable_qui_passait_par_la = function(){ alert('rouge')};

Et derrière on peut appeler la fonction ainsi:

une_variable_qui_passait_par_la();

Mais dans notre cas, on ne met pas la fonction dans une variable, on l'enferme dans des parenthèses, ce qui garde la référence le temps de l'appeler, et on l'appelle. On fait la même chose que précédement en fait, on saute juste une étape :

(function(){ alert('rouge');})()

La référence à la fonction est perdue, on ne pourra pas l'appeler de nouveau. Mais on s'en branle fortement, on voulait juste l'appeler une fois de toute façon.

Pour rendre le code plus lisible, on le met sur plusieurs lignes:

(function(){
    alert('rouge');
})() /* on éxécute la fonction cash pistache */

Maintenant pour comprendre ce que ça fait, il faut juste réaliser que le bout de code ci-dessous fait EXACTEMENT la même chose que le bout de code juste au dessus :

alert('route');

Car on déclare la fonction, mais on l'éxécute tout de suite. Donc dans les deux cas, au chargement du script, un aura une alerte immédiatement.

Mais putain si ça fait la même chose, pourquoi tu nous gonfles avec ça ?

Parceque dans le cas du dessus ça fait la même chose, mais ça a des propriétés en plus. Vous vous doutez bien qu'on tape pas 4 lignes en trop pour le plaisir (à part en Java).

En effet, en créant une fonction, on crée un scope. Ainsi, tout ce qui est déclaré avec var dans la fonction sera garanti de ne pas être une variable globale. Pas d'effet de bord sur le monde extérieur. Et le monde extérieur n'a PAS accès aux variables déclarées dans la fonction. Par contre la fonction a accès aux variables globales !

Par exemple, dans un navigateur, vous avez toujours accès à l'objet window et l'objet Date partout dans le code.

/*
    Ici vous avez accès à window et Date
*/
 
(function(){
 
    /* Ceci ne va pas changer l'objet window en dehors de la fonction
        car on redéclare la variable, à l'intérieur du scope de la fonction.
     */
    var window = '3.1';
 
    var rideau = 'vista';
 
    /* Ici vous avez accès à Date */
 
    alert(new Date())
 
 
})()
 
/*
    Ici vous n'avez PAS accès à "rideau"
*/

Il est donc une bonne pratique de mettre TOUT son script dans une fonction qui s'auto-appelle, pour l'isoler du monde exterieur et ne pas pourrir le namespace global. Si on avait pas fait ça, et qu'un autre script définissait aussi rideau, l'un des deux aurait écrasé l'autre. Et dans tous les cas on aurait écrasé window.

Encore mieux

Comme on crée un conteneur isolé, on peut faire ce que l'on veut dedans. Et on a deux moyens de communique avec ce conteneur. D'abord, les variables globales. Ensuite, les paramètres de la fonction auto-appelée.

On peut exposer le contenu de la fonction à travers les variables globales. Par exemple, vous faites un générateur de 'pouet', chose importante s'il en est :

(function(){
 
    /* ces variables ne sont pas accessibles depuis l'extérieur */
    var pouetGenerator = {};
    var variablePrivee = true;
 
    /* ces fonctions sont accessibles seulement depuis pouetGenerator */
    pouetGenerator.fairePouet = function(){
        alert('pouet')
    }
 
    pouetGenerator.nePasFairePouet = function(){
        if (variablePrivee) {
            alert('pas pouet');
        }
    }
 
    /* On rend notre pouetGenerator accessible au reste du monde */
    window.pouetGenerator = pouetGenerator;
 
})()
 
/* On peut utiliser notre générateur de pouet partout ailleurs */
pouetGenerator.nePasFairePouet()

En effet, tout ce qui est attaché à window devient une variable globale dans un navigateur. C'est une caractéristique de Javascript dans un navigateur Web: window est un objet accessible partout, et on a accès partout à ses attributs. On a donc notre code parfaitement isolé (la seule chose exposée c'est notre point d'entrée : pouetGenerator). On ne pollue pas le namespace. En prime personne n'a accès aux variables privées comme variablePrivee;

On peut aussi utiliser les paramètres et les valeurs de retour pour cela :

/* pouetGenerator, déclaré sans "var", est une variable globale */
pouetGenerator = (function(chaine_du_pouet){
 
    /* ces variables ne sont pas accessibles depuis l'extérieur */
    var pouetGenerator = {};
    var variablePrivee = true;
 
    /* ces fonctions sont accessibles seulement depuis pouetGenerator */
    pouetGenerator.fairePouet = function(){
        alert(chaine_du_pouet)
    }
 
    pouetGenerator.nePasFairePouet = function(){
        if (variablePrivee) {
            alert('pas pouet');
        }
    }
 
    /* On rend notre pouetGenerator accessible au reste du monde
        en la retournant, ce qui va mettre la référence dans le pouetGenerator
        qui est la variable globale.
    */
    return pouetGenerator;
 
/* On passe 'pouet' en paramètre, il va se retrouver dans chaine_du_pouet */
})('pouet')
 
/* On peut utiliser notre générateur de pouet au même niveau */
pouetGenerator.nePasFairePouet()

Il y a deux différences dans ce code. La première, c'est qu'on met la variable à disposition en la retournant plutôt qu'en l'attachant à document. La seconde, c'est qu'on passe 'pouet'.

On pourrait se dire que l'interêt est limité, mais considérez l'exempe suivant:

(function($){
    $('p').css('color', 'rouge-fluo')
})(jQuery.noConflic())

jQuery.noConflic() restaure $ à sa valeur précédente, permettant à d'autres libs d'utiliser $ (comme la lib prototype.js). Mais jQuery.noConflic() retourne aussi l'objet jQuery, que l'on passe en paramètre. Ce paramètre est nommé $ dans la signature de la fonction. Dans notre fonction, on peut donc QUAND MÊME utiliser $ pour faire référence à jQuery car tout ce qui est dans la fonction est isolée du reste du monde. Et le reste du monde peut utiliser $ pour autre chose que jQuery.

Best practice

Généralement un code de production pour un script complet ressemble donc à ça:

;(function(alias){
"use strict";
 
/* tout le code du script va ici */
 
var une_variable_privee;
 
window.une_variable_exposee = 'foo';
 
})(truc_a_aliaser)

Notez le ; au début qui permet de rajouter votre script à la suite d'un autre script même si celui-ci a oublié de mettre un ; sur sa dernière ligne (utile pour les minifieurs).

Notez aussi le "use strict"; qui dit au navigateur que tout ce qui se trouve dans cette fonction devra être traité par un parseur strict qui vous signalera plus d'erreurs de code. Cela ne s'applique qu'à la fonction, laissant le reste du monde le droit d'utiliser du code moins strict.

flattr this!

]]>
http://sametmax.com/la-fonction-anonyme-appelee-immediatement-en-javascript-function/feed/ 16
Les listes en intension VS map() en Python http://sametmax.com/les-listes-en-intentions-vs-map-en-python/ http://sametmax.com/les-listes-en-intentions-vs-map-en-python/#comments Thu, 04 Oct 2012 13:26:27 +0000 Sam http://sametmax.com/?p=2452 map() et sont souvent plus à l'aise avec elle qu'avec les listes en intention en Python. Les deux font pourtant la même chose, tant et si bien que Python 3 voit map() retiré de ses built-in.]]> sLes adeptes de la programmation fonctionnelle connaissent bien le principe de la fonction map() et sont souvent plus à l’aise avec elle qu’avec les listes en intension en Python.

Les deux font pourtant la même chose, tant et si bien que Python 3 voit map() retiré de ses built-in. Ouuuups. Dérapage un-con-trollé de Sam.

C’est plutôt une nouvelle chose car dans un language orienté objet comme Python, il y a une subtile différence entre map() et les comprehension lists. Quand on utilise une méthode plutôt qu’une fonction, map() tue le duck tuping:

>>> l = [u'      Le choix dans la date     ', '     Les nouilles cuisent au jus de canne     ']
>>> map(str.strip, l)
Traceback (most recent call last):
  File "<ipython-input-14-41df8a735feb>", line 1, in <module>
    map(str.strip, l)
TypeError: descriptor 'strip' requires a 'str' object but received a 'unicode'
 
>>> map(unicode.strip, l)
Traceback (most recent call last):
  File "<ipython-input-15-fc0fc8fef32d>", line 1, in <module>
    map(unicode.strip, l)
TypeError: descriptor 'strip' requires a 'unicode' object but received a 'str'
 
>>> [x.strip() for x in l]
[u'Le choix dans la date', 'Les nouilles cuisent au jus de canne']

Notez que je vous recomande d’éviter à tout prix de mélanger des chaînes encodées, et des chaînes unicode, de toute façon. J’utilise ceci uniquement parce que c’est un cas simple à comprendre.

Mais ici, le problème peut être résumé ainsi: nous avons une collection hétérogène d’objets (qui pourraient être de n’importe quels autres types, il y a des cas beaucoup plus légitimes), et nous appliquons la méthode de l’objet. Qui ne correspond pas forcément à son type.

Avoir des types différents avec la même interface est courant en Python pour profiter du duck typing, donc ce genre de chose n’est pas un edge case.

La solution est bien entendu de wrapper l’appel dans une fonction anonyme:

>>> map(lambda x: x.strip(), l)
[u'Le choix dans la date', 'Les nouilles cuisent au jus de canne']

Mais à ce stade, [x.strip() for x in l] est beaucoup plus lisible et rapide. Sans compter qu’il peut être transformé en générateur juste en changeant les [] en ().

flattr this!

]]>
http://sametmax.com/les-listes-en-intentions-vs-map-en-python/feed/ 2
Qu’est-ce qu’un callback ? http://sametmax.com/quest-ce-quun-callback/ http://sametmax.com/quest-ce-quun-callback/#comments Wed, 22 Aug 2012 17:19:07 +0000 Sam http://sametmax.com/?p=1831 mais c'est simple, il suffit de passer un callback".]]> mettreOn nous a parfois reproché de ne pas faire assez de tutos pour débutant. C’est pas faux, d’autant que quand j’ai commencé j’étais bien content que le site du zéro ait choisi de se spécialiser là dedans. Les tutos pour débutant sont vraiment la pierre angulaire de l’attractivité d’une techno.

Donc, un jour vous vous baladez avec vos premiers succès en prog, vous vous chauffer à utiliser une library externe (ce qui fait toujours peur au début) et il y a un truc que vous ne savez pas faire. Vous posez la question sur un forum, et on vous répond: “mais c’est simple, il suffit de passer un callback“.

Doh.

Rappel: on peut passer des fonctions en argument

Une fonction, ce n’est pas juste un bout de code auquel on donne un nom. C’est une unité de programmation à elle toute seule, en tout cas dans les langages modernes, et on dit dans ce cas qu’elles sont des “first class citizen” (citoyen de première catégorie, quoi, du vrai, du dur, du pur).

En pratique, ça veut dire qu’on peut manipuler la fonction sans l’éxécuter. En python ça donne ça:

>>> def dis_bonjour():
...     print "bonjour"
...
>>> print dis_bonjour # afficher l'objet fonction
<function dis_bonjour at 0x7f8cc6fce578>
>>> dis_bonjour.func_name # afficher le nom de la fonction
'dis_bonjour'

Ca veut dire aussi qu’on peut passer une fonction comme argument:

>>> def fonction_qui_appelle_une_fonction(fonction_a_appeler):
...     fonction_a_appeler()
...
>>> fonction_qui_appelle_une_fonction(dis_bonjour)
bonjour

Mais Za Koi ça sert ?

Et bien à dire qu’on va exécuter du code, même si on ne sait pas encore à l’avance quel est ce code. C’est très utile quand on code soit-même une bibliothèque pour permettre aux utilisateurs de celle-ci d’exécuter du code durant le fonctionnement de notre algo, sans avoir à mettre la main dedans.

C’est exactement ce que font les callback (ou appel en retour, traduit grossièrement).

Un callback, c’est une fonction passée en paramètre, qui va être appelée à une condition. La condition est la plus souvent “quand ceci arrive” et “ceci” est le plus souvent “quand le traitement est terminé”. Donc la grande majorité des callbacks sont des fonctions qu’on passe à d’autres fonctions pour qu’elles soient exécutées quand le traitement est terminé.

Des exemples, des exemples !

Si vous faites une interface graphique, vous voulez qu’un clic sur un bouton déclenche une action. Cette action est souvent passée comme un callback.

Exemple avec ce petit programme Tkinter (la lib d’UI installée par défaut avec Python):

>> from Tkinter import * # import de tout TK
>>> root = Tk() # création de la fenêtre
>>> def crie_ta_joie(): # notre callback
...     print "Yo !"
...
>>> b = Button(root, text="Go", command=crie_ta_joie) # création d'un bouton
>>> b.pack() # placement du bouton
>>> root.mainloop() # mise en route de l'UI

crie_ta_joie est passée à la classe Button via le paramètre command. Quand on cliquera sur le bouton ‘Go’, le callback crie_ta_joie sera donc appelé. ‘Yo !’ sera affiché dans le terminal.

C’est ce qu’on appelle la programmation événementielle: on écrit des fonctions qui sont appelées quand des événements arrivent, ici le clic sur un bouton.

Et en javascript…

Si vous utilisez jQuery, vous utilisez déjà des callbacks partout.

Ainsi, si vous faites:

$.get('/arretez/ou/ma/mere/va/tirer', function(){
    alert('Bang !');
});

jQuery va faire une requête ajax sur l’url, et le programme va continuer de fonctionner car les appels réseaux sont non bloquant. Mais quand la réponse arrivera, le callabck sera appelé, et fera alert(Bang !).

Les callbacks sont donc très utilisés pour la programmation asynchrone, c’est à dire quand on ne connaît pas le temps que vont mettre des opérations à s’effectuer mais qu’on veut réagir une fois qu’elle sont terminées.

L’injection de dépendances

Les callbacks sont aussi très utilisés pour une technique de programmation appelée “injection de dépendances” qui consiste à permettre à ceux qui utilisent votre code de choisir ce que feront certains bouts de code.

Imaginez une fonction (ici assez simplifiée) de téléchargement qui permet d’afficher la progression de celui-ci:

import urllib2, sys
def download(truc_a_telecharger, fichier_de_destination):
 
    # on ouvre la connection
    u = urllib2.urlopen(truc_a_telecharger)
 
    taille_des_bouts = 8192
    total_telecharge = 0
 
    # on ouvre le fichier pour écrire ce qu'on télécharge
    with open(fichier_de_destination, 'w') as f:
 
        # while True / if : break est un palliatif Python
        # pour l'absence d'instruction "until"
        while True:
 
            # on télécharge un petit bout du fichier
            bout = u.read(taille_des_bouts)
            total_telecharge += taille_des_bouts
 
            if not bout: # plus rien à télécharge: on sort
                break
 
            # on écrit le bout de fichier téléchargé
            f.write(bout)
 
            # on écrit un point sur le terminal pour noter qu'un bout a été
            # téléchargé
            sys.stdout.write('.')

Qui s’utilise comme ça:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4')

On pourrait faire beaucoup mieux que juste afficher un point pour chaque bout de fichier téléchargé. On pourrait par exemple afficher un pourcentage d’avancement. Ou écrire dans un log. Ou ne rien faire, et supprimer tout affichage.

Mais on veut garder le comportement par défaut car on pense que la plupart des gens l’utiliseront ainsi, et qu’il n’y a pas de raison qu’ils le recodent.

En modifiant la fonction, et en permettant de passer un callback, on permet cette flexibilité:

def download(truc_a_telecharger, fichier_de_destination,
             # on attend un callback en paramètre
             # mais on en passe un par défaut
             afficher_le_progres=lambda *x, **y: sys.stdout.write('.')):
 
    u = urllib2.urlopen(truc_a_telecharger)
    # on chope la taille du fichier, ça permettra plus de choses
    taille_du_fichier = int(u.info().getheaders("Content-Length")[0])
    taille_de_bloc = 8192
    total_telecharge = 0
 
    with open(fichier_de_destination, 'w') as f:
 
        while True:
 
            # ici on appelle le callback en lui passant un maximum de paramètres
            # pour qu'il puisse faire le plus de chose possible
            afficher_le_progres(truc_a_telecharger, fichier_de_destination,
                                taille_du_fichier, total_telecharge)
 
            bout = u.read(taille_de_bloc)
            total_telecharge += taille_de_bloc
 
            if not bout:
                break
 
            f.write(bout)

Et on l’utilise comme avant:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4')

Ou avec plus de puissance:

def log(truc_a_telecharger, fichier_de_destination,
       taille_du_fichier, total_telecharge):
    with open('progress.log', 'w') as f:
        pourcentage = str(total_telecharge * 100 / taille_du_fichier)
        f.write(pourcentage)
 
download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4', log)

Et si on veut supprimer tout affichage, on peut passe une fonction qui ne fait rien:

download('http://download.ted.com/talks/SirKenRobinson_2006.mp4', 'ted_talk_education.mp4', lambda *x: None)

Il y a plusieurs choses importantes ici:

  • on accepte un callback en paramètre
  • le paramètre possède une valeur par défaut. Hé oui, on peut mettre une fonction en valeur par défaut !
  • la fonction est une fonction anonyme. Ce n’est pas obligatoire, mais c’est pratique.
  • la fonction par défaut utilise l’opérateur splat pour accepter un nombre illimité de paramètres, même si elle ne va pas les utiliser.
  • on délègue le comportement de l’algo lors de l’affichage du progrès à la fonction passée en paramètre.
  • on passe un max de paramètres à cette fonction pour lui donner le plus de libertés possible. C’est aussi pour ça que notre fonction accepte par défaut un nombre illimité de paramètres: sinon ça ferait une erreur

Ce système est l’injection de dépendance: on délègue une partie du travail à du code injecté depuis l’extérieur. Cela permet une extensibilité de notre fonction, sans sacrifier sa simplicité puisqu’on a une valeur par défaut.

On peut pousser l’injection très loin: en passant carrément des listes de fonctions, et toutes les appeler, ou des objets, et appeler plusieurs méthodes de l’objet (ce dernier point est une extension de l’injection de dépendance appelé le pattern strategy).

flattr this!

]]>
http://sametmax.com/quest-ce-quun-callback/feed/ 16
Comment ne PAS utiliser une fonction anonyme (ou lambda) en Python http://sametmax.com/comment-ne-pas-utiliser-une-fonction-anonyme-ou-lambda-en-python/ http://sametmax.com/comment-ne-pas-utiliser-une-fonction-anonyme-ou-lambda-en-python/#comments Sun, 05 Aug 2012 13:56:44 +0000 Sam http://sametmax.com/?p=1457 Dans le dernier article sur les lambdas, j’avais précisé que les lambdas étaient une question de style et qu’on pouvait faire sans. Voici différentes stratégies pour s’en passer.

Utiliser une inner function

En python on peut définir et exécuter à la volée une fonction dans une autre fonction/méthode ou une classe.

class Banane(object):
 
    def blougou(): # pas de self :-)
        print "Sens giratoir inversé"
 
    blougou() # on peut appeler une fonction en dehors d'une méthode
 
    def methode(self): # une vraie méthode
        print "Vive Edgar Morin"
 
        # on peut définir une fonction dans une méthode
        def moukrene():
            print "A la glaviouse"
 
        moukrene()

Et ça donne:

>>> Banane().methode()
Sens giratoir inversé
Vive Edgar Morin
A la glaviouse

Préremplissez des arguments

Le module functools fournit tout un tas de goodies pour faire mumuse avec les fonctions, et notamment la fonction partial() qui prend une fonction en argument, et des arguments à passer à la fonction qu’on lui a passé en argument. Vous suivez ?

On s’en sert quand on veut utiliser une fonction, mais qu’on sait d’avance quels arguments on va lui passer:

>>> from functools import partial
>>> sum
<built-in function sum>
>>> sum([2, 2])
4
>>> mini_sum = partial(sum, [2, 2])
>>> mini_sum()
4

On pas besoin de donner tous les arguments de la fonction à enrober: on peut passer les autres après.

Comme partial() retourne une fonction prête à être appelée, on peut l’utiliser là où on utiliserait une lambda:

>>> from __future__ import print_function
>>> def profit(etape1="On vol des caleçons", etape2=lambda: print('...')):
...         print(etape1)
...         etape2()
...     
>>> profit()
On vol des caleçons
...

Se tranforme en:

>>> def profit(etape1="On vol des caleçons", etape2=partial(print, '...')):
...         print(etape1)
...         etape2()
...     
>>> profit()
On vol des caleçons
...

Utiliser les fonctions derrières les opérations de base

Quand vous utilisez un + ou un [], il s’agit ni plus ni moins d’une syntaxe raccourcie pour l’appel d’une fonction. Et Python vous laisse accéder à ces fonctions à travers le module operator.

>>> for operation in (lambda x: x[0], lambda x: x * 3):
    print(operation(ls))
...     
a
['a', 3, 'a', 3, 'a', 3]
>>> from operator import itemgetter, mul
>>> for operation in (itemgetter(0), partial(mul, 3)):
    print(operation(ls))
...     
a
['a', 3, 'a', 3, 'a', 3]

Le module operator est très riche: opérations logiques (incluant la négation), manipulation de slices, maths de base, set/get d’attributs/de clés/d’index… Il y a de quoi faire.

Mettez à la poubelle map et filter

Map permet d’appliquer une fonction a un itérable. Filter permet de choisir les éléments d’un itérable selon un critère pour former un autre itérable.

>>> nombres = range(10)
>>> paires = filter(lambda x: not x % 2, nombres)
>>> paires
[0, 2, 4, 6, 8]
>>> paires_au_carres = map(lambda x: x * x, paires)
>>> paires_au_carres
[0, 4, 16, 36, 64]

Les listes en intention remplacent avantageusement ces deux fonctions (je crois d’ailleurs qu’elles sont retirées des built-in en Python 3):

>>> [x * x for x in range(10) if not x % 2]
[0, 4, 16, 36, 64]

Je ne suis pas un allergique aux lambdas, et je les utilise assez souvent, mais il est bon de savoir qu’il existe des alternatives.

flattr this!

]]>
http://sametmax.com/comment-ne-pas-utiliser-une-fonction-anonyme-ou-lambda-en-python/feed/ 1