3.3Advanced functions: decorators, generators, lambdas, map/filter
The essentials in 30 seconds
- Lambda = one-line throwaway anonymous function: key=lambda t: t[1] to sort — def as soon as it gets a name or is reused.
- The decorator wraps without touching: @chrono adds measurement, logging or checks by receiving the function and returning a wrapper — with functools.wraps.
- The generator produces on demand: yield processes a huge file in constant memory — lists for the small, generators for the large.
Running mini-case — Nabil goes idiomatic
Nabil, junior data analyst in Algiers, sorts his sales with three-line single-use functions and loads a multi-GB export as a list — his machine crawls. He sorts in one line (key=lambda v: v["montant"]), compares map/filter with comprehensions, reads the export line by line via a yield generator, and times his processing with a @chrono decorator using functools.wraps. Same data, short code and a relieved machine.
This same case returns in the lesson, the diagram, the script and the 48h challenge.
1. Lambda, map/filter: the small toolkit
A lambda is a one-line anonymous function, made for throwaway use as an argument: sorted(ventes, key=lambda v: v["montant"]) sorts on a criterion without naming a function. map applies a function to each element, filter keeps only those meeting a condition — but the comprehension ([f(x) for x in xs if ok(x)]) is often more readable in Python.
Nabil's rule: lambda for short throwaway, def as soon as it gets a name, is reused or exceeds one line. A five-line lambda is a disguised def: name it, document it, test it. This toolkit shows up everywhere (sort keys, callbacks) — knowing it means reading other people's code fluently.
2. Decorators and generators: wrap and lighten
A decorator enriches a function without modifying its code: it receives it, returns a wrapper adding measurement, logging, caching or checks, via the @decorateur syntax. Always with functools.wraps to preserve name and docstring. Nabil writes a @chrono that prints execution time — and recognises the mechanism behind @app.route or @pytest.fixture.
A generator (a function with yield) produces its values one by one, on demand: suspended at each yield, resumed at the next call. A multi-GB export reads line by line in constant memory instead of blowing up RAM. Lists for the small and reused, generators for the large or infinite: that is the choice that scales.
The list that ate the RAM
Big stream, small yield: your memory thanks you.
🔎 Concrete example — Time without touching + read without loading
Nabil times his processing and reads a large export line by line.
import time
import functools
def chrono(f):
@functools.wraps(f)
def enveloppe(*args, **kwargs):
debut = time.perf_counter()
resultat = f(*args, **kwargs)
duree = time.perf_counter() - debut
print(f"{f.__name__}: {duree:.2f}s")
return resultat
return enveloppe
def lignes(fichier):
with open(fichier, encoding="utf-8") as f:
for ligne in f:
yield ligne.strip()
Reading: @chrono measures without touching the body; yield only loads one line at a time.
Lesson: wraps preserves the name and docstring of the decorated function.
Section vocabulary
- Lambda
- One-line anonymous function (lambda x: x * 2), useful for short processing passed as an argument.
Simple example: Nabil's key=lambda v: v["montant"] sort.
What it is not: a named, reused, documented function.
Reflex: throwaway lambda, def as soon as it gets a name.
- map / filter
- map applies a function to each element; filter keeps only those meeting a condition.
Simple example: map(lambda x: x ** 2, montants) then filtering evens at Nabil's.
What it is not: a comprehension, often more readable.
Reflex: compare with the comprehension before choosing.
- Generator
- A yield function producing its values one by one, on demand: minimal memory, ideal for large streams.
Simple example: Export lines read one by one by Nabil.
What it is not: a giant list loaded at once into RAM.
Reflex: yield as soon as it is large or infinite.
- yield
- Returns a value while suspending the function, resumed at the next call: the heart of generators.
Simple example: Each yield ligne.strip() of Nabil's export.
What it is not: a return ending the function.
Reflex: iterate without loading everything.
- Decorator
- A function wrapping another to add behaviour (timer, log, check), via @decorateur and functools.wraps.
Simple example: Nabil's @chrono on his processing steps.
What it is not: copying measurement code into each function.
Reflex: factor cross-cutting code into a decorator.
In practice — Write idiomatic Python
- Sort tuples by 2nd element with sorted(..., key=lambda t: t[1]) on Nabil's sales.
- Transform with map(lambda x: x ** 2, nombres) and filter with filter(lambda x: x % 2 == 0, nombres), then compare with comprehensions.
- Write a yield generator producing infinite evens; consume 10 of them with break.
- Create a @chrono decorator with functools.wraps that prints execution time, then apply it.
Ready-to-say script — presenting your idiomatic code (30 s)
“Throwaway lambda sort on the amount, yield generator for the bulky export in constant memory, chrono decorator with wraps to measure without touching. Map and filter compared with comprehensions case by case.”
Say in review; show the @chrono and the yield.
Copy-paste template — chrono decorator + generator
import time
import functools
def chrono(f):
@functools.wraps(f)
def enveloppe(*args, **kwargs):
debut = time.perf_counter()
resultat = f(*args, **kwargs)
print(f"{f.__name__}: {time.perf_counter() - debut:.2f}s")
return resultat
return enveloppe
@chrono
def total(montants):
return sum(montants)
ventes_triees = sorted([{"v": "Alger", "m": 9}, {"v": "Oran", "m": 4}], key=lambda v: v["m"])
Independent
One @chrono + one lambda sort is enough to track report performance.
SME in Algiers
Nabil reads the multi-GB checkout export as a generator without changing machine.
Product team
Enforce wraps in every decorator to keep names and docs usable.
Beginner
This week: one lambda sort and one infinite evens generator.
✓ Best practices
- Reserve lambda for short throwaway; name a def as soon as it is reused.
- Add functools.wraps in every decorator (name and doc preserved).
- Switch to yield as soon as data is large or infinite.
✗ Common pitfalls
- Writing a five-line unreadable lambda instead of a def.
- Forgetting wraps (lost name, broken introspection).
- Loading a giant file as a list instead of a generator.
Key takeaways
- LAMBDA = ONE-line anonymous function, as an argument (sorted key…); otherwise def.
- DECORATOR = wrapper WITHOUT touching (@ + wraps): factored measurement, log, checks.
- GENERATOR (yield) = ON-DEMAND production, constant memory: the choice for the large.
- These tools are EVERYWHERE in frameworks: handling them means reading pros' code.
48h challenge — apply tomorrow
Write 1 @chrono decorator with wraps and 1 yield generator on your own data.
- I sort in lambda with no named throwaway function.
- My decorator preserves name and doc via wraps.
- My large stream goes through a generator, not a list.
Frequently asked questions
When should you prefer a generator over a list?
When data is voluminous or infinite: a list loads everything into memory, a generator produces one value at a time. To read a multi-GB file line by line, the generator is essential; for a few reused items, a list is enough.
What is a decorator concretely for?
To add repetitive behaviour (timer, log, check, cache) to several functions without copying it everywhere. You already use some: @app.route, @staticmethod, @pytest.fixture — same mechanism, same elegance.
Why functools.wraps in a decorator?