Encapsulation and Inheritance
Encapsulation hides the internal details of an object, exposing only safe operational interfaces. Inheritance lets one class automatically have the functionality of another, avoiding repetition. These two concepts are the backbone of object-oriented programming.
1. Encapsulation and Private Attributes
Encapsulation means that an object's internal data should not be directly accessed from outside — it should be manipulated through methods.
Single Underscore _ — Convention for "Protected" Attributes
Example: Single and Double Underscore
class Person:
def __init__(self, name, age):
self.name = name
self._age = age # Single underscore: hint that it's internal
p = Person("Zhang San", 25)
print(p.name) # Zhang San — public attribute
print(p._age) # 25 — accessible, but not recommended
A single underscore is just a convention, not enforced — Python won't stop you from accessing it. It's like writing in the documentation "this is internal, don't touch."
Double Underscore __ — Name Mangling
Example: Bank Account Class
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self.__balance = balance # Double underscore: private attribute
def get_balance(self):
"""Safely get balance"""
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return True
return False
account = BankAccount("Zhang San", 10000)
print(account.owner) # Zhang San — public
# print(account.__balance) # Error! AttributeError
print(account.get_balance()) # 10000 — accessed via method
Double underscores trigger name mangling — __balance is internally transformed to _BankAccount__balance. This isn't true "privacy," just making accidental overriding less likely.
Tip: Python has no true private. It relies on the "we are all consenting adults" understanding — single underscore means "internal use," double underscore prevents name collisions. Other languages (like Java) have a true
privatekeyword; Python doesn't and isn't planning to add one.
Example: Temperature Class (Difficulty: Star-Star)
class Temperature:
def __init__(self, celsius=0):
self.__celsius = celsius
def to_fahrenheit(self):
return self.__celsius * 9 / 5 + 32
def set_celsius(self, value):
"""Safely set temperature — cannot go below absolute zero"""
if value < -273.15:
print("Temperature cannot be below absolute zero!")
return
self.__celsius = value
def get_celsius(self):
return self.__celsius
t = Temperature(100)
print(f"100°C = {t.to_fahrenheit():.1f}°F") # 212.0°F
t.set_celsius(-300) # Temperature cannot be below absolute zero!
t.set_celsius(0)
print(f"0°C = {t.to_fahrenheit():.1f}°F") # 32.0°F
2. @property: Elegant Access Control
Writing set_celsius() / get_celsius() is verbose. @property lets you control access while keeping the syntax of attribute access:
Example: Using @property
class Temperature:
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Get Celsius temperature"""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set Celsius temperature — auto-validate"""
if value < -273.15:
raise ValueError("Temperature cannot be below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Fahrenheit (read-only — no setter)"""
return self._celsius * 9 / 5 + 32
t = Temperature(100)
print(t.celsius) # 100 — accessed like an attribute
print(t.fahrenheit) # 212.0
t.celsius = 0 # Assigned like an attribute
print(t.celsius) # 0
# t.celsius = -300 # Would raise ValueError!
# t.fahrenheit = 100 # Error! No setter
Tip: Advantages of
@property: 1) No parentheses needed for access:t.celsiusinstead oft.get_celsius(). 2) Automatic validation on assignment:t.celsius = valueinstead oft.set_celsius(value). 3) Can convert a regular attribute to a computed one without breaking existing code.
3. Inheritance
Inheritance lets a child class have all the attributes and methods of its parent class:
Example: Animal Inheritance
class Animal:
"""Animal base class"""
def __init__(self, name):
self.name = name
def speak(self):
return "..."
def eat(self, food):
return f"{self.name} is eating {food}"
class Dog(Animal):
"""Dog — inherits from Animal"""
def speak(self):
return "Woof!"
class Cat(Animal):
"""Cat — inherits from Animal"""
def speak(self):
return "Meow!"
dog = Dog("Wangcai")
cat = Cat("Mimi")
print(dog.speak()) # Woof! (overrides parent method)
print(cat.speak()) # Meow! (overrides parent method)
print(dog.eat("bone")) # Wangcai is eating bone (inherited from parent)
print(cat.eat("fish")) # Mimi is eating fish (inherited from parent)
super(): Calling Parent Methods
Example: super() to Call the Parent
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Call the parent's __init__
self.breed = breed # Add child-specific attributes
def introduce(self):
return f"I'm {self.name}, a {self.breed}"
dog = Dog("Wangcai", "Golden Retriever")
print(dog.introduce()) # I'm Wangcai, a Golden Retriever
4. Mixin Pattern
A Mixin is a special class — not used independently, but "mixed in" to add functionality to other classes:
Example: Mixin Pattern
class FlyMixin:
"""Flying ability mixin"""
def fly(self):
return f"{self.name} is flying!"
class SwimMixin:
"""Swimming ability mixin"""
def swim(self):
return f"{self.name} is swimming!"
class Animal:
def __init__(self, name):
self.name = name
class Duck(Animal, SwimMixin, FlyMixin):
"""Duck — can both swim and fly"""
def speak(self):
return "Quack!"
class Fish(Animal, SwimMixin):
"""Fish — can only swim"""
def speak(self):
return "..."
duck = Duck("Donald")
fish = Fish("Nemo")
print(duck.speak()) # Quack!
print(duck.swim()) # Donald is swimming!
print(duck.fly()) # Donald is flying!
print(fish.swim()) # Nemo is swimming!
Tip: Mixin naming convention: Class names ending in
Mixinindicate they are for mixing in, not for standalone use. A class can mix in multiple Mixins plus one main base class — much more flexible than deep single inheritance.
Common Use Cases
- Data validation: Use
@property.setterto ensure data validity on assignment. - Read-only properties: Use
@propertywithout a setter for read-only attributes. - Code reuse: Put common functionality in the base class; subclasses only write the differences.
- Functional composition: Use Mixins to flexibly add features to different classes.
FAQ
@property and a regular method?@property makes a method look like an attribute — no parentheses for access, assignment triggers the setter. It's essentially an "attribute interceptor" — you write code as if manipulating an attribute, but a method actually executes.class C(A, B). However, multiple inheritance has the "diamond problem" (which parent method to use when both have the same name). Python uses MRO (Method Resolution Order), searching left to right. Mixins are a design pattern that leverages multiple inheritance, with Mixin classes being small and functionally independent, avoiding diamond issues.Summary
- Encapsulation:
_means "don't touch";__triggers name mangling;@propertyprovides elegant access control - Inheritance: child classes automatically have parent attributes and methods;
super()calls parent methods - Method overriding: child class redefines a method with the same name as the parent
- Mixin: use multiple inheritance to flexibly add functionality; preferable to deep inheritance
- Python has no true private attributes — relies on convention and name mangling
Exercises
-
Beginner (Difficulty: Star): Define a
Productclass with@propertyfor thepriceattribute — must be positive when setting; returns the original value when getting. -
Intermediate (Difficulty: Star-Star): Define a three-level inheritance:
Animal->Mammal->Dog/Cat.Animalhasnameandage;Mammaladdsfur_color;DogandCateach implement their ownspeak()method. -
Advanced (Difficulty: Star-Star-Star): Create a logging system with
LoggableMixin(provideslog_info(),log_error()methods) and a base classApplication(has anameattribute). HaveWebAppandCLIAppboth inherit fromApplicationand mix inLoggableMixinto implement logging functionality.



