Sam & Max » lambda http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Introduction au currying 3 http://sametmax.com/introduction-au-currying/ http://sametmax.com/introduction-au-currying/#comments Fri, 12 Dec 2014 19:37:00 +0000 http://sametmax.com/?p=12693 Le currying (ou Curryfication pour les frencofans) est le nom donné à une technique de programmation qui consiste à créer une fonction à partir d’une autre fonction et d’une liste partielle de paramètres destinés à celle-ci. On retrouve massivement cette technique en programmation fonctionnelle puisqu’elle permet de créer une fonction pure à partir d’une autre fonction pure. C’est une forme de réutilisabilité de code.

La forme la plus simple de currying est de réécrire une fonction appelant l’autre. Par exemple, soit une fonction pour multiplier tous les éléments d’un itérable :

def multiply(iterable, number):
    """ Multiplie tous les éléments d'un itérable par un nombre.
 
        Exemple :
 
            >>> list(multiply([1, 2, 3], 2))
            [2, 4, 6]
    """
    return (x * number for x in iterable)

On peut ensuite créer une fonction qui multipliera par 2 tous les éléments d’un itérable :

def doubled(iterable):
    """ Multiplie tous les éléments d'un itérable par un 2.
 
        Exemple :
 
            >>> list(doubled([1, 2, 3]))
            [2, 4, 6]
    """
    return multiply(iterable, 2)

C’est une forme de currying. On créé une fonction qui fait ce que fait une autre fonction, mais avec des arguments par défaut.

Python possède une fonction pour faire ça automatiquement avec n’importe quelle fonction :

>>> from functools import partial 
>>> tripled = partial(multiply, number=3) # on curryfie ici
>>> list(tripled([1, 2, 3])) # nouvelle fonction avec un seul argument
[3, 6, 9]

Cela marche car, je vous le rappelle, les fonctions sont des objets en Python. On peut mettre une fonction (je ne parle pas de son résultat) dans une variable, passer une fonction en paramètre ou retourner une fonction dans une autre fonction. Les fonctions sont manipulables.

Il n’est pas rare d’utiliser les fonctions anonymes comme outils curryfication. En Python, on ferait ça avec une lambda :

>>> tripled = lambda x: multiple(x, 3) 
>>> list(tripled([1, 2, 3]))
[3, 6, 9]

Certains outils, comme Ramda en Javascript, vont plus loin, et exposent des fonctions qui se curryfient automatiquement.

Pour ce faire, il faut inverser l’ordre qu’on mettrait intuitivement aux arguments dans la déclaration d’une fonction :

# au lieu de multiply(iterable, number), on a :
def multiply(number, iterable=None):
    # Si on a pas d'itérable passé, on curryfie
    if iterable is None:
        return partial(multiply, number=number)
    return (x * number for x in iterable)

Ainsi :

>>> list(multiply(2, [1, 2, 3])) # pas de currying
[2, 4, 6]
>>> quintuple = multiply(5) # currying automatique
>>> list(quintuple([1, 2, 3]))
[5, 10, 15]

L’intérêt de ce style, c’est qu’on peut composer des traitements à partir de plusieurs sous traitements, presque déclarativement :

def remove(filter, iterable=None):
    """ Retire tous les éléments d'un itérable correspondant au filtre.
 
        Exemple :
 
            >>> list(remove(lambda x: x >= 4, [1, 2, 3, 4, 5]))
            [1, 2, 3]
    """
    if iterable is None:
        return partial(remove, filter)
 
    return (x for x in iterable if not filter(x))
 
>>> smalls = remove(lambda x: x >= 4)
>>> list(smalls(tripled([0, 1, 2, 3, 4]))) # le traitement est auto descriptif
[0, 3]

Néanmoins, il faut savoir que ce style n’est pas pythonique. En effet, en Python on préférera généralement utiliser des suites suite de générateurs. Soit par intention, soit via yield.

Notre exemple serait alors :

>>> tripled = (x * 3 for x in [0, 1, 2, 3, 4])
>>> smalls = (x for x in tripled if x <= 4)
>>> list(smalls)
[0, 3]

De plus, cette technique suppose qu’on ne profitera pas de certaines fonctionnalités, comme les paramètres par défaut des fonctions Python.

C’est toutefois une bonne chose à connaître. C’est occasionnellement utile en Python et peut produire des solutions très élégantes. C’est également une bonne chose à comprendre pour aborder d’autres langages plus fonctionnels qui les utilisent bien plus comme le Javascript, le Lisp, ou carrément le Haskell.

]]>
http://sametmax.com/introduction-au-currying/feed/ 3
map(), filter() et reduce () ? 9 http://sametmax.com/map-filter-et-reduce/ http://sametmax.com/map-filter-et-reduce/#comments Thu, 14 Nov 2013 09:44:58 +0000 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
]]>
http://sametmax.com/map-filter-et-reduce/feed/ 9
S’affranchir des doublons d’un itérable en Python 9 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 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.

]]>
http://sametmax.com/saffranchir-des-doublons-dun-iterable-en-python/feed/ 9
Ordonner en Python 17 http://sametmax.com/ordonner-en-python/ http://sametmax.com/ordonner-en-python/#comments Mon, 21 Jan 2013 13:37:37 +0000 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.

]]>
http://sametmax.com/ordonner-en-python/feed/ 17
Les listes en intension VS map() en Python 2 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 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 ().

]]>
http://sametmax.com/les-listes-en-intentions-vs-map-en-python/feed/ 2
Qu’est-ce qu’un callback ? 18 http://sametmax.com/quest-ce-quun-callback/ http://sametmax.com/quest-ce-quun-callback/#comments Wed, 22 Aug 2012 17:19:07 +0000 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).

]]>
http://sametmax.com/quest-ce-quun-callback/feed/ 18
Comment ne PAS utiliser une fonction anonyme (ou lambda) en Python 3 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 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.

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