课程进度 课程大纲 已发布 24/24 课
Python 基础
数据与集合
构建可靠的程序
使用对象建模
专业 Python
高级 Python
当数据有规则时,使用蓝图
字典通常足以表示一条记录。当一个概念具有状态、保持该状态有效的规则,以及与其相关的操作时,类就派上用场了。
- 类 共享的蓝图
- 实例 根据蓝图创建的一个对象
- 状态 该对象自己的值
class StudySession:
def __init__(self, topic, minutes):
self.topic = topic
self.minutes = minutes
first = StudySession("Python", 40)
second = StudySession("Git", 25)
print(first.topic)
print(second.minutes)
输出为 Python 和 25。调用类会创建一个实例并运行 __init__。初始化必须返回 None;它负责准备新对象,而不是返回一个替代对象。
self 背着当前对象的背包
实例方法将 self 声明为第一个参数。当你通过实例调用该方法时,Python 会自动传入这个实例:
class StudySession:
def __init__(self, topic, minutes):
self.topic = topic
self.minutes = minutes
def label(self):
return f"{self.topic}: {self.minutes} minutes"
session = StudySession("Python", 40)
print(session.label())
print(StudySession.label(session))
两次调用都会打印 Python: 40 minutes。第一次使用绑定方法;第二次显式传入同一个实例。
-
通过实例调用
session.label() -
self 到达
它就是这个
session -
读取它的状态
self.topic - 返回结果 将显示操作留在外部
不变量是每个可用实例都必须遵守的规则。在公开状态之前先进行验证:
class StudySession:
def __init__(self, topic, minutes=0):
topic = topic.strip()
if not topic:
raise ValueError("topic must not be empty")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
self.topic = topic
self._minutes = minutes
def add_minutes(self, amount):
if isinstance(amount, bool) or not isinstance(amount, int) or amount <= 0:
raise ValueError("amount must be a positive integer")
self._minutes += amount
return self._minutes
这个状态转换方法先检查,再修改状态。按照约定,名称开头的下划线表示“非公开的实现细节”;它并不会强制实现私有性。
看起来相同的对象仍然可能不同
first = StudySession("Python", 30)
second = StudySession("Python", 30)
alias = first
print(first is second)
print(first is alias)
print(first == second)
对于这个没有自定义相等性规则的普通类,输出为 False、True、False。is 判断两个名称是否指向同一个对象。== 判断是否相等,但普通的用户自定义类在定义值相等规则之前,会退回到按身份判断。应将 is 用于 value is None 之类的身份检查,而不要用它代替普通的值比较。
每个实例自己的可变数据必须在 self 上创建:
class StudyPlan:
def __init__(self):
self.sessions = []
如果把 sessions = [] 放在类体中,每个实例找到的都会是同一个共享列表。类属性适合保存共享常量,而不适合充当每个对象自己的私人笔记本。
计算视图并保护笔记本
属性可以用属性访问语法呈现计算成本较低的信息。类方法则提供另一条用途明确的构造路径。
- @property 只读的计算视图
- @classmethod 另一种构造方式
- 元组快照 可以查看,但不能追加
class StudyPlan:
def __init__(self, sessions):
self._sessions = list(sessions)
@property
def total_minutes(self):
return sum(session.minutes for session in self._sessions)
@property
def sessions(self):
return tuple(self._sessions)
@classmethod
def from_rows(cls, rows):
return cls(StudySession(row["topic"], row["minutes"]) for row in rows)
plan.total_minutes 不需要括号,而且由于每次都会重新计算,所以不会过期。cls(...) 会保留调用 from_rows() 时所使用的类,同时普通构造函数仍会强制执行不变量。元组会阻止通过返回值调用 append,并捕获外层列表的当前状态,但它不会递归冻结其中的可变对象。
构建学习计划模型
将这个完整的、仅使用标准库的项目保存为 study_plan.py:
class StudySession:
def __init__(self, topic, minutes=0):
topic = topic.strip()
if not topic:
raise ValueError("topic must not be empty")
if isinstance(minutes, bool) or not isinstance(minutes, int) or minutes < 0:
raise ValueError("minutes must be a non-negative integer")
self.topic = topic
self._minutes = minutes
@property
def minutes(self):
return self._minutes
def add_minutes(self, amount):
if isinstance(amount, bool) or not isinstance(amount, int) or amount <= 0:
raise ValueError("amount must be a positive integer")
self._minutes += amount
return self._minutes
class StudyPlan:
def __init__(self, title):
title = title.strip()
if not title:
raise ValueError("title must not be empty")
self.title = title
self._sessions = []
def add_session(self, session):
if not isinstance(session, StudySession):
raise TypeError("session must be a StudySession")
self._sessions.append(session)
@property
def sessions(self):
return tuple(self._sessions)
@property
def total_minutes(self):
return sum(session.minutes for session in self._sessions)
@classmethod
def from_rows(cls, title, rows):
plan = cls(title)
for row in rows:
plan.add_session(StudySession(row["topic"], row["minutes"]))
return plan
def report(self):
lines = [self.title, f"Total: {self.total_minutes} minutes"]
lines.extend(session.topic for session in self._sessions)
return "\n".join(lines)
def main():
first = StudySession(" Python ", 30)
second = StudySession("Git", 25)
assert first.add_minutes(10) == 40
assert second.minutes == 25
plan = StudyPlan("August plan")
plan.add_session(first)
snapshot = plan.sessions
plan.add_session(second)
assert len(snapshot) == 1 and len(plan.sessions) == 2
rows = [{"topic": "Files", "minutes": 20}]
assert StudyPlan.from_rows("Extra", rows).total_minutes == 20
print("Tests passed.")
print(plan.report())
if __name__ == "__main__":
main()
输出:
Tests passed.
August plan
Total: 65 minutes
Python
Git
这两个学习时段彼此独立,构造和更新操作会保持状态有效,而且调用者永远不会获得计划内部的可变列表。
三个小任务与交接
- 阅读目标。 构建
ReadingGoal(title, target_pages),包含record(pages)和只读的completed属性。测试两个相互独立的目标,并测试恰好达到目标页数的情况。 - 共享列表侦探。 将
entries = []放入类体中,证明两个实例共享它;然后将它移到__init__中的self.entries上,并证明两个实例相互独立。 - 文本入口。 添加
StudySession.from_text("Python:45")。在其中进行解析,但让普通构造函数负责强制执行关于主题和分钟数的所有不变量。
容易踩坑的地方:__init__ 不能返回其他值;直接修改可能绕过不变量;is 判断的是身份,而不是值是否相等;返回内部列表会泄露第二条修改路径。
- 我能区分类和实例。
- 我能解释
self并初始化有效状态。 - 我会将每个实例自己的可变数据保存在
self上。 - 我能区分对象身份与看起来相同的状态。
- 我会使用属性和用途明确的类方法。
- 我能返回内部列表安全的外层快照。
- 我的完整模型通过了所有断言,并打印出 65 分钟。
- 我完成了三个小任务。
接下来,对于灵活的“拥有一个”关系,你将选择组合;对于真正可替换的“是一个”关系,你将选择继承;对于共享行为,你将选择多态。