R / Richie全部文章 ↑

Python · 7 分钟阅读

OOP 面向对象编程

OOP 把「数据」和「操作数据的函数」封装到 对象 里,强调「仔细设计」「代码复用」「大型程序组织」。本文用一组逐步演化的例子把 Python OOP 核心概念串起来:类与对象、__init__、self、继承与 super()、@property、魔术方法、抽象基类。

读完这一篇你应该能:

  • 定义类、创建对象
  • 理解继承与多态的关系
  • 知道什么时候用 @property / @classmethod / @staticmethod
  • 用魔术方法把对象接入 Python 协议
  • 用 abc.ABC 写明确的接口约定

1. 类与对象

类(class) 是创建对象的蓝图,封装了 属性(数据) 和 方法(行为);对象(object / instance) 是类的实例化产物。

Python 中一切都是对象——整数、字符串、列表、字典、函数、模块,都是某个类的实例。

类名建议用 大驼峰(MyClass):

class Dog:
    """一个简单的狗类。"""

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def sit(self) -> None:
        print(f"{self.name} 坐下了。")

    def roll_over(self) -> None:
        print(f"{self.name} 打滚了。")

一个类通常由以下部分组成:

成员 作用
类属性 所有实例共享,写在方法外
__init__ 构造器,初始化实例属性
实例方法 第一个参数是 self
类方法 @classmethod,第一个参数是 cls
静态方法 @staticmethod,不依赖类或实例
属性方法 @property,把方法当属性访问
魔术方法 __repr__ / __str__ / __eq__ / __hash__ / …

__init__ 是构造函数,每次 Person() 都会自动调用。它不构造对象本身(那是 __new__ 的事),而是初始化新对象的属性。

创建对象

my_dog = Dog("旺财", 3)
print(f"我的狗叫 {my_dog.name},它 {my_dog.age} 岁了。")
my_dog.sit()
my_dog.roll_over()

Dog("旺财", 3) 自动调用 __init__;my_dog.sit() 自动把 my_dog 作为 self 传入。


2. 实例方法与 self

实例方法的第一个参数 必须 是 self(约定俗成),表示"当前实例"。

class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def display(self) -> None:
        print(f"姓名: {self.name}, 年龄: {self.age}")

    def have_birthday(self) -> None:
        self.age += 1

调用 p.have_birthday() 时,Python 自动把 p 作为 self 传进去。


3. __str__ / __repr__

class Person:
    def __init__(self, name: str = "", age: int = 0) -> None:
        self.name = name
        self.age = age

    def __str__(self) -> str:
        return f"Person({self.name!r}, {self.age})"

    def __repr__(self) -> str:
        return f"Person({self.name!r}, {self.age})"
p = Person("Richie", 18)
print(p)        # Person(name='Richie', age=18)
print(repr(p))  # Person('Richie', 18)
方法 用途
__str__ 给 print / str() 用,面向用户
__repr__ 给 REPL / repr() 用,面向开发者;目标能复制回来重建对象

经验:写类时至少实现 __repr__;str() 在没有 __str__ 时会退回到 __repr__。


4. @property:把方法当属性用

直接暴露属性 vs. 用 @property 加验证:

class Person:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self._age = 0      # 用下划线表示"受保护"
        self.age = age     # 走 setter,做校验

    @property
    def age(self) -> int:
        return self._age

    @age.setter
    def age(self, value: int) -> None:
        if not (0 < value <= 150):
            raise ValueError("age 必须在 1~150 之间")
        self._age = value
Person("Mia", 30).age          # 30
# Person("Mia", 200)            # ValueError

“方法当属性用” 让你写 m.age 而不是 m.get_age();同时仍然能在赋值时做校验。


5. 私有与受保护

Python 没有真正的"私有"——只有约定和名字改编:

形式 含义
self.name 公有
self._name “受保护”,约定上不外部访问
self.__name 名字改编为 _ClassName__name,更难被意外访问
class Secret:
    def __init__(self) -> None:
        self.__token = "shh"        # 私有

s = Secret()
# s.__token           # AttributeError
s._Secret__token      # 'shh' —— 仍能访问,但不推荐

6. 继承与方法重写

继承允许"在新类里复用既有类的属性和方法":

class GoldenRetriever(Dog):
    """金毛猎犬,继承自 Dog。"""

    def fetch(self) -> None:
        print(f"{self.name} 捡回了物品。")
gold = GoldenRetriever("Luffy", 5)
gold.sit()           # 来自 Dog
gold.fetch()         # 自己的

方法重写 + super() 协同父类:

class Animal:
    def __init__(self, name: str) -> None:
        self.name = name

    def speak(self) -> str:
        return "..."


class Dog(Animal):
    def speak(self) -> str:
        return "Woof!"


class Cat(Animal):
    def speak(self) -> str:
        return "Meow!"


for a in [Dog("旺财"), Cat("小咪")]:
    print(a.name, "->", a.speak())
# 旺财 -> Woof!
# 小咪 -> Meow!

子类想扩展父类行为时用 super():

class Dog(Animal):
    def __init__(self, name: str, breed: str) -> None:
        super().__init__(name)        # 让父类初始化 name
        self.breed = breed

继承的几种姿势:

语法 说明
class B(A): 单继承
class C(A, B): 多继承(注意 MRO)
super().__init__(...) 调用父类的方法
class D(A, metaclass=...): 元类(高阶话题,初学不必深究)

7. 多态:鸭子类型

Python 的多态不依赖继承,而是 “只要有这个方法/属性,就能用”:

