先说个真实感受Python我用了好几年写业务代码、写脚本、做数据清洗都没问题但真正对面向对象编程产生“原来如此”的顿悟还是在系统翻完《Python3 面向对象编程第三版》之后。网上聊Python OOP的文章不少但大多停留在“类怎么定义”“继承怎么写”的语法层面很少讲清楚这些语法背后的设计意图。这篇我打算把自己重读这本书第一部分的收获整理出来结合一些实际项目里踩过的坑聊聊类与对象、属性封装、继承和组合、魔术方法这些核心机制到底应该怎么理解、怎么用。如果你正处于“会用class但没吃透OOP”的阶段或者想从非面向对象语言转过来、想知道Python的OOP为什么和别人不一样这篇文章应该能给你一些比官方文档更接地气的参考。1. 为什么一本写于几年前的OOP书放到现在仍有阅读价值1.1 这本书的核心定位与适合读者Dusty Phillips的《Python3 面向对象编程》第三版严格来说不是一本“入门语法书”。它预设读者对Python基础语法已经有了基本认知比如变量、循环、函数、异常处理这些都没问题然后在这个基础上帮你建立起“如何用面向对象的思维方式去分析和建模问题”的能力。书里大量使用贴近真实业务的案例——网络爬虫、数据解析、图形界面、游戏逻辑等每一个案例都有完整代码不是只贴片段这一点非常对我的胃口。我的理解是这本书最典型的读者有两类第一类是像我这样写Python有一段时间了但之前主要是过程式思维函数写了一大堆代码规模上去之后维护成本明显升高想真正掌握OOP来改善代码结构第二类是刚从Java或C过来的人想了解Python这种动态语言在面向对象上到底有哪些不一样的地方——比如没有真正的私有成员、支持鸭子类型、魔术方法体系极其丰富等。1.2 第三版里值得关注的内容更新相比早期版本第三版比较大的变化是加入了不少现代Python特性数据类dataclass、类型注解type hint在面向对象设计中的应用、以及Python 3.7之后的一些新写法。书里的代码实测在Python 3.8、3.9上都能直接跑起来建议读到代码不要光看一定自己敲一遍。有一点需要提前说明这本书的内容组织是“概念驱动”的前面的章节会为后面的案例做铺垫所以如果你有“跳着看”的习惯可能会漏掉一些关键铺垫。我比较推荐的读法是从第一部分开始顺序读但并非说每个字都要读——我对这本书的实际阅读体验是概念部分快速过代码部分慢慢啃练习题部分至少独立做一半。2. 类、实例与构造函数先把最基础的东西掰开揉碎2.1 关于__init__的一个重要事实它不是构造函数很多从Java转过来的朋友会习惯性地把__init__叫做构造函数这个说法在Python里其实是错的。__init__是初始化器真正创建对象的动作发生在对__new__的调用中。__new__是一个类方法负责分配内存并返回一个新的实例而__init__在实例被创建之后被自动调用用来设置初始状态。class Book: def __new__(cls, title, author): print(__new__ 被调用正在创建实例) instance super().__new__(cls) return instance def __init__(self, title, author): print(__init__ 被调用正在初始化实例) self.title title self.author author book Book(Python3 面向对象编程, Dusty Phillips)运行结果会依次打印__new__和__init__的执行顺序。99%的情况下你只需要定义__init__但如果你写过单例模式、不可变对象或者自定义元类就有机会用到__new__。理解两者的区别之后你再看一些开源框架的源码会更容易看懂它们的对象创建流程。class Singleton: _instance None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance super().__new__(cls) return cls._instance def __init__(self, name): self.name name a Singleton(A) b Singleton(B) print(a is b) # True print(a.name, b.name) # B B这个例子稍微演示了__new__的实际使用场景。注意一点如果__new__返回的实例不是cls的实例那么__init__不会被自动调用这是一个容易踩坑的细节。2.2 self到底是什么它为什么必须显式出现Python的实例方法第一个参数必须写成self这与Java/C统一使用this关键字且不用显式声明完全不同。很多初学者对此很困惑为什么我调用book.info()的时候并没有给info传参数但它下面的self却自动绑定了当前实例其实答案隐藏在函数调用的机制里。当你写下book.info()时Python解释器做了一件事它把book自动作为第一个参数传给了info方法。换句话说book.info()等价于Book.info(book)。显式声明self的好处是——方法本质就是一个普通函数它不依赖任何魔法上下文参数传递路径清晰可见。这让Python的方法在需要时可以被单独拿出来当普通函数用也让你能直观看到运行时绑定关系。class Book: def __init__(self, title): self.title title def show(self): print(f书名是{self.title}) book Book(Python3 面向对象编程) Book.show(book) # 等价于 book.show()我自己在实际工作中倒是很少直接这样调用但理解这一点对调试有实际帮助——比如你在代码里看到一个“missing 1 required positional argument: self”报错如果不理解上面的机制就会一头雾水。2.3 三种方法类型实例方法、类方法与静态方法的选择逻辑书中在讲到方法定义的时候花了不小篇幅区分实例方法、类方法和静态方法这一部分我认为是初学者最容易搞混的地方。简单来总结实例方法的第一个参数是self它可以访问和修改实例属性也可以访问类属性是使用频率最高的方法类型。类方法使用classmethod装饰第一个参数是cls它接收的是类本身而不是实例。它主要用于定义“备选构造函数”或操作类级别的状态。静态方法使用staticmethod装饰既不接收self也不接收cls它本质上是一个组织在类命名空间里的普通函数。使用它的理由通常是功能上跟类有逻辑关联但不需要访问类或实例的任何数据。class Date: def __init__(self, year, month, day): self.year year self.month month self.day day classmethod def from_string(cls, date_str): year, month, day map(int, date_str.split(-)) return cls(year, month, day) staticmethod def is_valid_date_str(date_str): parts date_str.split(-) if len(parts) ! 3: return False year, month, day map(int, parts) return 1 month 12 and 1 day 31 d Date.from_string(2024-06-15) print(Date.is_valid_date_str(2024-13-01)) # False这里有一个选型建议如果你不确定某个函数该不该放进类里先问自己三个问题——它是否需要访问实例数据是的话用实例方法它是否需要访问类状态或作为备选构造是的话用类方法两个都不需要那静态方法或者一个独立模块级函数都可以。很多老手倾向于在“纯粹的辅助函数”场景下直接用模块级函数这样职责更清晰代码也更容易测试。3. 属性与封装Python的“没有私有”不是缺陷而是设计哲学3.1 约定优于强制下划线背后的语言态度在Java或C里private、protected这些访问修饰符是语言强制的你写了别人就不能直接访问。但在Python里没有编译器挡在你前面所有的属性本质上都是可以访问的。那怎么表达“这个属性不应该被外部直接改”用命名约定一个下划线前缀如_internal表示“这是内部实现细节外部代码不要直接访问”双下划线前缀且不以双下划线结尾如__private则触发名称改编name mangling实际存储名变成了_ClassName__private。名称改编的实际效果并不是“没法访问”而是“让直接访问变得麻烦”同时在继承场景下避免属性意外覆盖。比如class A: def __init__(self): self.__value 1 def get_value(self): return self.__value class B(A): def __init__(self): super().__init__() self.__value 2 b B() print(b.get_value()) # 1这里B里的__value和A里的__value实际是不同的属性分别被改编为_B__value和_A__value所以get_value读取的仍然是A里的值。如果都用单下划线命名结果就会变成同一个属性被覆盖掉。这个机制在某些框架设计里作用很大但日常业务代码中我的建议是单下划线足够表达语义不要滥用双下划线。3.2 property装饰器把方法伪装成属性让调用方无感面向对象的封装不只是“私有化”更重要的是给你一个可以拦截和加工数据的机会。property装饰器正是为此设计的从外部看你访问的是一个属性从内部看实际上执行的是一段逻辑。class Temperature: def __init__(self, celsius): self._celsius celsius property def celsius(self): return self._celsius celsius.setter def celsius(self, value): if value -273.15: raise ValueError(温度不能低于绝对零度) self._celsius value property def fahrenheit(self): return self._celsius * 9 / 5 32 t Temperature(25) print(t.celsius) # 25 print(t.fahrenheit) # 77.0 t.celsius 30 print(t.fahrenheit) # 86.0我特别想强调一个实际工作里的经验如果你一开始就预见到某个属性在存取时可能需要校验、转换或触发副作用那么直接用property定义是合理的但如果只是存一个普通数据现在就写成property反而会造成冗长}。更好的做法是先从普通属性开始当后续出现校验需求时再改成property因为property对调用方完全透明重构成本很低。3.3 一个值得注意的坑属性名与方法名冲突在使用property时最常见的坑之一是属性名和实例变量名或者方法名冲突。举个例子你定义了一个name属性同时在__init__里用self.name ...如果这两个地方互相干扰程序运行起来会得到非常诡异的结果。更隐蔽的情况是你有一个save方法同时又用property定义了一个save属性那么类定义后边的方法就被覆盖掉了。我的建议是实例变量使用带下划线的内部名如self._name对外暴露的property用公开名property def name。这样视觉上一看就清楚也不容易踩到冲突。很多初学者在书里看到这个模式后不太理解为什么不能直接self.name ...等到真的踩过一次才会明白多写几年代码就会发现内部名加下划线是Python社区普遍的默契。4. 继承、多态与组合别把继承当万能药4.1 继承是为了“复用接口”还是“复用实现”书里第一部分最用力的一章就是继承与多态。作者指出了很多教材没有讲清楚的道理继承有两种截然不同的动机一是复用代码复用父类已实现的逻辑二是表达类型关系子类是父类的一种is-a关系。在理想的OOP设计里这两个动机应该一致但实际代码里经常不是这样。一个典型的反模式是因为子类和父类有几行相似代码就直接继承而没有考虑语义上子类是否真的是父类的子类型。比如“正方形继承长方形”的经典问题如果让Square继承Rectangle并重写width和height的setter来保证两者相等那么任何依赖“长方形的width和height可以独立设置”的代码都会在正方形上崩掉。这个问题在动态语言里更是隐晦因为Python不会在编译期帮你检查类型约束。我的经验是当你在两个类之间看到明显超过50%的相似逻辑时不要立刻想到继承先想想能否通过组合、提取公共模块函数、或者把公共逻辑放到一个通过委托关系的辅助类里来解决。组合优先于继承这在社区里被叫做“composition over inheritance”道理就是——继承把父类的内部实现细节强制暴露给子类父类任何一个改动都可能引发子类行为变化而组合只暴露一个清晰的接口内部怎么变都不影响外部。4.2 鸭子类型Python多态的独特表达多态在静态语言里意味着“不同类的对象通过同一个接口调用时表现出不同行为”而在Python里这种能力被鸭子类型天然地实现。你不需要为了多态而特意去定义一个统一接口或抽象基类只要两个对象都具有同名同签名的方法它们就能被同一个函数接受。class Dog: def speak(self): return 汪汪 class Cat: def speak(self): return 喵喵 def make_it_speak(animal): print(animal.speak()) make_it_speak(Dog()) make_it_speak(Cat())make_it_speak根本不管传入的是什么类型只要求对象能响应speak()。这正是“鸭子类型”的含义它走起来像鸭子、叫起来像鸭子那就可以把它当鸭子。在实际业务中这个特性给你带来的直接好处是降低耦合减少抽象继承层级。坏处是如果有人传入一个没有speak方法的对象直到运行时才会报错。所以后来Python引入了抽象基类abc和协议Protocol来做“结构性子类型检查”这套配合用起来会舒服很多。4.3 多重继承与MRO看着方便实际要小心翼翼Python支持多重继承但书里非常克制地提醒读者这是一个需要严格把关的功能。多重继承的核心问题是方法解析顺序MROPython使用C3线性化算法来确定一个类的方法查找顺序。类名.__mro__可以直接查看这个顺序。class A: def hello(self): print(A) class B(A): def hello(self): print(B) class C(A): def hello(self): print(C) class D(B, C): pass print(D.__mro__) # (class __main__.D, class __main__.B, class __main__.C, class __main__.A, class object) d D() d.hello() # B在D(B, C)的MRO里B排在C之前所以hello调用了B的实现。如果你的类继承关系更复杂MRO的计算结果一定要亲手打印出来看一遍不要想当然。我在项目中见过一次非常隐蔽的问题两个父类都继承自同一个基类但其中一个对某个方法的修改没有被另一个看到导致最终行为不符合预期排查了半天才发现MRO顺序跟直觉不一样。我的实际建议是优先使用单继承加组合如果真的需要多个“能力来源”可以使用Mixin类但Mixin类要保持“小而纯”不持有复杂状态只提供一组相关方法并且避免和主类里的方法名冲突。5. 魔术方法让自定义对象融入Python的生态体系5.1 为什么说魔术方法是“接口协议”有一段时间我写类只写普通方法后来发现总是跟Python自带的一些语法和函数“对不上”比如print(obj)输出的是__main__.Book object at 0x...而不是一眼能看懂的内容。这背后的原因是没有实现__str__。在Python里很多内置函数和运算符实际上都对应着一组“协议”比如len(obj)其实是调用obj.__len__obj[0]其实是调用obj.__getitem__(0)obj1 obj2其实是调用obj1.__add__(obj2)for x in obj依赖obj.__iter__这些方法统一被称为魔术方法或双下方法只要你实现对应协议你的自定义对象立刻就能获得Python语言层面的原生体验。这正是面向对象编程在Python里最强大的地方语言的设计者们把语法糖的钥匙交到了你的手里。5.2 实际案例实现一个可以用len()和索引访问的集合对象假设你要做一个Playlist类内部维护一个歌曲列表但是你希望它能直接支持len()、索引访问和迭代而不是写一堆get_song()、get_count()之类的方法。class Playlist: def __init__(self, songs): self._songs list(songs) def __len__(self): return len(self._songs) def __getitem__(self, index): return self._songs[index] def __iter__(self): return iter(self._songs) def __contains__(self, item): return item in self._songs def __repr__(self): return fPlaylist({self._songs!r}) pl Playlist([起风了, 晴天, 夜曲]) print(len(pl)) # 3 print(pl[0]) # 起风了 for song in pl: print(song) print(晴天 in pl) # True print(pl) # Playlist([起风了, 晴天, 夜曲])这段代码通过寥寥几个魔术方法就把这个自定义类打造成了“长得跟内置容器一样”的对象。调用方完全不需要知道内部是如何存储的未来就算你把内部从列表改成别的数据结构只要这几个协议方法保持行为不变外部代码一行都不用改。这就是封装和协议结合的好处。5.3str__与__repr该实现哪个区别在哪书里特别有实战价值的一节是对__str__和__repr__的区分。简单来说__repr__的目标对象是开发者它应该尽量返回一个“能够精确重现这个对象”的字符串__str__的目标对象是终端用户它应该返回一个可读性高的描述。但在实际开发里我的策略是如果只能选一个优先实现__repr__因为当__str__未定义时Python会回退到使用__repr__的结果。这样print(obj)和交互式终端里的obj输出都是可控的而且调试日志里也会显示更有意义的信息。class Book: def __init__(self, title, author): self.title title self.author author def __repr__(self): return fBook({self.title!r}, {self.author!r}) book Book(Python3 面向对象编程, Dusty Phillips) print(book) # Book(Python3 面向对象编程, Dusty Phillips) str(book) # 同上 repr(book) # Book(Python3 面向对象编程, Dusty Phillips)注意我在f字符串里用了!r它表示以repr()的形式格式化这样字符串会带引号输出结果更容易看出类型。如果你不写!r输出的内容里面就不带引号日志里读起来很容易混淆。5.4 __eq__与__hash__的联动陷阱另外一个高频踩坑点当你实现__eq__来判断对象相等时如果没有同步实现__hash__对象的哈希值会被设置为None导致对象无法放进set或作为dict的键。反过来如果你希望两个属性相同的对象在set里被认为是同一个元素就必须同时覆盖这两个方法且保证相等的对象有相等哈希值。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)) def __repr__(self): return fPoint({self.x}, {self.y}) p1 Point(1, 2) p2 Point(1, 2) print(p1 p2) # True print(hash(p1) hash(p2)) # True print(len({p1, p2})) # 1这里有一个小细节__eq__返回NotImplemented而不是直接返回False这样当比较对象不是同类型时Python会给对方一个机会去实现反向比较这是一种更规范的做法。只定义__eq__而不定义__hash__会导致hash(p1)报错如果你不打算让对象可哈希那没问题但要默认它是可哈希的就一定要成对处理。6. 现代Python的OOP新姿势dataclass与类型注解6.1 dataclass少写模板代码专注数据建模我记得第一次在旧版本Python里写一个只存数据的类每次都要重复写__init__、__repr__、__eq__容易出错也显得死板。后来Python 3.7引入了dataclasses.dataclass装饰器这个局面彻底改变了。它可以根据类注解自动生成__init__、__repr__、__eq__等方法让你只需要写属性定义。from dataclasses import dataclass, field dataclass class Book: title: str author: str pages: int 0 tags: list field(default_factorylist) book1 Book(Python3 面向对象编程, Dusty Phillips, 440) book2 Book(Python3 面向对象编程, Dusty Phillips, 440) print(book1) # Book(titlePython3 面向对象编程, authorDusty Phillips, pages440, tags[]) print(book1 book2) # True有一个特别容易出错的点如果你给可变类型比如list、dict、set写默认值dataclass会直接报错并提示你使用field(default_factorylist)。原因在于Python的默认参数是定义时求值复用的如果用[]作为默认值所有实例会共享同一个列表对象一个实例做了修改其他实例也会跟着变。用default_factory就能保证每个实例获取的是一个新的列表。6.2 类型注解在OOP中的作用不是强制约束而是沟通与工具Python是动态类型语言类型注解并不是运行时强制约束而是给开发者、IDE和静态检查工具看的“设计文档”和“校验依据”。在面向对象设计中合理运用类型注解可以显著提高代码的可读性和可维护性。from typing import Optional class User: def __init__(self, name: str, email: Optional[str] None) - None: self.name name self.email email def get_display_name(self) - str: return self.name if self.email is None else f{self.name} {self.email}我自己最喜欢的是把类型注解和抽象基类或者Protocol结合使用这样你既有身份检查的便利又不会被强制继承的麻烦束缚。比如from typing import Protocol class Speaker(Protocol): def speak(self) - str: ... class Dog: def speak(self) - str: return 汪汪 def make_it_speak(animal: Speaker) - None: print(animal.speak())这里Speaker是一个Protocol它不是父类Dog也没有继承它但在类型检查工具看来Dog实现了Speaker定义的结构所以可以传入。这比强制继承要灵活得多。运行时行为不受任何影响类型检查是静态层面完成的。6.3 dataclass与面向对象设计的结合使用建议有没有一种组合是把dataclass和继承放在一起用呢可以但要注意字段顺序和默认值顺序否则会报“non-default argument follows default argument”的错误。官方建议是父类定义没有默认值的字段子类在定义自己字段之前先继承父类的所有必填字段再增加自己的字段。另一个值得注意的细节是dataclass默认生成的__eq__会比较所有字段这有时过于严格。如果你只需要比较业务主键可以给dataclass传eqFalse手动实现__eq__和__hash__。这样既保留了自动生成__init__和__repr__的好处又能精确控制相等语义。7. 一个完整的OOP案例从需求到类设计再到实现7.1 需求描述与实体识别前面讲了大量概念这里我用一个贴近实际的小项目把前面的知识点串起来。假设要实现一个简易的个人图书管理系统核心需求有三个管理书籍信息书名、作者、ISBN、库存量、支持按作者或ISBN检索、能记录书籍借出和归还操作。面对这个需求先不要急着写类。我的习惯是先列出问题域里出现的关键名词思考它们之间是什么关系。这里很自然能拆分出几个核心实体Book书、Author作者可以作为Book的一个属性也可以单独建模、Library图书馆负责整体管理、LoanRecord借阅记录。值得强调的是实体识别没有唯一答案不同人建模方式可能不同但本质要求是一致的——每个类必须有清晰的职责边界避免一个类承担太多事情。7.2 类设计属性、方法与关系我采用dataclass加类型注解的方式定义这些实体。from dataclasses import dataclass, field from datetime import date from typing import Optional, List dataclass class Author: name: str email: Optional[str] None def __repr__(self) - str: return fAuthor({self.name!r}) dataclass class Book: title: str author: Author isbn: str total_copies: int 1 available_copies: int field(initFalse) def __post_init__(self) - None: if self.total_copies 0: raise ValueError(total_copies 必须大于0) self.available_copies self.total_copies def borrow(self) - bool: if self.available_copies 0: return False self.available_copies - 1 return True def return_book(self) - None: if self.available_copies self.total_copies: raise ValueError(没有可归还的副本) self.available_copies 1 dataclass class LoanRecord: book: Book borrower_name: str borrow_date: date field(default_factorydate.today) return_date: Optional[date] None这里有一个关键细节available_copies用field(initFalse)意味着它不出现在__init__的参数列表里而是在__post_init__中根据total_copies初始化。这样外部通过Book(...)创建对象时不需要也不应该手动传available_copies因为它是一个派生状态要从total_copies推导出来。如果你漏掉这个字段会得到一个AttributeError如果你没有标记initFalse调用方就会有两条途径设置同语义的字段容易出现不一致。7.3 实现Library类用组合把实体串起来Library类是管理入口它内部持有图书集合和借阅记录并提供检索与借还操作为主的方法。这里就是前面提到的“组合优先于继承”的典型应用Library不是一个特殊种类的Book更不是某本书的父类或子类它组合了一堆Book和LoanRecord对象对外提供统一接口。dataclass class Library: name: str books: List[Book] field(default_factorylist) records: List[LoanRecord] field(default_factorylist) def add_book(self, book: Book) - None: self.books.append(book) def search_by_title(self, keyword: str) - List[Book]: return [book for book in self.books if keyword.lower() in book.title.lower()] def search_by_author(self, author_name: str) - List[Book]: return [book for book in self.books if book.author.name author_name] def search_by_isbn(self, isbn: str) - Optional[Book]: for book in self.books: if book.isbn isbn: return book return None def borrow_book(self, isbn: str, borrower: str) - bool: book self.search_by_isbn(isbn) if book is None: raise ValueError(图书不存在) if book.borrow(): self.records.append(LoanRecord(bookbook, borrower_nameborrower)) return True return False def return_book(self, isbn: str, borrower: str) - None: book self.search_by_isbn(isbn) if book is None: raise ValueError(图书不存在) record next( (r for r in reversed(self.records) if r.book.isbn isbn and r.borrower_name borrower and r.return_date is None), None ) if record is None: raise ValueError(找不到对应的借阅记录) book.return_book() record.return_date date.today()这里要注意borrow_book先通过search_by_isbn查书查到后再调用Book的borrow()方法return_book则先找未归还的借阅记录再归还书。这样整个流程中每个类只管好自己的职责调用关系清晰。7.4 测试与使用让设计真正落地定义好类之后写一小段使用代码来验证整个流程是否顺畅。author Author(Dusty Phillips, dustyexample.com) book1 Book(Python3 面向对象编程, author, 978-1-78953-878-4, total_copies3) book2 Book(Python Playground, Author(Maud, maudexample.com), 978-1-59327-735-4, total_copies1) library Library(我的小书房) library.add_book(book1) library.add_book(book2) library.borrow_book(978-1-78953-878-4, 张三) library.borrow_book(978-1-78953-878-4, 李四) print(book1.available_copies) # 1 library.return_book(978-1-78953-878-4, 张三) print(book1.available_copies) # 2 print(library.records[0].return_date) # 今天的日期运行这段代码整个流程完全符合预期。通过这样的设计后续扩展也容易比如要给借阅加一个“到期日期”只需要给LoanRecord增加一个字段要实现“逾期未还名单”只需在Library里新增对应方法而这些改动对其他类影响很小。7.5 这个案例给我们的设计启示这个案例虽然小但完整走了一遍“分析需求→识别实体→确定关系→定义类→实现方法→验证流程”的路线图。书里第一部分的核心价值观在这个案例里都有体现每个对象都有自己的状态和行为对象之间的协作通过公开接口完成内部变化不会对外部造成连锁影响。我特别想提醒的是在刚学完类与对象的时候很容易陷入“万物皆类”的误区把一个简单的脚本硬生生拆出一堆类。我的判断标准很简单——当代码里出现明显的重复或逻辑纠缠需要抽象时才引入类如果只是线性执行几步操作优先用函数。面向对象是手段可读性和可维护性才是目的。写在最后的一点实战体会这本书的第一部分内容我前后翻了两遍。第一遍侧重语法第二遍才真正领悟到它想传达的“设计观”Python里的面向对象并不追求把数据牢牢锁死而是通过约定、协议和组合在灵活性和可控性之间找到平衡点。这本书适合放在手边当你写代码时遇到类设计上的犹豫不妨翻一翻相关章节很多困惑其实前人早就讨论过了。下一步我打算把书里关于“异常处理与对象生命周期”的部分整理出来那部分涉及with语句和上下文管理器也是实际开发里容易出错的地方。