Python 语法篇 · 第9章

面向对象编程(OOP)

用"类与对象"的方式描述世界、封装数据与行为。本章讲透封装、继承、多态、魔法方法与 dataclass。

🎯 学习目标

  • 理解类、对象、属性、方法四大概念,理解 self
  • 区分类属性与实例属性、实例/类/静态三种方法
  • 掌握封装手段:_x、__x 与 @property
  • 掌握继承、方法重写、super()、多继承 MRO 与多态
  • 理解 Python"鸭子类型"哲学
  • 掌握常用魔法方法、dataclass 与枚举

1类与对象:从一张"图纸"说起

类(class)是抽象模板(如"学生"),对象/实例(object/instance)是按模板造出的具体个体(如"学生张三")。对象包含两部分:

  • 属性(数据):姓名、年龄、成绩……
  • 方法(行为):学习、考试、自我介绍……

面向过程关注"先做什么再做什么",面向对象关注"谁(对象)来做、它有什么数据和能力"。

class Student:
    # __init__ 是构造方法,创建对象时自动调用
    def __init__(self, name, age, score):
        self.name = name        # 实例属性
        self.age = age
        self.score = score

    def introduce(self):        # 实例方法,第一个参数永远是 self
        return f"我是{self.name},{self.age}岁,成绩{self.score}"

    def is_excellent(self):
        return self.score >= 90


# 实例化:类名(...) 自动调用 __init__,创建具体对象
s1 = Student("张三", 20, 95)
s2 = Student("李四", 21, 76)

print(s1.name)                 # 张三        访问属性
print(s1.introduce())          # 我是张三…   调用方法
print(s2.is_excellent())       # False

self 到底是什么?它代表"当前这个对象自己"。调用 s1.introduce() 时,Python 自动把 s1 作为第一个参数传给 self,等价于 Student.introduce(s1)。方法内访问自己的属性/其他方法都要写 self.xxx。

2类属性与实例属性

class Dog:
    species = "犬科动物"        # 类属性:所有对象共享

    def __init__(self, name):
        self.name = name       # 实例属性:各对象独立

d1 = Dog("旺财")
d2 = Dog("大黄")
print(d1.species, d2.species)  # 犬科动物 犬科动物

Dog.species = "家犬"           # 修改类属性,所有实例都受影响
print(d2.species)              # 家犬

d1.species = "个体覆盖"        # 给 d1 新建同名实例属性(遮蔽类属性)
print(d1.species, d2.species)  # 个体覆盖 家犬

# 典型用途:统计创建了多少个对象
class Counter:
    count = 0
    def __init__(self):
        Counter.count += 1     # 修改类属性要通过类名
Counter(); Counter(); Counter()
print(Counter.count)           # 3

不要通过 self.类属性 = 值 去修改类属性——那只会给当前对象新建一个同名实例属性,类属性本身没变。

3三种方法

class Temperature:
    unit = "摄氏度"

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

    def show(self):                       # 实例方法:能访问实例数据(最常用,90%场景)
        return f"{self.celsius}{Temperature.unit}"

    @classmethod
    def from_fahrenheit(cls, f):          # 类方法:第一个参数是类本身,常用于"备选构造器"
        c = (f - 32) * 5 / 9
        return cls(c)                     # cls(...) 等价于 Temperature(...)

    @staticmethod
    def is_freezing(celsius):             # 静态方法:既不用 self 也不用 cls,就是放在类里的普通函数
        return celsius <= 0


t = Temperature(25)
print(t.show())                           # 25摄氏度

t2 = Temperature.from_fahrenheit(100)     # 用华氏度创建对象
print(t2.celsius)                         # 37.777...
print(Temperature.is_freezing(-5))        # True

4封装:保护对象的数据

封装 = 把数据和操作数据的方法绑在一起,并对外隐藏内部细节。Python 没有真正的 private,靠命名约定表达访问限制:

写法含义
self.name公开属性,外部可自由访问
self._name约定的"内部使用",请不要外部访问(但语法上可以)
self.__name双下划线触发名称改写为 _类名__name,外部不能直接 obj.__name 访问

