news 2026/10/31 4:32:20

Python类和对象:从语法到建模的封装、继承与多态实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python类和对象:从语法到建模的封装、继承与多态实践

第一次真正被"类和对象"卡住,是在我照着教程敲完一个学生信息类之后整整两天。教程里那几十行代码我每一行都能读懂,__init__认得,self认得,方法调用也认得,可是轮到我自己写一个"图书借阅"的小练习,手停在编辑器里,不知道第一行该敲什么。那种感觉很怪——知识点拆开看都会,合起来就无从下手。后来我才慢慢想明白,卡住的不是语法,是脑子里没有建立起"数据和操作数据的行为应该住在一起"这个模型。这篇内容就是围绕这件事展开的:把类和对象从语法层面拉回到建模层面,讲清楚类到底在描述什么、对象在内存里长什么样、封装的边界该画在哪里、继承和多态什么时候该用什么时候不该用。适合刚学完基础语法、能写函数但一写类就发懵的读者,也适合写过一些类但总觉得"设计得别扭"的人对照着看。下面所有例子以 Python 为主,关键处会拿 Java、C++ 做对照,因为很多概念在静态语言里反而更容易看清。

1. 为什么初学"类和对象"总会卡在同一道坎上

我观察过不少初学者的学习路径,卡点高度一致:函数和循环学得很顺,一进到类就开始机械模仿教程的写法,写出来的东西能跑,但自己都说不清为什么要这么写。问题不出在勤奋程度上,而出在缺少一次"从散装到成形"的思维转换训练。这一节先把这道坎拆开看。

1.1 散装变量和"有行为的数据"之间差了什么

假设要处理一个学生的成绩,最直觉的写法是用字典:

student = {"name": "A同学", "scores": [88, 92, 79]} average = sum(student["scores"]) / len(student["scores"])

这段代码没问题,但它有三个隐患。第一,键名是字符串,写错一个字母不会报错,只会在运行到某一行时突然KeyError;第二,计算平均分的逻辑散落在外面,如果有十个地方要算平均分,就有十份几乎一样的代码;第三,"学生"这个概念没有被固化下来,谁都可以往字典里塞任意键,student["age"] = "二十"也能顺利执行,类型和语义全凭自觉。

类解决的正是这三件事。把name和scores变成属性,键名拼错立刻变成AttributeError;把算平均分的行为变成方法,逻辑只有一份;把scores的写入收拢到一个入口里,可以在里面做校验。换句话说,类不是把函数塞进一个命名空间,而是给一组数据划定了一个有守卫的边界。

我第一次真正理解这一点,是写了一个带校验的add_score方法之后。以前我习惯在业务代码里到处append,改成必须走方法之后,非法分数在入口就被挡住了,后面所有读取这个列表的地方都省掉了防御性判断。这个体验很关键——它让你意识到封装不是形式主义,是在减少整条链路上的心智负担。

1.2 图纸和实物:类、对象、实例这三个词的关系

很多人把"对象"和"实例"当成两个不同的东西,其实在绝大多数语境里它们是同义词,只是视角不同:类(class)是描述,对象(object)/实例(instance)是按这份描述真正造出来的东西。用图纸和实物类比最直观,但类比要继续往下推才有价值。

图纸阶段,你只规定了"这栋楼有几层、每层几个房间、门朝哪开",这些信息在程序里对应类体中的属性和方法定义。造楼阶段,每栋楼都会拿到自己独立的一份房间状态——A 栋 3 楼住着谁,不影响 B 栋 3 楼。放到代码里验证:

class Student: school = "某中学" # 类属性,所有实例共享 def __init__(self, name): self.name = name # 实例属性,每个对象独立 self.scores = [] a = Student("A同学") b = Student("B同学") a.scores.append(90) print(a.scores, b.scores) # [90] [] print(a.school, b.school) # 某中学 某中学 print(id(a), id(b)) # 两个不同的地址

a.scores和b.scores是两个不同的列表对象,这一点初学者通常能理解;但school只有一个,存在类对象上,a.school实际是"顺着查找链往上找"找到的。这个差别在下一节会展开,因为它是后面一大半坑的源头。

还有一个容易被忽略的点:类本身也是对象。在 Python 里,Student这个类在定义语句执行完之后,就是一个存在于内存里的对象,你可以把它赋值给变量、放进列表、当参数传。理解这一点之后,type(Student)返回type、装饰器能修改类、元类这些概念才不会显得突兀。C++ 里没有这层,Java 里靠Class对象部分模拟,Python 则是把"类是对象"贯彻得比较彻底的语言,学习时值得专门留意。

