Object-Oriented Programming in Python
A class defines a new type. Each instance can hold state in attributes and expose behavior through methods. Classes are useful when data and the operations that preserve its rules belong together; a function or simple collection may be clearer for smaller tasks.
Defining a class
__init__ initializes a new instance. Instance methods receive that instance as their first argument, conventionally named self.
class Measurement:
unit = "°C"
def __init__(self, value, sensor):
self.value = value
self.sensor = sensor
def describe(self):
return f"{self.sensor}: {self.value}{self.unit}"
reading = Measurement(21.5, "lab")
print(reading.describe()) # lab: 21.5°C
unit is a class attribute shared through the class unless an instance overrides it. value and sensor are instance attributes.
Protecting invariants
Python does not enforce private fields in the same way as some languages. A leading underscore communicates that an attribute is non-public. Properties can expose validation while keeping normal attribute syntax.
class Percentage:
def __init__(self, value):
self.value = value
@property
def value(self):
return self._value
@value.setter
def value(self, new_value):
if not 0 <= new_value <= 100:
raise ValueError("percentage must be between 0 and 100")
self._value = new_value
completion = Percentage(75)
completion.value = 80
print(completion.value) # 80
Composition before inheritance
Composition gives one object another object to collaborate with and often keeps responsibilities clearer than a deep inheritance tree.
class MeanReducer:
def reduce(self, values):
if not values:
raise ValueError("values must not be empty")
return sum(values) / len(values)
class Report:
def __init__(self, reducer):
self.reducer = reducer
def summarize(self, values):
return {"count": len(values), "result": self.reducer.reduce(values)}
report = Report(MeanReducer())
print(report.summarize([2, 4, 6]))
Inheritance and polymorphism
Inheritance is appropriate when a subtype can safely satisfy the parent type's contract. Python's polymorphism is often structural: code can accept any object that provides the required behavior, without requiring a shared base class.
Prefer small public interfaces, avoid mutable class attributes for per-instance state, and test observable behavior rather than internal attributes.