第 6 章 · 面向对象

Python 的 OOP 比 Java 松弛得多:没有接口关键字、没有访问修饰符(下划线靠自觉)、方法就是普通的函数对象。本章讲 Python OOP 的骨架(class、self、继承)和灵魂(鸭子类型),以及「Pythonic 的 OOP」和 Java 式 OOP 的哲学差异。Java 课第 7、8 章的概念在此都有对照物,差异处才是重点。

6.1 class 与 self:把「对象」讲明白

class BankAccount:
    def __init__(self, owner, balance=0):     # 构造器(Java 的 constructor)
        self.owner = owner                    # 实例属性
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance

acc = BankAccount("思源", 100)     # 调用类 = 构造(没有 new!)
acc.deposit(50)                    # 方法调用,self 自动传

self 是 Java 的 this 的显式版:每个方法的第一个参数就是对象自己,Python 不藏(acc.deposit(50) 实为 BankAccount.deposit(acc, 50),计组课第 12 章「C 结构体函数指针手动传 &v」的同款机制,Python 只是语法糖更甜)。__init__ 是初始化钩子(new 分配之后调用);没有 Java 式的「重载构造器」——用默认参数(第 3 章)替代。

属性随写随有(动态):acc.currency = "CNY" 直接给实例添字段。纪律:在 __init__ 里把全部属性声明齐(读者一眼看全;漏声明是「用到才炸」的隐性炸弹)。

6.2 继承与 super:有,但要克制

class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return "..."

class Dog(Animal):                            # 单继承:括号里写父类
    def speak(self):                          # 重写(没有 @Override 注解)
        return f"{self.name}: 汪!"

class Puppy(Dog):
    def __init__(self, name):
        super().__init__(name)                # super() 不用写参数(3.0+)
    def speak(self):
        return super().speak() + "(奶声)"

for a in [Dog("旺财"), Puppy("小旺")]:
    print(a.speak())                          # 多态:调各自的重写

语法全对齐 Java(extends→括号、@Override 免了、super() 更短);多态靠运行时查找(方法在类里找不到就沿父类链找,Python 3 的 MRO 线性化处理多继承顺序,数据结构课的拓扑排序思想在类图上跑)。Python 支持多继承(Java 不许),威力大坑也大(菱形继承,MRO 解决但不建议靠它设计);经验:组合优先,继承最多一层(第 5 章软工的设计模式同款结论)。

「伪私有」约定:属性前加双下划线 self.__secret 触发名字改写(_ClassName__secret),防子类误撞;单下划线 _x 是纯约定「内部使用」。全靠自觉,不像 Java 的 private 由编译器把关——Python 的信任哲学:我们都是成年人。

6.3 鸭子类型:Python OOP 的灵魂

「如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子。」Python 不检查「你是什么类」,只检查「你能不能做这件事」:

class Duck:
    def quack(self): return "嘎"

class Robot:                                   # 和 Duck 无任何继承关系!
    def quack(self): return "电子嘎"

for thing in [Duck(), Robot()]:
    print(thing.quack())                       # 都能 quack,都算鸭子

对比 Java 课第 8 章:想多态必须 implements 接口;Python 看方法名不看血统。len(x) 对任何实现了 __len__ 的对象都工作,file-like 对象(任何有 read() 的东西)能互相替换(第 5 章「OpenAI 的响应对象换成 StringIO」的工程自由)。鸭子类型的红利:少写一堆接口层,灵活换实现;代价:契约靠文档和测试(鸭子类型 + 类型注解(第 7 章)+ mypy 是现代 Python 的组合拳)。

Python 官方的「协议」(protocol)是鸭子类型的形式化:__len__、__iter__、__enter__ 这些双下划线方法(魔法方法,第 9 章)就是语言级的「接口」——你的对象实现了它们,就自动获得了对应的能力(可求长、可迭代、可用于 with)。

6.4 dataclass:Python 的「正规数据类」 вырос

from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

p = Point(3, 4)          # 自动生成 __init__
print(p)                 # Point(x=3, y=4):自动 __repr__
p == Point(3, 4)         # True:自动 __eq__

@dataclass(装饰器,第 9 章)一行生成 __init__/__repr__/__eq__,消灭「字段 + 构造 + getter + repr」的样板(Java 课第 7 章 getter/setter 惯例的 Python 解法:直接不写,或用 dataclass)。配合类型注解,这是现代 Python 写「数据类」的标准姿势(Java 14 的 record 是同款思想,两门语言在互相学习)。

6.5 类方法、静态方法与「什么时候用类」

class MathUtils:
    @staticmethod
    def add(a, b): return a + b          # 静态:和类无关的工具

    @classmethod
    def from_string(cls, s):             # 类方法:cls 是类本身(工厂模式)
        x, y = map(float, s.split(","))
        return cls(x, y)

@staticmethod = Java 静态方法;@classmethod 收 cls 能造自己的子类实例(工厂);实例方法占 95%。最后一课:Python 不强求 OOP——函数 + 模块就能组织得很好(第 7 章),「一个类只有一个方法且只实例化一次」的 Java 习惯(为了「万物皆对象」)在 Python 里是噪音。该用类的时候(有状态要维护、有多个相关操作、需要多态),用;不需要的时候,函数伺候。

常见错误

  • 忘了 self 或写成 this。def deposit(amount) 少了 self,调用时参数错位报「takes 1 argument but 2 given」;报错信息里数一数参数个数。
  • 可变类属性当默认值。class C: items = [] 是类级共享(第 3 章可变默认参数的类版);实例属性在 init 里赋。
  • 继承 dict/list 却忘了调 super().init。内建容器的子类行为诡异;要定制容器,优先继承 collections.UserDict/UserList 或用组合。
  • 用异常控制正常流程的 OOP 设计(把 Java 的 getter/setter 全套搬来)。Python 直接公开属性;需要拦截时用 property(第 9 章)平滑升级,不用预写两层方法。

练习

  1. 动手:实现 Shape 基类 + Circle/Rect 子类(面积、周长),放进列表循环打印——对照 Java 课第 8 章的同题,数一数两边的样板代码量。
  2. 动手:鸭子类型实验:写 def total_area(shapes) 只要求元素有 area() 方法;造两个毫不相干的类(正常 Shape 和一个「恶作剧类」)都传进去,验证都工作。
  3. 实现:用 @dataclass 写一个 Student(name, scores: list),加方法 average() 和 grade()(≥90 A / ≥80 B / …);再造三个实例放进列表,按平均分排序输出。
  4. 思考:「鸭子类型 + 自觉的下划线」vs「静态类型 + 编译器把关」(Java)。从「团队规模」和「项目生命周期」两个维度,讨论什么项目适合哪种哲学,以及 Python 的类型注解 + mypy 是在向哪边移动。