2. 类定义的四个组成部分与它们的真实分工

一个类体里通常会出现四类东西:构造相关的方法、实例属性、类属性、各种方法。教程往往一次性铺开,但每一类的职责边界其实差别很大,混着讲就容易糊。这一节按照"对象从创建到销毁"的顺序,把分工讲清楚。

2.1__init__的边界:它负责初始化,不负责生产对象

初学者最常见的误解是把__init__当成构造函数。严格说,生产对象的是__new__,__init__只负责给已经存在的对象装上初始状态。在 Python 里这条链路是:调用Student("A同学")→ 触发Student.__new__(Student)分配内存并返回一个新对象 → 把这个对象和参数一起交给__init__。__init__的返回值必须是None,因为它改的是传进来的那个对象本身,如果返回别的东西,Python 会直接报错。

为什么这个区分重要?因为一旦你需要控制对象的创建过程——比如实现单例、对象池、或者让某个类只能通过特定工厂方法创建——改的就是__new__,而不是__init__。我在带新人的时候遇到过这样的需求:某配置类全进程只允许存在一份。有人第一反应是在__init__里加个标志位判断,但__init__拦不住对象已经被创建的事实,内存里仍然会出现多个实例,只是初始化被跳过。正确做法是在__new__里判断并返回已存在的那个。这个例子很能说明"生产"和"初始化"是两件事。

还有一处实践细节:__init__里尽量只做"赋值和轻量校验",不要在里面发起网络请求、读大文件、连数据库。原因很实际——构造失败时抛出的异常往往夹杂在这次调用的上下文里,排查时很难区分是"对象没造出来"还是"造出来了但配置读失败"。我现在的习惯是把重活放到一个显式的load()或connect()方法里,让对象的诞生过程保持廉价、可预测、可测试。

2.2 实例属性、类属性、方法三者的作用域边界

这三个东西的查找规则值得单独拿出来讲,因为大量"明明赋值了却没生效"的 bug 都源自这里。先看一张对照表:

类型定义位置存储位置修改影响范围典型用途
实例属性方法内self.x = ...实例的__dict__仅当前对象对象各自的状态
类属性类体中直接赋值类的__dict__所有未覆盖该名字的实例常量、共享配置
实例方法类体中def f(self)类的__dict__无(行为共享)操作对象状态
类方法@classmethod且首参为cls类的__dict__作用于类本身替代构造器、工厂
静态方法@staticmethod类的__dict__与类实例无关纯工具函数归置

关键在读取和写入的不对称。读取时,a.school先查a.__dict__,找不到再去Student.__dict__查;写入时,a.school = "某附中"不会去改类属性,而是在a.__dict__里新建一个同名条目,从此a用自己的,b和类本身不受影响。这解释了一个经典困惑:

class Counter: total = 0 def add_wrong(self): self.total += 1 # 相当于 self.total = self.total + 1,新建实例属性 def add_right(self): Counter.total += 1 # 显式作用在类上 c1, c2 = Counter(), Counter() c1.add_wrong(); c1.add_wrong() print(Counter.total, c1.total, c2.total) # 0 2 0

add_wrong每次执行都会给c1新建一个total,类上的total纹丝不动。这类 bug 在计数、缓存、状态标记场景里特别隐蔽,因为第一次调用看起来"结果是对的"——c1.total确实变成 1 了,直到你要统计全局数量时才发现数字不对。

顺便说清@classmethod和@staticmethod的实际取舍。类方法最实用的场景是提供多个构造入口,比如Student.from_dict(data),比在外部拼参数可读性好得多,而且子类继承时会自动拿到子类本身(因为cls是动态的)。静态方法我一般只在"这个函数逻辑上属于这个类,但完全不碰对象状态"时才用,比如日期解析、单位换算。如果连类都不想碰,那它就该是模块级函数——为了归置而强行塞进类里,反而增加了阅读时的跳转成本。

2.3self到底是什么:一次方法调用的完整链路

self是初学者另一个高频困惑点。它的本质非常简单:实例方法在类里定义时只是普通函数,第一个参数留着放"调用它的那个对象",Python 不会自动帮你填,是绑定机制帮你填的。

