# Les fonctions en profondeur : valeurs par défaut, fonctions comme objets, *args, **kwargs et lambda<no value>

## Qu'allons-nous apprendre ?

Dans le thème code propre et débogage, nous avons appris les bases : `def`, les paramètres, `return`. Aujourd'hui, nous allons apprendre quelques capacités qui rendent les fonctions beaucoup plus souples :

1. **Les valeurs par défaut** et **les arguments nommés**.
2. **Les fonctions comme objets** — une fonction est une valeur comme une autre.
3. **`*args`** — autant d'arguments ordinaires que l'on veut.
4. **`**kwargs`** — autant d'arguments nommés que l'on veut.
5. **Lambda** — une courte fonction sans nom.

## Pourquoi en avons-nous besoin ?

- `print("a", "b", "c", sep="-")` reçoit **autant d'arguments que l'on veut**, plus un argument **nommé**. Comment est-ce construit ? Avec `*args`.
- `sorted(students, key=...)` reçoit une **fonction** en paramètre, pour savoir selon quoi trier.
- **FastAPI** (les thèmes backend et bases de données) est entièrement construit sur l'idée qu'une fonction est un objet : vous écrivez une fonction, et FastAPI l'« enregistre » comme Endpoint et l'exécute quand une requête arrive.

## Explication à partir des bases

### Les valeurs par défaut

On peut donner à un paramètre une valeur qui sera utilisée **si aucun argument n'a été passé** :

```python
def greet(name, greeting="Hello"):
    return greeting + ", " + name
```

`greet("Dana")` → `"Hello, Dana"`. `greet("Dana", "Hi")` → `"Hi, Dana"`.

**Règle :** les paramètres avec une valeur par défaut viennent **après** les paramètres sans valeur par défaut.

### Les arguments nommés (Keyword Arguments)

Lors de l'appel, on peut indiquer le **nom** d'un paramètre, et alors l'ordre n'a plus d'importance :

```python
greet(greeting="Hey", name="Avi")
```

C'est très lisible quand il y a beaucoup de paramètres : `create_user("dana", age=24, is_admin=False)`.

### Les fonctions comme objets

En Python, **une fonction est un objet**, comme un nombre ou une liste. On peut donc :

- La stocker dans une variable : `say = greet`.
- La mettre dans une liste ou un dict.
- **La passer en argument** à une autre fonction.

**La différence essentielle :** `greet` (sans parenthèses) = **la fonction elle-même**. `greet("Dana")` (avec parenthèses) = **son exécution**, et son résultat.

{{< box "analogy" "Analogie : une recette face à un gâteau" >}}
`greet` est **la recette** : on peut la photographier et l'envoyer à un ami. `greet("Dana")` est **le gâteau** : le résultat de la cuisson selon la recette. Quand on passe une fonction à une autre fonction, on envoie la recette, pour qu'elle puisse cuisiner à partir d'elle quand elle en a besoin.
{{< /box >}}

### `*args` — un nombre variable d'arguments

Un astérisque devant le nom du paramètre dit : « rassemble **tous** les arguments ordinaires (par position) dans un **tuple** » :

```python
def total(*numbers):
    return sum(numbers)

total(1, 2)          # numbers = (1, 2)
total(5, 10, 20, 1)  # numbers = (5, 10, 20, 1)
```

Le nom `args` est une convention. C'est l'astérisque qui fait le travail.

### `**kwargs` — un nombre variable d'arguments nommés

Deux astérisques : « rassemble tous les arguments **nommés** dans un **dict** » :

```python
def describe(**details):
    for key, value in details.items():
        print(key, "=", value)

describe(name="Dana", age=24, city="Haifa")
# details = {"name": "Dana", "age": 24, "city": "Haifa"}
```

**kwargs** = **k**ey**w**ord **arg**ument**s**.

### Lambda

**lambda** est une façon d'écrire une **petite fonction, en une ligne, sans nom** :

```python
lambda x: x * 2
```

Équivalent à :

```python
def double(x):
    return x * 2
```

- Après `lambda` : les paramètres. Après `:` : **une seule expression** qui est la valeur renvoyée (sans `return`).
- L'usage principal : passer une fonction **courte** en argument, au lieu de définir un `def` séparé. Surtout avec `sorted`, `min`, `max`.

## Syntaxe

```python
# default value + keyword arguments
def func(required, optional="default"):
    ...
func("a")
func("a", optional="b")

# function as an object
f = func            # no parentheses: the function itself
f("a")              # call it through the new name

def apply(operation, value):
    return operation(value)    # call the function we received

# *args and **kwargs
def func(*args, **kwargs):
    # args   -> tuple of positional arguments
    # kwargs -> dict of keyword arguments
    ...

# lambda
lambda parameters: expression
sorted(items, key=lambda item: item["price"])
```

## Premier exemple de code

`week-2/functions_deep.py` :

```python
def make_greeting(name, greeting="Hello", punctuation="!"):
    return greeting + ", " + name + punctuation

print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punctuation="?"))

def shout(text):
    return text.upper()

def whisper(text):
    return text.lower() + "..."

def say(message, style):
    return style(message)

print(say("Hello there", shout))
print(say("Hello there", whisper))
```

