Dans ce TD, nous allons explorer les outils puissants tels que map
, filter
, iterators
, generators
et decorators
(ce qui font partie de la programmation fonctionnelle) en Python.
Rappelons que les fonctions lambda
sont des objets de fonction anonymes, sans nom, créés à la volée, généralement pour accomplir une petite transformation. Par exemple, les fonctions lambda
Considérons la fonction suivante :
(lambda val: val ** 2)(5) # => 25
(lambda x, y: x * y)(3, 8) # => 24
(lambda s: s.strip().lower()[:2])(' PyTHon') # => 'py'
En soi, les lambda
s ne sont pas particulièrement utiles, comme démontré ci-dessus. Habituellement, les lambda
s sont utilisés pour éviter de créer une définition de fonction formelle pour de petites fonctions jetables, non seulement parce qu'elles impliquent moins de codes (pas besoin de déclaration def
ou return
) mais aussi, et peut-être plus important, parce que ces petites fonctions ne pollueront pas l'espace de noms environnant.
Les lambdas sont aussi fréquemment utilisés comme arguments ou pour retourner des valeurs de fonctions d'ordre supérieur, comme map
et filter
.
Rappelez-vous de la classe que map(func, iterable)
applique une fonction sur les éléments d'un iterable.
Pour chacune des lignes suivantes, écrivez une instruction en utilisant map
qui convertit la colonne de gauche en colonne de droite :
Entrées | Sorties |
---|---|
['12', '-2', '0'] |
[12, -2, 0] |
['hello', 'world'] |
[5, 5] |
['hello', 'world'] |
['olleh', 'dlrow'] |
range(2, 6) |
[(2, 4, 8), (3, 9, 27), (4, 16, 64), (5, 25, 125)] |
zip(range(2, 5), range(3, 9, 2)) |
[6, 15, 28] |
Conseil : vous pouvez avoir besoin d'envelopper la sortie dans un constructeur list()
pour la voir affichée - c'est-à-dire, list(map(..., ...))
La fonction map
peut accepter un nombre variable d'itérables comme arguments. Ainsi, map(func, iterA, iterB, iterC)
est équivalent à map(func, zip(iterA, iterB, iterC))
. Cela peut être utilisé comme suit :
map(int, ('10110', '0xCAFE', '42'), (2, 16, 10)) # generates 22, 51966, 42
Cela fonctionne parce que int
prend un second argument optionnel spécifiant la base de conversion
Rappelez-vous que filter(pred, iterable)
ne conserve que les éléments d'un iterable qui satisfont une fonction de prédicat.
Écrivez des instructions en utilisant filter
qui convertissent les séquences suivantes de la colonne de gauche à la colonne de droite :
Entrées | Sorties |
---|---|
['12', '-2', '0'] |
['12', '0'] |
['hello', 'world'] |
['world'] |
['Stanford', 'Cal', 'UCLA'] |
['Stanford'] |
range(20) |
[0, 3, 5, 6, 9, 10, 12, 15, 18] |
Le rappel de classe qu'un itérateur est un objet qui représente un flux de données renvoyé une valeur à la fois.
Supposons que les deux lignes de code suivantes aient été exécutées :
it = iter(range(100))
67 in it # => True
Quel est le résultat de chacune des lignes de code suivantes ?
next(it) # => ??
37 in it # => ??
next(it) # => ??
itertools
Python est équipé d'un module spectaculaire pour la manipulation d'itérateurs appelés itertools
. Prenez un moment pour lire la page de documentation des itertools.
Prévoyez la sortie des morceaux de code suivants :
import itertools
import operator
for el in itertools.permutations('XKCD', 2):
print(el, end=', ')
for el in itertools.cycle('LO'):
print(el, end='') # N'execute pas, pourquoi?
itertools.starmap(operator.mul, itertools.zip_longest([3,5,7],[2,3], fillvalue=1))
Rappelons que les expressions génératrices sont un moyen de calculer paresseusement des valeurs à la volée, sans mettre en mémoire tampon tout le contenu de la liste en place.
Pour chacun des scénarios suivants, voyez s'il serait plus approprié d'utiliser une expression génératrice ou une compréhension de liste :
Ecrire un générateur infini qui donne successivement les nombres du triangle 0, 1, 3, 6, 10, ...
.
def generer_triangles():
pass # Votre code ici
Utilisez votre générateur pour écrire une fonction triangles_inf(n)
qui affiche tous les nombres de triangles strictement inférieurs au paramètre n
.
Qu'est-ce fait le code suivant? Est-ell efficace?
def primes_under(n):
tests = []
for i in range(2, n):
if not any(map(lambda test: test(i), tests)):
tests.append(make_divisibility_test(i))
yield i
Comment modifieriez-vous le code ci-dessus pour obtenir tous les nombres composés, plutôt que tous les nombres premiers ? Testez votre solution. Qu'est-ce que le 1000e nombre composite ?
En classe, nous avons vu que les fonctions peuvent être définies dans le cadre d'une autre fonction (rappel de la semaine précédente que les fonctions introduisent de nouvelles portées via un nouveau tableau de symboles locaux). Une fonction interne n'a de portée qu'à l'intérieur de la fonction externe, donc ce type de définition de fonction n'est généralement utilisé que lorsque la fonction interne est renvoyée au monde extérieur.
def outer():
def inner(a):
return a
return inner
f = outer()
print(f) # <function outer.<locals>.inner at 0x1044b61e0>
f(10) # => 10
f2 = outer()
print(f2) # <function outer.<locals>.inner at 0x1044b6268> (Différent au précédent)
f2(11) # => 11
Pourquoi les adresses mémoire sont-elles différentes pour f
et f2
?
Comme nous l'avons vu plus haut, la définition de la fonction interne intervient pendant l'exécution de la fonction externe. Cela implique qu'une fonction imbriquée a accès à l'environnement dans lequel elle a été définie. Par conséquent, il est possible de renvoyer une fonction interne qui se souvient de l'état de la fonction externe, même après que la fonction externe a terminé son exécution. Ce modèle est appelé une fermeture.
def make_adder(n):
def add_n(m): # Capturer la variable externe n dans une fermeture
return m + n
return add_n
add1 = make_adder(1)
print(add1) # <function make_adder.<locals>.add_n at 0x103edf8c8>
add1(4) # => 4
add1(5) # => 6
add2 = make_adder(2)
print(add2) # <function make_adder.<locals>.add_n at 0x103ecbf28>
add2(4) # => 6
add2(5) # => 7
Les informations contenues dans une fermeture sont disponibles dans l'attribut __closure__
de la fonction. Par exemple :
closure = add1.__closure__
cell0 = closure[0]
cell0.cell_contents # => 1 (ici n = 1 est passé dans make_adder)
Autre exemple :
def foo(a, b, c=-1, *d, e=-2, f=-3, **g):
def wraps():
print(a, c, e, g)
L'appel print
induit une fermeture des wraps
sur a
, c
, e
, g
du champ d'application de foo
. Ou bien, vous pouvez imaginer que wraps
"sait" qu'il aura besoin de a
, c
, e
, et g
de la portée englobante, donc au moment où wraps
est défini, Python prend une "capture d'écran" de ces variables de la portée englobante et stocke les références aux objets sous-jacents dans l'attribut __closure__
de la fonction wraps
.
w = foo(1, 2, 3, 4, 5, e=6, f=7, y=2, z=3)
list(map(lambda cell: cell.cell_contents, w.__closure__))
# = > [1, 3, 6, {'y': 2, 'z': 3}]
Que se passe-t-il dans la situation suivante ? Pourquoi ?
def outer(l):
def inner(n):
return l * n
return inner
l = [1, 2, 3]
f = outer(l)
print(f(3)) # => ??
l.append(4)
print(f(3)) # => ??
Rappelons qu'un décorateur est un type spécial de fonction qui accepte une fonction comme argument et (généralement) renvoie une version modifiée de cette fonction. En classe, nous avons vu le "débogueur" décorateur - revoyez les diapositives si vous vous sentez toujours mal à l'aise avec l'idée d'un décorateur.
De plus, rappelez-vous que la syntaxe @decorator
:
@decorator
def fn():
pass
est équivalent à
def fn():
pass
fn = decorator(fn)
Dans le cours, nous avons mis en place le décorateur debug
.
def debug(function):
def wrapper(*args, **kwargs):
print("Arguments:", args, kwargs)
return function(*args, **kwargs)
return wrapper
Assurez-vous de bien comprendre ce qui se passe dans les lignes ci-dessus. Pourquoi les arguments sur la deuxième ligne sont-ils *args
et **kwargs
au lieu d'autre chose ? Que se passerait-il si nous ne faisions pas return wrapper
à la fin de la fonction ?
Ecrivez une fonction de décorateur cache
qui mettra automatiquement en cache tout appel à la fonction décorée.
def cache(function):
pass # Votre code ici
Par exemple,
@cache
def fib(n):
return fib(n-1) + fib(n-2) if n > 2 else 1
fib(10) # 55 (prendre un moment avant d'exécution)
fib(10) # 55 (retourner immédiatement)
fib(100) # ne boucle pas infiniment
fib(400) # ne retourne pas RuntimeError
Conseil : Vous pouvez définir des attributs arbitraires sur une fonction (par exemple fn._cache
). Lorsque vous faites cela, la paire attribut-valeur est également insérée dans fn.__dict__
. Regardez par vous-même. Les attributs supplémentaires et .__dict__
sont-ils toujours synchrones ?
print_args
La fonction de décorateur debug
que nous avons écrit en classe n'est pas très bon. Il ne nous dit pas quelle fonction est appelée, et il nous donne juste un tas d'arguments de position et un dictionnaire d'arguments de mots-clés - il ne sait même pas quels sont les noms des arguments de position ! Si les arguments par défaut ne sont pas écrasés, il ne nous indique pas non plus leur valeur.
Utilisez les attributs de fonction pour améliorer notre décorateur debug
en un décorateur print_args
qui est aussi bon que vous pouvez le faire.
def print_args(function):
def wrapper(*args, **kwargs):
# (1) Vous pouvez faire qqc ici
retval = function(*args, **kwargs)
# (2) Vous pouvez faire qqc ici
return retval
return wrapper
Conseil : pensez à utiliser les attributs fn.__name__
et fn.__code__
. Vous devrez étudier ces attributs, mais je dirai que l'objet code fn.__code__
contient un certain nombre d'attributs utiles - par exemple, fn.__code__.co_varnames
. Regardez ça ! Plus d'informations sont disponibles dans la seconde moitié de TD3.