第 4 章 · OOP¶
OOP 是你的主场。但 Python 的 OOP 和 Java 有几处根本差异:没有真正的访问控制、支持多继承、"接口"是 Protocol 而非 nominal、运算符可重载、用 dataclass 一行替代 POJO 样板。本章对照着讲。
4.1 类、__init__ 与 self¶
-
Java
-
Python
三个关键差异:
self必须显式写为方法的第一个参数(Java 的this是隐式的)。调用时p.greet()会自动把p传给self。__init__是初始化器,不是完整构造器(创建对象是__new__,绝大多数情况不用管)。相当于 Java 构造器的方法体。- 不用
new:Person("Alice", 30)直接创建实例。 - 实例属性不预先声明:在
__init__里self.x = ...才创建。没有 Java 那种"字段声明区"。
4.2 实例变量 vs 类变量¶
对应 Java 的实例字段 vs 静态字段:
-
Java
-
Python
⚠️ Java 程序员的陷阱
通过实例访问类变量时,读没问题,但写 self.x = ... 会创建一个同名实例变量遮蔽类变量,不会修改类变量。要改类变量,用 ClassName.x = ... 或 type(self).x = ...。
类方法 / 静态方法:
class Foo:
@classmethod
def from_config(cls, cfg): # cls 是类本身,类似 Java 的静态工厂
return cls()
@staticmethod
def utility(): # 不需要 self/cls,类似 Java 静态方法
return "helper"
4.3 继承¶
-
Java(单继承 + 接口)
-
Python(可多继承)
super() 调用父类(Python 3 无需参数):
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # 调用父类 __init__
self.breed = breed
多继承与 MRO¶
Python 允许多继承,靠 C3 线性化 决定方法查找顺序(MRO)。查看顺序:
⚠️ 谨防菱形继承
多继承强大但危险(菱形继承、状态混乱)。实战经验:多继承主要用来混入无状态的行为(Mixin),而不是继承多个有状态的父类。super() 在多继承里会按 MRO 逐个调用,行为和单继承时不同,需要时再深究。
4.4 "接口":ABC 与 Protocol¶
Java 有 interface(nominal,必须显式 implements)。Python 有两条路:
① ABC(抽象基类)—— nominal,最接近 Java 抽象类/接口¶
from abc import ABC, abstractmethod
class Quacker(ABC):
@abstractmethod
def quack(self) -> str: ...
class Duck(Quacker):
def quack(self) -> str: # 必须实现,否则实例化报错
return "quack"
② Protocol —— 结构化类型("鸭子类型的静态版")¶
from typing import Protocol
class Quackable(Protocol):
def quack(self) -> str: ... # 只定义形状,不需要继承
class Duck: # 不写 (Quackable)
def quack(self) -> str: return "quack"
def make_sound(x: Quackable): # 任何有 quack() 的对象都符合
return x.quack()
核心认知
| Java | Python | |
|---|---|---|
| nominal 接口 | interface(必须 implements) |
ABC + @abstractmethod |
| 结构化类型 | (无) | Protocol(有方法就算,不需声明) |
Python 里"接口"的默认形态是 Protocol(结构化),这正对应"鸭子类型"。Protocol 让你拿到静态检查的好处,又不用像 Java 那样到处 implements。第 8 章会深入。
4.5 @property:替代 getter/setter¶
Java 用 getXxx()/setXxx() 或 Lombok。Python 用 @property 把方法伪装成属性:
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self): # 像字段一样访问:c.area
import math
return math.pi * self.radius ** 2
c = Circle(3)
print(c.area) # 28.27... 不用加括号
需要校验的 setter:
class Account:
@property
def balance(self): return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("balance can't be negative")
self._balance = value
4.6 dataclass:一行干掉 POJO 样板¶
这是你最爱 Java record / Lombok 的地方,Python 有 dataclass:
-
Java record(Java 16+)
-
Python dataclass
常用选项:
from dataclasses import dataclass, field
@dataclass(frozen=True) # 不可变(像 record)
class Config:
host: str
port: int = 8080
tags: list[str] = field(default_factory=list) # 可变默认值的安全写法
frozen=True:不可变(对应 record)。field(default_factory=...):可变默认值的标准写法(避免第 2 章那个陷阱)。- 自动生成
__init__/__repr__/__eq__,可用参数开关。
4.7 dunder 方法:运算符重载与协议¶
Java 几乎不允许运算符重载(只有 String +)。Python 全面支持——通过实现 dunder(双下划线)方法,让你的对象支持 len()、[]、+、==、str()、迭代等。
| dunder | 触发 | Java 对应 |
|---|---|---|
__init__ |
构造 | 构造器 |
__str__ |
print()/str() |
toString()(面向用户) |
__repr__ |
调试/REPL | 调试用的 toString() |
__eq__ / __hash__ |
== / 当 dict key |
equals() / hashCode() |
__lt__ 等 |
< > 排序 |
Comparable/Comparator |
__len__ |
len(obj) |
size() |
__getitem__ |
obj[key] |
map.get/数组 |
__iter__ |
for x in obj |
Iterable |
__add__ |
a + b |
(无,除 String) |
__enter__/__exit__ |
with |
try-with-resources(第 7 章) |
完整示例——一个可比较、可打印、可相加的二维向量:
import math
from dataclasses import dataclass
@dataclass
class Vector:
x: float
y: float
def __add__(self, other): # a + b
return Vector(self.x + other.x, self.y + other.y)
def __abs__(self): # abs(v) -> 向量长度
return math.hypot(self.x, self.y)
def __bool__(self): # bool(v) / if v:
return abs(self) > 1e-9
Java 程序员的视角
Python 的 len(x) 不是 x.length()——它是调用 x.__len__()。这种"协议方法"让任何对象都能融入 Python 的内置操作。你不需要继承某个 Comparable 接口,实现 __lt__ 就能被 sorted 排序。
4.8 封装:没有真正的 private¶
Java 有 private/protected/public。Python 没有真正的访问控制,只有约定:
| 写法 | 含义 |
|---|---|
name |
公开 |
_name |
约定私有("请别从外部碰我"),但仍可访问 |
__name |
名称改写(name mangling):实际存成 _ClassName__name,防子类意外覆盖,不是真私有 |
class Foo:
def __init__(self):
self.public = 1
self._internal = 2 # 约定私有
self.__secret = 3 # 改写为 _Foo__secret
f = Foo()
f.public # 1
f._internal # 2(能访问,但"你不应该")
f.__secret # AttributeError
f._Foo__secret # 3(绕过 mangling 仍能访问)
⚠️ Java 程序员的陷阱
别指望 Python 的 __x 提供 Java private 那样的强保证。Python 的哲学是 "我们都是成年人"——用 _ 前缀表达意图,信任调用者。需要真正的隔离,靠模块边界(第 6 章)和文档,而不是语言强制。
本章练习¶
练习 4.1
用 dataclass 定义一个不可变的 Money 类(amount: float、currency: str),并实现 __add__:相同货币才能相加,否则抛 ValueError。
参考答案
练习 4.2
解释下面代码为何 a == b 为 True,但 len({a}) 会抛 TypeError: unhashable,并修复。
参考答案
dataclass 生成了 __eq__(按字段比较),故 a == b 为 True。但默认 dataclass 的 __hash__ 被设为 None(因为定义了 __eq__ 默认取消可哈希),把 Point 放进 set 会 TypeError: unhashable。修复:加 frozen=True(不可变,自动生成 __hash__),或显式 @dataclass(unsafe_hash=True)。最佳实践是 frozen=True。
练习 4.3
用 Protocol 定义一个 Drawable(有 draw(self) -> None 方法),并写一个函数 render(d: Drawable)。再写两个不继承它的类,验证它们仍能传入 render。
参考答案
上一章:第 3 章 · 数据模型 | 下一章:第 5 章 · 异常。