news 2026/10/14 23:46:46

Python类和对象进阶:从会写类到会设计类

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python类和对象进阶:从会写类到会设计类

“类和对象(2)”这个标题放在目录里,翻译成大白话就是:类,你已经会建了;现在该聊聊怎么建得好。很多朋友第一课结束就能写出class Student: pass,然后student = Student()这种代码,觉得面向对象不过如此。但第二课一打开,看到类方法、静态方法、特殊方法、property、继承重写、多态这一堆名词,整个人就懵在原地。这篇内容我把“类和对象(2)”这部分拆开揉碎,从属性归属讲到继承多态,再到调试实战,目标是让你学完以后,不再只是“会写类”,而是“会设计类”。这堂课没有高深到让人劝退,但它决定了你和“背语法”之间的真正差距。

1. 为什么类和对象要拆成两课讲

1.1 第一课和第二课的分界线在哪里

第一课教的是语法骨架:class关键字长什么样、__init__怎么初始化、self指谁、怎么把一个对象建出来。这些属于“照着抄就能写”的东西。第二课教的是设计骨架:属性该放在类里还是实例里、方法该被谁调用、多个类之间怎么组织协作。很多人第一课学完觉得“太简单了”,就是因为只掌握了语法外壳,没接触真正费脑子的部分。

拿宠物举例子。第一课你可能会写一个Dog类,里面写了eat()、sleep()、bark(),然后很开心地创建了几只狗。第二课你会突然发现,如果你还要写Cat类,里面又要重复一遍eat()和sleep(),这时候就自然产生了一个疑问:能不能把公共的吃和睡抽出来,放到一个父类里,让Dog和Cat只保留自己不同的叫声?这个“抽取公共逻辑”的动作,就是第二课的核心价值。

所以分界线的本质是:从“会创建一个类”到“会设计一类对象的体系”。这中间隔着的不是知识点数量,而是思维方式的转变。

1.2 先看一遍第二课的完整任务清单

“类和对象(2)”通常会覆盖这样一组内容,每个点都对应一类具体问题:

  • 属性归属:类属性和实例属性到底有什么区别,哪些数据该共享,哪些数据该独占。
  • 方法类型:实例方法、类方法、静态方法分别解决什么问题,什么时候用哪个。
  • 特殊方法:怎么让自定义类支持print()、==、for、with这些语言级操作。
  • 继承与重写:如何从父类提取公共逻辑,子类如何做差异化扩展。
  • 封装与多态:数据访问如何加防线,代码如何做到“加新类型不改老逻辑”。

把这些点连起来看,你会发现它们其实是一条线:先搞清楚“单个类内部的数据和方法怎么组织”,再搞清楚“多个类之间怎么建立关系”,最后落到设计原则和调试实践上。这一课学完之后,你再看别人写的业务代码,就不会总觉得“这坨代码很怪但说不清哪里怪”了。

2. 属性归属与self的本质:先分清哪些数据是共享的

2.1 类属性和实例属性差在哪

先看一段最普通的代码:

class Student: school = "某中学" # 类属性 count = 0 # 类属性 def __init__(self, name): self.name = name # 实例属性 Student.count += 1

这里的school和count是类属性,属于类本身;而name是实例属性,每个学生对象都有自己的那一份。访问的时候,Python 会先到实例自己的属性字典里找,找不到再去类的属性字典里找,再找不到就去父类找。

区别用一张表看更清楚:

对比项实例属性类属性
定义位置__init__或方法内,用self.xxx赋值类体内直接赋值
归属每个实例单独一份全类共享一份
访问方式self.xxx类名.xxx或self.xxx
典型用途每个对象独有的数据,如姓名、学号公共配置、计数器、默认值

共享的意思要理解透:Student.count是全班共享的计数器,任何学生对象创建时给它加一,这个数是所有实例共同维护的。而s1.name = "A同学"不会影响s2.name,因为实例属性天然隔离。

2.2 通过实例赋值为什么改不到类属性

这是新手第二课最常踩的坑,没有之一。

class Student: school = "某中学" s = Student() s.school = "某大学" print(Student.school) # 某中学 print(s.school) # 某大学

看起来你“改了”school,但实际上s.school = "某大学"这行代码不是在修改类属性,而是在s自己的属性字典里新建了一个school,把类属性给遮蔽掉了。类属性仍然是“某中学”,只是这个实例再也看不到了。

如果你真想全局修改,必须走类名:

Student.school = "某大学"

注意:区分“读共享”和“写遮蔽”。读的时候实例能顺藤摸瓜找到类属性,但一旦用实例做赋值操作,就会在自己身上开一块新空间,把类属性盖住。面试里经常拿这个点出题,答错的人相当多。

