欢迎光临
我们一直在努力

Python面向对象编程(OOP)三大特性详解:封装、继承、多态与魔法方法

面向对象编程(Object-Oriented Programming,OOP)是现代编程语言的基石,它通过封装、继承、多态三大特性,让代码更易维护、复用和扩展。本文将深入讲解这三大特性在 Python 中的具体实现,并结合实际代码示例,帮助你彻底掌握 OOP 的核心思想。


一、封装(Encapsulation)

封装是 OOP 的第一大特性,它隐藏对象的内部状态和实现细节,只对外提供有限的访问接口。这就像一台电视机,你只需要知道按哪个按钮换台、调音量,而不需要了解内部的电路原理。

1.1 封装的核心价值

  • 提高安全性:防止外部代码随意修改对象的内部状态。
  • 简化接口:用户只需关注“做什么”,而不必知道“怎么做”。
  • 易于维护:内部实现可以自由修改,只要接口不变,外部代码无需调整。

1.2 Python 中的访问控制

Python 没有严格的 private、protected、public 关键字,而是通过命名约定来实现访问控制。

1.2.1 单下划线 _(受保护的)

以单下划线开头的属性和方法,约定为“受保护的”(protected),表示不建议在类外部直接访问,但 Python 并不会阻止你访问。

class Person:
def __init__(self, name, age):
self._name = name # 受保护的属性
self._age = age

def _display_info(self): # 受保护的方法
print(f"Name: {self._name}, Age: {self._age}")

# 可以访问,但不建议
p = Person("Alice", 25)
print(p._name) # 可以访问,但不推荐
p._display_info() # 可以调用,但不推荐

1.2.2 双下划线 __(私有的)

以双下划线开头的属性和方法,Python 会进行名称修饰(Name Mangling),使其在类外部无法直接访问,从而实现“私有”效果。

class Person:
def __init__(self, name, age):
self.__name = name # 私有属性
self.__age = age

def __private_method(self): # 私有方法
print("This is private")

p = Person("Bob", 30)

# 直接访问会报错
print(p.__name) # AttributeError: 'Person' object has no attribute '__name'
p.__private_method() # AttributeError: 'Person' object has no attribute '__private_method'

1.2.3 名称修饰的原理

Python 会将 __name 重命名为 _类名__name,所以你仍然可以通过这个“变形”后的名字访问,但强烈不建议这样做。

# 通过变形后的名字可以访问(不推荐!)
print(p._Person__name) # Bob

# 查看对象的所有属性
print(dir(p))
# 输出中包含 '_Person__name' 和 '_Person__age'

1.2.4 前后双下划线 __xxx__(魔法方法)

这是 Python 的魔法方法(Magic Methods),用于实现特殊功能,如 __init__(构造函数)、__str__(字符串表示)等。这些方法可以被外部调用,但通常不直接调用。

class Person:
def __init__(self, name):
self.name = name

def __str__(self):
return f"Person: {self.name}"

p = Person("Charlie")
print(p) # 自动调用 __str__,输出:Person: Charlie

1.3 封装的正确实践:使用属性装饰器

为了既保护数据又提供访问接口,Python 推荐使用 @property 装饰器。

class Person:
def __init__(self, name, age):
self._name = name
self._age = age

@property
def name(self):
"""获取姓名(只读)"""
return self._name

@property
def age(self):
"""获取年龄"""
return self._age

@age.setter
def age(self, value):
"""设置年龄,可以添加验证逻辑"""
if value < 0:
raise ValueError("年龄不能为负数")
self._age = value

p = Person("David", 28)
print(p.name) # David
print(p.age) # 28
p.age = 30 # 通过 setter 修改
print(p.age) # 30
# p.name = "Eve" # 报错:AttributeError: can't set attribute


二、继承(Inheritance)

继承是类与类之间的关系,子类可以继承父类的属性和方法,实现代码复用。就像孩子继承父母的基因,子类“天生”就拥有父类的功能。

2.1 继承的基本语法

class Animal: # 父类(基类)
def __init__(self, name):
self.name = name

