3.3 · Advanced functions: decorators, generators, lambdas, map/filter

Level 3 · Intermediate Python

3.3Advanced functions: decorators, generators, lambdas, map/filter

Goal: write idiomatic, performant Python (lambdas, map/filter, yield generators, @ decorators) — the tools of professional code and frameworks.
Estimated time: 16 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: “Lambda everywhere to keep it short, decorator = magic, list = always.” No. A long lambda becomes unreadable — name a def; a decorator without wraps loses name and doc and breaks introspection; a giant list loads everything into memory. Throwaway in lambda, cross-cutting in a decorator with wraps, large in a generator: the right tool, not the shortest.
Advanced functions: the right tool Throwaway in lambda, cross-cutting in decorator, large in generator lambda (throwaway)Sort in one line, def otherwise+Decorator (cross-cutting)Wrapper + wraps, untouched+yield (lazy)One item at a time, constant memory Lists for the small, generators for the large.
Throwaway in lambda, cross-cutting in decorator, large in generator
Text version of the diagram (SEO / screen readers / translation): 1) Throwaway: Nabil sorts his sales by amount with key=lambda in one line, with no named function. 2) Cross-cutting: his @chrono wraps each processing step to measure, untouched, with wraps. 3) Large: the multi-GB export reads via yield, one line at a time. Lesson: each form has its ground — short, cross-cutting, voluminous.

The list that ate the RAM

“My 8 GB export as a list: my machine ran hot all afternoon in Algiers.”
Generator learned that same evening, fan back at rest

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

  1. Sort tuples by 2nd element with sorted(..., key=lambda t: t[1]) on Nabil's sales.
  2. Transform with map(lambda x: x ** 2, nombres) and filter with filter(lambda x: x % 2 == 0, nombres), then compare with comprehensions.
  3. Write a yield generator producing infinite evens; consume 10 of them with break.
  4. Create a @chrono decorator with functools.wraps that prints execution time, then apply it.
You handle lambdas, map/filter, generators and decorators: idiomatic, concise, efficient code.

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.

Section exit checklist
  • 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.
Self-rating 1-5: does each lambda fit in one readable line?does one large processing still go through 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.

Check your understanding

Why functools.wraps in a decorator?

Additional resources (optional, to go further)

Level 3 examFresh, harder questions to test what you've learned