% Programmation avec Python (chapitre 13)
% Dimitri Merejkowsky

#

\center \huge Rappels

# Virtualenvs

* Création:

```
$ python3 -m venv /path/to/foo
```

* Utilisation:

```
$ /path/to/foo/bin/pip install requests
# ou
$ source /path/to/foo/bin/activate
(foo) $ pip install requests
```


# Tests

```python
# foo.py
def add_3(x):
	return x + 3

# test_foo.py
import foo

def test_add_3():
	result = foo.add_3(2)
	assert result == 5
```

# pytest

```
$ pytest test_foo.py
============== test session starts =================
test_foo.py .                                 [100%]
=========== 1 passed in 0.01 seconds ===============
```

# TDD

* Une discipline
* 4 règles
* Un cycle


# Règles


* Règle 1 : Il est interdit d'écrire du code de production, *sauf* si c'est pour faire passer un test qui
  a échoué.
* Règle 2 : Il est interdit d'écrire plus de code que celui qui est nécessaire pour provoquer une erreur
  dans les tests (n'importe quelle erreur)
* Règle 3 : Il est interdit d'écrire plus de code que celui qui est nécessaire pour faire passer
  un test qui a échoué
* Règle 4 : Une fois que tous les tests passent, il est interdit de modifier le code sans s'arrêter
  pour considérer la possibilité d'un refactoring.

# Cycle


* RED: on écrit un test qui échoue
* GREEN: on fait passer le test
* REFACTOR: on refactor le code de production et le code de test

#

\center \huge Générateurs

\vfill

\normalsize Ces objects qui sont "presque" des listes ...

# Retour sur les boucles

Itération sur une liste
```python
for i in [1, 2, 3]:
    print(i)
```

Clés d'un dictionnaire
```python
for key in {"a": 1, "b": 2, "c": 3}.keys():
	print(key)
```

Valeurs d'un dictionnaire
```python
for value in {"a": 1, "b": 2, "c": 3}.values():
	print(key)
```

# Pas des listes

```python
>>> my_dict = {"a": 1, "b": 2, "c": 3}
>>> my_dict.keys()
dict_keys(['a', 'b', 'c'])
>>> my_dict.keys()[0]
TypeError: 'dict_keys' object is not subscriptable
```

# Vues

En fait, `.keys()`, `.values()`, `.items()` renvoient des *vues*.

Elles changent quand le dictionnaire changent, mais on ne peut pas
s'en servir pour changer la taille du dictionnaire.

```python
>>> my_dict = {"a": 1, "b": 2, "c": 3}
>>> for k in my_dict.keys():
>>> 	if k == "a":
>>> 		del my_dict[k]
RuntimeError: dictionary changed size during iteration
```

# Vues (2)

Si vraiment vous en avez besoin, vous pouver les convertir en liste
avec `list()`.

```python
>>> my_dict = {"a": 1, "b": 2, "c": 3}
>>> for k in list(my_dict.keys()):
>>> 	if k == "a":
>>> 		del my_dict[k]
>>> my_dict
{"b": 2, "c": 3}
```

# Listes par compréhension

```python
# 1
sequence = ["a", "b", "c"]
new_sequence = []
for element in sequence:
	  new_sequence.append(element.upper())


# 2
sequence = ["a", "b", "c"]
new_sequence = [element.upper() for element in sequence]
```

# Listes par compréhension (2)

On peut transformer la séquence originale:

```python
>>> sequence = [element.upper() for element in sequence]
>>> sequence
["A", "B", "C"]
```

\vfill

On peut changer le type:

```python
>>> as_strings = ["1", "2", "3"]
>>> numbers = [int(x) for x in as_strings]
>>> numbers
[1, 2, 3]
```

# Filtrer une liste

```python
>>> numbers = [1, 2, 3, 4]
>>> odds = [x for x in numbers if x % 2 == 1]
>>> odds
[1, 3]
```

\vfill

```python
>>> numbers = [1, 2, 3, 4]
>>> odds_squared = [x*x for x in numbers if x % 2 == 1]
>>> odds_squared
[1, 9]
```

# Dictionnaires et ensembles par compréhension

Même principe:

```python
>>> scores = [("bob", 2), ("alice", 3)]
>>> my_dict = {t[0]:t[1] for t in scores}
>>> my_dict
{"bob": 2, "alice": 3}
```

\vfill

```python
>>> numbers = [-1, 2, -3, 3]
>>> numbers = {abs(x) for x in numbers}
>>> numbers
{1, 2, 3}
```

# Avantage des compréhensions

* Code plus succint
* Code plus rapide :)
* Code "idiomatique"

