大家好,我是正在实战各种 AI 项目的程序员晚枫。

Python 的接口和 Java、C# 不一样——没有 interface 关键字,没有强制实现。 那我们怎么保证代码的"契约"?靠 ABC(抽象基类)和 Protocol(协议类)。今天就把这两种方式讲透彻,让你的代码既灵活又可靠。


🦆 鸭子类型:Python 的默认接口哲学

核心理念

如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 不需要继承任何基类,只要方法签名匹配就行
class Dog:
def speak(self):
return "Woof!"

class Cat:
def speak(self):
return "Meow!"

class Robot:
def speak(self):
return "Beep!"

def make_sound(entity):
# 不检查类型,只调用方法
print(entity.speak())

make_sound(Dog()) # Woof!
make_sound(Cat()) # Meow!
make_sound(Robot()) # Beep!

# 问题:如果传入没有 speak 方法的对象呢?
# make_sound(42) # AttributeError: 'int' object has no attribute 'speak'

鸭子类型的优点与局限

优点局限
灵活,不需要继承体系运行时才报错
代码简洁缺乏文档约束
天然多态IDE 无法智能提示

💡 解决方案:用 ABC 做运行时约束,用 Protocol 做静态类型检查。


🎯 抽象基类 ABC:运行时的强制契约

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from abc import ABC, abstractmethod

class Animal(ABC):
"""动物抽象基类 —— 子类必须实现所有抽象方法"""

@abstractmethod
def speak(self):
"""发出声音"""
pass

@abstractmethod
def move(self):
"""移动"""
pass

def describe(self):
"""非抽象方法:子类可以直接继承"""
return f"{self.__class__.__name__}: {self.speak()}, {self.move()}"

# animal = Animal() # ❌ TypeError: 不能实例化抽象类

class Dog(Animal):
def speak(self):
return "Woof!"

def move(self):
return "Running"

dog = Dog() # ✅ 实现了所有抽象方法
print(dog.describe()) # Dog: Woof!, Running

# class IncompleteDog(Animal):
# def speak(self):
# return "Woof!"
# # 没实现 move → 实例化时报错 TypeError

抽象属性和类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from abc import ABC, abstractmethod

class DataSource(ABC):
"""数据源抽象基类"""

@property
@abstractmethod
def name(self):
"""数据源名称"""
pass

@classmethod
@abstractmethod
def from_config(cls, config):
"""从配置创建实例"""
pass

@abstractmethod
def read(self):
"""读取数据"""
pass

class CSVSource(DataSource):
def __init__(self, filepath):
self._filepath = filepath

@property
def name(self):
return f"CSV:{self._filepath}"

@classmethod
def from_config(cls, config):
return cls(config['filepath'])

def read(self):
with open(self._filepath) as f:
return f.read()

source = CSVSource("/data/test.csv")
print(source.name) # CSV:/data/test.csv

运行时类型检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from abc import ABC

class Drawable(ABC):
def draw(self):
pass

class Circle:
def draw(self):
print("Drawing circle")

# isinstance 检查
circle = Circle()
print(isinstance(circle, Drawable)) # False —— 没继承

# 注册虚拟子类
Drawable.register(Circle)
print(isinstance(circle, Drawable)) # True —— 虚拟子类
print(issubclass(Circle, Drawable)) # True

何时使用 ABC

  • ✅ 框架开发:需要强制用户实现某些方法
  • ✅ 运行时类型检查:用 isinstance() 判断
  • ✅ 明确的层次结构:确实有 is-a 关系
  • ❌ 简单脚本:过度设计
  • ❌ 只需要类型提示:用 Protocol 更合适

📋 Protocol 类型提示:静态检查的轻量契约

结构子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import Protocol

class Drawable(Protocol):
"""绘制协议 —— 不需要继承,只要结构匹配"""
def draw(self) -> None:
...

class Circle:
def draw(self) -> None:
print("Drawing circle")

class Square:
def draw(self) -> None:
print("Drawing square")

def render(shape: Drawable):
"""接受任何实现了 draw 方法的对象"""
shape.draw()

# 任何有 draw 方法的对象都可以传进来
render(Circle()) # Drawing circle
render(Square()) # Drawing square
# mypy 会检查传入的对象是否满足 Drawable 协议

带属性的 Protocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from typing import Protocol, runtime_checkable

@runtime_checkable
class Named(Protocol):
"""有名字的对象"""
name: str

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

