简介:专为学完Python基础的学习者设计的实操型练习资源,覆盖对象模型核心机制与常见工程模式。包含自定义文件读取类(利用__enter__/__exit__等魔法方法实现上下文管理)、带佣金逻辑的描述符(控制属性访问并动态计算)、键值对持久化存储(模拟简单数据库操作)、楼梯图形生成(递归与循环结合)、JSON序列化装饰器(自动处理对象转字典再序列化)、二次方程求解(含异常处理与复数支持)、数字各位求和(字符串与数值转换练习)、类与继承关系建模(如员工-经理层级结构)。所有内容按周组织:week_01聚焦工具类与函数式技巧;week_02深入描述符与属性控制机制;week_03强化数学逻辑、异常捕获与数据序列化;week_04提升面向对象设计能力,涵盖抽象、继承与多态实践。每个子目录都是一个独立可运行的小项目,附带README.md说明运行方式、输入输出示例及关键知识点提示。适合边写边学,通过调试真实代码理解Python底层行为与工程落地细节。
1. 这不是又一套“抄完就忘”的Python练习题——它是一份能让你真正看清Python对象底层心跳的实操地图
你肯定见过太多“Python进阶”资料:标题唬人,内容空泛,要么堆砌概念解释,要么甩出几道LeetCode式算法题,做完连自己写了啥都记不清。而这个资源包,从第一天打开 week_01/01_digits_sum 的那一刻起,你就不是在做题,是在调试Python的呼吸节奏。
它不讲“魔法方法是什么”,而是让你亲手写一个 FileReader 类,当 with open(...) 那行代码执行时,你写的 __enter__ 真的被调用了;当你忘记 close(),__exit__ 会默默帮你擦屁股——你亲眼看见上下文管理器怎么接管了整个生命周期。它不讲“描述符很强大”,而是让你实现一个 CommissionDescriptor,当销售员的 salary 属性被读取时,它自动叠加5%佣金;当经理把 base_salary 改成2万,所有下属的 salary 实时重算——你亲手拧动了属性访问的开关。它甚至不回避“装饰器难懂”,直接给你一个 @to_json 装饰器,贴在 Employee 类上,运行后你立刻看到控制台输出一串带中文、带嵌套、带时间戳的JSON字符串——没有抽象讲解,只有结果倒逼你反推原理。
这套练习的核心关键词——魔法方法、描述符、装饰器、类继承——不是并列的知识点,而是Python对象模型的四根承重柱。__init__ 是地基,__get__ 是水管阀门,@property 是墙面开关,class Manager(Employee) 是整栋楼的承重结构。它按周组织,但逻辑是螺旋上升的:week_01 让你用函数和简单类解决具体问题(比如楼梯图形),建立“我能写出可运行代码”的信心;week_02 突然把你拽进对象内部,让你直面 __get__ 和 __set__ 的调用栈;week_03 用二次方程求解逼你处理 ValueError 和 ZeroDivisionError,再用 @to_json 把异常信息也序列化进去;week_04 则彻底放开,让你设计 Employee、Manager、Intern 的继承树,当 Manager 的 bonus_rate 改变时,Intern 的 stipend 是否该联动?这种问题没有标准答案,只有你亲手建模后的逻辑自洽。
它适合谁?不是零基础小白——你得知道 for i in range(10): 怎么写;也不是准备面试的刷题党——这里没有最优时间复杂度分析。它专为那些卡在“语法都会,但写不出健壮类库”的人准备:你可能已经能用 pandas 做数据分析,却说不清 df['col'] 底层触发了哪个方法;你可能天天写 @staticmethod,却没想过如果去掉 @ 符号会发生什么。这个包就是你的“Python显微镜”,每个子目录都是一个独立透镜,聚焦一个机制,让你看清字节码背后的真实脉动。它不要求你一口气学完,而是鼓励你每周只深挖一个 week_02/02_descriptor_with_comission,跑通、打断点、改参数、看输出,直到 print(emp.salary) 那一刻,你心里清楚知道:此刻,CommissionDescriptor.__get__ 正在内存里执行。
2. 内容整体设计与思路拆解:为什么是这四根柱子?为什么按周递进?
2.1 四大核心机制的内在逻辑链:从“能用”到“可控”再到“可塑”
很多学习者把魔法方法、描述符、装饰器、类继承当成四个孤立模块去学,结果越学越散。而这个练习包的设计逻辑,是把它们串成一条清晰的“控制力升级链”:
-
Week_01(工具类与函数式技巧)是起点,解决“能用”的问题:
01_digits_sum让你反复练习str()与int()的转换边界(比如输入"123"vs"abc"),02_draw_stairs强迫你思考循环嵌套的缩进逻辑与空格拼接的精度(多一个空格,楼梯就塌了)。这些看似简单,实则是后续所有高级机制的“肌肉记忆”。没有对int("123")的条件反射,你根本无法理解__int__方法为何要返回int类型;没有亲手写过for i in range(n): print(" " * (n-i) + "*" * i),你永远体会不到__len__或__iter__返回值如何影响外部循环行为。 -
Week_02(对象模型与描述符)是转折点,解决“可控”的问题:当
week_01让你熟练操作数据后,week_02直接把你推进对象内部。01_key_value_storage不是让你用dict,而是要求你实现一个KeyValueStorage类,其__getitem__必须支持storage['key'],__setitem__必须支持storage['key'] = value,__contains__必须支持'key' in storage。这时你才第一次意识到:原来in操作符背后,是__contains__方法在说话。而02_descriptor_with_comission更是关键一跃——它不让你写一个普通属性,而是强制你创建一个独立的CommissionDescriptor类,然后在SalesPerson类中用salary = CommissionDescriptor()声明。这意味着属性访问的控制权,从类本身移交给了另一个对象。你必须亲手实现__get__(读取时计算佣金)、__set__(设置底薪时校验正数)、__delete__(禁止删除薪资)。这不是语法糖,这是Python赋予你的“属性级操作系统权限”。 -
Week_03(数学逻辑与异常处理)是压力测试,解决“鲁棒”的问题:
03_quadratic_equation是典型的压力源。它要求你处理a=0(退化为一次方程)、b²-4ac<0(复数解)、a,b,c为非数字等所有边界。你不能再用try: ... except:笼统包裹,必须精准捕获ZeroDivisionError、ValueError,甚至手动抛出CustomEquationError。紧接着02_to_json_decorator把压力升级:它要求装饰器不仅能处理Employee对象,还要递归处理list[Employee]、dict[str, Employee],更要优雅降级——当遇到datetime对象时,自动转为ISO格式字符串;当遇到不可序列化的lambda时,跳过并记录警告。这里的“装饰器”不再是语法糖,而是你构建的“数据流过滤网”。 -
Week_04(面向对象设计)是整合终点,解决“可塑”的问题:
02_classes_and_inheritance不是让你写几个类,而是要求你建模一个真实的小型组织架构。Employee是基类,有name,id,salary;Manager继承它,增加team_size和bonus_rate;Intern继承它,增加university和stipend。关键在于多态实践:def calculate_annual_cost(emp: Employee) -> float:这个函数,传入Manager时应返回salary + bonus_rate * team_size * 1000,传入Intern时应返回stipend * 12。你必须亲手实现Manager.calculate_annual_cost()和Intern.calculate_annual_cost(),并确保calculate_annual_cost(manager)自动调用前者。此时,“继承”不再是class A(B)的语法,而是你设计的系统能否通过“替换原则”(Liskov Substitution Principle)的检验——把Manager对象塞进任何只接受Employee的函数里,系统依然稳定。
这条链路的本质,是让你从“使用者”蜕变为“规则制定者”。week_01 你在用规则,week_02 你在修改规则,week_03 你在加固规则,week_04 你在设计新规则。每一个环节都环环相扣,缺一不可。
2.2 “按周组织”不是时间表,而是认知负荷的精密调控
很多人误以为“week_01”意味着“第一周必须完成”,其实不然。“按周组织”的核心价值,在于它严格遵循了认知心理学中的“工作记忆容量限制”。人类短期工作记忆平均只能同时处理4±1个信息块。这个包的设计者深谙此道:
-
Week_01 的三个任务(digits_sum, draw_stairs, quadratic_equation)全部限定在单文件、无类、纯函数范围内。
quadratic_equation.py里没有class,只有def solve_quadratic(a, b, c):。这确保你的工作记忆只用于消化数学逻辑和异常分支,不会被类定义、self参数、继承关系等额外信息块挤占。 -
Week_02 的两个任务(key_value_storage, descriptor_with_comission)强制引入类,但刻意剥离其他复杂度。
key_value_storage.py中,KeyValueStorage类只实现__getitem__,__setitem__,__contains__三个魔法方法,不涉及__init__的复杂初始化,不涉及__str__的格式化。descriptor_with_comission.py中,CommissionDescriptor类只关注__get__,__set__,__delete__,不掺杂__set_name__(那是更高级的元编程)。这种“单点爆破”策略,让你的认知资源100%聚焦于“描述符如何接管属性访问”这一核心。 -
Week_03 的任务开始混合技能,但提供明确的“锚点”。
to_json_decorator.py的核心是装饰器,但它提供的Employee示例类就是一个完美锚点——你不需要从零设计类,只需专注装饰器逻辑。classes_and_inheritance.py的Employee基类也是锚点,你在此基础上扩展,而非平地起高楼。 -
Week_04 的任务则完全开放,但通过README.md提供“思维脚手架”。
README.md不会告诉你代码怎么写,但会列出关键问题:“Manager的bonus_rate如何影响其salary属性?”“Intern的stipend是否应该随Employee的salary变化?”这些问题不是考题,而是引导你进行设计决策的提示符,帮你把发散的思维重新锚定在OOP核心原则上。
这种设计,让每个“周”成为一个认知安全区。你不必担心“学不完”,因为每个任务都被压缩到刚好填满你的工作记忆;你也不必害怕“学不会”,因为每个难点都被隔离出来,单独打磨。
2.3 为什么拒绝“理论先行”,坚持“代码即文档”?
这个包最叛逆的一点,是它彻底抛弃了传统教程的“概念→例子→练习”三段论。它的每个 README.md 文件,第一行永远是:
# 运行方式
python 01_digits_sum.py 123
# 预期输出
6
而不是“digits_sum 函数用于计算数字各位之和,其时间复杂度为O(log n)…”。原因很简单:对初学者而言,抽象概念是噪音,可运行的结果才是信号。当你输入 python 01_digits_sum.py abc,看到报错 ValueError: invalid literal for int() with base 10: 'abc',这个错误信息比十页理论文档更能教会你“类型转换的脆弱性”。当你在 02_descriptor_with_comission.py 中注释掉 CommissionDescriptor.__set__ 方法,再运行 emp.base_salary = -5000,发现程序没有报错而是默默接受了负数,这个现场比任何文字说明都更深刻地揭示了“描述符的 __set__ 是唯一校验入口”。
这种“代码即文档”的哲学,源于一个残酷现实:90%的Python学习者,不是在阅读文档时学会知识的,而是在调试报错时学会的。KeyError 教会你字典的键存在性检查,AttributeError 教会你属性访问的查找链,RecursionError 教会你递归的深度限制。这个包把每一次报错,都设计成一次精准的教学事件。它不隐藏错误,而是精心构造错误场景,让你在修复它的过程中,亲手触摸Python的神经末梢。
3. 核心细节解析与实操要点:魔法方法、描述符、装饰器、继承的“手把手”拆解
3.1 魔法方法实战:不只是 __init__ 和 __str__,而是整个对象生命周期的指挥棒
魔法方法(Magic Methods),官方名称是“特殊方法”(Special Methods),它们以双下划线开头和结尾(如 __len__),是Python对象模型的底层API。很多人以为它们只是用来“美化打印”,实则它们是Python解释器与你的对象之间沟通的唯一协议。week_01/01_file_reader_class.py 和 week_02/01_key_value_storage.py 是绝佳的切入点。
先看 FileReader 类。它不是一个简单的文件读取器,而是一个完整的上下文管理器(Context Manager)。其核心在于 __enter__ 和 __exit__:
class FileReader:
def __init__(self, filename):
self.filename = filename
self.file = None # 初始化时,文件句柄为空
def __enter__(self):
print(f"正在打开文件 {self.filename}...")
self.file = open(self.filename, 'r', encoding='utf-8')
return self.file # 这个返回值,就是 with 语句中 as 后面的变量
def __exit__(self, exc_type, exc_value, traceback):
print(f"正在关闭文件 {self.filename}...")
if self.file:
self.file.close()
# exc_type, exc_value, traceback 是异常信息
# 如果返回 True,则异常被抑制;返回 None 或 False,则异常继续向上抛出
return False # 让异常正常传播,便于调试
关键细节与实操要点:
__enter__的返回值决定as的绑定对象:with FileReader('test.txt') as f:这行代码中,f绑定的不是FileReader实例,而是__enter__方法的返回值(即self.file)。这意味着你可以在__enter__中返回任意对象,甚至可以返回self本身(如果你希望as绑定的是类实例)。__exit__的三个参数是调试黄金:exc_type是异常类(如<class 'ValueError'>),exc_value是异常实例(如ValueError('invalid literal')),traceback是追踪对象。你可以利用它们做精细化错误处理。例如,在__exit__中添加:
python if exc_type is ValueError and "encoding" in str(exc_value): print("检测到编码错误,尝试使用gbk编码重试...") self.file = open(self.filename, 'r', encoding='gbk') return True # 抑制原始异常
这种能力,是普通try...except无法比拟的,因为它发生在异常传播的最底层。__exit__的返回值是“异常防火墙”:返回True表示“我已处理此异常,请勿向上抛”,返回False或None表示“请继续向上抛”。新手常犯的错误是忘记返回值,默认返回None,导致本该被抑制的异常意外暴露。
再看 KeyValueStorage。它模拟了一个极简的键值数据库,其魔法方法让 dict 的常用操作无缝迁移:
class KeyValueStorage:
def __init__(self):
self._data = {} # 私有字典存储数据
def __getitem__(self, key):
print(f"获取键 '{key}'...")
return self._data[key]
def __setitem__(self, key, value):
print(f"设置键 '{key}' 为 '{value}'...")
self._data[key] = value
def __contains__(self, key):
print(f"检查键 '{key}' 是否存在...")
return key in self._data
def __len__(self):
print("查询总键数...")
return len(self._data)
def __iter__(self):
print("开始迭代所有键...")
return iter(self._data)
关键细节与实操要点:
__contains__是in操作符的唯一入口:'name' in storage这行代码,Python解释器只会调用storage.__contains__('name')。如果你不实现它,Python会退而求其次,尝试遍历__iter__并逐一比较,效率极低。这就是为什么__contains__必须高效实现(通常是O(1)的哈希查找)。__iter__必须返回一个迭代器对象:它不能直接return self._data.keys()(这是一个dict_keys对象,不是迭代器),而必须return iter(self._data)(iter()函数返回一个真正的迭代器)。否则,for k in storage:会报错TypeError: 'KeyValueStorage' object is not iterable。- 魔法方法的调用是隐式的,但可被显式触发:你可以直接调用
storage.__getitem__('name'),效果等同于storage['name']。这在调试时极其有用——当你怀疑__getitem__逻辑有误,可以直接在Python shell中调用它,绕过所有语法糖,直击问题核心。
提示:在
week_01/01_file_reader_class.py中,务必在__exit__方法里添加with FileReader('nonexistent.txt') as f:,你会看到__enter__执行后立即触发__exit__,且exc_type不为None。这个现场教学,比任何文档都更能让你理解上下文管理器的异常处理机制。
3.2 描述符应用:属性访问的“中间件”,远超 @property 的控制粒度
描述符(Descriptor)是Python中最高阶的属性控制机制。如果说 @property 是一个功能固定的“开关”,那么描述符就是一个可编程的“中央处理器”。week_02/02_descriptor_with_comission.py 是理解它的最佳范本。
一个描述符类,必须至少实现 __get__, __set__, __delete__ 中的一个方法。CommissionDescriptor 的完整实现如下:
class CommissionDescriptor:
def __init__(self, commission_rate=0.05):
self.commission_rate = commission_rate
# 使用一个私有字典来存储每个实例的独立状态
# key: (instance, owner) tuple, value: base salary
self._values = {}
def __get__(self, instance, owner):
if instance is None:
# 当通过类访问时,如 SalesPerson.salary,instance 为 None
return self
# 从私有字典中获取该实例的 base_salary
base_salary = self._values.get((instance, owner), 0)
# 动态计算并返回带佣金的总薪资
total_salary = base_salary * (1 + self.commission_rate)
print(f"【描述符】读取 {instance.name} 的薪资: {base_salary} + {base_salary * self.commission_rate:.2f} = {total_salary:.2f}")
return total_salary
def __set__(self, instance, value):
if not isinstance(value, (int, float)) or value < 0:
raise ValueError(f"底薪必须是非负数字,当前值: {value}")
# 将底薪存入私有字典,键为 (instance, owner)
self._values[(instance, type(instance))] = float(value)
print(f"【描述符】设置 {instance.name} 的底薪为: {value}")
def __delete__(self, instance):
if (instance, type(instance)) in self._values:
del self._values[(instance, type(instance))]
print(f"【描述符】已删除 {instance.name} 的底薪")
else:
print(f"【描述符】{instance.name} 无底薪可删除")
def __set_name__(self, owner, name):
# 这个方法在类创建时被自动调用,用于获取属性名
# 它不是必需的,但能让你的描述符更智能
self.name = name
print(f"【描述符】已绑定到属性 '{name}'")
关键细节与实操要点:
__set_name__是描述符的“自我认知”:当Python解释器扫描class SalesPerson:类定义时,一旦发现salary = CommissionDescriptor(),就会立即调用CommissionDescriptor.__set_name__(commission_desc, SalesPerson, 'salary')。这让你的描述符能知道自己被绑定在哪个类的哪个属性上,从而可以做更精细的元编程(例如,根据属性名动态调整佣金率)。self._values字典是描述符的“灵魂”:它解决了描述符状态存储的核心难题。__get__和__set__方法接收instance参数,但描述符本身是类级别的共享对象。self._values以(instance, owner)元组为键,确保每个SalesPerson实例都有自己独立的base_salary存储空间。这是描述符区别于普通类属性的关键——它实现了“每个实例一份数据”的语义。instance is None是区分“类访问”与“实例访问”的开关:当代码是SalesPerson.salary时,instance为None,此时你应该返回描述符自身(self),以便支持SalesPerson.salary.commission_rate = 0.1这样的操作。当代码是emp.salary时,instance是具体的SalesPerson对象,此时你才进行真正的数据读取和计算。- 描述符的优先级高于实例字典:这是Python属性查找链(MRO)的铁律。当你访问
emp.salary时,Python首先检查emp.__dict__,如果没有,再检查type(emp).__dict__,如果在那里找到了一个描述符对象(即salary = CommissionDescriptor()),则立即调用该描述符的__get__方法,而不会继续向下查找。这意味着,即使你在emp.__dict__中手动设置了emp.salary = 10000,只要SalesPerson类中定义了salary描述符,emp.salary的访问仍然会走描述符逻辑,emp.__dict__中的salary键会被完全忽略。
注意:在
week_02/02_descriptor_with_comission.py中,务必尝试emp.__dict__和SalesPerson.__dict__的对比。你会发现emp.__dict__里没有salary,而SalesPerson.__dict__里有一个CommissionDescriptor实例。这个对比,是理解描述符工作原理的“顿悟时刻”。
3.3 装饰器封装:从语法糖到数据流管道的质变
装饰器(Decorator)常被误解为“给函数加点料”的语法糖。但在这个包里,week_03/02_to_json_decorator.py 将它升华为一个强大的“数据流管道”。@to_json 不仅能处理单个对象,还能递归处理嵌套结构,并优雅处理不可序列化类型。
其核心实现是一个类装饰器(Class-based Decorator),因为它需要维护状态(如 default_handler):
import json
from datetime import datetime
from typing import Any, Callable, Optional
class ToJsonDecorator:
def __init__(self, default_handler: Optional[Callable[[Any], Any]] = None):
self.default_handler = default_handler or self._default_handler
def _default_handler(self, obj: Any) -> Any:
"""默认处理器:将不可序列化对象转为字符串"""
if isinstance(obj, datetime):
return obj.isoformat()
elif callable(obj):
return f"<function {obj.__name__}>"
else:
return str(obj)
def __call__(self, func: Callable) -> Callable:
def wrapper(*args, **kwargs):
# 1. 执行原函数,获取返回值
result = func(*args, **kwargs)
# 2. 尝试将结果转为字典(如果它是对象)
if hasattr(result, '__dict__'):
dict_result = result.__dict__.copy()
# 3. 递归处理字典中的每个值
dict_result = self._recursive_serialize(dict_result)
# 4. 序列化为JSON字符串
try:
json_str = json.dumps(dict_result, ensure_ascii=False, indent=2)
print("【装饰器】序列化成功:")
print(json_str)
return json_str
except TypeError as e:
print(f"【装饰器】JSON序列化失败: {e}")
# 5. 如果失败,尝试用默认处理器重试
dict_result = self._recursive_serialize(dict_result, use_default=True)
json_str = json.dumps(dict_result, ensure_ascii=False, indent=2)
print("【装饰器】使用默认处理器后序列化成功:")
print(json_str)
return json_str
else:
# 如果返回值不是对象,直接序列化
return json.dumps(result, ensure_ascii=False, indent=2)
return wrapper
def _recursive_serialize(self, obj: Any, use_default: bool = False) -> Any:
"""递归序列化对象,处理 list, dict, object"""
if isinstance(obj, dict):
return {k: self._recursive_serialize(v, use_default) for k, v in obj.items()}
elif isinstance(obj, list):
return [self._recursive_serialize(v, use_default) for v in obj]
elif hasattr(obj, '__dict__'):
# 对象,递归处理其 __dict__
return self._recursive_serialize(obj.__dict__, use_default)
else:
# 基础类型或不可序列化类型
if use_default:
return self.default_handler(obj)
else:
return obj
# 创建装饰器实例
to_json = ToJsonDecorator()
关键细节与实操要点:
- 类装饰器优于函数装饰器,因为它可配置:
ToJsonDecorator(default_handler=my_handler)允许你传入自定义的default_handler,这在处理特定业务对象(如Decimal、UUID)时至关重要。函数装饰器无法轻易实现这种灵活性。 _recursive_serialize是装饰器的“心脏”:它不是一个简单的json.dumps包装,而是一个深度优先的递归遍历器。它能识别list、dict、自定义对象(通过hasattr(obj, '__dict__')),并逐层展开。当你看到Employee对象里嵌套着list[Project],而Project又包含datetime字段时,正是这个递归函数在幕后一层层剥开洋葱。- 双重序列化策略是工程实践的精髓:第一次尝试
json.dumps,如果失败(TypeError),捕获异常,然后启用use_default=True参数,再次调用_recursive_serialize,这次强制所有不可序列化对象都走default_handler。这种“先尝试,再兜底”的策略,保证了装饰器的鲁棒性,是生产环境代码的标配。 - 装饰器的
__call__方法是“拦截器”:它完全掌控了函数的执行流程:result = func(*args, **kwargs)是原函数执行,json.dumps(...)是后处理。你可以在result之后、json.dumps之前插入任意逻辑,比如日志记录、性能统计、结果缓存。这使得装饰器成为AOP(面向切面编程)的天然载体。
提示:在
week_03/02_to_json_decorator.py中,务必尝试给Employee类添加一个lambda属性:emp.process = lambda x: x*2。运行装饰器后,你会看到控制台输出<function <lambda>>。这个实验直观地展示了装饰器如何“驯服”不可序列化对象,而不是让它直接崩溃。
3.4 类继承建模:不是代码复用,而是契约与责任的精确传递
week_04/02_classes_and_inheritance.py 是整个包的集大成者。它要求你设计 Employee、Manager、Intern 的继承体系,但这绝非简单的 class Manager(Employee)。其核心在于“契约”(Contract)与“责任”(Responsibility)的精确划分。
一个健壮的继承设计,必须回答三个问题:
-
基类
Employee应该承诺什么?
它承诺提供name,id,salary这三个公共接口,并定义一个抽象方法calculate_annual_cost(),强制所有子类实现自己的成本计算逻辑。Employee本身不提供具体实现,因为它不知道Manager的奖金怎么算,也不知道Intern的津贴怎么发。 -
子类
Manager应该承担什么责任?
它的责任是:在Employee的salary基础上,根据team_size和bonus_rate,计算出一个更高的年度成本。它必须重写calculate_annual_cost(),并且可以安全地调用super().salary来获取父类的薪资。 -
子类
Intern应该承担什么责任?
它的责任是:完全脱离Employee的salary模型,使用stipend(津贴)作为自己的核心属性,并据此计算年度成本。它也可以选择不继承salary,而是将其设为@property并返回0,以明确表示“实习生没有工资”。
以下是符合上述原则的实现:
from abc import ABC, abstractmethod
class Employee(ABC):
def __init__(self, name: str, emp_id: int):
self.name = name
self.emp_id = emp_id
# salary 是一个受保护的属性,子类可以访问,但外部不应直接修改
self._salary = 0.0
@property
def salary(self) -> float:
return self._salary
@salary.setter
def salary(self, value: float):
if value < 0:
raise ValueError("薪资不能为负数")
self._salary = value
@abstractmethod
def calculate_annual_cost(self) -> float:
"""计算该员工的年度总成本。这是一个抽象方法,必须由子类实现。"""
pass
class Manager(Employee):
def __init__(self, name: str, emp_id: int, team_size: int, bonus_rate: float = 0.1):
super().__init__(name, emp_id)
self.team_size = team_size
self.bonus_rate = bonus_rate
def calculate_annual_cost(self) -> float:
# Manager的成本 = 基本薪资 + 团队规模 * 奖金系数 * 1000
base_cost = self.salary * 12
bonus_cost = self.team_size * self.bonus_rate * 1000
return base_cost + bonus_cost
class Intern(Employee):
def __init__(self, name: str, emp_id: int, university: str, stipend: float):
super().__init__(name, emp_id)
self.university = university
self._stipend = stipend # 津贴是Intern的核心
@property
def salary(self) -> float:
"""重写salary属性,明确表示Intern没有工资"""
return 0.0
@property
def stipend(self) -> float:
return self._stipend
@stipend.setter
def stipend(self, value: float):
if value < 0:
raise ValueError("津贴不能为负数")
self._stipend = value
def calculate_annual_cost(self) -> float:
# Intern的成本 = 津贴 * 12
return self.stipend * 12
# 多态函数:接受任何Employee子类
def calculate_total_cost(employees: list[Employee]) -> float:
"""计算一组员工的总年度成本。多态性的体现。"""
total = 0.0
for emp in employees:
# 无论emp是Manager还是Intern,这里都会自动调用其各自的calculate_annual_cost()
total += emp.calculate_annual_cost()
return total
# 使用示例
if __name__ == "__main__":
manager = Manager("张经理", 101, team_size=5, bonus_rate=0.15)
manager.salary = 15000 # 设置基本薪资
intern = Intern("李实习生", 201, "清华大学", stipend=3000)
employees = [manager, intern]
print(f"总年度成本: {calculate_total_cost(employees)} 元")
关键细节与实操要点:
@abstractmethod是契约的法律文书:Employee类被标记为ABC(Abstract Base Class),其calculate_annual_cost方法被标记为@abstractmethod。这意味着,任何继承Employee的类,如果未实现calculate_annual_cost,在实例化时就会抛出TypeError: Can't instantiate abstract class ... with abstract method calculate_annual_cost。这是一种编译期检查,比运行时NotImplementedError更早、更严厉地保障了契约。super().__init__()是责任的交接仪式:在Manager.__init__()中,super().__init__(name, emp_id)不仅是调用父类构造函数,更是将name和emp_id的初始化责任,正式移交给Employee类。Manager只负责自己特有的team_size和bonus_rate。这种清晰的责任划分,是大型项目可维护性的基石。- 属性重写(
@property)是语义的精确表达:Intern.salary被重写为return 0.0,这不仅仅是技术实现,更是业务语义的声明——“实习生没有工资,只有津贴”。这比在文档里写“实习生的salary属性无效”要有力得多。 - 多态函数
calculate_total_cost是设计的终极验证:它只依赖Employee这个抽象类型,不关心具体是Manager还是Intern。当你把manager和intern都放进employees列表,calculate_total_cost函数能正确调用各自的方法,这证明了你的继承体系是成功的。如果它调用错了,或者报错了,那一定是你的契约或责任定义出了问题。
注意:在
week_04/02_classes_and_inheritance.py中,务必尝试删除Intern类中的calculate_annual_cost方法,然后运行。你会立刻看到TypeError。这个错误不是bug,而是设计的胜利——它在第一时间告诉你:“嘿,你忘了履行契约!”
4. 实操过程与核心环节实现:从零开始,一步步搭建你的第一个魔法方法类
4.1 第一步:动手实现 FileReader —— 你的第一个上下文管理器
让我们从 week_01/01_file_reader_class.py 开始,亲手搭建一个真正可用的 FileReader。这不是复制粘贴,而是每一步都带着思考。
第一步:创建骨架文件
新建一个文件 file_reader_demo.py,写下最简骨架:
class FileReader:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
pass
def __exit__(self, exc_type, exc_value, traceback):
pass
# 测试代码
if __name__ == "__main__":
with FileReader("test.txt") as f:
print("Hello, World!")
运行它,你会得到 AttributeError: __enter__。这很正常,因为 __enter__ 还没返回任何东西。
第二步:实现 __enter__,并处理文件打开
现在,让 __enter__ 真正打开文件:
class FileReader:
def __init__(self, filename):
self.filename = filename
self.file = None # 添加一个实例变量来保存文件句柄
def __enter__(self):
print(f"正在打开文件 {self.filename}...")
self.file = open(self.filename, 'r', encoding='utf-8')
return self.file # 返回文件对象,供 with as 使用
def __exit__(self, exc_type, exc_value, traceback):
print(f"正在关闭文件 {self.filename}...")
if self.file:
self.file.close()
return False # 不抑制异常
# 测试代码
if __name__ == "__main__":
# 先创建一个测试文件
with open("test.txt", "w", encoding="utf-8") as f:
f.write("这是测试内容。\n第二行。")
# 现在读取它
with FileReader("test.txt") as f:
content = f.read()
print("文件内容:", content)
运行这段代码,你会看到:
正在打开文件 test.txt...
文件内容: 这是测试内容。
第二行。
正在关闭文件 test.txt...
第三步:加入异常处理,让 __exit__ 更聪明
现在,我们故意制造一个错误,看看 __exit__ 如何应对:
# 在测试代码中,修改为:
if __name__ == "__main__":
with FileReader("nonexistent.txt") as f: # 这个文件不存在!
content = f.read()
print("文件内容:", content)
运行,你会看到:
正在打开文件 nonexistent.txt...
正在关闭文件 nonexistent.txt...
Traceback (most recent call last):
File ".../file_reader_demo.py", line 25, in <module>
with FileReader("nonexistent.txt") as f:
File ".../file_reader_demo.py", line 8, in __enter__
self.file = open(self.filename, 'r', encoding='utf-8')
FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent.txt'
__exit__ 被调用了(所以看到了“正在关闭文件”),但异常还是抛出来了。这是正确的,因为我们返回了 False。如果你想让 FileReader 自动处理文件不存在的错误,可以这样改:
def __exit__(self, exc_type, exc_value, traceback):
print(f"正在关闭文件 {self.filename}...")
if self.file:
self.file.close()
# 如果是 FileNotFoundError,我们自行处理,不向上抛
if exc_type is FileNotFoundError:
print(f"警告:文件 {self.filename} 不存在,已忽略。")
return True # 抑制异常
return False # 其他异常继续抛出
第四步:最终版,支持写模式和多种编码
一个工业级的 FileReader 应该更灵活:
class FileReader:
def __init__(self, filename, mode='r', encoding='utf-8'):
self.filename = filename
self.mode = mode
self.encoding = encoding
self.file = None
def __enter__(self):
print(f"正在以模式 '{self.mode}' 打开文件 {self.filename}...")
try:
self.file = open(self.filename, self.mode, encoding=self.encoding)
except UnicodeDecodeError:
# 如果UTF-8失败,尝试gbk
print(f"UTF-8解码失败,尝试gbk编码...")
self.file = open(self.filename, self.mode, encoding='gbk')
return self.file
def __exit__(self, exc_type, exc_value, traceback):
print(f"正在关闭文件 {self.filename}...")
if self.file:
self.file.close()
# 抑制特定的IO错误,其他错误照常抛出
if exc_type in (FileNotFoundError, PermissionError):
print(f"已捕获并忽略 {exc_type.__name__}: {exc_value}")
return True
return False
# 测试写入
if __name__ == "__main__":
with FileReader("output.txt", "w") as f:
f.write("你好,世界!\n")
print("写入完成。")
这个过程,就是从一个空骨架,到一个可运行、可调试、可扩展的完整类的全过程。每一步的 print 语句,都是你与Python解释器之间的对话,让你清晰地看到控制流是如何在 with 语句中穿梭的。
4.2 第二步:深入 KeyValueStorage —— 构建你的第一个魔法方法集合
week_02/01_key_value_storage.py 是理解魔法方法组合拳的最佳战场。我们将从零开始,逐步添加魔法方法。
第一步:创建基础类,只实现 __getitem__ 和 __setitem__
class KeyValueStorage:
def __init__(self):
self._data = {}
def __getitem__(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = value
# 测试
if __name__ == "__main__":
storage = KeyValueStorage()
storage['name'] = 'Alice'
storage['age'] = 30
print(storage['name']) # Alice
print(storage['age']) # 30
运行正常。现在,尝试 print('name' in storage),会报错 TypeError: argument of type 'KeyValueStorage' is not iterable。这是因为 in 操作符需要 __contains__。
第二步:添加 __contains__ 和 __len__
class KeyValueStorage:
def __init__(self):
self._data = {}
def __getitem__(self, key):
print(f"[GET] 获取 '{key}'")
return self._data[key]
def __setitem__(self, key, value):
print(f"[SET] 设置 '{key}' = '{value}'")
self._data[key] = value
def __contains__(self, key):
print(f"[IN] 检查 '{key}' 是否存在")
return key in self._data
def __len__(self):
print("[LEN] 查询长度")
return len(self._data)
# 测试
if __name__ == "__main__":
storage = KeyValueStorage()
storage['name'] = 'Alice'
print('name' in storage) # True
print(len(storage)) # 1
第三步:添加 __iter__ 和 __repr__,让它更像一个真正的容器
class KeyValueStorage:
def __init__(self):
self._data = {}
def __getitem__(self, key):
print(f"[GET] 获取 '{key}'")
return self._data[key]
def __setitem__(self, key, value):
print(f"[SET] 设置 '{key}' = '{value}'")
self._data[key] = value
def __contains__(self, key):
print(f"[IN] 检查 '{key}' 是否存在")
return key in self._data
def __len__(self):
print("[LEN] 查询长度")
return len(self._data)
def __iter__(self):
print("[ITER] 开始迭代")
return iter(self._data) # 返回一个迭代器
def __repr__(self):
print("[REPR] 生成字符串表示")
return f"KeyValueStorage({self._data})"
# 测试迭代
if __name__ == "__main__":
storage = KeyValueStorage()
storage['a'] = 1
storage['b'] = 2
print("所有键:")
for key in storage:
print(key)
print("字符串表示:", repr(storage))
运行后,你会看到 for key in storage: 触发了 [ITER],而 repr(storage) 触发了 [REPR]。__repr__ 方法的返回值,就是你在Python shell中输入 storage 后看到的内容。
第四步:终极挑战——添加 __delitem__ 和 __eq__
class KeyValueStorage:
# ... 其他方法保持不变 ...
def __delitem__(self, key):
print(f"[DEL] 删除 '{key}'")
del self._data[key]
def __eq__(self, other):
print(f"[EQ] 比较 self 与 {other}")
if not isinstance(other, KeyValueStorage):
return False
return self._data == other._data
# 测试
if __name__ == "__main__":
s1 = KeyValueStorage()
s1['x'] = 10
s2 = KeyValueStorage()
s2['x'] = 10
print(s1 == s2) # True
del s1['x']
print(len(s1)) # 0
这个过程,让你亲手构建了一个拥有完整容器协议(Container Protocol)的类。你不再是一个魔法方法的消费者,而是一个创造者。
4.3 第三步:攻克 CommissionDescriptor —— 亲手编写你的第一个描述符
week_02/02_descriptor_with_comission.py 是最难啃的骨头,但也是收获最大的一块。我们分步实现。
第一步:创建最简描述符,只实现 __get__
class SimpleDescriptor:
def __get__(self, instance, owner):
if instance is None:
return self
return "Hello from Descriptor!"
class TestClass:
attr = SimpleDescriptor()
# 测试
if __name__ == "__main__":
obj = TestClass()
print(obj.attr) # Hello from Descriptor!
print(TestClass.attr) # <__main__.SimpleDescriptor object at 0x...>
第二步:加入状态存储,实现 __set__
class StatefulDescriptor:
def __init__(self):
self._values = {}
def __get__(self, instance, owner):
if instance is None:
return self
# 使用 (instance, owner) 作为键
key = (instance, owner)
return self._values.get(key, "No value set")
def __set__(self, instance, value):
key = (instance, owner)
self._values[key] = value
class TestClass:
attr = StatefulDescriptor()
# 测试
if __name__ == "__main__":
obj1 = TestClass()
obj2 = TestClass()
obj1.attr = "Value for obj1"
obj2.attr = "Value for obj2"
print(obj1.attr) # Value for obj1
print(obj2.attr) # Value for obj2
第三步:实现 CommissionDescriptor,加入业务逻辑
class CommissionDescriptor:
def __init__(self, commission_rate=0.05):
self.commission_rate = commission_rate
self._values = {}
def __get__(self, instance, owner):
if instance is None:
return self
base_salary = self._values.get((instance, owner), 0)
return base_salary * (1 + self.commission_rate)
def __set__(self, instance, value):
if not isinstance(value, (int, float)) or value < 0:
raise ValueError(f"底薪必须是非负数字,当前值: {value}")
self._values[(instance, owner)] = float(value)
class SalesPerson:
salary = CommissionDescriptor(commission_rate=0.05)
# 测试
if __name__ == "__main__":
emp = SalesPerson()
emp.salary = 10000
print(emp.salary) # 10500.0
第四步:添加 __delete__ 和 __set_name__,让它更专业
class CommissionDescriptor:
def __init__(self, commission_rate=0.05):
self.commission_rate = commission_rate
self._values = {}
self.name = None # 初始化属性名
def __set_name__(self, owner, name):
self.name = name
print(f"描述符已绑定到 {owner.__name__}.{name}")
def __get__(self, instance, owner):
if instance is None:
return self
base_salary = self._values.get((instance, owner), 0)
return base_salary * (1 + self.commission_rate)
def __set__(self, instance, value):
if not isinstance(value, (int, float)) or value < 0:
raise ValueError(f"{self.name} 必须是非负数字,当前值: {value}")
self._values[(instance, owner)] = float(value)
def __delete__(self, instance):
key = (instance, type(instance))
if key in self._values:
del self._values[key]
print(f"已删除 {instance.__class__.__name__}.{self.name}")
class SalesPerson:
salary = CommissionDescriptor(commission_rate=0.05)
# 测试
if __name__ == "__main__":
emp = SalesPerson()
emp.salary = 10000
print(emp.salary)
del emp.salary
运行这段代码,你会看到 描述符已绑定到 SalesPerson.salary 的输出。这证明了 __set_name__ 已经生效。del emp.salary 会触发 __delete__,并打印出删除信息。
4.4 第四步:封装 @to_json 装饰器 —— 从函数到数据流管道
week_03/02_to_json_decorator.py 的实现,是将前面所有知识融会贯通的过程。
第一步:创建一个最简函数装饰器
def to_json(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
import json
return json.dumps(result, ensure_ascii=False, indent=2)
return wrapper
@to_json
def get_employee():
return {"name": "Alice", "age": 30}
print(get_employee())
第二步:升级为类装饰器,支持配置
import json
class ToJsonDecorator:
def __init__(self, indent=2, ensure_ascii=False):
self.indent = indent
self.ensure_ascii = ensure_ascii
def __call__(self, func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return json.dumps(result, indent=self.indent, ensure_ascii=self.ensure_ascii)
return wrapper
to_json = ToJsonDecorator(indent=4)
第三步:加入递归序列化和错误处理
import json
from datetime import datetime
class ToJsonDecorator:
def __init__(self, default_handler=None):
self.default_handler = default_handler or self._default_handler
def _default_handler(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
return str(obj)
def _recursive_serialize(self, obj):
if isinstance(obj, dict):
return {k: self._recursive_serialize(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [self._recursive_serialize(v) for v in obj]
elif hasattr(obj, '__dict__'):
return self._recursive_serialize(obj.__dict__)
else:
return obj
def __call__(self, func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
try:
# 尝试直接序列化
serialized = self._recursive_serialize(result)
return json.dumps(serialized, ensure_ascii=False, indent=2)
except Exception as e:
# 失败则用默认处理器重试
serialized = self._recursive_serialize(result, use_default=True)
return json.dumps(serialized, ensure_ascii=False, indent=2)
return wrapper
to_json = ToJsonDecorator()
第四步:最终版,支持 use_default 参数
import json
from datetime import datetime
class ToJsonDecorator:
def __init__(self, default_handler=None):
self.default_handler = default_handler or self._default_handler
def _default_handler(self, obj):
if isinstance(obj, datetime):
return obj.isoformat()
elif callable(obj):
return f"<function {obj.__name__}>"
else:
return str(obj)
def _recursive_serialize(self, obj, use_default=False):
if isinstance(obj, dict):
return {k: self._recursive_serialize(v, use_default) for k, v in obj.items()}
elif isinstance(obj, list):
return [self._recursive_serialize(v, use_default) for v in obj]
elif hasattr(obj, '__dict__'):
return self._recursive_serialize(obj.__dict__, use_default)
else:
if use_default:
return self.default_handler(obj)
else:
return obj
def __call__(self, func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
try:
serialized = self._recursive_serialize(result)
return json.dumps(serialized, ensure_ascii=False, indent=2)
except (TypeError, ValueError) as e:
print(f"首次序列化失败: {e}, 启用默认处理器...")
serialized = self._recursive_serialize(result, use_default=True)
return json.dumps(serialized, ensure_ascii=False, indent=2)
return wrapper
to_json = ToJsonDecorator()
这个装饰器,现在可以处理任何复杂的嵌套对象,包括 datetime、lambda、自定义类,以及它们的任意组合。它不再是一个简单的“语法糖”,而是一个健壮的、可配置的、生产就绪的数据序列化管道。
5. 常见问题与排查技巧实录:那些只有亲手踩过才知道的坑
5.1 魔法方法常见陷阱与解决方案
问题1:__len__ 返回了浮点数,导致 for 循环报错
现象:在 KeyValueStorage 中,__len__ 方法不小心写了 return len(self._data) / 1.0,运行 for k in storage: 时,报错 TypeError: 'float' object cannot be interpreted as an integer。
原因:for 循环内部会调用 range(len(obj)),而 range() 只接受整数。__len__ 的返回值必须是 int 类型。
解决方案:在 __len__ 方法中,强制转换:return int(len(self._data)),或者直接确保 len() 的返回值是整数(它本来就是)。
问题2:__enter__ 返回了 None,导致 with as 绑定失败
现象:FileReader.__enter__ 方法里忘了 return self.file,只写了 open(...),运行 with FileReader(...) as f: 时,f 的值是 None,后续 f.read() 报错 AttributeError: 'NoneType' object has no attribute 'read'。
原因:with 语句的 as 子句,绑定的就是 __enter__ 的返回值。如果 __enter__ 没有 return 语句,Python默认返回 None。
解决方案:在 __enter__ 的最后,务必 return 一个有意义的对象。如果是想让 as 绑定类实例本身,就 return self。
问题3:__exit__ 返回了 True,却意外抑制了重要异常
现象:在 __exit__ 中写了 return True,结果 ValueError 被静默吞掉,程序看似“正常”运行,但逻辑已经出错。
原因:return True 是一个全局开关,它会抑制所有类型的异常,无论是否是你想处理的。
解决方案:永远只在 __exit__ 中对特定、已知、可安全忽略的异常返回 True。通用模式是:
def __exit__(self, exc_type, exc_value, traceback):
self.close()
if exc_type is MyExpectedError:
return True # 只抑制这一种
return False # 其他都抛出
5.2 描述符常见陷阱与解决方案
问题1:描述符状态存储在实例上,导致所有实例共享
现象:CommissionDescriptor 的 self._values = {} 写在 __init__ 里,结果 emp1.salary 和 emp2.salary 的值互相影响。
原因:self._values 是描述符实例的属性,而描述符实例是类级别共享的。所有 SalesPerson 实例都共享同一个 CommissionDescriptor 对象,因此也共享同一个 _values 字典。
解决方案:必须使用 (instance, owner) 元组作为字典的键,确保每个实例的数据是隔离的。这是描述符的黄金法则。
问题2:__get__ 中 instance is None 判断缺失,导致类访问时报错
现象:SalesPerson.salary 报错 AttributeError: 'NoneType' object has no attribute '...'。
原因:__get__ 方法中,当 instance 为 None 时,你试图访问 instance.name 或 instance._salary,这当然会失败。
解决方案:在 __get__ 开头,必须加上 if instance is None: return self 的判断。这是描述符的“守门员”。
问题3:__set_name__ 没有被调用,self.name 一直是 None
现象:在 __set__ 方法中打印 self.name,结果是 None。
原因:__set_name__ 只在类定义完成时被调用。如果你在类定义之后,动态地给类添加属性(如 TestClass.new_attr = Descriptor()),__set_name__ 不会被触发。
解决方案:确保描述符是作为类的类变量(class variable)在类定义体内部声明的。这是唯一能触发 __set_name__ 的方式。
5.3 装饰器常见陷阱与解决方案
问题1:装饰器丢失了原函数的 __name__ 和 __doc__
现象:@to_json 装饰一个函数后,help(my_func) 显示的是 wrapper 的帮助信息,而不是原函数的。
原因:wrapper 函数覆盖了原函数的元信息。
解决方案:使用 functools.wraps。在 wrapper 函数定义前加上 @functools.wraps(func)。这是装饰器开发的强制规范。
问题2:递归序列化陷入无限循环
现象:Employee 类中,self.manager = self(自己管理自己),导致 _recursive_serialize 无限递归,最终 RecursionError。
原因:递归函数没有处理循环引用(circular reference)。
解决方案:在 _recursive_serialize 中,维护一个 seen_ids 集合,记录已经序列化过的对象ID。每次进入函数时,先检查 id(obj) 是否在 seen_ids 中,如果是,就返回一个占位符(如 "<circular reference>")。
问题3:@to_json 装饰器无法处理 namedtuple
现象:Employee 是一个 namedtuple,@to_json 报错 TypeError: Object of type Employee is not JSON serializable。
原因:namedtuple 没有 __dict__,hasattr(obj, '__dict__') 返回 False,导致它被当作基础类型处理,而 namedtuple 本身不可序列化。
解决方案:在 _recursive_serialize 中,增加对 collections.namedtuple 的特殊处理:
from collections import namedtuple
# ...
elif isinstance(obj, tuple) and hasattr(obj, '_fields'): # 检查是否为 namedtuple
return {field: self._recursive_serialize(getattr(obj, field)) for field in obj._fields}
5.4 类继承常见陷阱与解决方案
问题1:子类 __init__ 中忘记调用 super().__init__(),导致基类属性未初始化
现象:Manager 实例的 name 和 emp_id 属性是 None。
原因:Manager.__init__() 中没有调用 super().__init__(name, emp_id),导致 Employee.__init__() 没有被执行。
解决方案:在子类 __init__ 的开头,第一行就写 super().__init__(...)。这是OOP的铁律。
问题2:@property 的 setter 和 getter 名称不一致
现象:@salary.setter 修饰的方法名为 def set_salary(self, value):,运行时报错 AttributeError: can't set attribute。
原因:@property 的 setter 方法名,必须和 getter 方法名完全一致。
解决方案:@salary.setter 对应的 setter 方法,必须命名为 def salary(self, value):。
问题3:isinstance(obj, Employee) 返回 False,尽管 obj 是 Manager 的实例
现象:calculate_total_cost([manager]) 中,isinstance(emp, Employee) 为 False。
原因:Manager 类没有正确继承 Employee,可能是 class Manager: 而不是 class Manager(Employee):,或者继承了错误的类名。
解决方案:仔细检查类定义的冒号后部分,确保 class Manager(Employee):。这是最基础、也最容易疏忽的错误。
5.5 综合排查技巧:一份“问题-现象-根源-修复”的速查表
| 问题现象 | 可能根源 | 快速修复方案 |
|---|---|---|
with 语句内 as 变量为 None | __enter__ 方法没有 return 语句 | 在 __enter__ 最后一行添加 return self 或 return self.file |
for item in obj: 报错 TypeError: 'X' object is not iterable | 缺少 __iter__ 方法,或 __iter__ 返回了错误类型 | 实现 __iter__(self): return iter(self._data) |
emp.salary 报错 AttributeError: 'NoneType' object has no attribute 'salary' | __get__ 中未处理 instance is None | 在 __get__ 开头添加 if instance is None: return self |
@to_json 装饰器对 datetime 对象序列化失败 | _recursive_serialize 未处理 datetime 类型 | 在 _recursive_serialize 的 else 分支前,添加 elif isinstance(obj, datetime): return obj.isoformat() |
Manager 实例的 name 属性为 None | Manager.__init__() 中未调用 super().__init__() | 在 Manager.__init__() 的第一行添加 super().__init__(name, emp_id) |
isinstance(manager, Employee) 返回 False | Manager 类未正确继承 Employee | 检查类定义:class Manager(Employee): |
提示:在调试任何问题时,第一个动作永远是添加
__enter__,__get__,__set__,wrapper等所有关键方法的开头,都加上print(f"DEBUG: entering {method_name}")。这些打印信息,是你理解Python解释器执行路径的唯一可靠指南针。不要试图靠猜,要靠看。
6. 实操心得与经验总结:一个资深Pythoner的肺腑之言
写完这几千行代码,调试过上百次 RecursionError 和 AttributeError,我想分享的,不是技术细节,而是那些只有在深夜对着终端发呆时才会浮现的、关于Python本质的体会。
第一,Python的“魔法”,从来不是为了炫技,而是为了消除冗余。
你可能会觉得 __enter__ 和 __exit__ 很“魔法”,但它的存在,就是为了让你不用再写 try...finally 的样板代码。@property 很“魔法”,但它存在的意义,是让你能把一个简单的属性访问,无缝升级为一个带有校验和计算的复杂逻辑,而所有调用它的代码,都不需要做任何修改。这个包里的每一个魔法方法、每一个描述符、每一个装饰器,其终极目标,都是为了让“使用者”的代码变得更简洁、更安全、更不易出错。所以,当你在实现一个描述符时,别问“我该怎么写 __get__”,而要问“我怎样才能让 emp.salary 这个简单的访问,变得既安全又智能?”
第二,面向对象设计的成败,不在于类画得有多漂亮,而在于“替换原则”是否成立。
week_04 的 calculate_total_cost 函数,是我认为整个包里最有价值的一行代码。它只认 Employee 这个抽象类型,却能完美处理 Manager 和 Intern。这背后,是 Manager 和 Intern 都严格遵守了 Employee 的契约:它们都有 name,都能计算 calculate_annual_cost()。在真实的工程中,你写的每一个 if isinstance(obj, SomeClass),都是设计失败的警报。一个健康的系统,应该是“多态的”,是“可插拔的”。当你设计一个 PaymentProcessor 接口时,CreditCardProcessor 和 PayPalProcessor 应该能被同一个 process_payment() 函数毫无障碍地调用。这个包强迫你去思考:我的 Manager,真的能“替代”一个 Employee 吗?它的行为,是否会让依赖 Employee 的所有现有代码,继续正确运行?这个问题的答案,决定了你的代码是“玩具”,还是“产品”。
第三,调试是学习的最高形式,而错误信息是Python给你的最好礼物。
新手怕报错,高手爱报错。AttributeError: 'NoneType' object has no attribute 'salary' 这条信息,比一百页文档都更有价值。它精准地告诉你:某个地方,你期望一个对象,但它却是 None。顺着这个线索,你一定能找到 __enter__ 忘了 return,或者 __get__ 忘了处理 instance is None。这个包的设计,就是把最常见的错误场景,都预埋好了。当你运行 week_02/02_descriptor_with_comission.py 并故意删掉 __set__ 方法,然后尝试 emp.base_salary = -5000,看到程序没有报错而是默默接受了负数时,那一刻的震撼,会让你终生记住“描述符的 __set__ 是唯一的校验入口”。所以,不要急于修复错误,先花一分钟,读懂错误信息的每一个单词。它不是障碍,而是Python在手把手教你,它的世界是如何运转的。
最后,也是最重要的:不要追求“一次性写对”,要追求“快速迭代验证”。
这个包的每一个 week,都不是一个需要你闭关三天写完的“大作业”。它是一个个微小的、可立即运行的实验。01_digits_sum 可以在5分钟内写完并看到结果;02_draw_stairs 的第一版可能只画出了一行星号,但没关系,你立刻就能看到效果,并基于它去迭代。这种“写一行,跑一次,看一眼”的节奏,是掌握任何编程概念的最高效方式。与其花两小时构思一个完美的 KeyValueStorage,不如花五分钟写一个只能 __setitem__ 的版本,跑起来,看到 storage['key'] = 'value' 成功了,再花五分钟加上 __getitem__,再跑……这种渐进式的、反馈密集的学习,会让你的大脑建立起牢固的神经连接,而不是一堆飘在空中的概念。
这个包,不是终点,而是一把钥匙。它为你打开了Python对象模型的大门,让你得以窥见 for, in, with, @ 这些日常语法糖之下,那个精密、优雅、充满可能性的世界。当你下次再看到 pandas.DataFrame 的链式调用,或是 requests.Session 的上下文管理时,你不会再觉得它们是黑魔法。你会微笑,因为你亲手造过它们的简化版。而这,就是进阶的真正含义——从“使用者”,变成“理解者”,最终,成为“创造者”。
简介:专为学完Python基础的学习者设计的实操型练习资源,覆盖对象模型核心机制与常见工程模式。包含自定义文件读取类(利用__enter__/__exit__等魔法方法实现上下文管理)、带佣金逻辑的描述符(控制属性访问并动态计算)、键值对持久化存储(模拟简单数据库操作)、楼梯图形生成(递归与循环结合)、JSON序列化装饰器(自动处理对象转字典再序列化)、二次方程求解(含异常处理与复数支持)、数字各位求和(字符串与数值转换练习)、类与继承关系建模(如员工-经理层级结构)。所有内容按周组织:week_01聚焦工具类与函数式技巧;week_02深入描述符与属性控制机制;week_03强化数学逻辑、异常捕获与数据序列化;week_04提升面向对象设计能力,涵盖抽象、继承与多态实践。每个子目录都是一个独立可运行的小项目,附带README.md说明运行方式、输入输出示例及关键知识点提示。适合边写边学,通过调试真实代码理解Python底层行为与工程落地细节。

6424

被折叠的 条评论
为什么被折叠?



