Python Metaprogramming Quiz: Decorators and Descriptors

Test your Python metaprogramming with 12 questions and three programs on decorators, closures, descriptors, attribute hooks, metaclasses and a Vector type.

  • Course: Python study plan
  • Module: Decorators, descriptors and the data model
  • Kind: Checkpoint — cleared at 70%
  • Reading time: 25 min
  • Runtime: CPython 3.11

Checkpoint — Decorators, descriptors and the data model is the checkpoint that closes the Decorators, descriptors and the data model module: a graded quiz and whole-program exercises, passed at 70%.

Instructions

This checkpoint covers the whole module: decorators as f = deco(f) with @wraps, arguments and stacking, closures with cells, late binding and factories, the descriptor protocol behind properties, methods and validated attributes, the attribute hooks __getattr__, __getattribute__ and __setattr__ with reflection and __slots__, classes as objects with type, __new__, __init_subclass__ and metaclasses, and the rest of the data model through a Vector.

How it works. Twelve questions and three programs. You need 70% on the questions and every program accepted to clear the module. You can retake it as often as you like; your best score counts.

Before you start, make sure you can answer these from memory:

  • What does @retry(3) above a def expand to, and how many nested functions does retry need?
  • Why do three lambdas created in a loop all see the same i, and what are the two fixes?
  • What is the difference between a data and a non-data descriptor, and which one is property?
  • When is __getattr__ called, and what must it raise for names it does not handle?
  • What does __init_subclass__ receive, and what is it for?
  • What should a binary dunder return for an operand it does not understand?
  • Why must __setattr__ write through object.__setattr__?

The three programs are a decorator suite — timing-free tracing, counting and memoising — stacked in both orders, a small validation framework built from descriptors with __set_name__ and a registering __init_subclass__, and a complete Vector value type exercised through arithmetic, formatting and containment.

Common questions

What does @retry(3) above a def expand to?

f = retry(3)(f): retry(3) is called first and returns the decorator, which then wraps the function. So retry needs three nested levels: the factory taking the arguments, the decorator taking the function, and the wrapper that runs on each call.

When is __getattr__ called, and what must it raise?

Only when normal attribute lookup, through the instance dict, the class, descriptors and bases, has failed. For names it does not handle it must raise AttributeError, so hasattr and getattr with a default keep working.

Why must __setattr__ write through object.__setattr__?

Because __setattr__ runs on every assignment, self.name = value inside it would call it again and recurse forever. Writing through object.__setattr__(self, name, value) stores the value without re-entering the hook.

Exercises

Stacked both ways

Write traced (prints call <name>(<n>) on each invocation) and memoized (caches by argument), both with @wraps. Define fib twice: fib_a as @traced over @memoized, and fib_b as @memoized over @traced; each computes Fibonacci recursively through its own decorated name. Read n; call fib_a(n), count the trace lines it printed, then fib_b(n), count again, and print a traced <x>, b traced <y>, result <fib(n)>. (Capture the traces with contextlib.redirect_stdout into a StringIO and count its lines.)

Input: n. Output: three lines.

5

prints

a traced 9
b traced 6
result 5

A small validation framework

Build Field, a descriptor with __set_name__, __get__ and __set__ that checks isinstance(value, self.kind) and raises TypeError("<name> expects <kind>, got <type>"); and Model, whose __init_subclass__ records the subclass's Field attributes in cls.fields (names in definition order) and whose __init__(**kwargs) assigns each field from the keywords (missing → TypeError("missing <name>")). Two models are given: User(name=str, age=int) and Point(x=float, y=float). Commands: new Model k=v … (values parsed with ast.literal_eval; print ok <repr> or the error message) and fields Model.

Input: commands. Output: one line per command.

fields User
new User name="ada" age=36
new User name="bob" age="x"
new Point x=1.5

prints

name age
ok User(name='ada', age=36)
age expects int, got str
missing y

The complete Vector

Extend the Vector of lesson 6 with __getitem__ (an int returns a component; a slice returns a Vector), __hash__ consistent with __eq__, __bool__ (any non-zero component), __sub__, and __iadd__ that mutates in place and returns self. Read v, then commands: get i, slice a b, bool, sub x y … (prints v - Vector(...)), iadd x y … (applies += and prints v and whether it is still the same object as before), hash-eq x y … (prints whether v == Vector(...) and whether their hashes agree).

Input: a line of numbers, then commands. Output: one line per command.

1 2 3
get 0
slice 1 3
bool
sub 1 1 1
iadd 1 1 1
hash-eq 2 3 4

prints

1.0
Vector(2, 3)
True
Vector(0, 1, 2)
Vector(2, 3, 4) same True
eq True hash True

In this module: Decorators, descriptors and the data model

← The data model — the rest of the dunders, and a Vector that uses them · The GIL and the three models — threads, processes, asyncio →