@property:让方法"伪装"成属性(重点)

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance          # 真正"藏起来"的余额

    @property
    def balance(self):                     # 读取:account.balance(像属性,实为方法)
        return self.__balance

    @balance.setter
    def balance(self, value):              # 赋值时自动校验:account.balance = 100
        if value < 0:
            raise ValueError("余额不能为负")
        self.__balance = value

    @property
    def is_rich(self):                     # 计算型属性(只读)
        return self.__balance >= 1_000_000


acc = BankAccount("张三", 500)
print(acc.balance)                         # 500(不用加括号!)
acc.balance = 800                          # 自动走 setter 校验
print(acc.is_rich)                         # False
# acc.balance = -1                        # ValueError: 余额不能为负

property 的价值:① 对属性赋值做校验/联动;② 把计算属性暴露成 obj.x;③ 以后把普通属性改成受控属性时,调用方代码不用改。

5继承:站在父类的肩膀上

class Animal:                       # 父类(基类/超类)
    def __init__(self, name):
        self.name = name

    def eat(self):
        return f"{self.name}在吃东西"

    def speak(self):
        return "某种叫声"


class Cat(Animal):                  # 子类继承父类,自动拥有其全部属性方法
    def speak(self):                # 方法重写(override):同名覆盖
        return f"{self.name}:喵~"


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)      # 调用父类构造方法,复用初始化逻辑
        self.breed = breed          # 子类再扩展自己的属性

    def speak(self):
        return f"{self.name}:汪!"

    def fetch(self):
        return f"{self.name}去捡飞盘了"


c = Cat("咪咪")
d = Dog("旺财", "金毛")
print(c.eat())            # 咪咪在吃东西     ← 继承来的方法
print(c.speak())          # 咪咪:喵~        ← 重写后的方法
print(d.speak(), d.breed) # 旺财:汪! 金毛
print(isinstance(c, Animal))   # True  猫也是动物
print(isinstance(c, Cat))      # True

super() 用于调用父类方法,重写 __init__ 时几乎必须调用 super().__init__(...),否则父类的初始化逻辑丢失。

多继承与 MRO

class A:
    def hello(self): return "A"
class B(A):
    def hello(self): return "B"
class C(A):
    def hello(self): return "C"
class D(B, C):         # Python 支持多继承
    pass

print(D().hello())     # B
print(D.__mro__)       # 方法解析顺序:D -> B -> C -> A -> object
# Python 3 用 C3 线性化算法决定查找顺序,同名方法取 MRO 中最靠左的

多继承强大但复杂,易产生菱形继承等问题。工程上优先:单继承 + 组合(在类中持有其他类的对象);多继承主要用于 Mixin(混入)模式。所有类最终都继承自 object。

6多态与鸭子类型

多态:同一方法调用,不同对象表现出不同行为:

def make_sound(animal):
    print(animal.speak())       # 不关心具体类型,只要它有 speak 方法

make_sound(Cat("咪咪"))         # 咪咪:喵~
make_sound(Dog("旺财", "金毛")) # 旺财:汪!

Python 奉行鸭子类型(Duck Typing):"如果一只鸟走起来像鸭子、叫起来像鸭子,那它就是鸭子。"——不要求继承同一父类,只要具备相同方法/属性就能被同样使用:

class ToyDuck:                    # 没有继承 Animal
    def speak(self):
        return "嘎嘎(玩具声)"

make_sound(ToyDuck())           # 照样能用!因为它也有 speak()

# len() 就是鸭子类型的典范:列表、字符串、字典、集合都支持
len([1,2]); len("abc"); len({"a":1})

7魔法方法(双下方法)

形如 __init__、__str__ 的方法叫魔法方法/特殊方法,它们让自定义对象支持内置操作符和函数:

魔法方法触发时机用途
__init__对象创建时初始化属性
__str__str()/print()人类可读的字符串表示
__repr__调试/容器内显示开发者表示,建议形如 类名(参数)
__len__len(obj)长度
__eq__/__lt__/...== < >自定义比较(实现后可直接 sorted)
__add__/__sub__+ -运算符重载
__getitem__obj[k] / for 遍历让对象支持索引/切片/迭代
__contains__x in obj成员判断
__call__obj()让实例像函数一样被调用
class Money:
    def __init__(self, amount, currency="CNY"):
        self.amount = amount
        self.currency = currency

    def __str__(self):
        return f"{self.amount:.2f} 元"

    def __add__(self, other):                       # 支持 + 运算
        if self.currency != other.currency:
            raise ValueError("币种不同")
        return Money(self.amount + other.amount, self.currency)

    def __eq__(self, other):
        return self.amount == other.amount and self.currency == other.currency

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

