跳转至

第 4 章 · OOP

OOP 是你的主场。但 Python 的 OOP 和 Java 有几处根本差异:没有真正的访问控制支持多继承"接口"是 Protocol 而非 nominal运算符可重载dataclass 一行替代 POJO 样板。本章对照着讲。


4.1 类、__init__self

  • Java

    class Person {
        private String name;
        private int age;
        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String greet() {
            return "Hi, I'm " + this.name;
        }
    }
    
  • Python

    class Person:
        def __init__(self, name: str, age: int):
            self.name = name      # 实例属性在 __init__ 里动态创建
            self.age = age
    
        def greet(self) -> str:
            return f"Hi, I'm {self.name}"
    
    p = Person("Alice", 30)       # 不用 new
    

三个关键差异:

  1. self 必须显式写为方法的第一个参数(Java 的 this 是隐式的)。调用时 p.greet() 会自动把 p 传给 self
  2. __init__ 是初始化器,不是完整构造器(创建对象是 __new__,绝大多数情况不用管)。相当于 Java 构造器的方法体。
  3. 不用 newPerson("Alice", 30) 直接创建实例。
  4. 实例属性不预先声明:在 __init__self.x = ... 才创建。没有 Java 那种"字段声明区"。

4.2 实例变量 vs 类变量

对应 Java 的实例字段 vs 静态字段:

  • Java

    class Counter {
        static int instance_count = 0;  // 静态
        int count;                       // 实例
        Counter() { count = 0; instance_count++; }
    }
    
  • Python

    class Counter:
        instance_count = 0    # 类变量(静态)
    
        def __init__(self):
            self.count = 0    # 实例变量
            Counter.instance_count += 1
    

⚠️ 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(单继承 + 接口)

    class Dog extends Animal
        implements Runnable, Cloneable { ... }
    
  • Python(可多继承)

    class Dog(Animal, Runnable):   # 直接多继承
        ...
    

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)。查看顺序:

print(Dog.__mro__)    # (<class Dog>, <class Animal>, <class Runnable>, <class object>)

⚠️ 谨防菱形继承

多继承强大但危险(菱形继承、状态混乱)。实战经验:多继承主要用来混入无状态的行为(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+)

    public record Point(int x, int y) {}
    // 自动生成构造、equals、hashCode、toString
    
  • Python dataclass

    from dataclasses import dataclass
    
    @dataclass
    class Point:
        x: int
        y: int
    # 自动生成 __init__、__repr__、__eq__
    

常用选项:

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: floatcurrency: str),并实现 __add__:相同货币才能相加,否则抛 ValueError

参考答案
from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
    amount: float
    currency: str

    def __add__(self, other):
        if self.currency != other.currency:
            raise ValueError("currency mismatch")
        return Money(self.amount + other.amount, self.currency)
练习 4.2

解释下面代码为何 a == bTrue,但 len({a}) 会抛 TypeError: unhashable,并修复。

@dataclass
class Point:
    x: int
    y: int
a, b = Point(1, 2), Point(1, 2)

参考答案

dataclass 生成了 __eq__(按字段比较),故 a == bTrue。但默认 dataclass__hash__ 被设为 None(因为定义了 __eq__ 默认取消可哈希),把 Point 放进 setTypeError: unhashable。修复:加 frozen=True(不可变,自动生成 __hash__),或显式 @dataclass(unsafe_hash=True)。最佳实践是 frozen=True

练习 4.3

Protocol 定义一个 Drawable(有 draw(self) -> None 方法),并写一个函数 render(d: Drawable)。再写两个不继承它的类,验证它们仍能传入 render

参考答案
from typing import Protocol

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

def render(d: Drawable) -> None:
    d.draw()

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

render(Circle())   # 结构匹配,无需继承
render(Square())

上一章:第 3 章 · 数据模型 | 下一章:第 5 章 · 异常