Warm-up · Activity 1 of 7
// A1.6 · ~40 min · Advanced
Build: attribute hooks, class creation, Vector and Matrix
After this lesson you can hook into failed and all attribute lookups, run code whenever a subclass is created, recognise when a metaclass is the right tool, and you have built a Vector and Matrix pair with operators.
Lesson 6 of 6 in A1 The data model
You will be able to
- Choose between __getattr__ and __getattribute__ and write them without recursion
- Use __init_subclass__, with class keywords, to check or register subclasses
- Explain what a metaclass is and when it runs, compared with __init_subclass__
Predict · Activity 2 of 7
Predict before you read on: the class defines __getattr__. What does this print?
class Fallback: color = "red" def __getattr__(self, name): return f"<{name} missing>" f = Fallback() print(f.color, f.size)Practice · Activity 3 of 7
Fill in the hook so that missing attributes give "default" while existing ones keep their value.
class Defaults: known = "set" def ____(self, name): return "default"def (self, name):Practice · Activity 4 of 7
Counter overrides __getattribute__. What does this print?
class Counter: def __init__(self): self.value = 1 def __getattribute__(self, name): print("get", name, end="; ") return object.__getattribute__(self, name) c = Counter() print(c.value)Practice · Activity 5 of 7
Match each hook to when Python calls it.
Brain teaser · Activity 6 of 7
Brain teaser. B subclasses A, which subclasses Base. What does this print?
class Base: count = 0 def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) Base.count += 1 class A(Base): pass class B(A): pass print(Base.count)Apply · Activity 7 of 7
Mini-task. Write a base class Shape that checks each subclass as soon as its class statement runs: a subclass without an area method raises TypeError("<Name> must define area()"), and every accepted subclass name is added to Shape.registry.
Check your work against this list
Build it yourself
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
Records with fields from the class line
Record uses two hooks. __init_subclass__ reads the fields keyword from each class line and registers the subclass. __getattr__ turns the stored values into attributes, and only runs when normal lookup fails, so book.fields still comes from the class. Try adding a Movie subclass, or reading book._data before __init__ has run.
main.py
class Record:
"""Base class: each subclass names its fields in the class line."""
fields: tuple[str, ...] = ()
kinds: dict[str, type["Record"]] = {}
def __init_subclass__(cls, /, fields: str = "", **kwargs: object) -> None:
super().__init_subclass__(**kwargs)
cls.fields = tuple(fields.split())
Record.kinds[cls.__name__.lower()] = cls
def __init__(self, *values: object) -> None:
if len(values) != len(self.fields):
raise TypeError(f"{type(self).__name__} needs {len(self.fields)} values")
self._data = dict(zip(self.fields, values))
def __getattr__(self, name: str) -> object:
# Only called when the normal lookup has failed.
if name.startswith("_"):
raise AttributeError(name) # never look up _data through itself
try:
return self._data[name]
except KeyError:
raise AttributeError(f"{type(self).__name__!r} has no field {name!r}") from None
class Book(Record, fields="title author"):
pass
class Song(Record, fields="title artist year"):
pass
print(Record.kinds)
book = Book("Dune", "Herbert")
print(book.title, book.author, book.fields)
try:
book.year
except AttributeError as err:
print(err)
print(type(Book).__name__)
Run it with
python main.pyOutput
{'book': <class '__main__.Book'>, 'song': <class '__main__.Song'>}
Dune Herbert ('title', 'author')
'Book' has no field 'year'
type- fields="title author" is a class keyword; Python hands it to Record.__init_subclass__.
- The name check for "_" stops __getattr__ from looking up self._data through itself when _data does not exist yet.
- The KeyError is turned into AttributeError, so hasattr(book, "year") gives False.
- type(Book) is type: this needed no metaclass at all.
Change it and run it
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Exercises
Exercise 1 of 3
Step 1: a Vector container with named components
Module build, step 1 of 3. Give Vector the container protocol from A1.1 and A1.3: __repr__ as Vector(3, 4), __len__, __iter__, __getitem__, __eq__ and a matching __hash__. Then add __getattr__ so that v.x, v.y and v.z read components 0 to 2 when they exist; every other missing name raises AttributeError.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Build the repr with ", ".join(map(repr, self._components)) so that one component gives Vector(1.5).
Hint 2
Hash the same tuple that __eq__ compares: hash(self._components).
Hint 3
In __getattr__, index = self._names.find(name); accept it only if len(name) == 1 and 0 <= index < len(self._components). Check len(name) first: that way a missing _components never recurses.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
from collections.abc import Iterator
class Vector:
"""An immutable vector of numbers."""
_names = "xyz"
def __init__(self, *components: float) -> None:
self._components = tuple(components)
def __repr__(self) -> str:
return f"Vector({', '.join(map(repr, self._components))})"
def __len__(self) -> int:
return len(self._components)
def __iter__(self) -> Iterator[float]:
return iter(self._components)
def __getitem__(self, index: int) -> float:
return self._components[index]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self._components == other._components
def __hash__(self) -> int:
return hash(self._components)
def __getattr__(self, name: str) -> float:
index = self._names.find(name)
if len(name) == 1 and 0 <= index < len(self._components):
return self._components[index]
raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
if __name__ == "__main__":
v = Vector(3, 4)
print(v, len(v), list(v), v[1])
print(v.x, v.y, v == Vector(3, 4), {v, Vector(3, 4)})
try:
v.z
except AttributeError as err:
print(err)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
from collections.abc import Iterator
class Vector:
"""An immutable vector of numbers."""
_names = "xyz"
def __init__(self, *components: float) -> None:
self._components = tuple(components)
# Step 1 of the build. Add:
# __repr__ -> "Vector(3, 4)"
# __len__, __iter__ and __getitem__ over the components
# __eq__ (NotImplemented for non-vectors) and a matching __hash__
# __getattr__: v.x, v.y, v.z give components 0, 1, 2 if the vector
# has that many; any other name raises AttributeError.
if __name__ == "__main__":
v = Vector(3, 4)
print(v)
test_main.py
from main import Vector
def test_repr():
"""repr shows the components"""
assert repr(Vector(3, 4)) == "Vector(3, 4)", f"repr is {Vector(3, 4)!r}"
assert repr(Vector(1.5)) == "Vector(1.5)", f"repr of a one-component vector is {Vector(1.5)!r}"
def test_sequence_protocol():
"""len, iteration and indexing work"""
v = Vector(3, 4, 5)
got = (len(v), list(v), v[-1])
assert got == (3, [3, 4, 5], 5), f"(len(v), list(v), v[-1]) is {got!r}"
def test_equality_and_hash():
"""Equal vectors compare equal and hash alike"""
assert Vector(1, 2) == Vector(1, 2), "Vector(1, 2) != Vector(1, 2)"
assert Vector(1, 2) != Vector(2, 1), "Vector(1, 2) == Vector(2, 1)"
assert len({Vector(1, 2), Vector(1, 2)}) == 1, "equal vectors must have equal hashes"
assert Vector(1, 2) != (1, 2), "a Vector should not equal a tuple"
def test_named_components():
"""x, y and z read the first three components"""
v = Vector(3, 4, 5)
got = (v.x, v.y, v.z)
assert got == (3, 4, 5), f"(v.x, v.y, v.z) is {got!r}"
def test_missing_names_raise():
"""z on a 2D vector and unknown names raise AttributeError"""
for name in ["z", "w", "xy"]:
try:
getattr(Vector(3, 4), name)
except AttributeError:
continue
raise AssertionError(f"Vector(3, 4).{name} should raise AttributeError")
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 2 of 3
Step 2: vector operators
Step 2 of 3. Add the operators from A1.2: v + w, v - w and -v component by component, v * 2 and 2 * v, v @ w as the dot product, and abs(v) as the length. + and @ raise ValueError("lengths differ: 2 and 3") for different lengths. Every operator returns NotImplemented for operand types it does not support.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
A helper _same_length(other) keeps the length check in one place.
Hint 2
v - w can reuse the others: return self + -other.
Hint 3
Accept only int and float in __mul__, then write __rmul__ = __mul__. Without the type check, v * "a" would quietly build a vector of strings.
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import math
from collections.abc import Iterator
class Vector:
"""An immutable vector of numbers."""
_names = "xyz"
def __init__(self, *components: float) -> None:
self._components = tuple(components)
def __repr__(self) -> str:
return f"Vector({', '.join(map(repr, self._components))})"
def __len__(self) -> int:
return len(self._components)
def __iter__(self) -> Iterator[float]:
return iter(self._components)
def __getitem__(self, index: int) -> float:
return self._components[index]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self._components == other._components
def __hash__(self) -> int:
return hash(self._components)
def __getattr__(self, name: str) -> float:
index = self._names.find(name)
if len(name) == 1 and 0 <= index < len(self._components):
return self._components[index]
raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
def _same_length(self, other: "Vector") -> None:
if len(self) != len(other):
raise ValueError(f"lengths differ: {len(self)} and {len(other)}")
def __add__(self, other: object) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
self._same_length(other)
return Vector(*(a + b for a, b in zip(self, other)))
def __sub__(self, other: object) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
return self + -other
def __neg__(self) -> "Vector":
return Vector(*(-a for a in self))
def __mul__(self, scalar: object) -> "Vector":
if not isinstance(scalar, (int, float)):
return NotImplemented
return Vector(*(a * scalar for a in self))
__rmul__ = __mul__
def __matmul__(self, other: object) -> float:
if not isinstance(other, Vector):
return NotImplemented
self._same_length(other)
return sum(a * b for a, b in zip(self, other))
def __abs__(self) -> float:
return math.hypot(*self)
if __name__ == "__main__":
v, w = Vector(3, 4), Vector(1, 2)
print(v + w, v - w, -w)
print(v * 2, 2 * v, v @ w, abs(v))
try:
v + Vector(1, 2, 3)
except ValueError as err:
print(err)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import math
from collections.abc import Iterator
class Vector:
"""An immutable vector of numbers."""
_names = "xyz"
def __init__(self, *components: float) -> None:
self._components = tuple(components)
def __repr__(self) -> str:
return f"Vector({', '.join(map(repr, self._components))})"
def __len__(self) -> int:
return len(self._components)
def __iter__(self) -> Iterator[float]:
return iter(self._components)
def __getitem__(self, index: int) -> float:
return self._components[index]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self._components == other._components
def __hash__(self) -> int:
return hash(self._components)
def __getattr__(self, name: str) -> float:
index = self._names.find(name)
if len(name) == 1 and 0 <= index < len(self._components):
return self._components[index]
raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
# Step 2 of the build. Add operators; each returns NotImplemented for
# operands it does not support:
# v + w, v - w and -v, component by component; + and - raise
# ValueError("lengths differ: 2 and 3") for different lengths
# v * 2 and 2 * v (int or float only)
# v @ w, the dot product (same length check)
# abs(v), the length: math.hypot(*v)
if __name__ == "__main__":
v, w = Vector(3, 4), Vector(1, 2)
print(v + w, v - w, -w)
print(v * 2, 2 * v, v @ w, abs(v))
try:
v + Vector(1, 2, 3)
except ValueError as err:
print(err)
test_main.py
from main import Vector
def test_add_sub_neg():
"""+, - and unary - work component by component"""
v, w = Vector(3, 4), Vector(1, 2)
got = (v + w, v - w, -w)
assert got == (Vector(4, 6), Vector(2, 2), Vector(-1, -2)), f"(v + w, v - w, -w) is {got!r}"
def test_scalar_on_both_sides():
"""v * 2 and 2 * v give the same vector"""
v = Vector(1, 2)
got = (v * 2, 2 * v, 0.5 * v)
assert got == (Vector(2, 4), Vector(2, 4), Vector(0.5, 1.0)), f"(v * 2, 2 * v, 0.5 * v) is {got!r}"
def test_dot_and_abs():
"""@ is the dot product and abs() the length"""
dot = Vector(1, 2, 3) @ Vector(4, 5, 6)
assert dot == 32, f"Vector(1, 2, 3) @ Vector(4, 5, 6) is {dot!r}, expected 32"
assert abs(Vector(3, 4)) == 5.0, f"abs(Vector(3, 4)) is {abs(Vector(3, 4))!r}, expected 5.0"
def test_length_mismatch():
"""Vectors of different lengths raise ValueError"""
for text, op in [("+", lambda a, b: a + b), ("-", lambda a, b: a - b), ("@", lambda a, b: a @ b)]:
try:
op(Vector(1, 2), Vector(1, 2, 3))
except ValueError:
continue
raise AssertionError(f"Vector(1, 2) {text} Vector(1, 2, 3) should raise ValueError")
def test_unsupported_operands():
"""Other operand types end in TypeError through NotImplemented"""
cases = [
("Vector(1, 2) + (1, 2)", lambda: Vector(1, 2) + (1, 2)),
('Vector(1, 2) * "a"', lambda: Vector(1, 2) * "a"),
('"a" * Vector(1, 2)', lambda: "a" * Vector(1, 2)),
("Vector(1, 2) @ 3", lambda: Vector(1, 2) @ 3),
]
for text, op in cases:
try:
op()
except TypeError:
continue
raise AssertionError(f"{text} should raise TypeError")
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyExercise 3 of 3
Step 3: a Matrix built on Vector
Step 3 of 3. Matrix stores its rows as Vectors and already has shape, T (transpose), indexing with m[i, j], repr and equality. Add m + n (ValueError when the shapes differ) and m @ other: with a Vector it returns the Vector of row dot products, with a Matrix the matrix product. Return NotImplemented for anything else.
Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.
The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.
Hints
Hint 1
Rows are Vectors, so row @ other and a + b already do the arithmetic and the length checks.
Hint 2
Check isinstance(other, Vector) and isinstance(other, Matrix) separately in __matmul__; the result type differs.
Hint 3
Entry (i, j) of m @ n is row i of m @ column j of n. The columns of n are other.T.rows: Matrix([row @ column for column in columns] for row in self.rows).
Show a solution
One way to solve it. Yours can look different and still pass the checks.
import math
from collections.abc import Iterable, Iterator
class Vector:
"""An immutable vector of numbers."""
_names = "xyz"
def __init__(self, *components: float) -> None:
self._components = tuple(components)
def __repr__(self) -> str:
return f"Vector({', '.join(map(repr, self._components))})"
def __len__(self) -> int:
return len(self._components)
def __iter__(self) -> Iterator[float]:
return iter(self._components)
def __getitem__(self, index: int) -> float:
return self._components[index]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self._components == other._components
def __hash__(self) -> int:
return hash(self._components)
def __getattr__(self, name: str) -> float:
index = self._names.find(name)
if len(name) == 1 and 0 <= index < len(self._components):
return self._components[index]
raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
def _same_length(self, other: "Vector") -> None:
if len(self) != len(other):
raise ValueError(f"lengths differ: {len(self)} and {len(other)}")
def __add__(self, other: object) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
self._same_length(other)
return Vector(*(a + b for a, b in zip(self, other)))
def __sub__(self, other: object) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
return self + -other
def __neg__(self) -> "Vector":
return Vector(*(-a for a in self))
def __mul__(self, scalar: object) -> "Vector":
if not isinstance(scalar, (int, float)):
return NotImplemented
return Vector(*(a * scalar for a in self))
__rmul__ = __mul__
def __matmul__(self, other: object) -> float:
if not isinstance(other, Vector):
return NotImplemented
self._same_length(other)
return sum(a * b for a, b in zip(self, other))
def __abs__(self) -> float:
return math.hypot(*self)
class Matrix:
"""An immutable matrix, stored as a tuple of row vectors."""
def __init__(self, rows: Iterable[Iterable[float]]) -> None:
self.rows = tuple(Vector(*row) for row in rows)
if len({len(row) for row in self.rows}) > 1:
raise ValueError("all rows must have the same length")
@property
def shape(self) -> tuple[int, int]:
return len(self.rows), len(self.rows[0]) if self.rows else 0
@property
def T(self) -> "Matrix":
return Matrix(zip(*self.rows))
def __repr__(self) -> str:
return f"Matrix({[list(row) for row in self.rows]})"
def __getitem__(self, index: tuple[int, int]) -> float:
i, j = index
return self.rows[i][j]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Matrix):
return NotImplemented
return self.rows == other.rows
def __hash__(self) -> int:
return hash(self.rows)
def __add__(self, other: object) -> "Matrix":
if not isinstance(other, Matrix):
return NotImplemented
if self.shape != other.shape:
raise ValueError(f"shapes differ: {self.shape} and {other.shape}")
return Matrix(a + b for a, b in zip(self.rows, other.rows))
def __matmul__(self, other: object) -> "Matrix | Vector":
if isinstance(other, Vector):
return Vector(*(row @ other for row in self.rows))
if isinstance(other, Matrix):
columns = other.T.rows
return Matrix([row @ column for column in columns] for row in self.rows)
return NotImplemented
if __name__ == "__main__":
m = Matrix([[1, 2], [3, 4]])
identity = Matrix([[1, 0], [0, 1]])
print(m.shape, m[1, 0], m.T)
print(m + identity)
print(m @ Vector(1, 1))
print(m @ identity == m, m @ m)
Run it on your computer
Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.
main.py
import math
from collections.abc import Iterable, Iterator
class Vector:
"""An immutable vector of numbers."""
_names = "xyz"
def __init__(self, *components: float) -> None:
self._components = tuple(components)
def __repr__(self) -> str:
return f"Vector({', '.join(map(repr, self._components))})"
def __len__(self) -> int:
return len(self._components)
def __iter__(self) -> Iterator[float]:
return iter(self._components)
def __getitem__(self, index: int) -> float:
return self._components[index]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented
return self._components == other._components
def __hash__(self) -> int:
return hash(self._components)
def __getattr__(self, name: str) -> float:
index = self._names.find(name)
if len(name) == 1 and 0 <= index < len(self._components):
return self._components[index]
raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")
def _same_length(self, other: "Vector") -> None:
if len(self) != len(other):
raise ValueError(f"lengths differ: {len(self)} and {len(other)}")
def __add__(self, other: object) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
self._same_length(other)
return Vector(*(a + b for a, b in zip(self, other)))
def __sub__(self, other: object) -> "Vector":
if not isinstance(other, Vector):
return NotImplemented
return self + -other
def __neg__(self) -> "Vector":
return Vector(*(-a for a in self))
def __mul__(self, scalar: object) -> "Vector":
if not isinstance(scalar, (int, float)):
return NotImplemented
return Vector(*(a * scalar for a in self))
__rmul__ = __mul__
def __matmul__(self, other: object) -> float:
if not isinstance(other, Vector):
return NotImplemented
self._same_length(other)
return sum(a * b for a, b in zip(self, other))
def __abs__(self) -> float:
return math.hypot(*self)
class Matrix:
"""An immutable matrix, stored as a tuple of row vectors."""
def __init__(self, rows: Iterable[Iterable[float]]) -> None:
self.rows = tuple(Vector(*row) for row in rows)
if len({len(row) for row in self.rows}) > 1:
raise ValueError("all rows must have the same length")
@property
def shape(self) -> tuple[int, int]:
return len(self.rows), len(self.rows[0]) if self.rows else 0
@property
def T(self) -> "Matrix":
return Matrix(zip(*self.rows))
def __repr__(self) -> str:
return f"Matrix({[list(row) for row in self.rows]})"
def __getitem__(self, index: tuple[int, int]) -> float:
i, j = index
return self.rows[i][j]
def __eq__(self, other: object) -> bool:
if not isinstance(other, Matrix):
return NotImplemented
return self.rows == other.rows
def __hash__(self) -> int:
return hash(self.rows)
# Step 3 of the build. Add, returning NotImplemented for other types:
# m + n, row by row; ValueError("shapes differ: (2, 2) and (2, 3)")
# when the shapes differ
# m @ v with a Vector: a Vector of the dot products row @ v
# m @ n with a Matrix: a Matrix whose entry (i, j) is row i of m
# @ column j of n (the columns of n are the rows of n.T)
if __name__ == "__main__":
m = Matrix([[1, 2], [3, 4]])
identity = Matrix([[1, 0], [0, 1]])
print(m.shape, m[1, 0], m.T)
print(m + identity)
print(m @ Vector(1, 1))
print(m @ identity == m, m @ m)
test_main.py
from main import Matrix, Vector
def test_add():
"""m + n adds entry by entry"""
got = Matrix([[1, 2], [3, 4]]) + Matrix([[1, 0], [0, 1]])
assert got == Matrix([[2, 2], [3, 5]]), f"the sum is {got!r}"
def test_add_shape_mismatch():
"""Adding matrices of different shapes raises ValueError"""
try:
Matrix([[1, 2]]) + Matrix([[1], [2]])
except ValueError:
return
raise AssertionError("a (1, 2) plus a (2, 1) matrix should raise ValueError")
def test_matrix_times_vector():
"""m @ v gives a Vector"""
got = Matrix([[1, 2], [3, 4]]) @ Vector(1, 1)
assert isinstance(got, Vector) and got == Vector(3, 7), f"m @ Vector(1, 1) is {got!r}, expected Vector(3, 7)"
def test_matrix_times_matrix():
"""m @ n is the matrix product, also for non-square shapes"""
a = Matrix([[1, 2, 3], [4, 5, 6]])
b = Matrix([[7, 8], [9, 10], [11, 12]])
got = a @ b
assert got == Matrix([[58, 64], [139, 154]]), f"a @ b is {got!r}"
def test_unsupported_operands():
"""Numbers are not supported and end in TypeError"""
m = Matrix([[1, 2], [3, 4]])
for text, op in [("m @ 2", lambda: m @ 2), ("m + 1", lambda: m + 1)]:
try:
op()
except TypeError:
continue
raise AssertionError(f"{text} should raise TypeError")
On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.
Run the program:
python main.pyRun the checks (needs learnrun.py in the same folder):
python learnrun.py testDownload learnrun.pyCommon mistakes
Reading self.attr inside __getattribute__
class Logged:
def __init__(self):
self.count = 0
def __getattribute__(self, name):
self.count += 1
return object.__getattribute__(self, name)
print(Logged().count)
What Python prints
RecursionError: maximum recursion depth exceededWhy, and the fix
self.count inside __getattribute__ is itself an attribute read, so the method calls itself forever. Read and write your own attributes through object.__getattribute__(self, "count") and object.__setattr__, or use __getattr__ if you only need the failed lookups.
A class keyword that __init_subclass__ does not accept
class Plugin:
registry = {}
def __init_subclass__(cls):
super().__init_subclass__()
Plugin.registry[cls.__name__] = cls
class Csv(Plugin, name="csv"):
pass
What Python prints
TypeError: Plugin.__init_subclass__() got an unexpected keyword argument 'name'Why, and the fix
Keywords in the class line are passed to the parent's __init_subclass__. Declare the ones you use and forward the rest: def __init_subclass__(cls, /, name, **kwargs): super().__init_subclass__(**kwargs).
Mixing two unrelated metaclasses
class MetaA(type):
pass
class MetaB(type):
pass
class A(metaclass=MetaA):
pass
class B(metaclass=MetaB):
pass
class C(A, B):
pass
What Python prints
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its basesWhy, and the fix
C needs one metaclass that is a subclass of both MetaA and MetaB, and none exists. Define class MetaAB(MetaA, MetaB) and use it, or better, replace the metaclasses with __init_subclass__, which combines through normal inheritance.
Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source
Exit ticket
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.