# Apparté: Python est gourmand

Python évalue d'habitude de manière "gourmande":

```python
def foo():
	..


def bar():
	...


x = foo(bar(y))
```

On calcule d'abord `y`, puis `bar(y)` puis `foo(bar(y))`


# Générateurs

Concept: fournir les valeurs une par une, à la demande.

Le code *à l'intérieur* des compréhensions est un générateur.

On peut le voir si on utilise des parenthèses:

```python
>>> generator =  (x for x in range(0, 3))
>>> generator
<generator object <genexpr> at 0x7f654c272138>
>>> list(generator)
[0, 1, 2]
```

Les générateurs sont "feignants": ils ne calculent leur valeurs que quand
c'est demandé

# Les générateurs s'épuisent

```python
>>> generator =  (x for x in range(0, 3))
>>> list(generator)
[0, 1, 2]
>>> list(generator)
[]
```

# next()

On peut aussi appeler `next()` sur un générateur, au lieu de `for ... in`

```python
>>> generator =  (x for x in range(0, 2))
>>> next(generator)
0
>>> next(generator)
1
>>> next(generator)
StopIteration
```




# Fabriquer un générateur à l'aide d'une classe

Avec une méthode `__next__()`

```python
class Squares:
	def __init__(self):
		self.value = 0

	def __next__(self):
		self.value += 1
		return self.value ** 2
```

```python
>>> squares = Squares()
>>> next(squares)
1
>>> next(squares)
4
>>> next(squares)
9
```


# Les générateurs ne sont pas des itérateurs

```python
>>> [x for x in s if x < 10]
TypeError: 'Squares' object is not iterable
```

Pour faire `for in`, il faut un itérateur, en plus d'un générateur


# Fabriquer un itérateur à l'aide d'une classe

Avec `__iter__` *et* `__next__`:

```python
class SquaresLessThan:
	def __init__(self, stop_value):
		self.value = 0
		self.stop_value = stop_value

	def __iter__(self):
		return self

	def __next__(self):
		self.value += 1
		res = self.value ** 2
		if res > self.stop_value:
			raise StopIteration()
		return res
```



# Fabriquer un itérateur à l'aide d'une classe

```python
>>> squares = SquaresLessThan(100)
>>> [x for x in squares]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
```

On retrouve les propriétés des expressions génératrices:
```python
>>> squares = SquaresLessThan(100)
>>> squares[2]
TypeError: 'SquaresLessThan' object is not subscriptable
>>> squares = SquaresLessThan(100)
>>> list(squares)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
>>> list(squares)
[]
```

# yield

On peut aussi faire des générateurs avec `yield`
pour mettre le générateur "en pause", et `return` pour
terminer:


```python
def squares(max_value):
    x = 1
    res = 1
    while res <= max_value:
        yield res
        x += 1
        res = x * x
    return

>>> s = squares(100)
>>> [x for x in s]
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
```

# Combiner avec une classe génératrices

```python
class Squares:
    def __init__(self):
        self.value = 0

    def __next__(self):
        self.value += 1
        return self.value ** 2

def squares_less_than(max_value):
    squares = Squares()
    while True:
        x = next(squares)
        if x > max_value:
                return
        yield x
```

# yield from

On peut aussi chaîner les fonctions génératrices

```python
def integers(max_value):
	x = 0
	while x < max_value:
		yield x
		x += 1
	return


def zero_to_five():
	yield from integers(5)


>>> s = zero_to_five()
>>> [x for x in s]
[0, 1, 2, 3, 4]
```

#

\center \huge Consignes


#

* Repartir de git.e2li.org/dmerejkowsky/cours-python/sources/lister

* Ajouter le code pour gérer correctement l'attribut <is_directory> de l
  class Entry

* Utiliser TDD pour la suite!

* Faire en sorte d'afficher les répertoires avec un `/` à la fin
* Afficher la date de modification de manière plus lisible
  (voir `time` dans la bibliothèque standard)
* Grouper les éléments sur plusieurs colonnes
* Implémenter d'autres options (`--sort=time`, etc.)
* Réécrire parse_args() en utilisant `argparse`.

# Solutions et questions

Envoyez-moi un email à dimitri@e2li.org !
