Let’s combine everything into a high-quality OOP design: a plugin architecture for a data processing pipeline.
from abc import ABC, abstractmethod
from typing import List, Dict, Any
import importlib
class Plugin(ABC):
"""Base class for all plugins."""
name: str
@abstractmethod
def process(self, data: Dict[str, Any]) -> Dict[str, Any]:
pass
class Pipeline:
def init(self):
self._plugins: List[Plugin] = []
def register(self, plugin: Plugin) -> None:
if not isinstance(plugin, Plugin):
raise TypeError("Must be Plugin subclass")
self._plugins.append(plugin)
def run(self, initial_data: Dict[str, Any]) -> Dict[str, Any]:
data = initial_data
for plugin in self._plugins:
data = plugin.process(data)
return data
Most developers know __init__, but the real constructor is __new__.
Example: Singleton pattern using __new__:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True
Why does this matter for high-quality code?
Overriding __new__ allows you to control instance creation (e.g., caching, pooling, immutables). Never mutate __new__ without good reason, but understand it.
# Bad (inheritance)
class Engine:
def start(self): ...
class Car(Engine):
def drive(self):
self.start() python 3 deep dive part 4 oop high quality
This is where Python diverges from static languages like C++ or Java.
A metaclass is to a class what a class is to an instance. The default metaclass is type.
def my_meta(name, bases, dct):
dct['version'] = 1.0
return type(name, bases, dct)
class MyClass(metaclass=my_meta):
pass
print(MyClass.version) # 1.0
Practical use: Singleton metaclass:
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Database(metaclass=SingletonMeta):
pass
When to use metaclasses (rarely!):
High-quality rule: If you’re unsure, don’t use metaclasses. They make code magical and hard to debug. Prefer class decorators.
Why descriptors matter for high-quality code:
Without descriptors, you’d repeat property boilerplate everywhere.
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).
Python has no real access control. We rely on naming conventions:
Name mangling example:
class Foo:
def __init__(self):
self.__secret = 42
def get_secret(self):
return self.__secret
f = Foo()
print(f._Foo__secret) # 42 – still accessible, but harder to accidentally access
High-quality wisdom: Do not overuse __ (double underscore). It breaks subclassing. Use _ for internal attributes and trust other developers. Python is a "consenting adults" language.
},3000)
$("#google_esf").attr("title","Ads");
});