# Python 中的类与面向对象编程：__init__、self、方法、继承与 dataclass

URL: https://skillaria.top/zh/guides/python/klassy-python

Python 的面向对象编程以类为基础：类描述对象拥有的字段和方法，对象（实例）通过调用类创建，__init__ 负责填充字段，self 指向当前对象。继承让子类复用父类的行为并重写部分方法；装饰器 @dataclass 为主要用于存储数据的类自动生成 __init__、__repr__ 和 __eq__。

Python 中的类是一张“图纸”：它描述对象保存哪些数据、能做哪些操作。对象（实例）则是按照图纸制造出的具体产品。Python 的面向对象编程依赖三样东西：带有方法的类、继承，以及 `__init__`、`__repr__` 等特殊方法。下面通过一个贯穿始终的例子——记录支出——逐一讲解。

## 既然有字典，为什么还需要类

数据可以保存在字典中：`{"title": "咖啡", "amount": 250}`。问题在于，字典并不知道哪些键是正确的。拼写错误 `expense.get("amout", 0)` 不会报错——只会返回 0，报表就会悄悄地算错。

类的对象在声明时就规定了字段。访问不存在的属性会立即抛出 `AttributeError`，而现代 Python 还会提示正确的名字。此外，类把数据和对数据的操作放在一起：一笔支出自己就能计算含税金额，报表自己能算出合计。

## 如何在 Python 中创建类

用关键字 `class` 声明类，根据 PEP 8 的约定，类名使用 CamelCase 风格。最简单的类示例：

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

coffee = Expense("咖啡", 250)
taxi = Expense("打车", 300)
print(coffee.title, coffee.amount)   # 咖啡 250
```

调用 `Expense("咖啡", 250)` 会创建一个新的实例。`coffee` 和 `taxi` 是同一类型的两个不同对象，各自拥有独立的字段值。用 `isinstance(coffee, Expense)` 可以检查对象的类型，结果为 `True`。

### 构造器 __init__：初始化类的实例

人们常把 `__init__` 称为类的构造器，但更准确地说，它是初始化器。对象本身由 `__new__` 创建，`__init__` 紧随其后调用，把传入的参数写入各个字段。它不需要返回任何东西（确切地说，它必须返回 `None`）。

在 `__init__` 中验证输入数据很方便，这样程序中就不会出现明显错误的对象：

```
class Expense:
    def __init__(self, title, amount):
        if amount < 0:
            raise ValueError("金额不能为负数")
        self.title = title
        self.amount = amount
```

### self 在 Python 中是什么

`self` 是对当前正在处理的对象的引用。当你写 `coffee.with_tax()` 时，Python 实际上调用的是 `Expense.with_tax(coffee)`，并把对象作为第一个参数传入。因此在方法声明中 `self` 排在第一位，而调用时不需要写它。

`self` 不是关键字，而是约定：形参可以叫任何名字，但没有人这样做。新手常见的错误是在方法声明中忘记 `self`。这时调用会失败：`TypeError: Expense.with_tax() takes 0 positional arguments but 1 was given`。

## 类的方法与特殊方法

方法是在类中声明的函数，它通过 `self` 访问对象的字段：

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    def with_tax(self, rate=0.2):
        return self.amount * (1 + rate)

    def __repr__(self):
        return f"Expense({self.title!r}, {self.amount})"

coffee = Expense("咖啡", 250)
print(coffee.with_tax())   # 300.0
print(coffee)              # Expense('咖啡', 250)
```

如果没有 `__repr__`，输出会是 `<__main__.Expense object at 0x7f...>`，对调试毫无帮助。两侧都带双下划线的方法称为特殊方法（dunder 方法），由 Python 自动调用：`__str__` 负责面向用户的“友好”输出，`__eq__` 负责 `==` 比较，`__len__` 负责 `len(obj)`。

### @classmethod 与 @staticmethod

普通方法作用于实例。使用装饰器 `@classmethod` 的方法，第一个参数是类本身（`cls`），常用作替代构造器。静态方法 `@staticmethod` 既不接收对象，也不接收类：它只是一个在逻辑上属于该类的普通函数。

```
class Expense:
    def __init__(self, title, amount):
        self.title = title
        self.amount = amount

    @classmethod
    def from_csv(cls, line):
        title, amount = line.split(";")
        return cls(title, float(amount))

    @staticmethod
    def is_valid_amount(value):
        return value >= 0

e = Expense.from_csv("午饭;520")
print(e.amount)                    # 520.0
print(Expense.is_valid_amount(-1)) # False
```

### 析构方法 __del__