def speak(self):
return "动物叫"

class Dog(Animal): # 子类(派生类)
def speak(self):
return "汪汪汪"

class Cat(Animal):
def speak(self):
return "喵喵喵"

dog = Dog("旺财")
print(dog.name) # 旺财(继承自父类)
print(dog.speak()) # 汪汪汪(重写了父类方法)

2.2 继承的层次结构

Python 支持多重继承,所有类最终都继承自 object 类。

class A:
pass

class B(A): # 单继承
pass

class C(A, B): # 多重继承(不推荐,容易混乱)
pass

print(issubclass(B, A)) # True
print(issubclass(C, object)) # True,所有类都是 object 的子类

2.3 方法重写(Override)

子类可以重新定义父类的方法,实现自己的行为。

class Vehicle:
def move(self):
return "交通工具移动"

class Car(Vehicle):
def move(self): # 重写父类方法
return "汽车在路上行驶"

class Plane(Vehicle):
def move(self): # 重写父类方法
return "飞机在空中飞行"

car = Car()
print(car.move()) # 汽车在路上行驶

2.4 super() 函数

super() 用于调用父类的方法,在重写方法时保留父类的功能。

class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def introduce(self):
return f"我叫{self.name},今年{self.age}岁"

class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age) # 调用父类的 __init__
self.student_id = student_id

def introduce(self):
# 先调用父类的方法,再扩展
base_intro = super().introduce()
return f"{base_intro},学号是{self.student_id}"

stu = Student("小明", 18, "2023001")
print(stu.introduce()) # 我叫小明,今年18岁,学号是2023001

2.5 继承中的常见错误与修复

错误示例:属性访问问题

class Person:
def __init__(self, name, age):
self.__name = name # 私有属性
self.__age = age

def sing(self):
print(f"{self.name}爱唱歌") # 错误:访问的是 self.name,不是 self.__name

class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age)
print(f"我叫{self.name}") # 错误:同样访问 self.name

# 运行会报错:AttributeError: 'Student' object has no attribute 'name'

修复方案

class Person:
def __init__(self, name, age):
self._name = name # 改为受保护属性
self._age = age

@property
def name(self):
return self._name

@property
def age(self):
return self._age

def sing(self):
print(f"{self.name}爱唱歌") # 现在可以正确访问

class Student(Person):
def __init__(self, name, age, student_id):
super().__init__(name, age)
self.student_id = student_id
print(f"我叫{self.name},学号是{self.student_id}") # 通过 property 访问

stu = Student("小红", 20, "2023002")
stu.sing() # 小红爱唱歌


三、多态(Polymorphism)

多态是指同一个方法在不同的对象上会有不同的行为。就像“叫”这个方法,狗叫是“汪汪”,猫叫是“喵喵”,但都是“叫”。

3.1 多态的核心思想

  • 接口统一:不同对象对同一消息做出不同响应。
  • 提高灵活性:代码可以处理多种类型的对象,而无需关心具体类型。
  • 易于扩展:新增子类时,现有代码无需修改。

3.2 Python 中的多态实现

Python 是动态类型语言,多态是天然支持的。

class Animal:
def speak(self):
raise NotImplementedError("子类必须实现 speak 方法")

class Dog(Animal):
def speak(self):
return "汪汪汪"

class Cat(Animal):
def speak(self):
return "喵喵喵"

class Duck(Animal):
def speak(self):
return "嘎嘎嘎"

def animal_speak(animal):
"""多态的体现:同一个函数可以处理不同类型的对象"""
print(animal.speak())

# 创建不同动物对象
animals = [Dog(), Cat(), Duck()]

# 统一调用 speak 方法,但得到不同结果
for animal in animals:
animal_speak(animal)
# 输出:
# 汪汪汪
# 喵喵喵
# 嘎嘎嘎

3.3 鸭子类型(Duck Typing)

Python 的多态不依赖于继承,而是依赖于“鸭子类型”:如果一个对象走起来像鸭子、叫起来像鸭子,那么它就是鸭子。

class Dog:
def speak(self):
return "汪汪汪"

