⛏️ Object‑Oriented Programming in Python
(mining & drilling edition)

Understand OOP through real industrial equipment — drills, sensors, ore veins, and blasting teams. No abstract animals or shapes. Every concept includes a practical, domain‑driven example. Now with three advanced topics: metaclasses, context managers, and descriptors.

1. Classes & Instances — blueprint vs. real drill

A class defines shared structure; an instance holds its own state. Here MiningDrill has a class attribute category and per‑drill attributes. The self parameter gives access to the concrete object.

class MiningDrill:
    category = "rotary"

    def __init__(self, model: str, power_kw: int):
        self.model = model
        self.power_kw = power_kw
        self.is_running = False

    def start(self) -> str:
        self.is_running = True
        return f"{self.model} started — {self.power_kw} kW"

drill_a = MiningDrill("X900", 450)
drill_b = MiningDrill("Z1100", 620)
print(drill_a.start())
print(drill_b.category)

2. Inheritance — extending tools for specialization

Drill inherits from Tool, reusing __init__ and use() while adding power_kw. super() calls the parent method – perfect for the open/closed principle.

class Tool:
    def __init__(self, name: str): self.name = name
    def use(self) -> str: return f"using {self.name}"

class Drill(Tool):
    def __init__(self, name: str, power_kw: int):
        super().__init__(name)
        self.power_kw = power_kw
    def use(self) -> str:
        return super().use() + f" with {self.power_kw} kW"

hd = Drill("HammerDrill", 300)
print(hd.use())

🧠 Design tip: Prefer composition over deep inheritance when behavior varies widely. Mixins can share small pieces of functionality.

3. Encapsulation & properties — protect the sensor

Use underscored names (“protected”) and @property to add validation without breaking existing code. Here _celsius is hidden behind a property that rejects unsafe values.

class TemperatureSensor:
    def __init__(self):
        self._celsius = 20.0

    @property
    def celsius(self) -> float: return self._celsius

    @celsius.setter
    def celsius(self, value: float):
        if not (-50 <= value <= 150):
            raise ValueError("Out of safe range")
        self._celsius = value

    @property
    def fahrenheit(self) -> float: return self._celsius * 9/5 + 32

s = TemperatureSensor()
s.celsius = 85.0
print(s.fahrenheit)

4. Polymorphism & duck typing — just implement .action()

Python doesn’t need interfaces; any object with an action() method works. This duck typing enables flexible, decoupled code.

class Drill:
    def action(self): return "drilling rock"
class BlastingTeam:
    def action(self): return "setting explosives"
class Hauler:
    def action(self): return "transporting ore"

def perform_actions(equipment_list):
    for eq in equipment_list: print(eq.action())

team = [Drill(), BlastingTeam(), Hauler()]
perform_actions(team)

5. Magic methods — make ore veins behave like built‑ins

Implement __str__, __len__, __lt__, __add__ so custom objects work with print(), len(), comparison, and even +.

class OreVein:
    def __init__(self, mineral: str, grade: float):
        self.mineral = mineral; self.grade = grade
    def __str__(self): return f"{self.mineral} ({self.grade}%)"
    def __len__(self): return int(self.grade * 10)
    def __lt__(self, other): return self.grade < other.grade
    def __add__(self, other):
        return OreVein("mixed", (self.grade + other.grade)/2)

v1 = OreVein("gold", 5.2); v2 = OreVein("silver", 3.8)
print(v1, len(v1), v1 > v2, v1 + v2)

6. Class & static methods — alternative constructors, utilities

@classmethod receives the class (cls) and can create instances differently. @staticmethod is just a function attached to the class for organisation.

class Survey:
    total = 0
    def __init__(self, area): self.area = area; Survey.total += 1

    @classmethod
    def from_string(cls, data): return cls(data.split(":")[0])

    @classmethod
    def count(cls): return f"Surveys: {cls.total}"

    @staticmethod
    def is_valid_area(name): return len(name) > 3 and name.replace("_","").isalnum()

s2 = Survey.from_string("SouthPit:gold")
print(Survey.count(), Survey.is_valid_area("West_Zone"))

7. Composition — “has‑a” over “is‑a”

A DrillingRig contains an Engine and a DrillBit. This makes testing and swapping parts easy.

class Engine:
    def start(self): return "engine running"
class DrillBit:
    def __init__(self, mat): self.material = mat
    def spin(self): return f"{self.material} bit spinning"

class DrillingRig:
    def __init__(self, bit_mat):
        self.engine = Engine(); self.bit = DrillBit(bit_mat)
    def start_op(self):
        return f"{self.engine.start()} — {self.bit.spin()}"