2.3 三种方法到底怎么选

类里除了__init__,日常接触最多的是这几种方法:

class Student: count = 0 def __init__(self, name, score): self.name = name self.score = score Student.count += 1 def show_info(self): # 实例方法 return f"{self.name} 的分数是 {self.score}" @classmethod def from_dict(cls, data): # 类方法 return cls(data["name"], data["score"]) @staticmethod def is_valid_score(score): # 静态方法 return 0 <= score <= 100

选择逻辑其实很朴素:

  • 实例方法:需要读取或修改实例属性,方法体里离不开self,这是最常见的类型。
  • 类方法:方法逻辑和类本身相关,但不需要某个具体实例,装饰器@classmethod让它接收的第一个参数是cls而不是self。典型用途是“备选构造函数”,比如从字典创建学生对象:Student.from_dict({"name": "小A", "score": 88}),这样调用方不用关心类内部的构造细节。
  • 静态方法:方法逻辑和类、实例都没关系,只是恰好放在这个类里而已,用@staticmethod标记。比如分数是否合法的判断,你完全可以在模块级写个普通函数,但放在类里更便于调用和组织。

很多教程把类方法讲得很玄,其实它在真实项目里最常用的场景就是备选构造器和操作类级状态。抓住“第一个参数是 cls 还是 self”这个判断点,用起来就不纠结了。

3. 特殊方法实战:几行代码让类变得好用

3.1str__和__repr:打印对象时不再看到内存地址

新手第一次打印自定义对象,多半会得到类似<__main__.Student object at 0x7f8c1a2b3c40>的输出,看着就像天书。这就是因为没有实现特殊方法。

class Student: def __init__(self, name, score): self.name = name self.score = score def __str__(self): return f"Student({self.name}, {self.score})" def __repr__(self): return f"Student(name={self.name!r}, score={self.score!r})" s = Student("小A", 92) print(s) # 调用 __str__ print(repr(s)) # 调用 __repr__

__str__是给用户看的,要求简洁易读;__repr__是给开发者调试用的,理想情况下它的返回值能直接还原出这个对象。如果只实现一个,建议优先实现__repr__,因为当__str__缺失时,Python 会回落到__repr__。

注意:方法名千万别拼错。__str__写成__str_或者少了魔法方法两侧的双下划线,Python 都会把它当成普通方法,打印结果依旧是内存地址。

3.2 __eq__和比较相关的方法:让对象能用==比较内容

默认情况下,==比较的是两个对象的身份,也就是内存地址。两个学生对象内容一模一样,但它们不是同一个对象,所以==结果是False。这往往不符合业务直觉。

class Student: def __init__(self, student_id, name): self.student_id = student_id self.name = name def __eq__(self, other): if not isinstance(other, Student): return NotImplemented return self.student_id == other.student_id

这样两个学号相同的学生对象,用==判断就是相等的。学号是业务上唯一标识学生的东西,用它做相等性判断,比默认的内存地址靠谱得多。

需要注意,Python 3 里一旦实现了__eq__,对象的__hash__会被自动设为None,导致它无法作为字典的 key 或放进 set。如果你希望对象既能比较相等又能正常哈希,需要同时定义__hash__。但在大多数业务场景里,自定义类用==比较内容就够了,哈希的问题按需处理,不必一开始就把所有魔法方法都堆上去。

至于排序,自定义类不一定要实现__lt__。我更推荐直接用:

sorted(students, key=lambda s: s.score)

key参数让排序逻辑一目了然,比在每个类里都实现一堆比较运算符更省事。

3.3enter__和__iter:让类融入Python语言生态

特殊方法真正的威力,是让你的类能无缝配合 Python 语法。比如实现__iter__,对象就能直接放进for循环里:

class ScoreSheet: def __init__(self, students): self._students = students def __iter__(self): return iter(self._students) sheet = ScoreSheet([Student("小A", 92), Student("小B", 85)]) for s in sheet: print(s.name, s.score)

__iter__返回一个迭代器,iter(self._students)直接复用列表的迭代器即可。这样设计之后,调用方不需要关心ScoreSheet内部是用列表还是元组存数据,只要它能被遍历就行。

同理,实现__enter__和__exit__可以让对象配合with语句做资源管理,比如自动关闭文件、自动释放连接。这背后的逻辑不复杂,但理解了“特殊方法让自定义类融入语言生态”这个思想,以后再看到len(obj)、with obj、for x in obj这种写法,你就知道它们统统能挂到自定义类身上。

