Kodokon kodokon.com

面向对象:类、__init__、方法与继承

用设计良好的类来组织代码:初始化、实例属性与类属性、继承及其取舍。

10 分钟 · 3 题

在 Kodokon 中打开本课

你一直都在和 Python 对象打交道:字符串、列表、文件。编写自己的类,意味着把状态(属性)和行为(方法)打包到一个连贯的接口背后。一个常被误解的术语问题:__init__ 并不是构造函数。当 __init__ 收到 self 时,对象早已存在(由 __new__ 创建);它的职责是初始化这个实例。而 self 也没有任何神奇之处:它是每个实例方法的第一个参数,Python 在调用时会自动传入它。

PYTHON
class Account:
    def __init__(self, owner, balance=0.0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("amount must be > 0")
        self.balance += amount
        return self.balance

    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

account = Account("Ada")
account.deposit(120.0)
print(account)
一个完整的类:__init__、一个业务方法和 __repr__

在生产环境中一个至关重要的区分:类属性声明在类体中,被所有实例共享;实例属性创建在 self 上,通常写在 __init__ 里。读取时,Python 先在实例上查找,找不到再退回到类,然后是父类。而 self.x = ... 这样的赋值总是创建一个实例属性,它会遮蔽同名的类属性。经典的现实陷阱:一个可变的类属性。

PYTHON
class Basket:
    items = []

    def add(self, item):
        self.items.append(item)

a = Basket()
b = Basket()
a.add("apple")
print(b.items)  # ['apple']: shared list!
可变类属性的陷阱

继承表达的是一种 "是一个" 的关系:Car 是一个 Vehicle。子类继承父类的方法,可以重写它们,而 super() 让它得以调用父类的版本 - 这在 __init__ 中不可或缺,否则你会短路掉父类的初始化。Python 沿着 MRO(方法解析顺序)来解析调用。不要习惯性地伸手去用继承:如果只是想复用代码而并不存在 "是一个" 的关系,请优先选择组合(一个对象持有另一个对象)。层级过深的继承树很脆弱:每个父类都会变成其所有子类的隐式 API。

PYTHON
class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def describe(self):
        return self.brand

class Car(Vehicle):
    def __init__(self, brand, doors):
        super().__init__(brand)
        self.doors = doors

    def describe(self):
        base = super().describe()
        return f"{base}, {self.doors} doors"

print(Car("Aster", 5).describe())
继承与 super():无需重写即可扩展

知识检测

确认你已牢记本课的重点内容。

  1. __init__ 的作用到底是什么?
    • 在内存中创建对象
    • 初始化一个已经被创建的实例
    • 声明类属性
    • 在每次访问属性时被调用
  2. 某个类在类体中声明了 items = []。在执行了会调用 self.items.append(1) 的 a.add(1) 之后,另一个实例 b 看到的是什么?
    • b.items 是一个空列表
    • b.items 里含有 1:这个列表是共享的
    • Python 抛出 AttributeError
  3. 在 Car.__init__ 中,调用 super().__init__(brand) 的意义何在?
    • 创建第二个 Vehicle 实例
    • 执行父类中定义的初始化
    • 把 Vehicle 的方法复制到 Car 中
    • 检查 Car 是否真的继承自 Vehicle