class Demo: def greet(self): return f"hi, {self.name}" d = Demo() d.name = "A同学" print(d.greet()) # 等价于 Demo.greet(d) print(Demo.greet(d)) # 显式传,结果一样

d.greet这一步并不是直接取到函数。Python 会先看Demo.__dict__["greet"]拿到函数对象,发现它实现了描述符协议(有__get__),于是返回一个绑定方法,把d预先绑到第一个参数上。所以你调用d.greet()时不需要传self。这也是为什么把方法从类里取出来单独存成变量会出问题:

f = d.greet print(f()) # 正常,绑定关系在取出的那一刻就固定了 g = Demo.greet # g() # 报错:缺少位置参数 self

理解这条链路之后,一些"魔法"就不神秘了:为什么self名字可以随便改(叫this也行,只是约定俗成用self)、为什么忘记写self会在调用时报参数数量错误、为什么继承时父类方法里self实际上指向子类实例。最后这一点是理解多态的关键前置,第 4 节会展开。

3. 我在练习中反复踩到的六个坑

下面这六个问题,几乎每一个我都在不同阶段遇到过,而且它们都有共同特征:代码能跑通,结果不一定对,排查时容易被表象误导。所以我不只写"正确做法",也把当时的排查过程写出来,方便你遇到类似现象时对照。

3.1 可变默认参数:一个列表被所有对象悄悄共享

这是类和对象学习中杀伤力最大的坑,没有之一:

class Cart: def __init__(self, items=[]): # 危险 self.items = items c1, c2 = Cart(), Cart() c1.items.append("书") print(c2.items) # ['书'] —— c2 也中招了

原因在于默认参数的值是在函数定义时求值一次的,[]只创建了一个列表对象,之后所有没传参的实例都拿着同一个引用。这和类属性共享是两回事,即使你把items写成实例属性也躲不过,因为源头是函数默认值。

正确的写法是用None占位,在函数体内再创建:

class Cart: def __init__(self, items=None): self.items = list(items) if items else []

这里用list(items)而不是直接self.items = items,是有意做了一次浅拷贝,防止调用方传进来的列表在外部被改动时连带影响对象状态。这条经验在写库代码时尤其重要——构造器不应该持有外部传入的可变对象引用,除非你明确就是要共享。当年我排查一个"两个订单的商品莫名混在一起"的问题,查了大半天最后定位到就是这个默认参数,改完之后顺手把所有构造器里的可变默认值都清了一遍。

顺带一提,同理的还有在类体里直接写data = {}当实例状态用的写法,本质是类属性共享,症状相似,根因不同,排查时看__dict__最快:

print(c1.__dict__, c2.__dict__) # 看看各自的字典里到底有什么

3.2 用==比较对象却得到意外结果

自定义类的对象默认用身份比较,两个内容完全一样的对象==返回False:

class Point: def __init__(self, x, y): self.x, self.y = x, y print(Point(1, 2) == Point(1, 2)) # False

这个行为本身不算 bug,但当你把对象放进list、用in判断、或者放进set去重时,结果就会非常反直觉——去重完全失效,因为每个对象身份都不同。修复要成对实现__eq__和__hash__:

class Point: def __init__(self, x, y): self.x, self.y = x, y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return (self.x, self.y) == (other.x, other.y) def __hash__(self): return hash((self.x, self.y))

两个细节值得强调。返回NotImplemented而不是False,是为了让 Python 有机会去尝试对方的反射比较,这在跨类型比较时更健壮;实现__eq__后必须显式实现__hash__,否则 Python 会把__hash__置为None,对象直接不可哈希,放进set会报错。我第一次遇到这个报错时完全没往__eq__上想,因为报错信息指向的是set.add那一行。

3.3super()的调用位置和继承链顺序

继承写多了之后,super()的坑集中在两处。第一处是忘记调用父类初始化:

class Animal: def __init__(self, name): self.name = name class Dog(Animal): def __init__(self, name, breed): self.breed = breed # 漏了 super().__init__(name)

这样写不会报错,但dog.name访问时才炸,错误信息和真正的根因隔了好几层调用。第二处更隐蔽,出现在多重继承里。Python 用 MRO(方法解析顺序)决定找方法的路径,菱形继承下super()不是"调父类",而是"调 MRO 链上的下一个"。我用一个简化例子演示:

