Python 3 Deep Dive Part 4 Oop High Quality Instant
class PositiveInt(int): def new(cls, value): if value <= 0: raise ValueError("PositiveInt must be > 0") return super().new(cls, value)
In languages like Java, private attributes are accessed via getters/setters. In Python, we start with public attributes and refactor to properties when needed.
Bad (Java-style):
class BadCircle:
def __init__(self, radius):
self._radius = radius
def get_radius(self):
return self._radius
def set_radius(self, value):
if value < 0:
raise ValueError("Radius cannot be negative")
self._radius = value
Good (Pythonic):
class Circle: def __init__(self, radius): self.radius = radius # Uses setter if defined@property def radius(self): return self._radius @radius.setter def radius(self, value): if value < 0: raise ValueError("Radius cannot be negative") self._radius = value @property def area(self): return 3.14159 * self._radius ** 2
Key insight: Properties allow you to evolve an attribute into logic without changing the API. Your users still write circle.radius = 5, not circle.set_radius(5).
super() is not "parent". It is a proxy that walks the Method Resolution Order (MRO).
The Diamond of Death:
class A: def f(self): return "A" class B(A): def f(self): return "B" + super().f() class C(A): def f(self): return "C" + super().f() class D(B, C): def f(self): return "D" + super().f()
print(D().f()) # DBCA (not DB CA or D A)
Why? Python uses C3 linearization. MRO of D: [D, B, C, A]. super() follows that chain.
Explicit MRO inspection:
print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
Best practice: In cooperative multiple inheritance, always use super() and pass all arguments through *args, **kwargs. Never hardcode parent classes.
class LoggedMixin: def __init__(self, **kwargs): print(f"Init self.__class__.__name__") super().__init__(**kwargs)
class Shape: pass class Colored(LoggedMixin, Shape): pass # MRO ensures LoggedMixin runs first
Effective Python OOP requires shifting away from rigid struct-like definitions toward understanding the dynamic dictionary model and the descriptor protocol.
By mastering __dict__, MRO, and the descriptor protocol, you gain the ability to write code that is not only functional but deeply integrated into the Python ecosystem.
The dynamic nature of __dict__ consumes significant memory. If you are creating millions of instances, you can optimize memory by using __slots__. This tells Python to use a static array for attributes instead of a dynamic dictionary.
class Optimized:
__slots__ = ['x', 'y'] # Fixed attribute set
def __init__(self, x, y):
self.x = x
self.y = y
o = Optimized(1, 2)
o.z = 3 # Raises AttributeError! Cannot add new attributes.
Trade-off: You save memory but lose the ability to dynamically add attributes.
| Anti‑pattern | Why it’s bad | Fix |
|------------------------------|------------------------------------------|--------------------------------------|
| God object | Single class does everything | Split into smaller, focused classes |
| Getter/setter for every attr | Un‑Pythonic, verbose | Use @property only when needed |
| Mutable defaults in __init__ | Shared across instances | Use None + create new inside |
| Using type() to check class | Breaks polymorphism (subclasses) | Use isinstance() |
| Inheriting from builtins incorrectly | Can break if not careful | Prefer composition or subclass collections.abc | python 3 deep dive part 4 oop high quality