class Robot:
def speak(self):
return "我是机器人"

class Car:
def speak(self):
return "滴滴滴"

def make_sound(obj):
"""不关心 obj 的类型,只关心它有没有 speak 方法"""
print(obj.speak())

# 不同类型的对象,只要有 speak 方法就能调用
make_sound(Dog()) # 汪汪汪
make_sound(Robot()) # 我是机器人
make_sound(Car()) # 滴滴滴

3.4 多态的实际应用:函数参数

class Shape:
def area(self):
pass

class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height

def area(self):
return self.width * self.height

class Circle(Shape):
def __init__(self, radius):
self.radius = radius

def area(self):
return 3.14 * self.radius ** 2

def print_area(shape):
"""多态:可以接收任何 Shape 子类的对象"""
print(f"面积: {shape.area()}")

rect = Rectangle(5, 3)
circle = Circle(4)

print_area(rect) # 面积: 15
print_area(circle) # 面积: 50.24


四、魔法方法(Magic Methods)详解

魔法方法是 Python 面向对象编程的高级特性,它们以双下划线开头和结尾,让自定义对象能够像内置类型一样工作。

4.1 为什么需要魔法方法?

假设你写了一个自定义列表类:

class MyList:
def __init__(self, data):
self.data = data

def get_length(self):
return len(self.data)

def get_item(self, index):
return self.data[index]

ml = MyList([1, 2, 3])
print(ml.get_length()) # 3
print(ml.get_item(1)) # 2

这样写虽然能用,但不 Pythonic!Python 开发者习惯用 len(obj) 而不是 obj.get_length(),用 obj[1] 而不是 obj.get_item(1)。

4.2 常用魔法方法示例

4.2.1 __len__:实现 len() 函数

class MyList:
def __init__(self, data):
self.data = data

def __len__(self):
return len(self.data)

ml = MyList([1, 2, 3, 4, 5])
print(len(ml)) # 5,自动调用 __len__

4.2.2 __getitem__:实现索引访问 []

class MyList:
def __init__(self, data):
self.data = data

def __getitem__(self, index):
return self.data[index]

def __setitem__(self, index, value):
self.data[index] = value

def __delitem__(self, index):
del self.data[index]

ml = MyList([10, 20, 30])
print(ml[1]) # 20,自动调用 __getitem__
ml[1] = 200 # 自动调用 __setitem__
print(ml[1]) # 200
del ml[0] # 自动调用 __delitem__
print(len(ml.data)) # 2

4.2.3 __str__ 和 __repr__:对象字符串表示

class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
"""用户友好的字符串表示,用于 print()"""
return f"点({self.x}, {self.y})"

def __repr__(self):
"""开发者友好的字符串表示,用于调试"""
return f"Point(x={self.x}, y={self.y})"

p = Point(3, 4)
print(p) # 点(3, 4) – 调用 __str__
print(repr(p)) # Point(x=3, y=4) – 调用 __repr__

4.2.4 __iter__ 和 __next__:实现迭代器

class CountDown:
def __init__(self, start):
self.current = start

def __iter__(self):
return self

def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value

for num in CountDown(5):
print(num, end=" ") # 5 4 3 2 1

4.2.5 __call__:让对象可调用

class Adder:
def __init__(self, n):
self.n = n

def __call__(self, x):
return self.n + x

add5 = Adder(5)
print(add5(10)) # 15,对象像函数一样被调用
print(add5(20)) # 25

4.3 完整示例:自定义列表类

class MyList:
"""一个功能完整的自定义列表类"""

def __init__(self, *args):
self.data = list(args)

def __len__(self):
return len(self.data)

def __getitem__(self, index):
return self.data[index]

def __setitem__(self, index, value):
self.data[index] = value

def __delitem__(self, index):
del self.data[index]

def __str__(self):
return str(self.data)

def __repr__(self):
return f"MyList({self.data})"

def __add__(self, other):
"""实现 + 运算符"""
if isinstance(other, MyList):
return MyList(*(self.data + other.data))
return MyList(*(self.data + list(other)))

def append(self, item):
self.data.append(item)