class A: def __init__(self): print("A") super().__init__() class B(A): def __init__(self): print("B") super().__init__() class C(A): def __init__(self): print("C") super().__init__() class D(B, C): def __init__(self): print("D") super().__init__() D() print([c.__name__ for c in D.__mro__])

输出顺序是 D → B → C → A,而不是 D → B → A 然后 C。原因是super()沿着D.__mro__往后走,B 里的super()指向的是 C。这个机制保证了每个父类的初始化只执行一次,但也要求所有协作的类都必须一致地调用super(),只要有一个环节漏掉,整条链就断了。实务上我的建议很直接:多重继承尽量只用于"混入"式的窄接口,继承层次不要超过两层,复杂场景优先用组合。

3.4 属性名和方法名撞车

这个坑我犯过两次,都是因为起名太随意:

class User: def __init__(self, name): self.name = name def name(self): # 方法名和属性名重复 return self.name

类体里先定义__init__,再定义name方法,两者在类__dict__里会互相覆盖(这里是方法覆盖了……其实不会,因为self.name是实例属性,位于实例字典里,优先级更高)。实际运行时u.name拿到的是字符串,u.name()就会报"str is not callable"。这类报错信息看起来像类型问题,实际是命名冲突。

更麻烦的是反过来:先有属性赋值,后来加了个同名方法,所有原来读属性的地方突然读到方法对象,报错位置全都变了。我的规避办法很简单——动词化的方法名、名词化的属性名,get_name()这种写法虽然啰嗦,但在团队协作里歧义最小;同时避免用list、dict、id、type这类内置名做属性名,否则在方法里想用内置函数时会被self.xxx遮蔽,得写builtins.list才行,非常别扭。

3.5 对象之间互相持有引用导致的状态混乱

做练习项目时很容易写出这种结构:Order里有User,User里又有orders列表,两边都存一份。任何一侧更新,另一侧就不同步。这类问题不是语法坑,是建模坑,但初学阶段特别容易踩,因为双向引用写起来最"自然"。

我的处理原则是单向持有 + 查询方法:订单持有用户引用(因为订单必然属于某个用户),用户不直接持有订单列表,需要时通过仓储对象或外部查询获得。这样状态只有一个真相来源,避免了"改了这边忘了那边"的经典问题。如果确实需要双向导航,那就要把同步逻辑收进唯一一个入口方法里,绝不允许外部直接改其中一侧的集合。

3.6 在方法里无节制地链式访问

封装被破坏最常见的形态不是有人直接改私有属性,而是有人写出了order.user.address.city.name这样的长链。乍看是只读访问没问题,但它把三个类的内部结构硬编码进了一个调用点,任何一层结构变动都会连锁影响到这里。

我在重构时遇到过一段代码,统计"某城市的订单量",写的是遍历订单然后链式取城市名。后来地址结构从address.city改成address.region.city,全项目几十处链式访问全要改。如果当初在Order上提供city_name()这么个方法,改动点就只有一个。迪米特法则听起来很抽象,落到实操就是一句话:一个对象应该只和它的直接伙伴说话。

4. 封装、继承、多态落到代码里的真实样子

这三个词被讲烂了,但真正的问题从来不是"它们是什么意思",而是"什么时候该用、用错了会怎样"。这一节按这个角度来拆。

4.1 封装的控制点画在哪里

封装的本质是给状态变更划定唯一入口,语言层面的下划线只是提示,不是强制。Python 里_x是约定,__x会做名字改写(变成_ClassName__x),但都不是真正的外部不可访问。所以封装的可靠性来自设计纪律,而不是语法。

判断封装是否到位,我一般用两个问题自测。第一,这个属性有没有可能被改成一个非法值?如果有可能,就必须有方法来做校验。第二,这个属性的变更会不会引起其他属性不一致?如果会,就必须把变更收进方法里,在方法内部同时维护多个字段。

举个具体例子。一个账户类如果有balance和frozen两个状态,直接允许外部改balance,就会出现"已冻结账户余额还在变"的不一致。更好的写法是只暴露deposit、withdraw、freeze三个操作,余额只读:

class Account: def __init__(self, balance=0): self._balance = balance self._frozen = False @property def balance(self): return self._balance def deposit(self, amount): if self._frozen: raise RuntimeError("账户已冻结") if amount <= 0: raise ValueError("金额必须为正") self._balance += amount

@property在这里的作用不是"把属性变成方法",而是保留读取语法的同时去掉写入入口,读起来像属性,改起来必须走方法。这个折中在 Python 里非常好用,Java 里对应的写法是 getter 加私有字段,思路一致。