class Company:
def __init__(self, name: str):
self.name = name

def greet(entity: Named):
print(f"Hello, {entity.name}!")

greet(Person("Alice")) # Hello, Alice!
greet(Company("TechCo")) # Hello, TechCo!

# 运行时检查(需要 @runtime_checkable)
print(isinstance(Person("Bob"), Named)) # True

Protocol 的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from typing import Protocol

class Closeable(Protocol):
"""可关闭"""
def close(self) -> None: ...

class Readable(Protocol):
"""可读取"""
def read(self, size: int = -1) -> str: ...

class Writable(Protocol):
"""可写入"""
def write(self, data: str) -> int: ...

# 组合协议
class FileLike(Readable, Writable, Closeable, Protocol):
"""文件-like 对象"""
pass

def process_file(f: FileLike):
"""接受任何文件-like 对象"""
f.write("hello")
content = f.read()
f.close()

⚖️ ABC vs Protocol:如何选择

对比表

特性ABCProtocol
检查时机运行时静态类型检查(mypy)
强制实现是(实例化时检查)否(仅类型检查器警告)
需要继承否(结构匹配)
isinstance 支持原生支持@runtime_checkable
灵活性低(必须显式继承)高(只要结构匹配)
适用场景框架、运行时检查类型提示、接口文档
Python 版本全版本3.8+

实战选择指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 场景1:框架开发,需要强制契约 → ABC
class BasePlugin(ABC):
@abstractmethod
def execute(self):
pass

# 场景2:类型提示,不强制继承 → Protocol
class Serializable(Protocol):
def to_dict(self) -> dict: ...

# 场景3:两者结合 → Protocol 为主,ABC 为辅
from typing import Protocol
from abc import ABC, abstractmethod

# Protocol 定义接口(给 mypy 用)
class DataProcessor(Protocol):
def process(self, data: bytes) -> str: ...

# ABC 提供默认实现(给运行时用)
class BaseProcessor(ABC):
@abstractmethod
def process(self, data: bytes) -> str:
...

def validate(self, data: bytes) -> bool:
"""默认实现"""
return len(data) > 0

🚫 避坑指南

陷阱1:Protocol 不能当运行时约束

1
2
3
4
5
6
7
8
9
10
11
12
13
from typing import Protocol

class Drawable(Protocol):
def draw(self) -> None: ...

class Broken:
pass # 没有 draw 方法

# ❌ 运行时不会报错!
def render(shape: Drawable):
shape.draw()

render(Broken()) # AttributeError! mypy 会警告,但运行不报错

陷阱2:ABC 过度使用

1
2
3
4
5
6
7
8
# ❌ 不需要 ABC 的简单场景
class Greeting(ABC):
@abstractmethod
def say_hello(self): pass

# ✅ 鸭子类型就够了
def say_hello(greetable):
greetable.say_hello()

陷阱3:Protocol 与 dataclass 配合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import Protocol
from dataclasses import dataclass

class HasName(Protocol):
name: str

@dataclass
class Person:
name: str
age: int

def print_name(obj: HasName):
print(obj.name)

print_name(Person("Alice", 30)) # ✅ mypy 检查通过

🎯 本讲总结

鸭子类型:Python 的默认哲学——不检查类型,只看行为。灵活但缺乏保障。

抽象基类 ABC:运行时强制子类实现方法。适合框架开发和运行时类型检查。

Protocol:结构子类型,静态类型检查。不需要继承,只要结构匹配即可。适合类型提示和接口文档。

选择原则:框架用 ABC,类型提示用 Protocol,简单场景用鸭子类型。


📚 推荐教材

《Python 编程从入门到实践(第 3 版)》 | 《流畅的 Python(第 2 版)》 | 《CPython 设计与实现》

学习路线: 零基础 → 《从入门到实践》 → 《流畅的 Python》 → 本门课程 → 《CPython 设计与实现》


🎓 加入《流畅的 Python》直播共读营

学到这里,如果你想系统吃透这本书——欢迎加入我的直播共读课。

  • 每周直播精讲,逐章拆解核心知识点
  • 专属学习群,随时答疑交流
  • 试运营特惠:499 元299 元

👉 【立即报名《流畅的 Python》共读课】https://mp.weixin.qq.com/s/ivHJwn1nNx5ug4TFrapvGg

🔗 课程导航

上一讲:继承与组合 | 下一讲:描述符


💬 联系我

平台账号/链接
微信扫码加好友
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!

fluent-python.png