def pop(self, index=1):
return self.data.pop(index)

# 使用示例
ml = MyList(1, 2, 3)
print(len(ml)) # 3
print(ml[1]) # 2
ml[1] = 20
print(ml) # [1, 20, 3]
ml.append(4)
print(ml) # [1, 20, 3, 4]

ml2 = MyList(5, 6)
ml3 = ml + ml2
print(ml3) # [1, 20, 3, 4, 5, 6]


五、综合案例:员工管理系统

让我们用一个完整的案例,综合运用封装、继承、多态和魔法方法。

class Employee:
"""员工基类"""

def __init__(self, name, emp_id, salary):
self._name = name
self._emp_id = emp_id
self._salary = salary

@property
def name(self):
return self._name

@property
def emp_id(self):
return self._emp_id

@property
def salary(self):
return self._salary

@salary.setter
def salary(self, value):
if value < 0:
raise ValueError("工资不能为负数")
self._salary = value

def work(self):
"""工作方法,子类需要重写"""
raise NotImplementedError("子类必须实现 work 四、排序与反转:sort / reverse / sorted
## 1. sort()、reverse() 是**原地修改**,无返回值
这是**超级高频错误**:误以为会返回新列表
```python
# 错误写法
lst = [3,1,2]
new_lst = lst.sort()
print(new_lst) # None!sort 没有返回值

  • lst.sort() / lst.reverse():直接改原列表,返回 None
  • 想要新列表、保留原列表 → 用内置函数 sorted() / 切片反转

2. 不同数据类型不能一起排序

列表中同时存在数字、字符串,调用 sort() 直接报错

lst = [1, "2", 3]
lst.sort() # 报错:TypeError: '<' not supported between instances of str and int

3. reverse() 只是反转顺序,不是降序

  • reverse():单纯颠倒元素位置,不做大小比较
  • sort(reverse=True):先排序,再变成降序

lst = [3,1,2]
lst.reverse()
print(lst) # [2, 1, 3] 不是降序

4. sorted 可以反转、指定规则

sorted 不修改原列表,支持 reverse、key 参数,用法和 sort 一致。


五、列表复制:深浅拷贝大坑(重中之重)

1. 直接赋值 = 引用传递,不是复制

new_lst = lst 只是两个变量指向同一个列表,改一个全变

lst = [1,2,3]
new_lst = lst
new_lst.append(4)
print(lst) # [1,2,3,4] 原列表被篡改

2. 浅拷贝:copy() / 切片 [:]

适用于一维简单列表(数字、字符串),正常使用;
但嵌套列表(二维/多维) 只会拷贝外层,内层还是引用:

lst = [[1,2], 3]
new_lst = lst.copy()
new_lst[0][0] = 99
print(lst) # [[99, 2], 3] 原列表内层被修改

解决嵌套列表:使用 copy 模块的 deepcopy 深拷贝

import copy
lst = [[1,2], 3]
new_lst = copy.deepcopy(lst)
new_lst[0][0] = 99
print(lst) # 原列表不变


六、其他通用易错点

1. 列表索引从 0 开始,负数索引倒着数

  • 正索引:0,1,2… 从左到右
  • 负索引:-1 最后一个,-2 倒数第二个

lst = [10,20,30]
print(lst[1]) # 30

易错:取最后一个元素别写成 lst[len(lst)],会索引越界。

2. 列表是可变对象,不能作为字典的键

字典键要求不可变类型(字符串、数字、元组),list 可变,直接报错:

d = {[1,2]: 100} # 报错:TypeError: unhashable type: 'list'

3. 切片操作不会越界报错

和索引取值不同,切片 lst[a:b] 起止索引超出范围不会报错,会自动截断:

lst = [1,2,3]
print(lst[10:20]) # []
print(lst[10:1]) # [1]


快速避错总结(背诵版)

  • 加元素:单个用 append,序列拆解用 extend
  • 删元素:remove/index 先判存在再调用,避免报错;循环删元素别直接遍历原列表
  • 排序:sort/reverse 原地修改、返回 None,要新列表用 sorted
  • 复制:一维用 copy/[:],嵌套列表必须深拷贝,千万别直接 = 赋值
  • 索引:取值别超界,切片不怕越界;空列表不能 pop
  • class list:
    def __init__(self,lst):
    self.lst = lst
    def _getthing_(self,index):
    return self.lst[index]
    def _removething_(self,index):
    del self.lst[index] #除了return,还有del可以使用
    def _len_(self):
    return len(self.lst)
    def append(self,item):
    self.lst.append(item)

    如果

    myList = [1,2,4,5,6,7,89]
    douElg = list(myList)
    print(douElg._len_())
    print(douElg.append(10))
    print(douElg._len_())

    则输出为7
    None
    8
    我先从你的代码运行结果解释入手,再彻底讲清楚魔法方法 vs 普通方法的核心区别、能不能不用、以及为什么必须掌握它。


    一、先解释你的代码输出

    你的代码运行结果完全正确,先拆解每一步:

    myList = [1,2,4,5,6,7,89]
    douElg = list(myList) # 调用你自定义的list类的__init__方法
    print(douElg._len_()) # 手动调用你写的_len_(),返回len([1,2,4,5,6,7,89]) → 7
    print(douElg.append(10)) # 调用你写的append(),它内部调用了原生list.append()
    # 原生append是原地修改,返回None → 输出None
    print(douElg._len_()) # 列表现在多了一个10,长度8 → 输出8

    ⚠️ 关键纠正:你写的 _len_()、_getthing_()、_removething_() 根本不是魔法方法!

    • 真正的Python魔法方法是前后各两个下划线:__len__、__getitem__、__delitem__
    • 单下划线 _xxx_ 只是你自己随便起的普通方法名,Python解释器完全不认识

    二、魔法方法 vs 普通方法:核心区别

    用一张表直接对比,一目了然:

    维度魔法方法(Magic Method)普通方法(Normal Method)
    命名规范 严格:前后各两个下划线 __xxx__ 任意合法标识符,通常用小写+下划线
    调用方式 Python解释器自动调用,用户几乎不手动写 必须用户手动调用
    触发时机 对应特定Python语法/内置函数 只有你写 obj.method() 时才执行
    作用 实现Python语言的标准协议,让自定义对象拥有和内置类型一样的行为 实现你自己的业务逻辑
    归属 Python语言层面定义的标准接口 开发者自定义的功能
    示例 __len__ → 触发 len(obj)__getitem__ → 触发 obj[index]__init__ → 触发 obj() 创建对象 你写的 _len_()、append()、calculate()

    三、不使用魔法方法能运行吗?

    完全可以! 你的代码就是最好的证明。

    你完全可以不用任何魔法方法,所有功能都用自己的普通方法实现:

    • 想获取长度 → 写 get_length() 手动调用
    • 想按索引取值 → 写 get_item(index) 手动调用
    • 想删除元素 → 写 delete_item(index) 手动调用

    但这样写会有灾难性的问题,我用你的代码改一下给你看:

    不用魔法方法的写法(反例)

    class MyList:
    def __init__(self, lst): # 其实__init__也是魔法方法,这里先保留
    self.data = lst

    # 自己写的普通方法
    def get_length(self):
    return len(self.data)

    def get_item(self, index):
    return self.data[index]

    def delete_item(self, index):
    del self.data[index]

    def append(self, item):
    self.data.append(item)

    # 使用时必须记住所有自定义方法名
    ml = MyList([1,2,3])
    print(ml.get_length()) # 3
    print(ml.get_item(1)) # 2
    ml.delete_item(0)
    print(ml.get_length()) # 2

    用魔法方法的写法(正例)

    class MyList:
    def __init__(self, lst):
    self.data = lst

    # 实现魔法方法,Python自动识别
    def __len__(self):
    return len(self.data)

    def __getitem__(self, index):
    return self.data[index]

    def __delitem__(self, index):
    del self.data[index]

    def append(self, item):
    self.data.append(item)

    # 使用时和Python原生list一模一样!
    ml = MyList([1,2,3])
    print(len(ml)) # 直接用内置函数len() → 3
    print(ml[1]) # 直接用[]语法 → 2
    del ml[0] # 直接用del语法
    print(len(ml)) # 2


    四、为什么必须使用魔法方法?(核心价值)

    魔法方法不是Python的“炫技”,而是让你的代码变得Pythonic、易用、可维护的核心手段。

    1. 与Python内置语法无缝兼容,行为一致

    这是最核心的原因。

    • 所有Python开发者都知道 len(obj) 是获取长度,obj[index] 是取值,del obj[index] 是删除
    • 如果你用自己的 get_length()、get_item(),别人用你的类时必须先读你的文档,记一堆特殊方法名
    • 实现了魔法方法,你的自定义类就和原生 list、dict、str 行为完全一致,零学习成本

    2. 自动触发,代码更简洁优雅

    魔法方法让你能用语法糖代替繁琐的方法调用:

    不用魔法方法用魔法方法
    ml.get_length() len(ml)
    ml.get_item(1) ml[1]
    ml.set_item(1, 100) ml[1] = 100
    ml.delete_item(1) del ml[1]
    for i in ml.get_iterator(): for i in ml:

    3. 能使用Python所有内置功能和标准库

    Python的大量内置函数、标准库都是基于魔法方法工作的:

    • for 循环依赖 __iter__ 或 __getitem__
    • sorted() 依赖 __lt__(小于比较)
    • print(obj) 依赖 __str__
    • if obj: 依赖 __bool__

    如果你不实现对应的魔法方法,这些功能都用不了:

    ml = MyList([1,2,3])
    # 如果你没实现__iter__,这行直接报错!
    for num in ml:
    print(num)

    # 如果你没实现__str__,print会输出丑陋的内存地址
    print(ml) # <__main__.MyList object at 0x7f8a1b2c3d30>

    4. 实现强大的自定义行为

    魔法方法让你能自定义几乎所有Python语法的行为:

    • 运算符重载:__add__ → a + b,__mul__ → a * b
    • 比较运算:__eq__ → a == b,__lt__ → a < b
    • 上下文管理器:__enter__、__exit__ → with obj:
    • 可调用对象:__call__ → obj() 像函数一样调用

    五、针对你的代码的修正建议

    把你写的普通方法改成对应的标准魔法方法,让它变成一个真正好用的自定义列表类:

    class MyList: # 注意:不要和Python原生的list重名!容易出bug
    def __init__(self, lst):
    self.lst = lst

    # 对应 len()
    def __len__(self):
    return len(self.lst)

    # 对应 [] 取值
    def __getitem__(self, index):
    return self.lst[index]

    # 对应 del [] 删除
    def __delitem__(self, index):
    del self.lst[index]

    # 对应 [] 赋值
    def __setitem__(self, index, value):
    self.lst[index] = value

    def append(self, item):
    self.lst.append(item)

    # 对应 print() 和 str()
    def __str__(self):
    return str(self.lst)

    # 现在用起来和原生list一模一样!
    myList = [1,2,4,5,6,7,89]
    douElg = MyList(myList)

    print(len(douElg)) # 7
    douElg.append(10)
    print(len(douElg)) # 8
    print(douElg[0]) # 1
    douElg[0] = 100
    print(douElg) # [100, 2, 4, 5, 6, 7, 89, 10]
    del douElg[1]
    print(len(douElg)) # 7


    六、最后总结

  • 魔法方法的本质:Python解释器预留的“钩子”,当你使用特定语法时,解释器会自动调用对应的钩子方法。
  • 能不能不用:能,但你的代码会变得繁琐、难用、无法和Python生态兼容。
  • 为什么要用:让自定义对象拥有和内置类型一致的行为,代码更简洁、易读、可维护,能利用Python所有强大的内置功能。
  • 命名规范:必须是前后各两个下划线 __xxx__,单下划线 _xxx_ 只是普通方法。
  • 赞(0)
    未经允许不得转载:171主机测评 » Python面向对象编程(OOP)三大特性详解:封装、继承、多态与魔法方法
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址