不过要提醒一句:不是所有字段都值得包一层property。我见过把每个字段都写一遍 getter/setter 的类,几十行样板代码,除了增加阅读负担没有任何校验逻辑,这种封装是无效封装。只有当读取或写入需要附加行为时,包一层才有意义。

4.2 继承的取舍:什么时候组合更划算

继承表达的是"是一种"的关系,这个判断标准大家都知道,但真到写代码还是容易滥用。我的经验是加一道额外检查:子类能不能完全替代父类而不出问题。如果替换之后行为变了、调用方需要额外判断类型,那这个继承关系就是错的。

一个典型反例:

class Bird: def fly(self): ... class Penguin(Bird): # 企鹅是鸟,但不会飞 def fly(self): raise NotImplementedError

强行继承的结果是调用方拿到Bird之后还得判断能不能飞,多态直接失效。更好的做法是把"飞行"抽成能力接口,让会飞的类去实现;或者干脆不用继承,用组合——Penguin持有一个游泳行为的对象。

组合的优势在于耦合更松:被组合的对象可以随时替换、可以注入不同的实现用于测试、不会因为父类加了个方法就莫名继承一堆用不着的东西。代价是多一层跳转,读代码时要在两个文件之间来回看。我的实际取舍是:行为复用优先组合,接口统一优先继承。换句话说,我为了"少写重复代码"时用组合,为了"让调用方按同一套约定处理不同对象"时才用继承(通常是抽象基类或协议)。

还有一点关于抽象基类的实践体会。Python 里可以用abc.ABC加@abstractmethod来强制子类实现某些方法,好处是把"接口约定"从口头约定变成运行时检查——子类漏实现时,实例化那一刻就报错,而不是等到某个分支被跑到才炸。代价是引入了一点继承依赖。在团队项目里我倾向于用它,因为新人漏实现方法的概率实在不低;个人小工具里就宽松一些,靠文档和类型注解约束。

4.3 多态的判定标准:同一调用、不同实现

多态最常见的误解是"父类引用指向子类对象"这句话本身,它描述的是现象,不是标准。真正的标准只有一条:调用方用同一段代码处理不同子类,且不需要知道具体类型。

class Shape: def area(self): raise NotImplementedError class Circle(Shape): def __init__(self, r): self.r = r def area(self): return 3.14159 * self.r ** 2 class Rect(Shape): def __init__(self, w, h): self.w, self.h = w, h def area(self): return self.w * self.h def total_area(shapes): return sum(s.area() for s in shapes) # 不关心具体是哪种形状

total_area完全不知道shapes里装的什么,只依赖"有area()方法"这一约定。这段代码要新增三角形,total_area一行都不用改——这才是多态带来的实际收益。

由此可以反推一个判断方法:如果你在调用方写了isinstance或type() ==来判断分支,那么多态就没有用起来。这种代码在改动时是最痛的,每加一个类型就要去改所有分支。我重构过一段用if type == "A"处理接口响应的代码,把每个分支抽成一个处理类之后,新增类型变成了"加一个文件",而不是"改五个地方"。

在 Python 里还要补一句:多态不依赖继承。只要两个类都有同名方法,就能一起用,这叫鸭子类型。这种灵活性很好用,但也意味着错误发现得更晚——把方法名拼错,要到运行时才报AttributeError。缓解办法是用typing.Protocol声明期望的接口,配合类型检查工具在写代码阶段就发现不一致。这个技巧在接口边界多、参与人多的时候价值很高。

5. 用一个记账本把知识点串成一条线

光看概念容易散,我习惯用一个小项目把知识兜一遍。下面这个记账本足够小,但覆盖了构造、属性、方法、封装、继承、多态六个点,可以当成练习模板。

5.1 先把职责拆出来再动手写类

动手之前先问一句:这个系统里有几类"会变化的东西"?记账本里有两类:一笔记录(Entry)和账本(Ledger)。金额、类别、日期属于记录的属性;增删查、汇总属于账本的行为。这样的划分理由是:记录是值对象,创建后基本不变;账本是容器,负责维护集合的一致性。

再往上一层,如果要做多种报表输出,就会引出第三个抽象:报表(Report),它接收账本、产出文本。这类"输入数据、输出结果"的对象很适合用多态来组织,因为报表种类会不断新增,而账本的代码不该跟着改。

