Customizing Classes with Magic Methods
Customizing Classes with Magic Methods
Beyond basic arithmetic and string representations, Python's Data Model enables deep customization of user-defined classes. By implementing container protocols, callable behaviors, and attribute access hooks, you can create objects that behave like custom dictionaries, custom lists, dynamic proxies, or stateful function pipelines.
1. Emulating Containers: Sequence and Mapping Protocols
In Python, an object becomes a collection or container by implementing the Sequence Protocol or Mapping Protocol:
| Operation | Dunder Method | Example Syntax |
|---|---|---|
| Read element | __getitem__(self, key) | val = container[key] or container[start:stop] |
| Assign element | __setitem__(self, key, value) | container[key] = value |
| Delete element | __delitem__(self, key) | del container[key] |
| Membership check | __contains__(self, item) | if item in container: |
| Length count | __len__(self) | total = len(container) |
Implementing a Typed, Key-Value Memory Store
2. Handling Slices in __getitem__
When an indexing expression includes colons (e.g. obj[1:5:2]), Python constructs a built-in slice object and passes it to __getitem__.
3. Callable Instances: The __call__ Method
By implementing __call__(self, *args, **kwargs), an instance can be invoked directly with parentheses obj(), behaving like a function while maintaining persistent internal state.
4. Attribute Interception: __getattr__ vs __getattribute__
Python provides distinct hooks for intercepting attribute access:
__getattribute__(self, name): Intercepts every single attribute access unconditionally. Modifying this requires extreme caution to avoid infinite recursion.__getattr__(self, name): The graceful fallback. It is only called if the attribute was NOT found in normal dictionary lookup.
5. Architectural Comparison Summary
| Method | Trigger Syntax | Critical Caveat |
|---|---|---|
__getitem__ | obj[key] | Must handle slice objects if sequence behavior is expected. |
__setitem__ | obj[key] = val | Should validate keys according to container domain rules. |
__contains__ | item in obj | Must return a boolean. If omitted, Python falls back to iterating with __iter__. |
__call__ | obj(*args) | Makes callable(obj) == True. Ideal for stateful closures and middleware. |
__getattr__ | obj.attr | Fallback only; runs only when normal lookup fails. |
__getattribute__ | obj.attr | Always runs. Must use super().__getattribute__ to avoid infinite recursion. |
Multiple Choice Questions
1.
Which dunder method is executed when an element is retrieved via square bracket notation value = obj["my_key"]? A. __get__ B. __getattr__ C. __getitem__ D. __access__
obj[key] is mapped directly to __getitem__(self, key).2.
What object type does Python pass as the key argument to __getitem__ when an expression like obj[2:10:2] is executed? A. A tuple containing (2, 10, 2) B. A built-in slice object with start=2, stop=10, step=2 C. A range object range(2, 10, 2) D. A string "2:10:2"
slice(2, 10, 2) instance and passes it directly to __getitem__ when colon slicing syntax is used.3.
What built-in function returns True when evaluated on an instance whose class defines the __call__ method? A. isfunction() B. callable() C. hasattr() D. isinstance(obj, FunctionType)
callable(obj) returns True for any object whose class implements the __call__ dunder method.4.
What is the key difference between __getattr__ and __getattribute__? A. __getattr__ is called on every attribute access, while __getattribute__ is only a fallback. B. __getattribute__ is called unconditionally on every attribute lookup, while __getattr__ is only called if standard attribute resolution fails. C. __getattr__ is private, whereas __getattribute__ is public. D. __getattribute__ is deprecated in Python 3.
__getattribute__ intercepts every attribute access unconditionally. __getattr__ is only invoked as a fallback mechanism when the attribute is not found in the object's instance dictionary or class hierarchy.5.
Inside __getattribute__, how should you safely access attributes on the instance without triggering infinite recursion? A. Directly access self.__dict__[name] B. Call self.name C. Delegate via super().__getattribute__(name) or object.__getattribute__(self, name) D. Call getattr(self, name)
self.__dict__ or calling getattr(self, ...) inside __getattribute__ triggers __getattribute__ again, causing infinite recursion and a RecursionError. One must use super().__getattribute__(name).Project: Vector Class with Overloaded Operators
Continue learning with hands-on practice, examples, and exercises in the upcoming topic.
Related Lessons
| Previous Lesson | Next Lesson |
|---|---|
| Operator Overloading | Project: Vector Class with Overloaded Operators |
Practice Quiz
Test your understanding of this lesson with 5 questions. Each question has one correct answer.