Protecting Object Attributes
There are two ways to modify an object's attributes:
- Direct assignment:
object.attribute = value - Indirect modification via a method:
object.method()
To ensure attribute safety and prevent arbitrary changes, a common approach is:
- Make attributes private
- Provide public methods that enforce validation logic
class Person:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, new_name):
if len(new_name) >= 5:
self.__name = new_name
else:
print("Error: name must be at least 5 characters long")
# This will raise an AttributeError
# xiaoming = Person("dongGe")
# print(xiaoming.__name)
xiaoming = Person("dongGe")
xiaoming.set_name("wanger")
print(xiaoming.get_name())
xiaoming.set_name("lisi")
print(xiaoming.get_name())
Key Points
- Python does not have explicit
publicorprivatekeywords like C++. Instead, name mangling is used: a double underscore prefix (__) makes an attribute (or method) private. - Private members are not directly accessible from outside the class; they are accessed through public methods.
The __del__ Method
When an object is created, Python cals the __init__ method. When an object is deleted, Python calls the __del__ method (the destructor).
import time
class Animal:
def __init__(self, name):
print('__init__ called')
self.__name = name
def __del__(self):
print("__del__ called")
print(f"{self.__name} is about to be destroyed...")
# Create and delete an object
dog = Animal("Husky")
del dog
# Reference counting example
cat = Animal("Persian")
cat2 = cat
cat3 = cat
print("--- deleting cat")
del cat
print("--- deleting cat2")
del cat2
print("--- deleting cat3")
del cat3
print("Program ending in 2 seconds")
time.sleep(2)
Observation: The destructor runs only when the reference count drops to zero. In the second part, cat, cat2, and cat3 all refer to the same object; the __del__ method is called only after the last reference is deleted.
Summary on Reference Counting
- Every variable that holds a reference to an object increments that object's reference count.
- Using
delreduces the reference count by one, not necessarily removing the object immediately. - The object is truly deleted when its reference count becomes zero.
Inheritance and Single Inheritance
Concept
Inheritance models an "is-a" relationship. For example, a Dog is an Animal, and a PersianCat is a Cat.
Example
class Cat:
def __init__(self, name, color="white"):
self.name = name
self.color = color
def run(self):
print(f"{self.name} is running")
class PersianCat(Cat):
def rename(self, new_name):
self.name = new_name
def eat(self):
print(f"{self.name} is eating")
bs = PersianCat("Indian Cat")
print(f"Name: {bs.name}")
print(f"Color: {bs.color}")
bs.eat()
bs.rename("Persian")
bs.run()
Even though the subclass does not define __init__, it inherits the parant's constructor.
Rules
- The parent class is specified in parentheses during class definition.
- The subclass inherits all public attributes and methods from the parent.
Important Note on Private Members
Private attributes and methods (double underscore) are not inherited by subclasses. They are accessible only within the defining class.
class Animal:
def __init__(self, name='animal', color='white'):
self.__name = name
self.color = color
def __secret(self):
print(self.__name, self.color)
def reveal(self):
print(self.__name, self.color)
class Dog(Animal):
def dog_test(self):
# print(self.__name) # Not accessible
print(self.color)
# self.__secret() # Not accessible
self.reveal()
a = Animal()
print(a.color)
# print(a.__name) # Error
a.reveal()
d = Dog(name="spot", color="brown")
d.dog_test()
Key takeaway: Private members (both attributes and methods) are name-mangled and cannot be accessed from subclasses or outside the class.
Multiple Inheritance
Python supports multiple inheritance, where a class can inherit from more than one parent.
class A:
def print_a(self):
print('----A----')
class B:
def print_b(self):
print('----B----')
class C(A, B):
def print_c(self):
print('----C----')
obj = C()
obj.print_a()
obj.print_b()
Method Resolution Order (MRO)
When two parents have a method with the same name, MRO determines which one is called.
class Base:
def test(self):
print('----Base test----')
class A(Base):
def test(self):
print('----A test----')
class B(Base):
def test(self):
print('----B test----')
class C(A, B):
pass
obj = C()
obj.test() # Calls A.test, because A appears before B in inheritance
print(C.__mro__) # Shows search order
Overriding and Calling Parent Methods
Overriding
A subclass can define a method with the same name as a parent method, which overrides the parent's implementation.
class Cat:
def greet(self):
print("halou-----1")
class PersianCat(Cat):
def greet(self):
print("halou-----2")
p = PersianCat()
p.greet()
Calling Parent Methods
If you want to extend (not completely replace) the parent's method, you can explicitly call it.
class Cat:
def __init__(self, name):
self.name = name
self.color = 'yellow'
class PersianCat(Cat):
def __init__(self, name):
# Option 1: Python 2 style (not recommended)
# Cat.__init__(self, name)
# Option 2: explicit super (Python 2 and 3)
# super(PersianCat, self).__init__(name)
# Option 3: recommended Python 3 style
super().__init__(name)
def get_name(self):
return self.name
bosi = PersianCat('xiaohua')
print(bosi.name)
print(bosi.color)
Polymorphism and Duck Typing
Polymorphism in languages like Java requires explicit type hierarchies. Python embraces "duck typing" — if an object has the required method, it can be used regardless of its type.
class F1:
def show(self):
print('F1.show')
class S1(F1):
def show(self):
print('S1.show')
class S2(F1):
def show(self):
print('S2.show')
def display(obj):
obj.show()
s1 = S1()
display(s1) # Output: S1.show
s2 = S2()
display(s2) # Output: S2.show
The function display can accept any object that has a show method.
Class Attributes vs Instance Attributes
- Class attributes belong to the class itself and are shared by all instances.
- Instance attributes are unique to each instance.
class People:
country = 'china' # class attribute
# Access via class or instance
print(People.country)
p = People()
print(p.country)
# Modifying via instance creates a new instance attribute
p.country = 'japan'
print(p.country) # instance attribute shadows class attribute
print(People.country) # class attribute unchanged
del p.country
print(p.country) # now refers to class attribute again
Class Methods and Static Methods
Class Methods
A class method takes cls as the first parameter and can modify class state.
class People:
country = 'china'
@classmethod
def get_country(cls):
return cls.country
@classmethod
def set_country(cls, country):
cls.country = country
p = People()
print(p.get_country()) # instance call
print(People.get_country()) # class call
p.set_country('japan')
print(p.get_country()) # now returns 'japan'
print(People.get_country()) # also 'japan'
Static Methods
A static method does not receive any special first parameter. It behaves like a regular function but belongs to the class's namespace.
class People:
country = 'china'
@staticmethod
def get_country():
return People.country
print(People.get_country())
Summary
- Instance methods have
selfand can access both instance and class attributse. - Class methods have
clsand can only access class attributes (they are useful for factory methods or modifying class state). - Static methods do not have
selforcls; they are independent functions grouped under the class.