有心的人会注意到,这个拆解过程本身没有写一行代码,但它决定了后面所有代码的形状。我见过太多一上手就写class Manager然后什么都往里塞的例子,几百行之后谁也不敢改动。类的边界应该在写第一行之前就想清楚,而不是写完再拆。

5.2 核心代码走读

from dataclasses import dataclass, field from datetime import date @dataclass(frozen=True) class Entry: amount: float category: str day: date class Ledger: def __init__(self): self._entries = [] def add(self, entry): if entry.amount == 0: raise ValueError("金额不能为零") self._entries.append(entry) def total(self, category=None): items = self._entries if category is not None: items = [e for e in items if e.category == category] return sum(e.amount for e in items) def __len__(self): return len(self._entries) def __iter__(self): return iter(self._entries)

这里几个选择都值得说明。Entry用@dataclass(frozen=True)而不是手写__init__,是因为它是纯数据,不需要变,冻结之后可以安全地放进set、当作字典键,也避免了"记录被中途改掉导致汇总对不上"的问题。Ledger的_entries用单下划线,表示内部状态;add里做了一次基本校验,把非法数据挡在入口。__len__和__iter__让账本可以直接用len()和for,调用方不用关心内部是列表还是别的结构。

接着用多态组织报表:

class Report: def render(self, ledger) -> str: raise NotImplementedError class CategoryReport(Report): def render(self, ledger): cats = {} for e in ledger: cats[e.category] = cats.get(e.category, 0) + e.amount lines = [f"{k}: {v:.2f}" for k, v in sorted(cats.items())] return "\n".join(lines) class MonthlyReport(Report): def render(self, ledger): months = {} for e in ledger: key = e.day.strftime("%Y-%m") months[key] = months.get(key, 0) + e.amount return "\n".join(f"{k}: {v:.2f}" for k, v in sorted(months.items()))

现在新增一种报表,只需要写一个新类,Ledger和已有的报表代码都不用动。这就是第 4 节讲的多态在真实代码里的收益。而且报表类不持有账本,每次render传入,避免了双向引用,也方便拿固定数据做测试。

5.3 测试切入点与几个扩展方向

写完代码后我一般先测三个地方。第一是边界值:金额为零、负数、超大值;第二是集合操作的一致性:add之后len是否同步、迭代出来的顺序是否稳定;第三是多态的替换性:任何Report的子类都能被同一个调用方正确使用。

扩展方向上比较自然的有三个。加一个remove方法,但要注意frozen的Entry怎么定位——这正好引出"值对象用相等性查找"的话题;加持久化,把Ledger的读写抽成一个Storage接口,文件、内存实现各写一份,用组合注入到Ledger里;加过滤报表的装饰器,比如"只统计最近 30 天",用包装而非修改原有报表类的方式实现。这三个扩展都不需要改动已有逻辑,是检验设计是否松耦合的好办法。

6. 从会写类到会设计类的进阶路子

能写出正确的类之后,下一个台阶是写"让别人愿意读"的类。这个阶段更多是习惯和约定问题,语法层面的新东西不多。

6.1 魔术方法要挑着学,别一次铺开

Python 的魔术方法清单很长,但日常真正高频的就那么几个,按使用频率我排个序:__init__、__repr__、__str__、__eq__、__len__、__iter__、__call__,再往后是上下文管理的__enter__/__exit__、比较运算的一组、属性访问的__getattr__/__setattr__。

其中__repr__的优先级被我故意提前——它是调试时的第一手信息。默认的<object object at 0x7f...>打印出来对你毫无帮助,而一句Entry(amount=12.0, category="餐饮", day=...)能省掉无数次断点。我的习惯是凡是自定义的数据类,第一件事就是补__repr__,用dataclass的话自动就有了。

__getattr__和__setattr__这类要格外小心,因为它们会拦截所有未定义的属性访问,一旦写错,AttributeError会被吞掉或者打出让人崩溃的递归调用。我一般只在做代理对象或者懒加载时用__getattr__,并且第一行就把下划线开头的属性放行,避免内部访问也被拦。

6.2 一份可以照着自测的清单

下面这十条是我给阶段自查用的,能全部答上来,说明类和对象的基本功已经过关:

