很多自学 Python 的朋友都有过这种体验:语法书翻得挺熟,list、dict、if、for都写得来,函数也能封装几个,但一看到“面向对象”三个字就开始犯怵。更常见的情况是,概念背得滚瓜烂熟——“类是模板,对象是实例”“封装继承多态”——但真拿到一个需求,还是不知道该怎么设计类,写出来的代码怎么看都像披着类的皮的面向过程。
这篇东西就是冲着这个问题来的。我会把 Python 面向对象编程里的核心概念、设计思路和实操中真正值得注意的细节一起捋一遍,包括类与对象的本质、封装怎么把握尺度、继承的正确使用姿势、特殊方法怎么让类用起来顺手,最后用一个完整的实战案例展示从需求到 OOP 设计的全过程。适合有基础 Python 语法、想真正用 OOP 写项目的人,也适合想系统复习一轮面向对象设计的人。
1. 先搞清楚:OOP 到底解决了什么问题
很多人学 OOP 之所以觉得虚,是因为不知道它要解决的问题是什么。语法学了一堆,却没搞明白这些语法是冲着什么痛点去的。
1.1 从一段“能跑但难维护”的代码说起
先看一段很典型的面向过程写法,模拟一个简单图书管理场景:
books = [] borrow_records = [] def add_book(title, author): books.append({"title": title, "author": author, "borrowed": False}) def borrow_book(title, user): for book in books: if book["title"] == title and not book["borrowed"]: book["borrowed"] = True borrow_records.append({"title": title, "user": user}) return True return False def return_book(title): for book in books: if book["title"] == title and book["borrowed"]: book["borrowed"] = False return True return False add_book("Python编程", "张三") add_book("算法导论", "李四") borrow_book("Python编程", "王五") print(books)这段代码能正常运行,但随着需求扩张,问题会一个一个冒出来。
第一,数据和操作是分离的。books这个列表散落在模块级,borrow_book、return_book这些函数都直接操作这个全局列表。一旦数据格式从字典改成别的结构,所有函数都得跟着改。
第二,状态一致性靠自觉。比如某个需求要记录“谁借了这本书”,那你就得改borrow_book,同时保证books里的borrowed字段和borrow_records里新增的记录是同步的。手一抖就会漏掉一处,导致数据错乱。
第三,职责不清晰。图书本身的行为(可借、可还)、借阅记录的行为(谁借的、什么时候借的)全被拍平在函数里。代码少的时候还能忍,多到几千行的时候就很难维护了。
1.2 面向过程与面向对象的核心差异:数据和行为不再分离
OOP 解决这三个问题的方式很直接:把“数据”和“操作这些数据的方法”打包在一起,形成类。类是一个定义,真正运行起来的是它的实例——对象。
还是图书管理,用类的思路重新建模后大概是这样的:
class Book: def __init__(self, title, author): self.title = title self.author = author self.is_borrowed = False self.borrower = None def borrow(self, user): if self.is_borrowed: return False self.is_borrowed = True self.borrower = user return True def return_book(self): if not self.is_borrowed: return False self.is_borrowed = False self.borrower = None return True现在关于“一本书”的状态和行为都收拢在Book类里。要加需求,比如记录借阅历史,直接在类里加字段、加方法就行,不会影响别的地方。
这背后的思想并不玄妙:现实世界里很多事物天生就是“数据 + 行为”的结合体。一本书有书名、有作者,也能被借、被还。OOP 只是让代码结构去贴合这种自然认知,而不是强行把行为拆出去晾在一边。
很多人在初学时纠结“类到底是啥”,我的建议是别在哲学层面打转。你就先把它理解成“把相关的数据和操作放在一起的容器”,用起来自然就有了感觉。
2. 类与对象:比“模板和实例”更深的层面
大多数的教程会说“类是模板,对象是模板做出来的实例”,这句话没错,但远远不够。真正写代码的时候,类背后有一堆细节决定着你写出来的东西是精巧还是别扭。
2.1 理解 __init__ 和 self:第一个常被误解的地方
__init__是 Python 类里最常见的特殊方法,但它不是构造函数。
严格来说,Python 创建对象的流程是先调用__new__分配内存、创建空对象,再调用__init__对该对象进行初始化。__init__是初始化器,不是构造器。这个区分在绝大多数场景不影响使用,但理解准确能帮你少走弯路——比如你想实现单例模式,要改的是__new__而不是__init__。
self也是新手的重点困惑对象。它只是“当前实例”的约定名称,传参时不需要手动传,Python 会自动把调用该方法的实例作为第一个参数传进去。
class Person: def __init__(self, name): self.name = name def greet(self): print(f"你好,我是{self.name}") p = Person("小明") p.greet() # 等价于 Person.greet(p)注意Person.greet(p)这种写法,它揭示了self的本质:实例方法其实就是“把实例自己作为第一个参数传进去的一个普通函数”。理解了这一点,后面理解类方法、静态方法就会轻松很多。
2.2 类变量与实例变量的区别:一个坑点
这是写类时最容易踩坑的地方之一。先看一段反直觉的代码:
class Dog: tricks = [] # 类变量 def __init__(self, name): self.name = name def add_trick(self, trick): self.tricks.append(trick) a = Dog("豆豆") b = Dog("球球") a.add_trick("打滚") print(b.tricks) # ['打滚'],b 被影响了!问题出在self.tricks的查找链条上。当实例没有tricks属性时,Python 会沿着继承链向上找,找到了类的tricks列表。a.add_trick("打滚")修改的是类级列表,所以 b 也“被学会了”打滚。
遇到这种需求的正确做法是在__init__里创建实例变量:
class Dog: def __init__(self, name): self.name = name self.tricks = [] def add_trick(self, trick): self.tricks.append(trick)类变量适合放那些所有实例共享的值,比如类的版本号、默认配置。一句话记忆:可变类型别放类变量里,除非你确实想共享同一个对象。
2.3 类方法、静态方法与实例方法:三种方法的适用边界
Python 的类里可以定义三种方法,很多初学者分不清它们的用场。
- 实例方法:第一个参数是
self,访问和修改实例状态。绝大多数方法都是这种。 - 类方法:用
@classmethod装饰,第一个参数是cls,访问类本身的状态,常用于提供备选构造器。 - 静态方法:用
@staticmethod装饰,第一个参数没有特殊约定,就是一个放在类命名空间里的普通函数。
class Date: def __init__(self, year, month, day): self.year = year self.month = month self.day = day @classmethod def from_string(cls, text): year, month, day = map(int, text.split("-")) return cls(year, month, day) @staticmethod def is_valid_month(month): return 1 <= month <= 12from_string这种写法很典型:把字符串解析的逻辑封装在类里,调用时Date.from_string("2024-05-01")直接生成实例,不需要外部写一坨字符串处理代码。is_valid_month和实例状态无关,放类里只是为了逻辑归位。
选哪种方法有个简单判据:如果方法需要操作实例数据,用实例方法;如果方法需要操作类属性,用类方法;如果跟两者都没关系,只是恰好逻辑上属于这个类,用静态方法。
3. 封装与属性管理:从“能用”到“用得安全”
封装常被解释成“把属性藏起来,外部不能直接访问”。这个解释在 Python 里其实有点误导,因为 Python 压根没有真正意义上的私有变量。封装的核心价值不是“不让外部看到”,而是“给外部一个稳定、安全的访问接口”。
3.1 私有变量约定:Python 没有真正的 private
Python 约定用单下划线前缀(_name)表示“这是内部细节,外部别动”。它只是一个君子协定,不强制。用双下划线(__name)会触发名称改写机制,把__name变成_ClassName__name,这主要是为了避免在继承中子类意外覆盖父类属性,并不是为了做强制的私有保护。
class Account: def __init__(self, owner, balance): self.owner = owner self._balance = balance # 约定内部使用 def deposit(self, amount): if amount <= 0: raise ValueError("存款金额必须大于0") self._balance += amount外部如果非要访问_balance,技术上做得到,但大家都默认不去碰。真正重要的事情是:提供受控的方法来修改内部状态。直接暴露balance让外部随便赋值,很容易出现account.balance = -100这种脏数据。
3.2 用 property 控制属性访问:一段“看似没用但其实很关键”的代码
很多人在学property时觉得这就是“把方法包装成属性”,然后用一句话带过。实际上它在真实项目中非常重要,因为它允许你“修改属性访问逻辑而不破坏已有调用代码”。
假设一开始Account类就是直接暴露balance的:
class Account: def __init__(self, owner, balance): self.owner = owner self.balance = balance acc = Account("小明", 1000) print(acc.balance)后来需求变了:每次访问余额都要记录日志。如果直接把balance改成私有变量,再写get_balance()方法,那么所有调用方都得改。但用property就能原地换实现:
import logging class Account: def __init__(self, owner, balance): self.owner = owner self._balance = balance @property def balance(self): logging.info(f"访问账户余额,当前值: {self._balance}") return self._balance @balance.setter def balance(self, value): if value < 0: raise ValueError("余额不能为负数") self._balance = value acc = Account("小明", 1000) print(acc.balance) # 调用方代码不用改 acc.balance = 1200 # 走 setter 校验这就是property的价值:它让你在“保持公共接口不变”的前提下,替换内部的实现。这种能力在项目演进中极其重要——你不需要一上来就做很重的抽象,可以先用简单的公有属性写,等确实需要校验和日志时再加property改造。
3.3 接口设计与最小暴露原则
封装落实到日常编码,核心准则就一条:能不给外部看的,就不要暴露。
暴露的属性和方法越多,外部代码对内部实现的依赖就越重,以后你越不敢改。有些团队规定类的所有属性都设为“私有”+property,虽然有点极端,但确实能减少后续的契约负担。
另一个实用技巧是区分“数据属性”和“行为方法”。数据属性尽量用名词命名,行为方法尽量用动词命名。看到book.title你知道它是数据,看到book.return_book()你知道它在执行一个动作。命名清楚本身就是一种封装。
4. 继承体系设计:组合优先于继承
继承是 OOP 里最容易写过头、也最容易引起争议的主题。很多人学完继承后非常兴奋,看到什么都想继承一把,结果制造出一堆“钻石继承”“脆弱的基类问题”。这里我想聊聊实际项目中怎么用继承才稳。
4.1 继承的正确姿势:什么时候该用继承
继承的本质是“is-a”关系。子类是父类的一种特殊形态,能复用父类的接口和实现。如果两个类之间不是“is-a”关系,只是有部分公共代码,那应该用组合而不是继承。
举例来说,Duck继承Bird是合理的,因为鸭子确实是鸟的一种;UserService继承DatabaseMixin通常不合理,因为用户服务不是数据库的一种,它只是“用到了”数据库。
想要判断得多,可以直接问自己:把子类传给一个期待父类类型的函数,逻辑上通不通?如果你有一个处理Bird的函数,传Duck进去没问题,那继承就站得住。
class Bird: def __init__(self, name): self.name = name def fly(self): print(f"{self.name}在飞") class Duck(Bird): def __init__(self, name): super().__init__(name) def quack(self): print("嘎嘎") def show_bird(bird): bird.fly() show_bird(Duck("唐老鸭")) # 正常4.2 super() 与 MRO:多继承中的那个知名难题
Python 的多继承因为 C3 线性化算法(MRO)表现得还算有序,但理解起来并不直观。简单说,super()不是“调父类的方法”,而是“沿着 MRO 找下一个符合条件的方法”。
看一个实际案例:
class A: def who(self): print("A") class B(A): def who(self): print("B") super().who() class C(A): def who(self): print("C") super().who() class D(B, C): def who(self): print("D") super().who() d = D() d.who()输出顺序是:
D B C AD的 MRO 是[D, B, C, A, object]。super()不是跳到“父类”,而是跳到 MRO 里的下一个。这也是为什么super()能实现“协作式多继承”——每个类都只负责自己的部分,然后接力往下传。
但请记住:多继承是高级特性,代价是高复杂度。团队项目里如果没见过mro()输出,建议尽量别引入多继承。我见过很多号称需要多继承的需求,最后用组合或者 Mixin 的单一继承路径都解决了。
4.3 多态:用一个统一接口处理不同行为
多态的价值在于:调用方不需要关心对象的真实类型,只需要知道它支持某个方法。Python 的动态类型让多态用起来特别轻——不需要显式的接口定义,只要对象有对应的方法就行。
class Cat: def speak(self): return "喵" class Dog: def speak(self): return "汪" def animal_sound(animal): print(animal.speak()) animal_sound(Cat()) animal_sound(Dog())两种类没有任何继承关系,但都有speak方法,animal_sound就能统一处理。这种“基于行为的接口约定”被称为鸭子类型:如果它走起来像鸭子、叫起来像鸭子,那它就是鸭子。
多态跟继承是两回事。继承是代码复用和接口统一的手段,多态是运行时行为的动态分发。哪怕不用继承,多态照样能实现。很多经验丰富的 Python 开发者会告诉你:能用鸭子类型解决,就不用专门搭继承体系。
4.4 为什么不建议滥用继承
继承的麻烦在于它把父子类绑定得很紧。父类改一个方法,所有子类都可能受影响。而继承层次一旦深了,代码的可读性会急剧下降——你看到一个方法,得沿着继承链往上找好几层才知道它到底干了什么。
实际开发中,“组合优先于继承”几乎是 OOP 设计的共识。组合的意思是:一个类持有另一个类的实例,通过调用对方的公有方法来完成任务。
class Engine: def start(self): print("引擎启动") class Car: def __init__(self): self.engine = Engine() # 组合关系 def start(self): self.engine.start() print("汽车启动")Car包含Engine,但Car不是Engine的一种。这样两者之间的耦合远低于继承,可以独立修改、独立测试。
我的经验是:当你在设计中犹豫“要不要加一层继承”时,先试着用组合描述同样的逻辑。很多时候组合写起来更直白,也更好测。
5. 特殊方法:让你的类用起来像原生类型
Python 的特殊方法(魔术方法)是 OOP 中最能提升使用体验的部分。它们让自定义类的对象能够参与语言内置操作:打印、比较、加减乘除、迭代、上下文管理,等等。
5.1 __repr__ 和 __str__:调试体验从这开始
新手经常忽略这两个方法,直到在某次调试时看到一堆<__main__.Product object at 0x7f8c...>才意识到问题。
__repr__是给开发者看的,要求尽量无歧义,最好能直接用来重建对象。__str__是给最终用户看的,要求可读性好。- 只实现一个时,优先实现
__repr__,因为 Python 在找不到__str__时会回退到__repr__。
class Product: def __init__(self, name, price): self.name = name self.price = price def __repr__(self): return f"Product({self.name!r}, {self.price})" def __str__(self): return f"{self.name}:¥{self.price}" p = Product("机械键盘", 399) print(p) # 机械键盘:¥399 print([p]) # [Product('机械键盘', 399)],列表里用 repr这个细节能让调试效率明显提升。你打印一个列表,里面每个对象都显示成清晰的结构化描述,而不是内存地址,这种体验用过就回不去。
5.2 运算符重载与 __eq__:让对象支持比较
默认情况下,自定义类的两个实例比较是否相等,用的是内存地址比较,也就是“是不是同一个对象”。但很多时候我们需要的“相等”是“内容相等”。
class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y)) p1 = Point(1, 2) p2 = Point(1, 2) print(p1 == p2) # True s = {p1, p2} print(len(s)) # 1,有了 __hash__ 才能正确放进集合注意__eq__和__hash__是一对。如果两个对象相等,它们的哈希值必须一致,否则对象放进set或作为dict的键时会出现严重 bug。实现了__eq__后,Python 默认会把__hash__设为None,导致对象不可哈希。所以要么同时实现__hash__,要么明确把类设成不可哈希的(比如可变对象建议别实现__hash__)。
运算符重载方面,__add__、__sub__这类方法让类支持+ -运算。比如实现一个表示金额的类,让两个金额对象可以直接相加:
class Money: def __init__(self, amount): self.amount = amount def __add__(self, other): return Money(self.amount + other.amount) def __repr__(self): return f"Money({self.amount})" total = Money(100) + Money(50) print(total) # Money(150)使用场景要克制。如果语义不清晰、用户不明所以,就别为了花哨重载运算符。重载的目的是让代码更自然地表达领域逻辑,不是炫技。
5.3 上下文管理器 __enter__ / __exit__:告别手动释放资源
凡是需要“用完必须清理”的场景——文件、网络连接、数据库会话——都应该尽量做成上下文管理器。with语句保证即使中间抛异常,__exit__也会执行。
class DatabaseConnection: def __enter__(self): print("建立数据库连接") return self def __exit__(self, exc_type, exc_value, traceback): print("关闭数据库连接") return False with DatabaseConnection() as conn: print("执行查询")__exit__的返回值有讲究:返回True表示异常已被处理,不会继续向上抛;返回False或None,异常会正常传播。除非你确实要吞掉某个类型的异常,否则建议返回False,别让错误悄无声息地消失。
用标准库contextlib.contextmanager可以更简洁地写同样的逻辑,把函数改造成上下文管理器。多数场景下推荐这种方式,代码更短、更直观。
from contextlib import contextmanager @contextmanager def db_session(): print("建立连接") try: yield finally: print("关闭连接")6. 实战案例:一个从需求到 OOP 设计的完整过程
理论学习再多,不落地还是虚的。这一节我们完整走一遍从需求到代码的分析过程,展示怎么用 OOP 思路把问题拆清楚。
6.1 需求描述:一个简单的库存管理系统
假设要开发一个小型仓库库存管理系统,核心需求如下:
- 仓库存储多种商品,每件商品有名称、编号、单价、库存数量。
- 支持入库和出库操作,出库时不能超过当前库存。
- 商品分普通商品和生鲜商品,生鲜商品有过期时间,超过保质期不能出库。
- 需要记录每一次入库/出库操作,方便以后查账。
需求看起来很平常,但已经包含了建模的基本素材。接下来就是典型的“找名词、找行为、定关系”。
6.2 数据分析与类设计:先找名词,再找行为
把需求里的名词圈出来:商品、编号、名称、单价、库存、入库、出库、生鲜商品、过期时间、操作记录。
操作是行为,不是名词。把名词归类后,候选类就出来了:
Product:商品,有基础属性和出入库行为。PerishableProduct:生鲜商品,是商品的一种,继承Product并增加过期时间。InventoryRecord:操作记录,记录某次操作的商品、数量、类型、时间。
类之间是什么关系?PerishableProduct是Product的子类,满足 is-a 关系;库存管理系统会持有多个Product对象,这是组合;每次出入库会产生一条InventoryRecord,这也是一种关联。
from datetime import datetime, date class Product: def __init__(self, sku, name, price, quantity=0): self.sku = sku self.name = name self.price = price self.quantity = quantity def restock(self, amount): if amount <= 0: raise ValueError("入库数量必须大于0") self.quantity += amount def sell(self, amount): if amount <= 0: raise ValueError("出库数量必须大于0") if amount > self.quantity: raise ValueError("库存不足") self.quantity -= amount def __repr__(self): return f"Product({self.sku}, {self.name}, 库存={self.quantity})" class PerishableProduct(Product): def __init__(self, sku, name, price, expiry_date, quantity=0): super().__init__(sku, name, price, quantity) self.expiry_date = expiry_date def sell(self, amount): if date.today() >= self.expiry_date: raise ValueError(f"商品 {self.name} 已过期,不能出库") super().sell(amount)注意PerishableProduct.sell的写法:先做过期检查,再调用父类的sell做数量校验和扣减。这体现了继承的一个常见且合理的用法——在复用父类逻辑的基础上增加前置条件。
6.3 编码实现与逐步优化
再补一个管理库存的Inventory类,负责维护商品列表和执行出入库记录:
class InventoryRecord: def __init__(self, sku, change, created_at=None): self.sku = sku self.change = change self.created_at = created_at or datetime.now() def __repr__(self): return f"InventoryRecord({self.sku}, {self.change}, {self.created_at})" class Inventory: def __init__(self): self.products = {} self.records = [] def add_product(self, product): if product.sku in self.products: raise ValueError("商品编号已存在") self.products[product.sku] = product def restock(self, sku, amount): product = self.products[sku] product.restock(amount) self.records.append(InventoryRecord(sku, amount)) def sell(self, sku, amount): product = self.products[sku] product.sell(amount) self.records.append(InventoryRecord(sku, -amount)) def report(self): for product in self.products.values(): print(product)这里有个设计取舍值得说:为什么把restock/sell的调用放在Inventory类里,而不是让调用方直接操作Product?
因为Inventory承担了一个额外职责——记录每一次操作。如果调用方绕过Inventory直接调product.sell(),记录就不会产生。这个设计让“库存变动”和“记录保存”之间的不变式被代码结构保护住了,而不是靠调用方自觉。
运行一下这个最小系统:
inv = Inventory() inv.add_product(Product("A001", "手机", 2999, 10)) inv.add_product(PerishableProduct("B001", "牛奶", 15, date(2025, 5, 1), 20)) inv.restock("A001", 5) inv.sell("A001", 3) inv.sell("B001", 5) inv.report()输出:
Product(A001, 手机, 库存=12) Product(B001, 牛奶, 库存=15)这个案例虽然简单,但已经覆盖了类设计、继承、组合、状态校验、操作记录这些 OOP 核心要素。把案例里的逻辑吃透,再往里头加需求——比如支持不同币种的价格、支持批量导入、支持按品类统计——你会发现每加一个需求,类的扩展路径都是清晰的,这正是 OOP 最大的回报。
7. 自学 OOP 阶段我踩过的坑与避坑经验
最后这部分是我自己在写 Python 项目过程中踩过的坑、复盘过的问题,挑几个最有代表性的分享出来,希望能帮你少走弯路。
7.1 过度设计:一上来就做一大堆抽象
我见过不少初学者(也包括当年的我)学完继承、抽象类、多态之后,兴奋得不得了,写一个计算器也要先定义一个BaseOperation抽象类,再让加减乘除各自继承。结果代码量翻倍,可读性却没提升。
抽象本身不是目的,解决实际问题才是。我的建议是:先用最直白的面向过程写法跑通需求,再根据重复代码出现的次数、需求变化的频率来判断要不要引入类和抽象层。不做提前设计,但允许自己“演进式设计”——先写简单版本,等确实感觉到代码变臭了,再重构出合理抽象。这种节奏在实践中远比一上来就来一顿猛如虎的架构设计来得靠谱。
7.2 可变对象作为参数默认值:经典坑
前面提过类变量的坑,函数参数的默认值也有类似的版本:
class ShoppingCart: def __init__(self, items=[]): # 错误示范 self.items = items cart1 = ShoppingCart() cart1.items.append("苹果") cart2 = ShoppingCart() print(cart2.items) # 输出 ['苹果']items=[]这个默认列表只在函数定义时创建一次,所有实例共享同一个列表。任何实例往里面 append,其他实例都会看到。正确的做法是:
class ShoppingCart: def __init__(self, items=None): self.items = items if items is not None else []这个坑看起来低级,但它在真实项目中出现的频率非常高,尤其是有前端背景、习惯了 JavaScript 那种默认参数按值求值的开发者,特别容易中招。记住一句话:默认参数只在定义时求值一次,永远别用可变对象当默认值。
7.3 isinstance 与鸭子类型之争
很多从 Java 转过来的开发者,习惯性地在方法里写一堆isinstance检查:
def speak(animal): if isinstance(animal, Dog): print("汪汪") elif isinstance(animal, Cat): print("喵喵")这种写法的问题在于每增加一种动物,speak函数都要改。更 Pythonic 的做法是依赖鸭子类型,直接调用方法:
def speak(animal): animal.speak()但这里也有个度。当你真的需要区分类型时(比如__eq__里检查isinstance(other, Point)),用isinstance是合理且必要的。真正的坑是把 isinstance 当成分发机制到处用,那说明你的多态设计还没做对。正确做法是优先让类自己提供统一接口,用多态代替类型判断。
7.4 在错误的地方使用继承
再重复一遍那个最痛的经验:很多本来看起来很适合继承的场景,其实用组合更好。
比如“用户”和“管理员”。管理员确实是用户的一种,用继承可以做,但管理员通常还需要权限管理、操作审计这些独立能力。你把它们都塞进继承体系,基类会越来越胖,最终变成一个“上帝类”,所有子类都被迫背负一堆跟自己无关的属性。
我后来的做法是:把权限相关的能力抽成独立的类或 Mixin,通过组合方式让AdminUser拥有这些能力。继承只保留最核心的属性——用户名、ID、登录状态。类变得小而清晰,扩展起来也灵活得多。
class User: def __init__(self, username, user_id): self.username = username self.user_id = user_id class PermissionMixin: """为需要权限管理的用户类提供权限校验""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.permissions = set() def grant_permission(self, perm): self.permissions.add(perm) def has_permission(self, perm): return perm in self.permissions class AdminUser(PermissionMixin, User): pass admin = AdminUser("root", 0) admin.grant_permission("delete_any_post") print(admin.has_permission("delete_any_post")) # True写在最后。OOP 的核心不是语法,而是一种组织代码的思维方式。语法几天就能学会,但从“会写类”到“设计得好”之间,靠的是大量实际项目中的试错和复盘。别指望读完一篇指南就能成为设计高手,更重要的是带着这套思路回到你自己的代码里,找到那些不舒服的地方,动手重构,让类承担它该承担的职责。希望这篇内容能成为你路上一个还算好用的参考。