Skip to content

第 09 章 · 类与对象:把数据和操作打包在一起 ​

本章目标 ​

会定义类、创建对象、用继承复用代码,理解"数据 + 行为"的封装思想。

9.1 为什么需要类 ​

前面写的是"函数 + 一堆变量"。当数据变多、又有一堆相关操作时,会乱:

python
dog_name = "旺财"
dog_age = 3

def dog_bark(name):
    print(f"{name}:汪汪!")

类做的事:把属于同一个东西的数据和行为,装进一个盒子里。

python
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        print(f"{self.name}:汪汪!")

    def birthday(self):
        self.age += 1


d = Dog("旺财", 3)      # 创建一个对象(实例)
d.bark()                # 旺财:汪汪!
d.birthday()
print(d.age)            # 4

9.2 三个核心概念 ​

  • 类(class):模板/图纸,如 Dog
  • 实例(instance):按图纸造出来的具体东西,如 d = Dog("旺财", 3)
  • 属性(attribute):对象身上的数据,如 d.name
  • 方法(method):对象身上的函数,如 d.bark()

__init__ 和 self ​

python
class Dog:
    def __init__(self, name):    # 造对象时自动执行
        self.name = name         # 把参数存到对象身上
  • __init__ 叫初始化方法,Dog("旺财") 时自动被调用
  • self 代表"这个对象自己",必须是第一个参数(Python 自动传,你不用管)
  • 实例方法里访问自己的数据,一律写 self.xxx

9.3 属性:类属性 vs 实例属性 ​

python
class Dog:
    species = "犬科"          # 类属性:所有实例共享

    def __init__(self, name):
        self.name = name       # 实例属性:每个对象自己的


a, b = Dog("旺财"), Dog("来福")
print(a.species, b.species)    # 犬科 犬科

Dog.species = "犬科动物"
print(a.species)               # 犬科动物

⚠️ 易错点:类属性是可变对象时,所有实例会共享同一份:

python
class Bad:
    items = []          # ❌ 所有实例共用这个列表

x, y = Bad(), Bad()
x.items.append(1)
print(y.items)          # [1]  ← y 也被影响了

class Good:
    def __init__(self):
        self.items = []          # ✅ 每个实例一份

9.4 打印对象更友好:__str__ / __repr__ ​

python
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):          # print(obj) 时显示
        return f"User({self.name}, {self.age})"

    def __repr__(self):         # 调试/列表里显示
        return f"User(name={self.name!r}, age={self.age})"


print(User("小明", 18))         # User(小明, 18)
print([User("小明", 18)])       # [User(name='小明', age=18)]

这叫魔术方法(双下划线开头结尾)。类似的还有 __len__、__eq__,需要时再查。

9.5 继承:少写重复代码 ​

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} 发出了声音")


class Dog(Animal):              # 继承 Animal
    def speak(self):            # 覆盖父类方法
        print(f"{self.name}:汪汪!")


class Cat(Animal):
    def __init__(self, name, indoor=True):
        super().__init__(name)     # 调用父类初始化
        self.indoor = indoor


Dog("旺财").speak()     # 旺财:汪汪!
Cat("咪咪").speak()     # 咪咪 发出了声音(用父类的)
  • class 子类(父类): 就是继承
  • 子类可以重写父类方法,也可以用 super() 复用父类逻辑
  • 多个类有共同行为时,抽到父类里去,重复代码就消失了

多重继承(了解) ​

python
class A:
    def hello(self):
        print("A")

class B(A):
    pass

class C(A):
    pass

class D(B, C):        # 同时继承两个
    pass

D().hello()

Python 用 MRO(方法解析顺序)决定走谁的方法,规则复杂。多重继承的解析顺序较复杂,通常用组合(把别的对象当属性)更清晰。

9.6 "私有"变量 ​