4. 继承与重写:把公共逻辑收上来,把差异逻辑放下去

4.1 继承的写法与父子分工

继承的第一直觉是“复用代码”,但更准确地说,它是在表达“is-a”的关系:学生是一个人类,猫是一个动物类。父类放公共逻辑,子类放差异逻辑。

class Person: def __init__(self, name, age): self.name = name self.age = age def introduce(self): return f"我叫{self.name},今年{self.age}岁" class Student(Person): def __init__(self, name, age, student_id): super().__init__(name, age) self.student_id = student_id def introduce(self): return super().introduce() + f",学号是{self.student_id}"

这里Student继承了Person的name、age和introduce,同时新增了student_id,还重写了introduce在原有基础上追加学号信息。父类的公共逻辑只用写一遍,子类各自扩展差异点,这就是继承的典型分工。

写继承的时候有一个原则很重要:如果两个类之间没有明确的“is-a”关系,不要为了复用强行继承。比如“订单”和“用户”之间是“属于”的关系,不是“是一种”的关系,这时候更应该把用户作为订单的属性成员,而不是让订单继承用户。组合优于继承,是比语法更优先的设计判断。

4.2 super().init()为何不能省略

很多新手在子类里写了自己的__init__,但不调用super().__init__(),结果创建子类对象后发现父类里定义的属性全都不存在,一脸疑惑。

原因是:父类的__init__并不会自动执行,子类重写了__init__之后,父类的初始化逻辑就完全被覆盖了。如果你希望子类对象拥有父类设置的属性,就必须显式调用super().__init__(参数)。

class Student(Person): def __init__(self, name, age, student_id): # 如果漏掉这一行,self.name 和 self.age 都不存在 super().__init__(name, age) self.student_id = student_id

建议在任何子类重写__init__时,第一行就写super().__init__(...)把父类需要参数传进去。这是一种强制性习惯,不是可选项。调试时如果发现子类实例缺属性,优先检查__init__里有没有调用父类初始化。

4.3 重写方法时千万别破坏调用方的预期

重写方法不是随心所欲乱改。父类方法相当于对调用方做了契约承诺,子类重写后,必须保持行为上的兼容。

举个反例:父类Person.introduce()返回“我叫X,今年X岁”,子类重写后只返回“我是学生”,把名字年龄全丢了,调用方本来期望拿到完整介绍的,结果信息缺失。这种做法就破坏了调用方对父类的预期。

用一句大白话概括:凡是能用父类的地方,换成任何子类都应该能正常跑。这叫“里氏替换”,听起来高大上,落到具体写法就是两件事:

  • 子类方法的返回值、抛出异常的行为,要和父类大致兼容。
  • 如果只是在父类逻辑前后加点东西,优先用super()调用父类方法,而不是把整个方法逻辑推倒重写。

比如introduce里先super().introduce()拿到原始介绍,再拼接学号,这种“增强式重写”就是安全的重写方式。

5. 封装与多态:面向对象的两根定海神针

5.1 单下划线与双下划线:Python的“私有”到底是怎么回事

很多从其他语言转过来的同学,第一反应是找private关键字,结果发现 Python 没有。它用的是约定和名字重整机制。

  • _name:单下划线开头,是约定。看到它的人会明白“这是内部成员,外部不要直接碰”,但它并不会阻止你访问。
  • __name:双下划线开头,会触发名字重整。在类定义内部,它会变成_ClassName__name,主要目的是防止子类不小心覆盖父类的内部属性,而不是真的加密。
class Student: def __init__(self, name): self.__name = name s = Student("小A") print(s._Student__name) # 小A,名字被重整了 print(s.__name) # AttributeError,因为类外部访问不到这个原始名字

把这段讲清楚,能避免新人在调试里看到_Student__name这种奇怪属性时一头雾水。封装在 Python 里的意义不是“绝对禁止访问”,而是“明确告知边界”,剩下的靠开发者自觉。

5.2 property实战:给属性加一道校验关卡

直接暴露属性虽然方便,但无法控制赋值。比如分数属性,如果外部直接student.score = 150,业务逻辑就崩了。用property可以把赋值动作拦截下来做校验:

class Student: def __init__(self, score): self._score = score @property def score(self): return self._score @score.setter def score(self, value): if not (0 <= value <= 100): raise ValueError("分数必须在0到100之间") self._score = value s = Student(88) s.score = 95 # 正常 s.score = 150 # 抛出 ValueError

外部调用的时候写的是student.score = 95,看起来还是在直接操作属性,但背后已经经过了校验。这就是property最妙的地方:接口保持不变,内部实现随意升级。将来你想把score改成“平时成绩加期末成绩加权计算”,调用方一行代码都不用改。