Python 中有 `__del__` 方法，它会在垃圾回收器销毁对象之前调用。不要依赖它来释放资源：调用时机无法保证。对于文件和连接，应使用上下文管理器 `with`，或显式调用 `close()`。

## 类属性与实例属性

通过 `self.name = ...` 设置的字段属于具体对象。直接写在类体中的变量是类属性，所有实例共享。通常用它保存类的常量。

```
class Expense:
    CURRENCY = "RUB"          # 类属性，常量

    def __init__(self, title, amount):
        self.title = title    # 实例属性
        self.amount = amount

a = Expense("咖啡", 250)
b = Expense("茶", 150)
print(a.CURRENCY, Expense.CURRENCY)   # RUB RUB
```

一个危险的陷阱是可变的类属性。如果在类体中写 `rows = []`，然后通过 `self.rows.append(...)` 添加内容，这个列表将被所有对象共享。可变字段应当始终在 `__init__` 中创建。

要查看对象的字段，可以使用 `vars(obj)` 或 `obj.__dict__`；要按字符串名读取属性，使用 `getattr(obj, "amount")`。

## 类的属性：@property

属性（property）是一种方法，从外部读取时像普通字段一样，不需要括号。它适合用于计算得出的值：报表的合计不需要单独保存，也就不会忘记更新。

```
class Report:
    def __init__(self):
        self.rows = []

    def add(self, expense):
        self.rows.append(expense)

    @property
    def total(self):
        return sum(e.amount for e in self.rows)

r = Report()
r.add(Expense("咖啡", 250))
r.add(Expense("打车", 300))
print(r.total)   # 550
```

如果需要控制赋值，可以通过 `@total.setter` 添加设值器。没有设值器时，执行 `r.total = 0` 会抛出 `AttributeError`。

关于封装：Python 中没有严格意义上的私有字段。单下划线（`_cache`）是“请勿从外部使用”的约定；双下划线（`__secret`）会启用名称改写（name mangling），防止子类意外覆盖该字段。

## Python 类的继承

继承允许基于父类创建子类：子类获得父类的所有字段和方法，并可以重写其中的一部分。父类写在括号中。

```
class MonthlyReport(Report):
    def __init__(self, month):
        super().__init__()        # 调用父类的 __init__
        self.month = month

    def add(self, expense):       # 重写方法
        print(f"[{self.month}] + {expense}")
        super().add(expense)      # 保留父类的行为

m = MonthlyReport("十月")
m.add(Expense("咖啡", 250))   # [十月] + Expense('咖啡', 250)
print(m.total)                # 250 — 属性已继承
```

`super()` 返回一个代理对象，用于调用父类的方法。在子类的 `__init__` 中忘记写 `super().__init__()` 是经典错误：`rows` 字段不会被创建，第一次调用 `add` 就会抛出 `AttributeError`。

可以用 `issubclass(MonthlyReport, Report)` 检查继承关系。在 Python 3 中，所有类都隐式继承自 `object`，这也是基础方法 `__repr__`、`__eq__` 等的来源。Python 也支持多重继承（`class C(A, B)`），方法的查找顺序可以通过 `C.__mro__` 查看。

### 抽象类

抽象类定义了一套接口：子类必须实现哪些方法。抽象类不能直接实例化。这需要使用 `abc` 模块：

```
from abc import ABC, abstractmethod

class Exporter(ABC):
    @abstractmethod
    def export(self, report): ...

class CsvExporter(Exporter):
    def export(self, report):
        return "\n".join(f"{e.title};{e.amount}" for e in report.rows)

Exporter()   # TypeError: Can't instantiate abstract class Exporter ...
```

如果子类没有实现所有抽象方法，它仍然是抽象的，创建对象时会抛出同样的错误。

## 用组合代替深层继承

学会继承之后，人们很容易搭建层层继承的结构：基础支出、货币支出、人民币支出——一直延伸到五层。在 Python 中这种写法并不常见。继承描述的是“是一个”的关系，而组合描述的是“包含”的关系：报表不是一个列表，而是在字段 `rows` 中包含了一个列表。

实用的原则是：如果只需要使用另一个对象的功能，就把它放进字段中，而不是继承它。对于数据，dataclass 通常就足够了；业务逻辑则可以放在普通函数中。

## Python 的 dataclass：不写样板代码的类

对于主要存储数据的类，手写 `__init__`、`__repr__` 和 `__eq__` 很繁琐。来自 `dataclasses` 模块的装饰器 `@dataclass` 会根据字段的注解自动生成它们：

```
from dataclasses import dataclass, field

@dataclass
class Expense:
    title: str
    amount: float
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("金额不能为负数")

e = Expense("咖啡", 250)
print(e)                           # Expense(title='咖啡', amount=250, tags=[])
print(e == Expense("咖啡", 250))   # True
```