print(Money(10) + Money(5))        # 15.00 元
print(Money(10) == Money(10))      # True
print(repr(Money(10)))             # Money(10, 'CNY')

8dataclass 与枚举

dataclass:数据类(Python 3.7+)

当一个类主要用来存数据,__init__、__repr__ 等样板代码可自动生成:

from dataclasses import dataclass

@dataclass
class Article:
    title: str            # 类型注解必填
    category: str = "未分类"
    views: int = 0

a = Article("Python入门", "编程", 1000)
print(a)                 # Article(title='Python入门', category='编程', views=1000)
print(a.title)           # 属性照常访问
a.views += 1

# 自动生成了 __init__、__repr__、__eq__,NLP 中表示"文档/样本"非常合适

枚举 Enum:固定的一组取值

from enum import Enum

class Sentiment(Enum):
    POSITIVE = "正面"
    NEGATIVE = "负面"
    NEUTRAL = "中性"

label = Sentiment.POSITIVE
print(label)             # Sentiment.POSITIVE
print(label.name, label.value)   # POSITIVE 正面
print(label == Sentiment.POSITIVE)   # True
# 比直接用字符串 "positive" 更安全:避免拼错、取值受限、代码可提示

9NLP 场景示例:文档与分词器

class Document:
    """表示一篇待分析的文档。"""
    def __init__(self, doc_id, text):
        self.doc_id = doc_id
        self.text = text
        self.tokens = []          # 分词结果

    def word_count(self):
        return len(self.tokens)

    def __len__(self):
        return len(self.text)

    def __repr__(self):
        return f"Document(id={self.doc_id}, {len(self.text)}字)"


class Tokenizer:
    """简易分词器(NLP 篇会换成 jieba)。"""
    def __init__(self, stopwords=None):
        self.stopwords = set(stopwords or [])   # 停用词集合

    def __call__(self, doc):                   # tokenizer(doc) 直接切词
        import re
        words = re.findall(r"[\u4e00-\u9fff]|[a-zA-Z]+", doc.text)
        doc.tokens = [w for w in words if w not in self.stopwords]
        return doc.tokens


doc = Document(1, "Python 很 好 学 python")
tk = Tokenizer(stopwords={"很"})
print(tk(doc))            # ['Python', '好', '学', 'python']
print(doc.word_count())   # 4
print(len(doc))           # 文本字符数

10本章小结与练习

📌 关键结论

  • 类是模板,实例是具体对象;__init__ 初始化,实例方法首参是 self。
  • 类属性共享、实例属性独立;类方法 @classmethod 适合做备选构造器。
  • 封装靠约定:单下划线"内部使用",property 控制读写校验。
  • 继承用 super() 复用父类逻辑;多态 + 鸭子类型是 Python 风格核心。
  • 数据容器优先 @dataclass;固定标签集合用 Enum。
练习1:self 必须叫这个名字吗?调用 obj.method() 时 self 是怎么传进去的?
self 只是约定俗成的参数名(语法上可叫别的,但强烈不建议)。obj.method() 会被自动转成 类.method(obj),即把对象本身作为第一个位置参数传入,所以方法第一个形参拿到的就是调用者对象。
练习2:设计 Rectangle 类,含 width/height,提供 area()、perimeter(),并让两个长宽相等的矩形 == 相等。
__init__ 存 width/height;area 返回 width*height,perimeter 返回 2*(width+height);重写 __eq__:return self.width==other.width and self.height==other.height(必要时用 isinstance 判断 other 类型)。
练习3:为什么不推荐滥用多继承?什么是鸭子类型?
多继承会带来 MRO 复杂、菱形继承、同名方法冲突等维护问题,优先用单继承+组合(持有对象)解决复用。鸭子类型指不关心对象的具体类,只关心它是否具备所需方法/行为,例如任何实现了 speak() 的对象都能传给 make_sound(),无需共同父类。