注意:内部存的是self._score,外部访问的是self.score。命名上故意用单下划线区分“内部存储”和“对外接口”,这是 Python 社区很常见的写法。

5.3 多态的典型写法:只关心方法,不关心类型

多态用大白话说就是:调用方只关心对象会不会某个方法,不关心它到底是什么类。

class Dog: def sound(self): return "汪汪" class Cat: def sound(self): return "喵喵" class Bird: def sound(self): return "啾啾" def make_sound(animal): print(animal.sound()) make_sound(Dog()) make_sound(Cat()) make_sound(Bird())

make_sound函数根本不需要检查传入对象的类型,也不要求它们有共同的父类,只要你有sound这个方法,我就能统一调用。这就是“鸭子类型”:走路像鸭子、叫起来像鸭子,那它就是鸭子。

这种设计带来的好处是扩展性极强。以后想加一个Cow,实现sound()返回“哞哞”,make_sound一个字都不用改。在真实项目里,多态配合继承会更强:父类定义了接口,子类各自实现不同行为,外部代码永远只面向父类接口编程。加新子类、改某一种行为,都不会波及调用方。

6. 常见报错与排查技巧实录

6.1 可变类属性:一个实例改了,全班数据都变了

这是我见过最经典的共享陷阱。

class Student: tags = [] def __init__(self, name): self.name = name s1 = Student("小A") s1.tags.append("优等生") s2 = Student("小B") print(Student.tags) # ['优等生'] print(s2.tags) # ['优等生'],小B莫名也被打上了标签

问题出在tags = []是类属性,所有实例共享同一个列表。一个实例往里append,所有实例都能看到变化。解决办法很简单:可变属性放到__init__里,用self.tags = []创建实例独立的列表。

排查技巧也很固定:如果一个实例修改的数据“波及”到了其他实例,十有八九就是可变对象被做成了类属性。检查的时候先看类体里有没有直接定义[]、{}、set()。

6.2 __init__的默认参数和返回值:两个隐藏雷区

可变默认参数是函数层面的经典坑,放到类里照样踩:

def __init__(self, name, tags=[]): # 错误示范 self.name = name self.tags = tags

默认参数在函数定义时就创建一次,所有没传tags的实例会共享同一个列表。正确写法是把默认值设为None:

def __init__(self, name, tags=None): self.name = name self.tags = tags if tags is not None else []

另一个雷区是__init__的返回值。__init__这个方法只能返回None,如果写了return 1,解释器直接抛TypeError: __init__() should return None。很多人把__init__想象成构造函数,觉得可以返回一个对象,但实际上创建对象的过程由__new__负责,__init__只负责初始化,不负责返回东西。

6.3 用vars()和dir()快速定位缺属性和隐藏属性

排查类相关问题,有两个内置工具特别好用。

  • vars(obj)返回对象的属性字典,也就是obj.__dict__,可以清楚地看到实例上有哪些属性、值是什么。
  • dir(obj)列出对象所有可访问的属性名和方法名,包括从父类继承来的。

比如子类实例缺属性,先执行vars(instance),如果里面没有name、age这些应该存在的键,基本可以断定父类__init__没被调用。再比如调试时看到_Student__name这种奇怪属性,用dir(obj)能看到全貌,就不会觉得代码“见鬼”了。

6.4 常见问题速查表

现象可能原因排查思路
一个实例改了列表,其他实例也变了可变对象被定义成类属性检查类体里是否有[]、{}、set()
子类实例缺父类的属性子类__init__忘了调用super().__init__()用vars(instance)查看属性字典
打印对象输出一串内存地址没实现__str__或__repr__补实现,并确认方法名拼写正确
两个内容一样的对象用==是 False没实现__eq__实现__eq__并定义相等逻辑
通过实例给类属性赋值后好像没生效实例属性遮蔽了类属性全局修改必须用类名.属性 = 新值
__init__里return一个非 None 值__init__不允许返回值删掉return语句

7. 把类写得更像生产代码的几个习惯

7.1 判断一个类设计好坏的三个标准

第一,一个类能不能用一句话说清它是干什么的。如果答案里出现“还有”“顺便”这些词,说明职责不够单一。比如“这个类管理学生信息,还负责发邮件”,这就是两个职责搅在一起,以后任何一个需求变了都要跑来改这个类。

第二,类之间的关系是组合还是继承。两个类之间如果没有明确的“is-a”关系,优先考虑组合。比如“订单”包含“商品列表”,这就是组合;只有“学生”确实是“人类”的一种,继承才合理。

