
我入行这些年大大小小的项目见了无数最常被新人追问的就是同一个问题别人写的Python类怎么这么优雅我写的怎么就又长又难维护其实答案很简单——不是天赋问题是技巧积累的问题。Python的类机制非常灵活同样的功能有无数种写法但真正优雅的写法往往藏着几个关键技巧。这一篇我把自己在真实项目中反复验证过的10个Python类技巧整理了出来覆盖内存优化、样板代码消除、对象模型语义、接口设计这些最核心的方向。每个技巧都配了完整的代码和踩坑说明不管你是刚入门的新手还是写过几年但还是觉得类写得不顺手的老选手都能从中找到能立刻用上的东西。1. 技巧全景这10个技巧是怎么选出来的1.1 三类痛点对应三类技巧写烂类烂在哪我复盘了手头几十个项目里的代码发现绝大多数问题可以归成三类。第一类是类写出来太啰嗦。一个简单的数据实体为了赋初值写五遍同样的代码为了能打印对象再写一个__repr__为了能比较对象又写一个__eq__。类变成了样板代码的堆积场真正有业务逻辑的部分反而被淹没在噪音里。第二类是对象语义没有设计好。相等怎么定义、能否放进集合、作为字典的key会不会出问题、对象能不能hash、打印出来是什么样子。这些看起来是小问题但它们决定了一个类的行为是否符合直觉也决定了别人用起来顺不顺手。第三类是内存和性能问题。Python对象的属性默认是存在一个字典里的这个设计非常灵活但也是有代价的。当你创建成千上万个同类对象时字典的内存开销会大到离谱写出来的代码再漂亮也会被性能问题拖垮。下面这10个技巧就是分别从这几个维度去解决问题。我把它们做成了一个速查表方便你在日常写代码时快速找到目标。1.2 10个技巧速查表编号技巧解决问题使用频率学习成本1__slots__节省内存、限制属性中低2dataclass消除样板代码极高低3property隐藏getter/setter极高低4__str__与__repr__对象输出可读性高低5classmethod灵活的多重构造函数高中6staticmethod组织类内工具方法中低7__eq__与__hash__对象比较与哈希语义高中8__enter__和__exit__上下文资源管理高中9cached_property缓存昂贵计算结果中低10组合优于继承降低类层次复杂度高高顺序上我特意做了安排前5个技巧解决的是类怎么写得省事后面5个解决的是类怎么写才正确、才经得起推敲。建议你别跳着看尤其是第7个技巧90%的人都在那里踩过坑。2. 先解决最基础也是最痛的五个问题2.1 技巧1用__slots__干掉多余内存开销先来看一段你大概率见过的代码class Point: def __init__(self, x, y): self.x x self.y y这个类看起来没什么问题但它的内部实现其实藏着一个隐形开销。每个Point实例除了存x和y两个值之外还会自动创建一个__dict__属性这是一个普通字典用来存放实例里的所有属性名和属性值。字典是一个哈希表实现灵活但内存开销很大。单个Point实例你可能感觉不到但如果你要创建100万个Point对象呢我实际测试过光__dict__的空字典就要占88字节左右再算上哈希表的扩张和缓存每个实例白白多出来的内存非常可观。解决办法是在类里声明__slots__class Point: __slots__ (x, y) def __init__(self, x, y): self.x x self.y y加了这行之后Python就不再为实例创建__dict__而是把属性存储在一组固定的内部数据结构里地址连续、访问更快。同样100万个点内存占用能下降30%到50%属性访问速度也略有提升。__slots__还有两个容易被忽略的副作用在实战中反而常常是加分项。第一它禁止了你给实例动态添加新属性相当于给类加了一道约束能防止手误打出p.xx 1这种静默扩展。第二它让代码自文档化Point有哪些字段一眼就数得清。但有一个坑必须提醒你继承时__slots__不会自动延续。如果父类定义了__slots__子类没定义子类实例依然会有__dict__父类的__slots__只对父类自己的实例生效。所以写子类时记得重新定义__slots__并且把父类的字段也包含进来。注意如果类需要支持weakref需要在__slots__里手动加上__weakref__。这个细节很容易被忽略等你在项目里做缓存、做观察者模式时就会遇到。2.2 技巧2用dataclass跟样板代码说再见以前我们定义一个简单的数据类要写多少重复代码我数给你看class Book: def __init__(self, title, author, price): self.title title self.author author self.price price def __repr__(self): return fBook(title{self.title!r}, author{self.author!r}, price{self.price!r}) def __eq__(self, other): if not isinstance(other, Book): return NotImplemented return (self.title, self.author, self.price) (other.title, other.author, other.price)一个字段还有没法实际项目里动辄七八个字段每个字段都要在__init__里写一遍赋值再写一遍__repr__再写一遍__eq__。写的人烦看的人更烦真正的业务逻辑全被淹没了。Python 3.7开始引入的dataclass就是为了解决这个问题from dataclasses import dataclass, field dataclass class Book: title: str author: str price: float 0.0 tags: list field(default_factorylist)装饰器会自动帮我们生成__init__、__repr__、__eq__方法。类型注解在这里不是摆设它直接参与字段的定义和默认值的处理所以必须有实际类型标注写title不写: str是不行的。我第一次用dataclass时差点犯一个低级的错误这里也替你们避一避。当时我图省事给一个列表字段直接写了tags: list []结果创建的所有Book实例共享同一个列表修改一个全体中招。这是因为Python的默认参数只计算一次[]在类定义阶段就创建好了之后所有实例都拿同一个对象当默认值。正确写法是tags: list field(default_factorylist)field(default_factorylist)的意思是每次创建实例时都调用一次list()来生成一个新的空列表从根源上杜绝了共享问题。dataclass还支持两个非常实用的参数。dataclass(frozenTrue)让实例不可变修改字段直接抛异常适合值对象、配置对象dataclass(orderTrue)自动生成比较方法让实例可以按字段排序。这两个参数一加很多手写的逻辑都可以直接删掉。唯一要留意的是Python版本dataclass在3.7开始可用3.10之后支持slotsTrue可以直接和技巧1合体使用。2.3 技巧3用property把getter/setter藏起来很多从Java转过来的朋友写Python类时习惯性写一堆get_name、set_name方法。不是说不可以而是这一点都不Pythonic。Python里更好的方案是property它把getter、setter包装成普通属性对外暴露的永远是obj.name这种自然写法内部实现怎么变都不影响调用方。举个最经典的例子class Circle: def __init__(self, radius): self._radius radius property def area(self): return 3.14159 * self._radius ** 2 property def radius(self): return self._radius radius.setter def radius(self, value): if value 0: raise ValueError(radius must be non-negative) self._radius value调用方式变成circle.area和circle.radius后面没有括号干净得像直接访问字段。但这又不是字段——area是每次现算的radius在写入时经过了校验。property真正厉害的地方在于它让先定义一个普通属性、后改成受控属性这个操作变得无痛。最开始你写的类可能是circle.radius 5调用方已经大量使用了circle.radius。这时候你发现用户传入负数会引发问题需要加校验。在没有property的世界里你只能改成set_radius()方法然后全项目搜调用点一个一个改。有property以后你只需要把self.radius改成self._radius再补上property和setter调用方一行都不用动。这就是接口稳定的价值。我一直觉得property不是语法糖那么简单它给编码进化留了一扇后门。2.4 技巧4用__str__和__repr__把对象说清楚调试的时候我见过太多人直接在IDE的变量窗口里看到__main__.User object at 0x7f8d19c8efd0这种输出。不是不能看是看完之后你根本不知道这个对象到底是什么内容还得一层层展开属性面板。遇到循环引用或深层嵌套的结构整个人都麻了。解决方案是实现__repr__和__str__两个魔术方法它们决定了Python怎么把对象转成字符串class User: def __init__(self, uid, name): self.uid uid self.name name def __repr__(self): return fUser(uid{self.uid!r}, name{self.name!r}) def __str__(self): return f{self.name}(#{self.uid})两者的分工是__repr__面向开发者在调试器、交互式解释器、容器打印元素时调用理想情况下这个字符串应该可以让你重新构造一个完全相同的对象也就是repr(obj)的结果可以传给eval()还原出原对象__str__面向普通用户在print()和str()时调用目的是给人看的所以更友好一些。它们的调用优先级也容易踩坑如果没有定义__str__Python会拿__repr__的结果顶替但反过来不行。还有个特别容易误导的点——打印列表时列表里元素的显示用的是__repr__而不是__str__所以就算是print([user1, user2])User里写得多漂亮的__str__也不会生效走的还是__repr__。这也是我强烈建议先把__repr__写好的原因。__repr__里我习惯用!r转换符它会让格式化时调用repr()而不是str()。比如User(uid1)和User(uid1)在调试信息里的差别一眼就能看穿这种细节在排错时能救你一命。2.5 技巧5用classmethod造一个备用的构造函数假设你设计了一个用户类class User: def __init__(self, username, email): self.username username self.email email现在要从一个字典创建用户或者从一行业务数据里创建你大概会这样写外部函数def create_user_from_dict(data): return User(data[username], data[email])能用但问题在于这个函数和User的耦合很松散子类想复用时还得单独处理。更好的做法是把这种创建逻辑直接放到类里面用classmethod实现class User: def __init__(self, username, email): self.username username self.email email classmethod def from_dict(cls, data): return cls(data[username], data[email])这里面的门道在于第一个参数cls。它接收的是类本身而User.from_dict(...)调用时cls就是User如果是VipUser.from_dict(...)cls就是VipUser。因为用的是cls(...)而不是User(...)所以子类可以原封不动地继承这个工厂方法创建出来的依然是子类实例完全不用改一行代码。这正是classmethod作为备选构造函数的核心价值。实际项目里from_dict、from_csv_row、from_json、from_bytes都是非常常见的命名约定。刻意把构造函数设计成一个主入口加若干工厂方法比每个人按自己习惯随便写外部函数要清晰得多。顺带一提classmethod和staticmethod经常被放一起比较很多新手搞不清楚区别。区别其实只有一条classmethod拿到的第一个参数是类cls可以用它来创建实例、访问类属性staticmethod什么都不拿就是一个放在类里面的普通函数。需要让子类动态感知类型时用classmethod只是想把一堆方法归类整理时用staticmethod后者正好是下一个技巧。3. 剩下的五个技巧把代码推到下一个层次3.1 技巧6用staticmethod整理类的工具方法写订单系统的时候经常需要判断一个状态值是不是合法的。有人会在Order类外面写个独立函数is_valid_status(status)然后所有模块都import它。这样也不是不行但调用的时候你感受不到它和Order类的关系项目一大了这种散装函数会变得很难找。staticmethod的场景就是这种逻辑上和类强相关但方法的执行不需要访问实例的属性也不需要访问类本身的状态。一个典型的例子class Order: VALID_STATUS {pending, paid, shipped, done} def __init__(self, order_id, status): self.order_id order_id self.status status staticmethod def is_valid_status(status): return status in Order.VALID_STATUS调用时用Order.is_valid_status(paid)语义一目了然状态判断这件事是Order的职责之一而不是散落在模块里的孤儿函数。它还能被子类继承和重写在某些框架代码里派得上用场。不过需要泼一盆冷水staticmethod不要滥用。如果一个方法需要频繁改动类的常量、需要读取类级别的配置那就该用classmethod因为你拿不到cls时想动态感知子类的配置是做不到的。我的判断标准很简单方法体里如果出现了硬编码的类名Order.VALID_STATUS说明它其实想要cls那就换成classmethod如果方法体从头到尾和类状态毫无关系只是放在这里好管理才用staticmethod。3.2 技巧7正确实现__eq__和__hash__让对象可比较、可收藏这个技巧是我见过踩坑最多的地方没有之一。先看现象。你定义了一个类实现了__eq__用于值比较然后想把它塞进set去重class Money: def __init__(self, amount, currency): self.amount amount self.currency currency def __eq__(self, other): if not isinstance(other, Money): return NotImplemented return self.amount other.amount and self.currency other.currency然后执行set([Money(500, USD)])直接抛出一个吓人的TypeError: unhashable type: Money。新手往往想不通我明明没招谁惹谁怎么会不可哈希原因在于Python有一条安全规则如果一个类重写了__eq__但没有重写__hash__Python会自动把__hash__设置为None。为什么这么做因为字典和set依赖哈希值定位存储位置哈希值在对象生命周期里必须保持不变。但如果你重写了相等逻辑让两个原本不同身份的对象在业务上相等那么Python为了安全起见干脆禁止你哈希它避免出现对象相等但哈希值不同的诡异状态。要修复就得自己同时定义__eq__和__hash__并且保证哈希值只基于相等比较时用到的字段class Money: def __init__(self, amount, currency): self.amount amount self.currency currency def __eq__(self, other): if not isinstance(other, Money): return NotImplemented return self.amount other.amount and self.currency other.currency def __hash__(self): return hash((self.amount, self.currency))两个关键点。第一__eq__里遇到类型不对时返回NotImplemented而不是False。这是给Python一个机会让other那边的__eq__也参与比较两种写法在日常小项目里看不出区别但一旦涉及跨类型比较和反向比较NotImplemented才是正确姿势。第二__hash__里要打包相等比较用到的所有字段像这里是把(amount, currency)装进tuple再取哈希。如果你__eq__比较了三个字段而__hash__只哈希了两个那相等的两个对象就可能落到不同的哈希桶里set和dict的行为就会变得随随便便。3.3 技巧8用__enter__和__exit__实现资源管理很多人以为只有open()才能配with其实任何实现了上下文管理器协议的类都可以。我自己写过一个数据库连接管理器核心代码就是这么设计的class DatabaseConnection: def __init__(self, dsn): self.dsn dsn def __enter__(self): self.conn connect(self.dsn) return self.conn def __exit__(self, exc_type, exc_val, exc_tb): self.conn.close() return False一旦类实现了__enter__和__exit__它就能配合with语句使用with DatabaseConnection(postgresql://...) as conn: conn.execute(SELECT 1)执行流程是进入with块之前调用__enter__取得资源with块里的代码无论正常结束、还是抛出异常、还是被return中断都会走一遍__exit__。这就保证连接一定会被关闭不会因为中途抛异常就把资源泄漏在那边。这个模式和文本文件、网络连接、线程锁、临时文件等场景高度匹配。__exit__收到三个参数exc_type是异常类型exc_val是异常对象exc_tb是traceback。如果with块里没有异常它们三个全是None。返回值False表示不吞异常——异常会继续向外抛如果返回True异常就被悄悄拦下来这种写法除非明确知道在做什么否则我强烈不建议用。如果实现协议的两个方法比用contextlib.contextmanager生成器写起来麻烦我一般会优先考虑后者一行装饰器就搞定。但如果你需要同时管理多个进入和退出动作或者需要配合异常类型做精细化处理还是手写上下文管理器更直白。3.4 技巧9用cached_property缓存昂贵的计算结果再看一个业务里非常常见的性能优化场景。假设Report对象需要统计总营收这个统计要遍历海量数据耗时好几秒。如果每次report.total_revenue都要重算一遍用户体验直接拉垮。传统做法是在第一次计算后把结果存入私有属性class Report: def __init__(self, data): self.data data self._total_revenue None property def total_revenue(self): if self._total_revenue is None: self._total_revenue sum(item.revenue for item in self.data) return self._total_revenue这写法没问题就是样板代码有点多。Python 3.8之后的functools.cached_property把这个模式封装成了装饰器from functools import cached_property class Report: def __init__(self, data): self.data data cached_property def total_revenue(self): # 这里假设是很重的计算比如遍历数十万条明细 return sum(item.revenue for item in self.data)第一次访问report.total_revenue时执行计算然后把结果缓存到实例自己的__dict__里后续访问直接读缓存不再触发计算。这个行为本质上是惰性初始化缓存的语法糖在模型层处理统计值、查询结果、格式化产物时特别实用。用的时候要注意两个点第一cached_property的数据存到了实例的__dict__里如果类定义了__slots__且没有__dict__就会报AttributeError第二如果对象是可变的依赖字段变了缓存不会自动失效。所以cached_property最适合用在计算依赖的字段一旦设置就不变的对象上。在分析型模型、报告对象里它非常好用但放在一个字段频繁更新的数据模型上就要慎用否则你会看到明明数据已经改掉了读出来的还是旧值排查半天才想起来是缓存闹的。3.5 技巧10优先组合让继承退居二线很多教程把继承吹得天花乱坠但实际项目里我见过太多被继承坑惨的例子。一个Animal基类下面按行为分出几个子类然后又要加会飞的、会游的、会叫的各种多继承开始交叉组合最后形成一个蜘蛛网一样的菱形继承结构改一个基类方法全项目跟着抖三抖。继承最大的问题是它把类和类之间的耦合变得极深。子类和父类在实现上绑定在一起父类的任何一个方法改动子类都有可能被影响而且这种影响常常是隐性的。组合的思路完全相反不要把一个类做成无所不能的大全集而是把能力拆成独立的对象用有一个的关系把它们组装起来。比如你有一个动物类移动方式可能是走路、飞行或游泳。用继承硬写class Animal: def move(self): raise NotImplementedError class WalkingAnimal(Animal): def move(self): print(walking)以后要加一个水陆两栖动物就麻烦了它应该继承谁WalkingAnimal还是SwimmingAnimal组合的改法是这样的class MovementStrategy: def move(self): raise NotImplementedError class WalkMovement(MovementStrategy): def move(self): print(walking) class SwimMovement(MovementStrategy): def move(self): print(swimming) class Animal: def __init__(self, name, movement): self.name name self.movement movement def move(self): self.movement.move()构造时可以传入不同的移动策略duck Animal(duck, WalkMovement()) fish Animal(fish, SwimMovement())以后要加一个能走又能游的动物只需要在外部组合一次给Animal配一个复合运动对象或者直接切换策略根本不需要大改类体系。组合的另一个好处是测试。你想测试Animal的move逻辑只要mock一个假的movement传入即可完全不用牵动整个继承链。那继承就完全不能用了吗也不是。语义上确实是is-a关系、子类确实就是父类的特殊形态、而且父类的接口足够稳定时继承依然是合理选择。我的实践原则是默认先考虑组合只有当组合带来的代码复杂度和间接性反而超过了继承时才回头选继承。4. 实战把这些技巧揉进一个完整的类4.1 需求背景技巧单独看都懂合起来怎么用我拿一个真实的业务场景来串一遍。假设我们要做一个简化版的购物车模块要求是商品不可变购物车条目可以累加数量订单支持从字典反序列化创建订单金额能缓存计算并能放进set里去重。4.2 完整代码实现from dataclasses import dataclass, field from functools import cached_property dataclass(frozenTrue, orderTrue) class Product: sku: str name: str price: float def __post_init__(self): if self.price 0: raise ValueError(price cannot be negative) dataclass class CartItem: product: Product quantity: int 1 property def total(self): return self.product.price * self.quantity def increase(self, delta): if delta 0: raise ValueError(delta must be positive) self.quantity delta dataclass class Order: order_id: str user_id: str items: list field(default_factorylist) classmethod def from_dict(cls, data): return cls(order_iddata[order_id], user_iddata[user_id]) cached_property def total_amount(self): return sum(item.total for item in self.items) def __hash__(self): return hash(self.order_id)4.3 代码拆解这个示例短但里面融合了多个技巧我一个个拆开讲。Product使用了dataclass(frozenTrue, orderTrue)。frozenTrue保证商品一经创建就不可修改价格、SKU这些字段不会在状态流转中被意外改掉orderTrue则自动生成了比较方法支持按字段排序。__post_init__是dataclass特有的钩子在__init__执行完字段赋值后调用适合做数据校验。这里我写了价格不能为负的校验非法数据在创建对象的那一刻就被拦下来。CartItem里的property total把单价乘数量这个计算逻辑封装成只读属性外部访问item.total时无需关心怎么算的。increase方法里做了入参校验防止数量被改成负数这也是防御式编程的常见姿势。Order里加cached_property total_amount的原因很直白——订单条目多了以后每次读总价都去遍历求和就很浪费。用cached_property只算一次后续读直接命中缓存性能提升非常明显。但要注意我正在刻意演示一个知识点如果订单的items后续被修改了缓存不会自动失效。这里假设业务上订单创建后items只增不改所以缓存是安全的。最后是__hash__。因为Order是可变的dataclass默认情况下它是不可哈希的但业务要求Order能放进set去重。这里我用hash(self.order_id)作为哈希值等于告诉set顺序单号相同就是同一张订单。注意我在实现时已经把订单相等的语义简化为订单ID相等那么hash基于order_id来算就是一致的这个设计是自洽的。如果业务上两个字段都要参与相等判断比如user_id也要相同才认为是同一张订单那就应该给Order手动实现一个基于完整字段的__eq__同时让__hash__基于相同字段计算。这是一个值得反复记住的配对原则__eq__和__hash__永远要基于同一套字段不能各说各话。5. 高频踩坑与排查技巧实录5.1 踩坑记录自定义对象放进set报TypeError这个错误几乎每个写Python的都会遇到一次。现象很统一自己实现了__eq__然后把对象放进set或作为字典的key运行时报TypeError: unhashable type: XXX。排查思路就一条检查这个类是否定义了__eq__但没有定义__hash__。Python的默认行为是一旦你重写了__eq____hash__就会被自动置为None对象直接从可哈希变成不可哈希。解决办法在技巧7里讲过这里再补一个快速判断的口诀如果你希望对象能做值比较并且能进set那么__eq__和__hash__必须同时实现而且二者基于的字段要完全一致。如果你的对象只是想在业务上比较相等不需要放进集合那就老老实实接受它不可哈希的事实不要绕路去骗解释器。5.2 踩坑记录property无限递归爆栈这是我见过新手最爱踩的坑之一。需求是要给属性加校验于是脑子一热写成class Circle: def __init__(self, radius): self.radius radius property def radius(self): return self.radius这个类的实例一创建直接递归到爆栈。原因很简单self.radius在property内部仍然会去调用property方法本身它不会因为你在类内部就无法访问于是无限套娃。正确写法是让内部存储用私有变量比如self._radiusproperty只负责把_radius暴露出去class Circle: def __init__(self, radius): self._radius radius property def radius(self): return self._radius radius.setter def radius(self, value): if value 0: raise ValueError(radius must be non-negative) self._radius value记住这个模式之后就不会再踩了什么时候用self.xxx什么时候用self._xxx取决于你是在普通方法里还是在property实现里。property实现里一定要用私有下划线字段这是惯例。5.3 踩坑记录dataclass的可变默认值陷阱这个坑我在前面讲field(default_factorylist)时提过但因为它太典型值得单独再说一次。错误写法dataclass class Cart: items: list []这个[]在类定义阶段就被创建了之后所有Cart实例的items都指向同一个列表对象。你往一个实例的items里append一个商品其他实例的items也跟着多出这个商品。这种bug极其隐蔽因为有时候单独测试一个实例测不出来两个实例放在一起对比才暴露。排查方法也很笨但有效把两个实例的items打印出来看id()是否相同相同就是共享了。修复方法就一句话可变类型默认值一律用field(default_factory...)字符串、数字、tuple这类不可变类型可以直接给默认值。5.4 踩坑记录__slots__在继承中失效这个坑最容易在明明加了__slots__内存还是没降下来的场景里出现。原因基本只有一种子类没有重新声明__slots__。在Python里__slots__不会自动继承子类一旦没有声明它依然会创建__dict__子类实例的内存开销就回到了原来的水平。还有一种衍生场景父类有__slots__子类也定义了__slots__但子类的字段里漏了父类的字段。这时实例无法访问父类声明的那个属性会直接报AttributeError。解决办法是子类的__slots__要把父类的字段一并写上class Base: __slots__ (x,) class Child(Base): __slots__ (x, y)这白白增加了一点维护成本。所以我的建议是如果类层次深、字段经常变__slots__带来的收益可能被踩坑成本冲抵要仔细权衡如果确实是高性能场景比如大量数值对象、游戏实体那就值得为它多花点心思。5.5 踩坑记录cached_property和__slots__不兼容最后说一个组合问题。cached_property把缓存写到self.__dict__上如果一个类用__slots__且没有声明__dict__那么访问带cached_property装饰的属性时第一次计算就会触发AttributeError。解决办法有二。一是给__slots__里手动加一个__dict__class Report: __slots__ (data, __dict__)这样实例同时保有紧凑的slot存储和灵活字典存储缓存也有了落脚的地方。二是干脆不用cached_property改成手动缓存到slot字段里比如在__init__里初始化一个self._total None然后在属性里判断。这两条路都通我更倾向后者因为手动缓存逻辑从外表上一眼即可看穿不依赖魔法装饰器。写在最后的一些体会这些技巧我用了很多年但它们不是万金油。我自己在实际项目中感受最深的不是某一个技巧本身而是它们背后共通的思维方式类设计的核心是语义清晰、接口稳定、实现可替换。数据类就交给dataclass去生成样板代码属性访问就用property做一层保护壳对象要能比较要能哈希就要把__eq__和__hash__定义得严丝合缝职责能用组合拆开就别用继承硬撑。最后再分享一个小习惯每写完一个类我会刻意做一次删除演练——试着问自己如果不写这个类用普通函数加字典能不能完成同样的功能如果答案是不能那就说明类的抽象是站得住脚的。如果答案是能那就再想想这个类存在的意义是什么。只有能经得起这种自问的类才配得上优雅这两个字。