python
class Account:
    def __init__(self, balance):
        self._balance = balance      # 单下划线:约定"别乱碰"
        self.__secret = "内部"        # 双下划线:名字会被"改写"

    def show(self):
        return self._balance


a = Account(100)
print(a._balance)          # 100  ← 单下划线只是约定,挡不住你(但请不要这样用)
# print(a.__secret)        # ❌ AttributeError
print(a._Account__secret)  # 内部  ← 双下划线被改写成 _类名__属性名

所以准确的说法是:

  • _xxx:约定的礼貌——"这是我内部用的,你别动"
  • __xxx:会被改名为 _类名__xxx(叫 name mangling),挡住的是"不小心写重名",不是安全机制(真想访问照样能访问)

Python 的哲学是"我们都是成年人":不设硬门禁,靠约定和文档。看到别人类里带下划线的属性,请用它的公开方法,不要直接改。

9.7 几种常用方法类型 ​

python
class Circle:
    pi = 3.14159

    def __init__(self, r):
        self.r = r

    def area(self):              # 实例方法:要用 self
        return self.pi * self.r ** 2

    @classmethod
    def from_diameter(cls, d):   # 类方法:常用于"另一种构造方式"
        return cls(d / 2)

    @staticmethod
    def describe():              # 静态方法:就是放在类里的普通函数
        return "圆是一个平面图形"

    @property
    def diameter(self):          # 属性方法:像属性一样用,不用加括号
        return self.r * 2


c = Circle.from_diameter(10)
print(c.area())          # 78.53975
print(c.diameter)        # 10  ← 注意没有括号

@property 很实用:对外看起来是数据,内部可以加计算或校验逻辑。

9.8 迭代器与生成器 ​

术语先分清(面试也常问):

  • 可迭代对象(iterable):能用 for 遍历的东西 —— 列表、字典、字符串、文件…… 判断标准是实现了 __iter__(老代码也允许用 __getitem__)
  • 迭代器(iterator):iter(可迭代对象) 拿到的东西,内部记着"读到哪了",实现了 __next__
python
data = [1, 2]
it = iter(data)         # 把列表变成迭代器
print(next(it))         # 1
print(next(it))         # 2
# print(next(it))       # ❌ StopIteration(遍历完了)

想自己造一个可迭代对象,就给类实现 __iter__ / __next__(下面是个最小可运行例子,看懂结构就行):

python
class CountDown:
    """倒计时:for x in CountDown(3) 依次得到 3 2 1"""

    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self

    def __next__(self):
        if self.current <= 0:
            raise StopIteration      # 用这个异常告诉 for:没了
        value = self.current
        self.current -= 1
        return value


for n in CountDown(3):
    print(n)      # 3 2 1

日常写代码很少需要手写这个,因为用下面的生成器更短。

更常用的是生成器——用 yield 的函数,逐个"产出"值,省内存:

python
def countdown(n):
    while n > 0:
        yield n          # 产出后暂停,下次从这里继续
        n -= 1

for x in countdown(3):
    print(x)             # 3 2 1

生成器表达式(把列表推导式的方括号换成圆括号):

python
nums = [1, 2, 3, 4, 5]
gen = (n * n for n in nums)      # 不一次性算完,用一个算一个
print(sum(gen))                  # 55

处理大文件时特别有用:

python
def read_lines(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            yield line.rstrip()

9.9 练一练 ​

  1. 写 Student 类:姓名、成绩列表,方法有 average()、is_pass(),__str__ 输出"姓名:平均分"。
  2. 写 BankAccount 类:deposit()、withdraw()(余额不足抛异常)、balance 用 @property 暴露。
  3. 写 Shape 父类和 Rectangle、Circle 子类,各自实现 area()。
  4. 用生成器实现 fib(n),产出前 n 个斐波那契数。

本章关键词 ​

class、__init__、self、实例属性 / 类属性、__str__、继承与 super()、@property / @classmethod / @staticmethod、yield

试图用自己的方式,重新定义属于我的人生