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 adefexpand to, and how many nested functions doesretryneed? - 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 throughobject.__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 5A 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 yThe 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 TrueIn this module: Decorators, descriptors and the data model
- Decorators — functions that wrap functions
- Closures and late binding — cells, factories and stateful callables
- Descriptors — how properties, methods and validated attributes work
- Attribute access — __getattr__, __getattribute__, __setattr__, __dict__ and __slots__
- Classes as objects — type, __new__, __init_subclass__ and metaclasses in outline
- The data model — the rest of the dunders, and a Vector that uses them
- Checkpoint — Decorators, descriptors and the data model (this lesson)
← The data model — the rest of the dunders, and a Vector that uses them · The GIL and the three models — threads, processes, asyncio →