# Python如何实现封装、继承和多态?
面试官问:"Python如何实现封装、继承和多态?"
这是面向对象的经典三问,很多人会背定义,但说不清Python的实现细节。真正要讲清楚的是:Python的封装靠命名约定而非访问控制符,继承支持单继承和多继承,多态依赖鸭子类型而不强制类型检查。 Python的面向对象比Java、C++更灵活,但也更容易误用。
# 简要回答
封装是把数据和操作数据的方法放在一起,隐藏内部实现细节。Python通过单下划线_attr表示"内部使用",双下划线__attr触发名称改写(name mangling),但没有真正的私有属性。
继承是子类获得父类的属性和方法。Python用class Child(Parent)实现单继承,用class Child(Parent1, Parent2)实现多继承。子类可以重写父类方法,通过super()调用父类方法。
多态是不同类的对象可以响应相同的方法调用。Python采用鸭子类型(duck typing):"如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。" 对象不需要继承共同父类,只要有同名方法就能多态调用。
# 详细回答
# 封装
封装的目的是隐藏实现细节,只暴露必要的接口。 Python没有private、protected关键字,而是用命名约定:
- 公开属性:普通命名,如
self.name,任何地方都能访问。 - 内部使用:单下划线开头,如
self._internal,表示"这是内部实现,外部不应直接使用",但技术上仍可访问。 - 名称改写:双下划线开头(且不以双下划线结尾),如
self.__private,Python会改写成_ClassName__private,外部直接访问obj.__private会报错。
class BankAccount:
def __init__(self, balance):
self.__balance = balance # 名称改写
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def get_balance(self):
return self.__balance
account = BankAccount(1000)
account.deposit(500)
print(account.get_balance()) # 1500
# 直接访问会报错
# print(account.__balance) # AttributeError
# 但可以通过改写后的名字访问(不推荐)
print(account._BankAccount__balance) # 1500
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
名称改写不是真正的私有。 它只是把__balance改成了_BankAccount__balance,防止子类意外覆盖父类的私有属性。Python哲学是"我们都是成年人",不强制隐藏,但约定俗成地尊重单下划线和双下划线的语义。
# 继承
子类继承父类的所有属性和方法:
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some sound"
class Dog(Animal):
def speak(self): # 重写父类方法
return f"{self.name} says: Woof!"
dog = Dog("Buddy")
print(dog.name) # Buddy,继承了父类的__init__
print(dog.speak()) # Buddy says: Woof!,使用子类的speak
2
3
4
5
6
7
8
9
10
11
12
13
14
super()用于调用父类方法:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # 调用父类的__init__
self.breed = breed
dog = Dog("Buddy", "Golden Retriever")
print(dog.name) # Buddy
print(dog.breed) # Golden Retriever
2
3
4
5
6
7
8
9
10
11
12
不用super()也可以直接调用父类方法,但在多继承时会出问题,super()会按MRO(方法解析顺序)调用,避免重复调用。
Python支持多继承:
class Flyable:
def fly(self):
return "Flying"
class Swimmable:
def swim(self):
return "Swimming"
class Duck(Flyable, Swimmable):
pass
duck = Duck()
print(duck.fly()) # Flying
print(duck.swim()) # Swimming
2
3
4
5
6
7
8
9
10
11
12
13
14
多继承时,方法查找按MRO顺序。如果多个父类有同名方法,第一个父类的方法会被调用。详细见下一篇《Python多继承的MRO是什么?》。
# 多态
Python的多态不依赖继承关系,只要对象有同名方法,就能多态调用:
class Dog:
def speak(self):
return "Woof!"
class Cat:
def speak(self):
return "Meow!"
class Car:
def speak(self):
return "Beep!"
def make_sound(obj):
print(obj.speak()) # 不关心obj是什么类型
make_sound(Dog()) # Woof!
make_sound(Cat()) # Meow!
make_sound(Car()) # Beep!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
make_sound不检查对象类型,只要对象有speak方法就行。这就是鸭子类型:"如果它有speak方法,我就能调用它的speak。"
与静态类型语言的区别:
在Java或C++中,多态通常依赖继承和接口。你需要定义一个Animal接口或父类,Dog和Cat都继承它,函数参数声明为Animal类型。
Python不需要这样。函数参数不声明类型,运行时只要对象有对应的方法,就能调用。这种灵活性很强大,但也容易出错:如果传入的对象没有speak方法,会在运行时报AttributeError。

# 鸭子类型与协议
Python 3.8+引入了typing.Protocol,可以显式声明鸭子类型的契约:
from typing import Protocol
class Speakable(Protocol):
def speak(self) -> str:
...
def make_sound(obj: Speakable):
print(obj.speak())
class Dog:
def speak(self) -> str:
return "Woof!"
make_sound(Dog()) # 类型检查通过,即使Dog没有继承Speakable
2
3
4
5
6
7
8
9
10
11
12
13
14
Protocol是静态类型检查工具(mypy、Pyright)用的,运行时不强制。它让鸭子类型有了显式的接口定义,但不改变Python的动态特性。
# 知识拓展
Q1:Python有没有真正的私有属性?
没有。双下划线只是名称改写,仍然可以通过_ClassName__attr访问。Python认为"私有"是约定,不是语言强制的。如果你非要访问内部属性,你承担后果。
Q2:单下划线和双下划线什么时候用?
- 单下划线:表示"这是内部实现,外部不应直接用"。如
self._cache、def _helper()。 - 双下划线:避免子类意外覆盖父类的同名属性。如果你的类可能被继承,且有些属性绝对不能被子类覆盖,用双下划线。
大多数情况用单下划线就够了。双下划线只在你明确需要名称改写时用。
Q3:为什么Python多态不需要继承?
因为Python是动态类型语言,运行时才查找方法。调用obj.method()时,Python在obj的类和父类里查找method,找到就调用,不管obj是什么类型。
静态类型语言需要在编译时确定方法,所以需要共同的父类或接口。Python没有这个限制。
Q4:多继承会有什么问题?
最大的问题是钻石继承(菱形继承):
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
class C(A):
def method(self):
print("C")
class D(B, C):
pass
d = D()
d.method() # 输出什么?
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Python按MRO解决:D -> B -> C -> A。d.method()会调用B.method()。但如果继承关系复杂,MRO可能不符合直觉,所以多继承要谨慎使用。
Q5:能不能在子类里调用爷爷类的方法,跳过父类?
技术上可以,但不推荐。如果一定要,可以显式调用:
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
class C(B):
def method(self):
A.method(self) # 跳过B,直接调用A
C().method() # A
2
3
4
5
6
7
8
9
10
11
12
13
但这会破坏继承链,如果有多继承或MRO变化,可能出bug。正确做法是每个类都用super()调用下一个,让MRO自然解析。
上一篇:__new__和__init__有什么区别?
下一篇:Python多继承的MRO是什么?super()如何工作?
评论
验证登录状态...