**Sortie :**

```text
Hello, Dana!
Good morning, Avi!
Hello, Noa?
HELLO THERE
hello there...
```

## Analyse du code ligne par ligne

| Ligne | Explication |
|---|---|
| `def make_greeting(name, greeting="Hello", punctuation="!")` | `name` est obligatoire. Les deux autres sont facultatifs, avec des valeurs par défaut. |
| `make_greeting("Dana")` | Les deux paramètres facultatifs prennent leur valeur par défaut. |
| `make_greeting("Avi", "Good morning")` | Un deuxième argument **par position** va dans `greeting`. |
| `make_greeting("Noa", punctuation="?")` | **Un argument nommé** : on saute `greeting` (valeur par défaut) et on ne définit que `punctuation`. |
| `def say(message, style):` | `style` est un paramètre qui attend une **fonction**. |
| `return style(message)` | **L'exécution** de la fonction reçue. `say` ne sait pas et ne se soucie pas de quelle fonction il s'agit. |
| `say("Hello there", shout)` | On passe `shout` **sans parenthèses**, c'est-à-dire la fonction elle-même. |

## Un autre exemple

**`*args`, `**kwargs` et `lambda` dans un petit système réel.** `week-2/store_tools.py` :

```python
def order_total(*prices, discount=0):
    subtotal = sum(prices)
    return subtotal - subtotal * discount / 100

print(order_total(10, 20, 30))
print(order_total(100, 50, discount=10))

def build_user(username, **extra):
    user = {"username": username}
    for key, value in extra.items():
        user[key] = value
    return user

print(build_user("dana"))
print(build_user("avi", age=30, city="Haifa", is_admin=True))

products = [
    {"name": "Laptop", "price": 3500},
    {"name": "Mouse", "price": 80},
    {"name": "Desk", "price": 900},
]

by_price = sorted(products, key=lambda product: product["price"])
for product in by_price:
    print(product["name"], product["price"])

cheapest = min(products, key=lambda product: product["price"])
print("Cheapest:", cheapest["name"])
```

**Sortie :**

```text
60.0
135.0
{'username': 'dana'}
{'username': 'avi', 'age': 30, 'city': 'Haifa', 'is_admin': True}
Mouse 80
Desk 900
Laptop 3500
Cheapest: Mouse
```

**Analyse :**

- `def order_total(*prices, discount=0):` — `*prices` rassemble **n'importe quel** nombre de prix dans un tuple. `discount` vient **après** `*prices`, il ne peut donc être passé **que par son nom** (`discount=10`). Sinon, Python ne saurait pas où s'arrêtent les prix.
- `build_user("avi", age=30, ...)` — `username` reçoit `"avi"`, et tout le reste est rassemblé dans `extra` sous forme de dict : `{"age": 30, "city": "Haifa", "is_admin": True}`.
- `sorted(products, key=lambda product: product["price"])` — `sorted` ne sait pas comparer des dicts. Le `key` est une **fonction** qui lui dit : « pour chaque produit, utilise son prix pour comparer ». La lambda est appelée une fois pour chaque produit.
- `min(..., key=...)` — la même idée : « le minimum selon le prix ». Il renvoie **le produit entier**, pas seulement le prix.

## Erreur courante

**Erreur 1 : passer un résultat au lieu d'une fonction.**

```python
print(say("Hello", shout()))
```

```text
TypeError: shout() missing 1 required positional argument: 'text'
```

`shout()` avec des parenthèses essaie d'**exécuter** `shout` immédiatement, sans argument. Quand vous passez une fonction, écrivez **seulement son nom** : `shout`.

**Erreur 2 : une liste ou un dict comme valeur par défaut.**

```python
def add_item(item, cart=[]):
    cart.append(item)
    return cart

print(add_item("milk"))
print(add_item("bread"))
```

```text
['milk']
['milk', 'bread']
```

Nous attendions `['bread']` au second appel ! La valeur par défaut `[]` est créée **une seule fois**, quand la fonction est définie, et tous les appels **partagent la même liste** (Mutable, la leçon précédente !). La correction en usage :

```python
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart
```

**Erreur 3 : une lambda trop compliquée.**

```python
result = sorted(users, key=lambda u: (0 if u["active"] else 1, -u["score"], u["name"].lower()))
```

Cela fonctionne, mais personne ne le comprendra dans une semaine (KISS !). Si une lambda n'est pas lisible d'un coup d'œil, écrivez un `def` avec un nom clair.

**Erreur 4 : un paramètre avec valeur par défaut avant un paramètre obligatoire.**

```python
def greet(greeting="Hello", name):
```

```text
SyntaxError: parameter without a default follows parameter with a default
```

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## Exercices

Les exercices n'utilisent que ce qui a été vu dans cette leçon et les précédentes. Essayez d'abord seul, et n'ouvrez la solution qu'ensuite.

### Exercice 1: Valeurs par défaut et arguments nommés

Écrivez une fonction `make_greeting(name, greeting="Hello", punct="!")`.