第三,对外接口是否稳定。类的方法名和参数尽量少变,因为外部调用方为了等你改接口要跟着改代码。宁可内部实现翻天覆地,也要保持对外方法签名稳定,这其实就是封装带来的直接约束。

7.2 先写最小可用类,再考虑“高大上”特性

很多初学者一上来就想写个大而全的类,类方法、静态方法、特殊方法、继承全都要用上,结果写出来的类又绕又难调。我的建议是:先写一个最简单但能跑的类,只包含__init__和一个核心业务方法。让它先产生几个对象,把数据流跑通。然后再根据实际使用的不方便,逐步加__str__、__eq__、property,最后再考虑是不是要抽父类。

类就是这样,一开始只是一个对象工厂,用着用着发现很多地方重复了,再提炼。与其一开始设计一个“完美”的类,不如让它从简单需求里自然生长出来。这种“需求驱动”的做法,比“功能堆砌”更能训练设计能力。

7.3 学习路径建议:怎么把这一课吃透

看完这篇别急着翻到下一节,立刻做一个综合小练习:设计一个Order类,包含订单项列表、总价计算、格式化输出(实现__str__)、按订单号判断相等(实现__eq__)、支持for遍历所有订单项(实现__iter__)。再做一个小项目:一个MemberOrder继承它,多一个会员折扣方法。做完这个,第二课的内容你就基本消化了。

我每次带新人,都会让他们把类和对象的例子在纸上画出“哪些数据属于类,哪些数据属于实例”,这张图画明白,后面继承、多态、property 都顺了。“类和对象(2)”难不是难在语法,难在思维转换:从“写流程”切换到“设计类型”。多写、多改、多看别人优秀的代码,这个弯你一定会转过来的。

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

从状态向量求轨道六要素:例题4.3完整计算与验证

每次拿到一组“位置速度”的数字&#xff0c;我最想知道的不是这串数字本身&#xff0c;而是它背后的轨道长什么样。状态向量确定轨道&#xff0c;是轨道力学里最基础、也最常被问起的操作&#xff1a;给定某时刻地心赤道惯性系下的位置矢量 r 和速度矢量 v&#xff0c;反推出轨…

作者头像 李华
网站建设 2026/10/14 23:44:25

内容安全红线:为何某些标题无法处理

抱歉&#xff0c;这个标题我没法处理。该标题疑似涉及访问境外网络工具或相关内容的代称/隐语&#xff0c;属于内容安全红线&#xff0c;我不能就此生成任何内容。建议更换一个明确、合规的项目标题&#xff0c;我可以正常帮你创作。

作者头像 李华
网站建设 2026/10/14 23:43:18

端侧轻量模型商用与AI实战:从端侧推理到制造质检与长上下文陷阱

早上刷了一圈消息&#xff0c;今天的AI信息量不算小。2026年10月9日的日报&#xff0c;我挑了几个真正值得看的点&#xff1a;一款端侧模型开放商用&#xff0c;两个开源项目更新比较有料&#xff0c;制造质检那边出了一个完整落地案例&#xff0c;另外开发圈在讨论长上下文的实…

作者头像 李华
网站建设 2026/10/14 23:41:51

基于Linux和ROS2的六轴机械臂开发实战

“机械臂Linux”这个标题&#xff0c;我盯着看了很久。它不像“XXX机器人项目”那样直白&#xff0c;反而透着一股硬核玩家才懂的默契&#xff1a;把机械臂这种本来活在PLC、单片机裸机里的东西&#xff0c;搬到Linux这个生态里来折腾。我做的这个项目&#xff0c;就是一台六轴…

作者头像 李华
网站建设 2026/10/14 23:41:41

Core Animation实战:用CAShapeLayer实现流畅数据图表动画

前两天我刚处理完一个足球数据播报页的需求&#xff1a;比赛结束之后&#xff0c;要把两队全场数据用动画逐项呈现出来&#xff0c;控球率、射门次数、传球成功率、跑动距离&#xff0c;每一项都要“长”出来&#xff0c;而且动画要顺滑&#xff0c;不能掉帧。当时我没有引入任…

作者头像 李华
网站建设 2026/10/14 23:41:35

TensorFlow与Python高级技巧二合一:中级程序员工程化实战指南

1. 从“会跑通”到“能落地”&#xff1a;中级程序员为什么需要这份二合一指南如果你已经能用 Python 写脚本、能照着教程跑通一个 MNIST 手写数字识别&#xff0c;甚至能自己搭一个三四层的全连接网络&#xff0c;但一到真实项目就卡壳——模型不收敛、显存爆了、训练慢得离谱…

作者头像 李华