以前在 `__init__` 中编写的验证，现在可以放入 `__post_init__` 中——它会在自动生成的初始化器之后立即调用。可变的默认值只能通过 `field(default_factory=list)` 设置：写成 `tags: list = []` 会被 dataclass 禁止，并抛出 `ValueError`。

### frozen 与 slots

参数 `frozen=True` 让对象不可变，`slots=True`（自 Python 3.10 起）会生成 `__slots__`，从而节省内存。修改冻结对象的字段会引发 `FrozenInstanceError`。如果需要带有新值的对象，就创建一个副本：

```
from dataclasses import dataclass, replace

@dataclass(frozen=True, slots=True)
class Expense:
    title: str
    amount: float

e = Expense("咖啡", 250)
# e.amount = 300   -> dataclasses.FrozenInstanceError
e2 = replace(e, amount=300)
print(e, e2)   # Expense(title='咖啡', amount=250) Expense(title='咖啡', amount=300)
```

自 Python 3.13 起，通用函数 `copy.replace()` 也能做到同样的事。冻结的 dataclass 对象可以放入集合，也可以用作字典的键——它们有 `__hash__` 方法。

## 类的新特性：Self、@override、__slots__

在类型标注方面，有两个有用的工具。`typing.Self`（Python 3.11，PEP 673）表示“返回同一个类的对象”，它在继承的方法中同样适用。装饰器 `@override`（Python 3.12，PEP 698）标记一个必须重写父类方法的方法：如果你把方法名拼错了，mypy 或 pyright 会报告父类中没有这个方法。

```
from typing import Self, override

class Report:
    def __init__(self):
        self.rows = []

    def add(self, e) -> Self:
        self.rows.append(e)
        return self              # 可以链式调用

class MonthlyReport(Report):
    @override
    def add(self, e) -> Self:
        return super().add(e)
```

在 Python 3.15 中取消了一个狭窄的限制：`tuple` 的子类现在可以使用非空的 `__slots__`，并且 `__dict__` 与 `__weakref__` 可以写入任何类的 `__slots__`。在 3.14 中，对元组子类做这样的声明会抛出 `TypeError: nonempty __slots__ not supported for subtype of 'tuple'`。

## 类的常见错误

| 错误 | 现象 | 如何修正 |
| --- | --- | --- |

| 方法中忘记 `self` | `TypeError: ... takes 0 positional arguments but 1 was given` | 方法的第一个参数始终是 `self` |

| 在 `__init__` 中忘记 `self.` | 赋值的值在 `__init__` 结束后消失 | 写成 `self.amount = amount` |

| 列表作为类属性 | 数据在对象之间“串流” | 在 `__init__` 中创建列表，或使用 `default_factory` |

| 没有调用 `super().__init__()` | 访问父类字段时出现 `AttributeError` | 调用父类的初始化器 |

| 把属性当作方法调用 | `TypeError: 'int' object is not callable` | 不加括号读取属性：`r.total` |

## 小结

Python 的面向对象编程从简单的内容开始：类描述字段和方法，`__init__` 填充字段，`self` 指向当前对象。继承和 `super()` 用于扩展行为，抽象类用于定义接口，`@property` 用于计算字段。对于数据类，使用 `@dataclass`；对于深层层次结构，优先选择组合。这一主题在课程《Python 3.15：从新手到专家》中同样结合实践讲解，使用的是支出记录器的例子。

## FAQ

**类和对象在 Python 中有什么区别？**

类是对类型的描述：它有哪些字段和方法。对象（实例）通过调用类创建，例如 Expense("咖啡", 250)，它保存具体的字段值。

**类中一定要写 __init__ 吗？**

不一定。如果没有声明 __init__，就使用从 object 继承的版本，它不接受参数。在 dataclass 中，这个方法由装饰器生成。

**__str__ 与 __repr__ 有什么区别？**

__repr__ 面向开发者和调试器，应当无歧义地描述对象；__str__ 面向用户，由 print 和 f-字符串使用。如果没有定义 __str__，Python 会使用 __repr__。

**什么时候使用 dataclass，什么时候使用普通类？**

当类主要用来保存数据，并且需要自动生成 __init__、__repr__ 和比较方法时，使用 dataclass。当初始化逻辑复杂，或者行为比数据更重要时，使用普通类更合适。

**Python 中有私有字段吗？**

没有严格的私有性。名字开头的单下划线是“不要在外部使用”的约定；双下划线会启用名称改写，防止子类意外覆盖该字段。


---
Source: Skillaria.Top (https://skillaria.top)
