Object creation and design
Part of the free LogicWiz Python course, Module 9: Object-oriented programming. The examples and practice run in the app.
Canteen Counter must have exactly one settings object, or the GST rate on the till and on the receipt may disagree. And Meher is tired of typing __init__, __repr__ and __eq__ for every little class. Python has tools for both.
You'll learn to
- Explain `__new__` vs `__init__`, and build a Singleton
- Use `__slots__`, `@dataclass` and `Enum`
- Recognise the Singleton, Factory and Observer patterns
- Picture what a metaclass is
Learn
__new__ builds, __init__ fills
Making an object is two steps:
__new__(cls, ...)creates and returns the new, empty object. It is the real constructor.__init__(self, ...)receives that object and fills in its attributes. It returns nothing.
You almost never write __new__. You need it when the object must be decided before it exists: returning an existing object (a Singleton), or subclassing immutable types like int and str, whose value is fixed at birth.
Watch the order, then build a Singleton
class Item:
def __new__(cls, *args):
print("1. __new__ makes the object")
return super().__new__(cls)
def __init__(self, name):
print("2. __init__ fills it in")
self.name = name
x = Item("Chai")
print(x.name)
class Settings:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.gst = 5
return cls._instance
a = Settings()
b = Settings()
b.gst = 12
print(a is b, a.gst)
Every Settings() call returns the same object, so changing b changes a. That is a Singleton: one shared instance.
__slots__: a fixed set of attributes
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
p.x = 10
try:
p.z = 3
except AttributeError as e:
print("AttributeError:", e)
print(p.x, hasattr(p, "__dict__"))
Normally each object carries a __dict__ for its attributes. __slots__ replaces it with fixed slots: objects use less memory, and a typo like p.z fails loudly. Useful when you make millions of small objects.
@dataclass and Enum
from dataclasses import dataclass, field
from enum import Enum
class Status(Enum):
PLACED = 1
READY = 2
PAID = 3
@dataclass(order=True)
class Item:
price: int
name: str
tags: list = field(default_factory=list)
a = Item(15, "Samosa")
b = Item(10, "Chai", ["hot"])
print(a)
print(a == Item(15, "Samosa"), sorted([a, b])[0].name)
s = Status.READY
print(s, s.name, s.value, s == Status.READY)
@dataclass writes __init__, __repr__ and __eq__ for you from the annotated fields; order=True adds < (comparing fields in order). Enum gives named constants, so a typo like Status.REDY fails at once, unlike a string "redy".
Three design patterns
A design pattern is a named, reusable solution.
- Singleton: only one instance ever (you just saw it with
__new__). In Python, a module-level object often does the same job more simply. - Factory: a function or classmethod that decides which class to create, so callers don't need to know:
make_payment("card"). - Observer: an object keeps a list of subscribers and notifies them all when something happens: the order is ready, so the screen and SMS both update.
Factory and Observer
class Cash:
def fee(self, amt):
return 0
class Card:
def fee(self, amt):
return round(amt * 0.02, 2)
def make_payment(kind): # Factory
kinds = {"cash": Cash, "card": Card}
return kinds[kind]()
class OrderBoard: # Observer: the subject
def __init__(self):
self.watchers = []
def subscribe(self, fn):
self.watchers.append(fn)
def ready(self, token):
for fn in self.watchers:
fn(token)
print(make_payment("card").fee(500))
board = OrderBoard()
board.subscribe(lambda t: print("Screen: token", t, "ready"))
board.subscribe(lambda t: print("SMS: your order", t, "is ready"))
board.ready(42)
Metaclass in one picture
class Item:
pass
# object --made by--> class --made by--> metaclass (type)
print(type(Item()), type(Item), type(type))
Snack = type("Snack", (), {"price": 20}) # build a class at run time
print(Snack.price, Snack.__name__)
class Upper(type): # a tiny custom metaclass
def __new__(mcs, name, bases, ns):
ns = {(k if k.startswith("__") else k.upper()): v for k, v in ns.items()}
return super().__new__(mcs, name, bases, ns)
class Menu(metaclass=Upper):
chai = 10
print(Menu.CHAI)
Objects are made by classes, and classes are made by a metaclass, normally type. A custom metaclass changes how classes themselves get built (here, upper-casing their attributes). Frameworks like Django use this; in your own code a decorator or @dataclass is almost always simpler.
Interview corner for this module: the four pillars (lesson 9.3), MRO and super() (9.4), and __new__ vs __init__ (here). For __new__ vs __init__, say: __new__ is a static method that creates and returns the instance; __init__ initialises it and returns None; if __new__ returns an object of another class, __init__ is skipped.
In one line
__new__ creates and returns the object, __init__ fills it; __slots__ fixes attributes; @dataclass writes the boilerplate; Enum names constants; Singleton, Factory and Observer are everyday patterns.
Interview corner
What is the difference between `__new__` and `__init__`?
__new__(cls, ...) is the real constructor: an implicit static method that creates and returns a new instance. __init__(self, ...) then initialises that instance and must return None. __init__ runs only if __new__ returns an instance of the class. Override __new__ for Singletons, caching, or subclassing immutable types like int, str and tuple.
What is a metaclass?
A class whose instances are classes. The default metaclass is type: type(int) is type. A custom metaclass (subclass type and override __new__/__init__) changes how classes are built, which is used for ORMs, registries and validation. Most needs are met more simply with class decorators or __init_subclass__.
Why use `@dataclass`?
It generates __init__, __repr__ and __eq__ from annotated fields (and ordering with order=True, immutability with frozen=True, __slots__ with slots=True). It removes boilerplate for classes that mainly hold data. Use field(default_factory=list) for mutable defaults.
How do you implement a Singleton in Python?
Override __new__ to create the instance once, store it on the class, and return it every time. Simpler Pythonic alternatives: a module-level instance (modules are imported once), or a decorator that caches the instance. Note that __init__ still runs on every call unless you guard it.