```python
print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))
```

**Résultat attendu :**

```text
Hello, Dana!
Good morning, Avi!
Hello, Noa?
```

{{< solution >}}
```python
def make_greeting(name, greeting="Hello", punct="!"):
    return greeting + ", " + name + punct


print(make_greeting("Dana"))
print(make_greeting("Avi", "Good morning"))
print(make_greeting("Noa", punct="?"))
```

```text
Hello, Dana!
Good morning, Avi!
Hello, Noa?
```

Dans le troisième appel, `punct="?"` est passé **par son nom**, on peut donc sauter `greeting` et lui laisser sa valeur par défaut.
{{< /solution >}}

### Exercice 2: *args

Écrivez une fonction `average(*numbers)` qui reçoit un nombre quelconque de nombres et renvoie la moyenne. Si aucun nombre n'a été passé — elle lance une `ValueError` avec le message indiqué dans le résultat attendu.

```python
print(average(80, 90))
print(average(70, 80, 90, 100))
try:
    average()
except ValueError as error:
    print("error:", error)
```

**Résultat attendu :**

```text
85.0
85.0
error: need at least one number
```

{{< solution >}}
```python
def average(*numbers):
    if len(numbers) == 0:
        raise ValueError("need at least one number")
    return sum(numbers) / len(numbers)


print(average(80, 90))
print(average(70, 80, 90, 100))
try:
    average()
except ValueError as error:
    print("error:", error)
```

```text
85.0
85.0
error: need at least one number
```

`*numbers` rassemble tous les arguments par position dans un **tuple**.
{{< /solution >}}

### Exercice 3: `**kwargs`

Écrivez une fonction `format_profile(name, **details)` qui renvoie une ligne avec le nom, suivi de chaque détail supplémentaire au format `key=value`.

```python
print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))
```

**Résultat attendu :**

```text
Dana: age=24 city=Haifa
Avi:
```

{{< solution >}}
```python
def format_profile(name, **details):
    line = name + ":"
    for key, value in details.items():
        line = line + " " + key + "=" + str(value)
    return line


print(format_profile("Dana", age=24, city="Haifa"))
print(format_profile("Avi"))
```

```text
Dana: age=24 city=Haifa
Avi:
```

`**details` rassemble tous les arguments **nommés** dans un **dict**.
{{< /solution >}}

### Exercice 4: lambda dans un tri

```python
products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]
```

1. Triez par prix, du moins cher au plus cher, et affichez chaque produit.
2. Affichez le nom du produit le plus cher.

**Résultat attendu :**

```text
Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop
```

{{< solution >}}
```python
products = [("Laptop", 3500), ("Mouse", 80), ("Desk", 900)]

for name, price in sorted(products, key=lambda p: p[1]):
    print(name, price)

most_expensive = max(products, key=lambda p: p[1])
print("Most expensive:", most_expensive[0])
```

```text
Mouse 80
Desk 900
Laptop 3500
Most expensive: Laptop
```

`lambda p: p[1]` est une courte fonction qui renvoie le prix — `sorted` et `max` comparent selon elle.
{{< /solution >}}

### Exercice 5: Une valeur par défaut mutable

```python
def add_item(item, cart=[]):
    cart.append(item)
    return cart

print(add_item("milk"))
print(add_item("bread"))
```

1. Qu'est-ce qui sera affiché au second appel, et pourquoi ?
2. Corrigez la fonction.

{{< solution >}}
1. `['milk', 'bread']` sera affiché. La valeur par défaut `[]` est créée **une seule fois**, quand la fonction est définie — et tous les appels partagent **la même** liste.
2. Utilisez `None`, et créez une nouvelle liste dans la fonction :

```python
def add_item(item, cart=None):
    if cart is None:
        cart = []
    cart.append(item)
    return cart


print(add_item("milk"))
print(add_item("bread"))
```

```text
['milk']
['bread']
```
{{< /solution >}}

<!-- exercises:end -->

## Résumé

- **Une valeur par défaut** : `def f(a, b=10)`. Les paramètres avec une valeur par défaut viennent en dernier. Jamais `[]` ou `{}` comme valeur par défaut (utilisez `None`).
- **Un argument nommé** : `f(a=1, b=2)`. Lisible, et l'ordre n'a pas d'importance.
- **Une fonction est un objet** : `f` = la fonction, `f()` = son exécution. On peut la stocker dans une variable, dans un dict, et la passer en argument.
- `*args` rassemble les arguments ordinaires dans un **tuple**. `**kwargs` rassemble les arguments nommés dans un **dict**.
- `lambda x: expression` est une courte fonction sans nom. Parfaite pour le `key=` de `sorted`/`min`/`max`. Si elle est compliquée, un `def` est préférable.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Documentation officielle

- [Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
- [Arbitrary Argument Lists](https://docs.python.org/3/tutorial/controlflow.html#arbitrary-argument-lists)
- [Keyword Arguments](https://docs.python.org/3/tutorial/controlflow.html#keyword-arguments)
- [Lambda Expressions](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions)

<!-- docs:end -->