rig = DrillingRig("diamond")
print(rig.start_op())

8. Abstract base classes — enforce a contract

Using ABC and @abstractmethod you force subclasses to implement operate(). Great for frameworks.

from abc import ABC, abstractmethod
class MiningEquipment(ABC):
    def __init__(self, name): self.name = name
    @abstractmethod
    def operate(self): pass

class Excavator(MiningEquipment):
    def operate(self): return f"{self.name} excavating"

e = Excavator("CAT 320"); print(e.operate())

9. Dataclasses — minimal boilerplate for ore deposits

@dataclass generates __init__, __repr__, __eq__ automatically. Perfect for data‑centric classes.

from dataclasses import dataclass, field

@dataclass
class MineralDeposit:
    name: str
    depth: float
    grade: float
    active: bool = True
    tags: list = field(default_factory=list)

    def extractable(self) -> bool:
        return self.grade > 1.5 and self.depth < 1000

d1 = MineralDeposit("copper", 350.2, 2.3)
d2 = MineralDeposit("gold", 820.0, 5.1, tags=["high-grade"])
print(d1, d1.extractable())

10. Builder pattern — fluent configuration of a drill

When an object has many optional parameters, a builder provides a readable, chainable API.

class DrillConfiguration:
    def __init__(self, model, power):
        self.model = model; self.power = power
        self.depth_cap = 1000; self.bit_type = "standard"; self.autolube = False

class DrillBuilder:
    def __init__(self, model, power):
        self._config = DrillConfiguration(model, power)
    def set_depth(self, depth): self._config.depth_cap = depth; return self
    def set_bit(self, bit): self._config.bit_type = bit; return self
    def enable_autolube(self): self._config.autolube = True; return self
    def build(self): return self._config

cfg = (DrillBuilder("MegaDrill",800)
       .set_depth(2500).set_bit("diamond").enable_autolube().build())
print(cfg.bit_type, cfg.autolube)

11. Metaclasses — the class of a class

Metaclasses are the "classes of classes" — they control how classes are built. You can use them to enforce conventions, register subclasses, or add methods automatically. In mining, a metaclass could ensure every piece of equipment has a safety checklist.

class EquipmentMeta(type):
    def __new__(cls, name, bases, namespace):
        # Automatically add a safety_inspection method if missing
        if 'safety_inspection' not in namespace:
            def safety_inspection(self):
                return f"⚠️ Basic safety check passed for {self.name}"
            namespace['safety_inspection'] = safety_inspection
        return super().__new__(cls, name, bases, namespace)

class Drill(metaclass=EquipmentMeta):
    def __init__(self, name): self.name = name

d = Drill("Rotary 77")
print(d.safety_inspection())

Metaclasses are powerful but rare — use them when you need to modify classes systematically.

12. Context managers — setup & teardown with with

Context managers handle resource acquisition and release automatically. In a mining operation, opening a tunnel might require ventilation startup and later shutdown. Implement via __enter__/__exit__.

class TunnelVentilation:
    def __init__(self, tunnel_id):
        self.tunnel_id = tunnel_id

    def __enter__(self):
        print(f"🌀 Ventilation ON for tunnel {self.tunnel_id}")
        return self   # object to use in 'as'

    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"🌀 Ventilation OFF for tunnel {self.tunnel_id}")
        return False   # propagate exceptions if any

    def read_air_quality(self):
        return "Air quality: good"

with TunnelVentilation("T-12") as tv:
    print(tv.read_air_quality())

The with statement guarantees cleanup even if an error occurs — essential for safety-critical systems.

13. Descriptors — reusable attribute logic

Descriptors let you define reusable attribute access (get/set/delete) and share it across classes. Perfect for validated fields like pressure or temperature in mining sensors.

class ValidatedAttribute:
    def __init__(self, min_val, max_val):
        self.min = min_val
        self.max = max_val
        self.data = "_internal"   # name mangled later

    def __set_name__(self, owner, name):
        self.private_name = '_' + name   # avoid collisions

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        if not (self.min <= value <= self.max):
            raise ValueError(f"Value {value} out of range [{self.min}, {self.max}]")
        setattr(obj, self.private_name, value)

class HighPressureSensor:
    pressure = ValidatedAttribute(0, 500)   # psi range
    def __init__(self, pressure):
        self.pressure = pressure

sensor = HighPressureSensor(350)
print(sensor.pressure)
# sensor.pressure = 600  # would raise ValueError

Descriptors are the machinery behind @property, @staticmethod, and @classmethod.


📌 Core + Advanced OOP principles — recap

All examples use mining equipment to make OOP tangible. Reusable, testable, and directly applicable to simulation, control, or data processing in industrial software.