class Cat:
    def make_sound(self) -> None:
        print("喵喵")


class Dog:
    def make_sound(self) -> None:
        print("汪汪")


def animal_sound(animal) -> None:
    animal.make_sound()      # 不在意 animal 是哪一类


animal_sound(Cat())
animal_sound(Dog())

同一段代码能作用于不同类型的对象,就是多态。Python 的多态主要靠 鸭子类型 实现:只要对象有 make_sound 这个方法就能用。

这就是 鸭子类型(Duck Typing):“如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子”。这种"行为决定类型"的风格让代码更灵活,但也更需要单元测试来兜底。


8. 类方法与静态方法

from datetime import date


class Person:
    species = "Homo sapiens"         # 类属性

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name: str, year: int) -> "Person":
        """另一种构造方式:给定出生年份。"""
        return cls(name, date.today().year - year)

    @staticmethod
    def is_adult(age: int) -> bool:
        """不依赖实例/类的纯逻辑。"""
        return age >= 18
p = Person.from_birth_year("Bob", 2000)
print(p.age)                # 26(视当前年份而定)
print(Person.is_adult(20))  # True
  • @classmethod:第一个参数是 cls(类本身),常用于替代构造。
  • @staticmethod:与类/实例都没关系,组织上挂在类下而已。

9. 魔术方法(部分速查)

方法 触发时机
__init__ / __new__ 实例化
__del__ 销毁(不保证立即触发)
__repr__ / __str__ 字符串 / 打印
__len__ len(obj)
__getitem__ / __setitem__ 索引 / 切片
__iter__ / __next__ for ... in obj
__contains__ x in obj
__eq__ / __lt__ / __hash__ 比较与哈希
__call__ obj() 像函数一样调用
__enter__ / __exit__ with obj:

示例:让对象可比较、可哈希:

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Point):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self) -> int:
        return hash((self.x, self.y))

    def __repr__(self) -> str:
        return f"Point({self.x}, {self.y})"

定义 __eq__ 时默认会让对象不可哈希;需要哈希就显式实现 __hash__,二者一起改才安全。


10. 抽象基类(abc)

当一组类应该有同样的接口时,用 abc.ABC 明确表达"必须实现":

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...
    @abstractmethod
    def perimeter(self) -> float: ...


class Rect(Shape):
    def __init__(self, w: float, h: float) -> None:
        self.w, self.h = w, h

    def area(self) -> float:
        return self.w * self.h

    def perimeter(self) -> float:
        return 2 * (self.w + self.h)
  • 子类必须实现所有抽象方法才能被实例化。
  • @abstractmethod 只是声明约束;具体方法、属性都可以在抽象基类中提供。

11. 综合案例:Undercut 游戏

把所有点串起来做一个简单的小游戏,看 OOP 怎么自然地让代码长出形状:

import random


class Player:
    def __init__(self, name: str) -> None:
        self._name = name
        self._score = 0

    def get_name(self) -> str:
        return self._name

    def reset_score(self) -> None:
        self._score = 0

    def incr_score(self) -> None:
        self._score += 1

    def get_move(self) -> int:
        raise NotImplementedError

    def __str__(self) -> str:
        return f"Player(name={self._name!r}, score={self._score})"


class Human(Player):
    def get_move(self) -> int:
        while True:
            try:
                n = int(input(f"{self.get_name()} move (1-10): "))
                if 1 <= n <= 10:
                    return n
            except ValueError:
                pass
            print("Oops! 请重新输入 1~10 的整数。")


class Computer(Player):
    def get_move(self) -> int:
        return random.randint(1, 10)


def play_undercut(p1: Player, p2: Player) -> Player | None:
    p1.reset_score()
    p2.reset_score()
    m1, m2 = p1.get_move(), p2.get_move()
    print(f"{p1.get_name()}: {m1}   {p2.get_name()}: {m2}")

    if m1 == m2 - 1:
        p1.incr_score()
        return p1
    if m2 == m1 - 1:
        p2.incr_score()
        return p2
    return None  # 平局


if __name__ == "__main__":
    winner = play_undercut(Human("Lia"), Computer("Hal-9000"))
    print("winner:", winner or "draw")

观察 OOP 在这里带来的好处:

  • Player 把"共有行为"抽出来;
  • Human / Computer 只重写"自己不一样"的部分(get_move);
  • play_undercut 只依赖 Player 接口,不关心是人是机器。

12. 设计建议

  • 先私有再公开:先 _age,等真有需要再升级为 @property。
  • 不要写"什么都装的 god class":一个类一个清晰职责。
  • 优先组合而不是继承:has-a 关系比 is-a 关系更灵活。
  • 魔术方法按需实现:__repr__ 几乎总要;其它按需。
  • 类型注解加上:__init__(self, name: str, age: int = 0) 让 IDE / mypy 帮忙查错。
  • 测试覆盖:unittest / pytest,把每个 public 方法都过一遍。

13. 小结

概念 关键点
类 / 对象 class 是蓝图,对象是实例
__init__ 构造时初始化,self 指向当前实例
继承 子类复用父类逻辑;super().__init__() 协同父类
多态 鸭子类型:不靠继承,靠"行为存在"
@property 用方法语法做属性访问,附带校验逻辑
类/静态方法 @classmethod(cls) / @staticmethod(无依赖)
抽象基类 abc.ABC + @abstractmethod 明确接口契约
魔术方法 __repr__ / __eq__ / __hash__ / __len__ …