序号自测问题答不上来通常意味着
1__new__和__init__的调用顺序和职责差别对对象创建过程不清楚
2类属性和实例属性在读写时的不对称规则还没理解属性查找链
3为什么可变对象不能直接当默认参数没意识到默认值只求值一次
4self是绑定机制填的,不是语言强制的名字只用过不会解释
5__eq__和__hash__为什么必须成对没碰过集合去重的坑
6MRO 顺序怎么算,super()到底调谁没碰过多重继承
7什么时候该用组合而不是继承设计层面还没建立起判断标准
8多态的判定标准是不需要isinstance还停留在语法记忆
9property适合用在什么场景封装停留在形式层面
10鸭子类型的风险以及怎么缓解没用过Protocol或类型检查

我在实际带人的过程中发现,第 6 条和第 7 条是最能区分"知道语法"和"会设计"的。前者要求你在脑子里能跑一遍 MRO,后者要求你放弃"复用代码就用继承"的直觉。等这两个问题能张口就答,类和对象这块基本就过去了。

最后分享一个我自己用的练习方法,比读教程有效得多:找一个自己写过的小脚本(比如处理 CSV 的、爬取页面的、算统计的),把它重构成两三个类,重构前后各写一份 README 说明结构。这个过程中你会被迫回答"这个函数为什么放在这个类里"这类问题,而正是在回答这些问题的过程中,类和对象才从语法变成你手里的工具。我当年这么练完之后,再看到别人的类定义,脑子里想的第一件事已经从"这段语法是什么"变成了"他为什么这样划分边界",这个视角的转变,就是入门和入门之后的分水岭。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 4:31:16

AI Agent长期记忆选型指南:6大方案核心差异与生产实测

1. 为什么“长期记忆”成了Agent开发绕不开的坎儿最近三个月&#xff0c;我陆陆续续帮五家不同行业的客户搭过AI Agent系统——有做智能客服中台的SaaS公司&#xff0c;有给制造业产线做设备巡检助手的技术团队&#xff0c;还有两家在试水私域内容自动运营的MCN机构。几乎每聊到…

作者头像 李华
网站建设 2026/10/31 4:30:56

AI大模型如何重塑外科手术范式:从辅助到自主决策

1. 项目概述&#xff1a;AI大模型如何重塑外科手术范式在手术室里&#xff0c;主刀医生正通过3D影像观察患者腹腔内的胆管结构&#xff0c;手术机器人机械臂上的电钩精准悬停在目标组织上方0.2毫米处。与传统场景不同的是&#xff0c;这次机械臂的运动轨迹并非完全由医生操控&a…

作者头像 李华
网站建设 2026/10/31 4:30:34

GPT代充系统架构设计与风控优化实践

1. 项目背景与痛点分析去年底GPT-4 Turbo发布后&#xff0c;我经营的AI工具导航站开始提供GPT Plus代充服务。最初使用Stripe作为支付通道&#xff0c;但连续遭遇三次风控封号&#xff1a;第一次因为短时间内相同金额交易过多&#xff0c;第二次因跨境支付验证失败&#xff0c;…

作者头像 李华
网站建设 2026/10/31 4:30:31

回文链表判断:快慢指针与链表反转实战

1. 回文链表问题解析回文链表是LeetCode热题100中的经典题目&#xff08;编号234&#xff09;&#xff0c;要求判断一个单链表是否为回文结构。所谓回文链表&#xff0c;就是正读和反读都相同的链表结构&#xff0c;比如[1,2,2,1]或[1,2,3,2,1]。这个问题看似简单&#xff0c;但…

作者头像 李华
网站建设 2026/10/31 4:30:27

从jq22扒代码到工程化复用:前端插件选型、封装与避坑指南

前端这行干久了&#xff0c;多少都有过"逛资源站逛到凌晨两点"的经历。jq22 这类前端资源分享站&#xff0c;对很多人来说是"抄作业圣地"——轮播、图表、弹窗、加载动画&#xff0c;搜索框里敲几个字&#xff0c;几十个能直接跑的 demo 就摆在眼前。但我见…

作者头像 李华
网站建设 2026/10/31 4:29:56

三维地理数据格式OSGB与I3S的技术对比与转换实践

1. 三维地理数据格式的演进与挑战在数字孪生和智慧城市建设的浪潮下&#xff0c;三维地理信息系统&#xff08;3D GIS&#xff09;正经历着从专业领域向大众化应用的转型。作为这个领域的核心载体&#xff0c;数据格式的演进直接决定了三维场景的构建效率和应用广度。OSGB&…

作者头像 李华