Python类和对象从零讲透:self、__init__与类变量实战

发布时间:2026/10/20 15:25:03

Python类和对象从零讲透:self、__init__与类变量实战 刚接触编程的人十有八九都会在“类和对象”这里卡上一阵子。我自己带过不少新人也见过形形色色的学习代码发现大家不是听不懂“类”是什么而是实在想不明白为什么非要绕这么大一圈把一个简简单单的函数调用搞出一堆“对象”“实例”“属性”“方法”的名词来我当年的感受也是一样类这个东西看起来不难但总感觉隔着一层窗户纸。这篇就打算把窗户纸捅破。我先讲清楚为什么要用类、类的核心逻辑是什么再手把手拆解class语法、__init__、self、类变量与实例变量的区别最后把新手写类时最容易踩的报错和误区全部列出来。内容定位是“类和对象上”所以继承、多态、魔法方法这些先不展开把基础地基打牢下一篇再往深走。适合刚学完函数、开始接触面向对象的人也适合工作里用了很久代码却一直没系统梳理过类概念的开发者。1. 什么时候该用类从“散装函数”到“数据与行为绑定”1.1 一个真实的翻车现场用列表记宠物数据先看一个我经常拿来举例的场景。假设你要给一个小区的宠物登记系统写代码最早的需求很简单记下每只宠物的名字、种类、年龄还有是否打过疫苗。很多刚学会列表和字典的人第一时间会写出这样的代码pet_names [旺财, 小白, 豆包] pet_ages [2, 5, 3] pet_types [狗, 猫, 狗] pet_vaccinated [True, False, True] def print_pet(index): print(f{pet_names[index]}是一只{pet_types[index]}今年{pet_ages[index]}岁打完疫苗了没{pet_vaccinated[index]})写完之后看着好像挺工整四个列表一摆函数一调完事。但过了一个星期需求来了要给每只宠物加一个“绝育状态”你要改几个列表再加一个“最近一次看病日期”呢更麻烦的是如果某只宠物去世或被领养你需要同时从四个列表里删掉同一下标的数据——漏删一个所有数据就错位了。有一次我带的新人就是在这种代码上翻车的他在print_pet里用了pet_types[index]但另一个函数里删数据时只删了pet_names结果后续所有打印的内容全都指错了宠物。排查到半夜最后发现是“多个平行列表的同步维护”这个设计问题而不是某个逻辑写错了。这就是典型的数据与行为分离造成的维护灾难。你手里拿着四组不相干的数据需要用下标把它们的“关系”强行关联起来而这个关系完全没有被代码本身表达出来。1.2 类的本质一张可以反复使用的“设计图纸”类和对象解决的就是上面这个问题。它的核心思想很简单把“一只宠物”应该拥有哪些数据以及能够做哪些事情打包成一个整体。你可以把类理解成一张设计图纸。图纸上写着一只宠物有名字、有年龄、要能叫、要能打疫苗。这张图纸本身不是什么实际的东西但它规定了将来生产出来的每一只宠物长什么样。而对象就是按照图纸实际造出来的那一个具体的“东西”——旺财、小白、豆包都是对象它们都符合图纸的规定但各自的属性值不同。用代码来表达就是class Pet: def __init__(self, name, age, pet_type, vaccinated): self.name name self.age age self.pet_type pet_type self.vaccinated vaccinated def print_info(self): print(f{self.name}是一只{self.pet_type}今年{self.age}岁打完疫苗了没{self.vaccinated}) wangcai Pet(旺财, 2, 狗, True) xiaobai Pet(小白, 5, 猫, False)你看数据不再躺在四个互不相干的列表里而是作为属性挂在每个对象身上。想增加“绝育状态”只需在Pet类里加一个属性所有对象立刻都有了想找某只宠物的信息直接从对象上拿不再依赖下标对齐。这就是类的第一个价值把数据和行为绑定在一起让代码里的每一个对象都对应现实世界里的一个具体事物读起来直观维护起来也安全。1.3 什么时候不该用类不过我也要泼一盆冷水类不是什么时候都要用。很多人学完面向对象之后会产生一种冲动——不管写什么都要塞进一个类里结果一个二十行的小脚本硬是写成了三五个类互相调用的“大工程”完全没必要。比如你就是算个文件里有多少行、统计一下高频词用几个函数串联就足够了。类的优势在于“多个同类型对象并存、且各自的内部状态需要被组织和管理”。如果全程只有一个“对象”或者根本没有任何状态需要保存那用类只会增加阅读负担。判断标准很简单如果你发现自己在一个项目里反复维护多组并列的数据结构或者多个函数都在操作同一组数据那就可以考虑用类来重构了。如果只是临时处理一次数据写函数就够了。这算是我的个人经验——先让代码痛了再引入对象理解会更深刻反过来一上来就把一切包进类里反而经常写出为了结构而结构的形式主义代码。2. 亲手写出第一个类语法骨架与__init__的底层逻辑2.1 一个最小可用的类到底长什么样很多教程上来就甩一个类定义然后一行行解释但新手看着看着就迷了。我习惯先给出一个“最小可用”的版本让读者先感受一下“这个类真的能跑起来”的感受。class Dog: def __init__(self, name, age): self.name name self.age age def bark(self): print(f{self.name}汪汪) dog1 Dog(旺财, 2) print(dog1.name) dog1.bark()这段代码里一共有四个关键部分class Dog:定义了一个名为Dog的类。def __init__(self, name, age):这个__init__方法负责在创建对象时接收参数、给对象设置初始属性。self.name name中的self指代将来创建出来的那个对象本身它用于把参数“保存到对象上”。def bark(self):是普通的方法函数描述这个对象能做什么。创建对象靠的是Dog(...)这种写法。很多人会误以为Dog(旺财, 2)是在调用某个叫Dog的函数这个理解其实不算全错——严格来说它调用的是类的构造过程。在 Python 里这个过程会先创建出一个空白实例然后自动调用__init__把参数填进去。等整行执行完dog1就是一个真实的Dog实例有自己的名字和年龄。建议你动手跑一遍然后试着自己往类里加一个新的属性比如hair_color在创建对象时一并传入。这种“加属性”的练习能最快帮助你建立“类定义就是模板、对象是模板的实例”这个认知。2.2__init__不是构造函数是初始化函数这里有个很常见的概念误区值得单独拿出来说。很多语言比如 Java、C 里类的构造方法负责“造出对象并初始化它”但在 Python 里真正的“构造过程”被分成了两步第一步是__new__负责实际分配内存、创建出一个空对象第二步才是__init__负责给这个空对象填入初始值。所以__init__严格说应该是“初始化方法”而不是“构造方法”。大部分情况下你根本不会重写__new__只需要用__init__把各种属性填好就行。这样设计的好处是创建对象时你只需要关心“我应该提供哪些初始数据”而不需要操心内存分配的底层细节。再补一个点__init__里不强制要求定义所有属性。你可以只初始化一部分其他属性之后动态添加class Dog: def __init__(self, name): self.name name dog2 Dog(小白) dog2.age 3 # 动态添加一个新属性Python 允许这么做 print(dog2.age)这种写法虽然灵活但对于真实项目我会建议尽量在__init__里把所有属性一次性定义清楚并且给出默认值。否则不同对象拥有的属性可能不一致代码里到处都是hasattr()判断维护起来非常痛苦。“能跑”和“好维护”是两回事这一点在写类的时候特别明显。2.3 点号访问属性对象本质上是命名空间当你写下dog1.name时Python 做了一件很简单的事在dog1这个对象的内部字典里查找name这个键。每个对象在 Python 内部都维护着一个字典键是属性名值就是属性值。你可以通过dog1.__dict__把它打出来看看print(dog1.__dict__) # {name: 旺财, age: 2}这个理解非常有用。它解释了为什么同一个类创建出来的不同对象之间互不干扰因为每个对象都有自己独立的那本“小字典”你给dog1.name重新赋值dog2.name完全不受影响。这也解释了一个新手经常糊涂的点为什么类的属性访问格式是“对象名.属性名”实际上就是“去哪本小字典里找哪个键”。方法也不例外——dog1.bark本质上是去小字典里找bark找到后发现它对应的是一个函数对象于是把它取出来调用。这个“命名空间”视角能帮你把之后遇到的类变量、实例变量、作用域等问题都串起来。3. self参数为什么必须存在初学者最容易卡住的那道坎3.1 self的真实身份谁调用谁就是self到了这一步大家基本能照葫芦画瓢写出简单的类了但内心一定有个疑问一直盘旋着这个self到底是什么为什么每个方法都要写它去掉行不行先给结论在 Python 里当你写object.method(...)的时候Python 会隐式地把object自身作为第一个参数传给method而这个参数在定义时写的就是self。换句话说dog1.bark()等价于Dog.bark(dog1)在第二种写法里你清楚地看到dog1被作为参数传进去了。于是self就是“接收这次调用消息的那个对象本身”。当dog1.bark()执行时self就是dog1当dog2.bark()执行时self就是dog2。所以self使得同一个方法可以被不同对象共用一套逻辑而内部通过self拿到的是各自的数据。如果没有selfbark方法根本不知道自己是“哪只狗”在叫也就拿不到名字了。你可以自己验证一下故意把dog1.bark()换成Dog.bark(dog1)跑结果完全一样。遇到任何“为什么 self 能访问到那些属性”的疑问用这种方式写出来基本立刻能看懂。3.2 名字可以换但请遵守约定有同学会问self 必须是这个名字吗答案不是只是约定。你完全可以把第一个参数命名为this或meclass Dog: def __init__(me, name, age): me.name name me.age age这样写 Python 也能正常运行。但强烈建议不要这么干。原因很实际几乎所有 Python 开发者都已经习惯了看到第一个参数写self就意味着“这是实例方法”。你换来一个奇怪的名字阅读你代码的人包括两个月后的你自己都容易愣一下然后还得去翻定义确认这里到底是什么逻辑。而且很多 IDE 和静态检查工具也默认self来做自动补全和提示。我之前见过一个同事把所有 self 换成了 this理由是“写起来更像 Java”结果代码风格跟整个项目里其他类完全不统一代码评审时候大家一致要求改回来。类本身就是一种通过命名来表达结构的工具连约定性的self都改了就破坏了这种表达力。3.3 什么时候不需要 self静态方法和类方法的引入理解 self 之后自然会出现一个疑问如果某个方法根本用不到实例的任何数据——它不读self.name也不写任何属性——那它还要不要写在类里这就是引入staticmethod和classmethod的实际场景。先看一个静态方法的例子class Dog: species 犬科 def __init__(self, name): self.name name staticmethod def get_common_species(): return 犬科 classmethod def get_species(cls): return cls.species区别在哪里实例方法普通方法第一个参数是self调用方式通常是instance.method()它访问和修改实例数据。类方法第一个参数是cls通过class.method()或instance.method()都可以调用它访问的是类本身的数据比如类变量。静态方法既不需要self也不需要cls相当于放在类命名空间里的普通函数只是逻辑上和这个类相关才放进来。可能对初学者来说这三者的区分显得有些早。但我见过太多人学完基础之后一旦往类里加了不需要 self 的方法就报错然后陷入“为什么它给我传了参数”的迷茫再夜不能寐。所以这里先简单打个铺垫你不需要现在完全搞懂只需要知道方法不一定都要带 self如果某个方法不使用实例属性可以考虑不用 self。等你回头看到报错里出现“missing 1 required positional argument: self”时再回来对照这一节会非常有帮助。4. 类变量与实例变量共享与独立的分界线4.1 变量写在谁那里决定它属于谁很多人学了类的基本语法之后会卡在一个特别隐蔽但特别关键的地方同样是“变量”写在class内部但不在任何方法里和写在__init__里用self.前缀定义到底有什么区别区别大了。直接看对比class Dog: species 犬科 # 类变量所有实例共享 count 0 # 类变量记录整个类的状态 def __init__(self, name): self.name name # 实例变量每个实例各自持有 Dog.count 1 # 修改类变量用 Dog.count变量位置决定了它的归属变量类型定义位置归属修改方式示例典型用途类变量class 方法体内、方法外类自身所有实例共享同一个值Dog.count 1全局统计数、常量、共享配置实例变量方法内通过self.定义单个实例互不影响dog1.name 新名字描述每个对象自身的状态用图纸类比的话类变量是“图纸上统一的规格”实例变量是“每间房交付时业主自己装修的风格”。图纸改了未来造出来的房子都会受影响但业主装修只影响自己那一套不会改动图纸。什么时候使用哪种变量我的原则是**如果一个数值对所有对象来说都应该是相同的且沿类型统一变化就用类变量如果每个对象可能各自不同就是实例变量。**比如所有狗都是“犬科”这是一个类变量每只狗的名字、年龄一定是实例变量。4.2 经典陷阱可变对象当类变量的惨痛教训类变量看似简单但有个特别经典的坑几乎所有写过类的人都踩过——把可变对象列表、字典、集合当作类变量然后在实例方法里直接修改它。看这个例子class Dog: friends [] # 类变量列表是可变对象 def __init__(self, name): self.name name def add_friend(self, friend_name): self.friends.append(friend_name) dog1 Dog(旺财) dog2 Dog(小白) dog1.add_friend(来福) print(dog2.friends) # [来福] 是不是很意外我只给dog1加了一个朋友结果dog2.friends里也出现了“来福”。为什么因为friends是类变量它在类创建时只初始化一次所有实例共享同一个列表对象。self.friends.append(来福)这个操作虽然是通过self写的但self.friends仍然指向那个类级别的列表——于是你修改的是所有狗共享的那份列表。这种情况怎么处理最简单的方法干脆不要在类变量里放可变对象所有需要每个实例独立持有的数据一律放到__init__里class Dog: def __init__(self, name): self.name name self.friends []如果确实需要类级别共享一份数据那你可以显式地用Dog.friends来操作或者在修改时复制一份再赋给实例。重点在于你要在心里建立一个判断读类变量和写类变量是两个完全不同的动作。读取时实例找不到自己的属性会顺着去类的字典里找这个设计可以给我们便利但写入时如果你用的是append、extend这类修改原地对象的方法那改的就是类变量本身于是所有实例全部中招。这也是我经常看到新人崩溃的时刻提前了解能帮你少踩一个深坑。4.3 修改类变量的两种姿势以及“遮蔽”现象既然说到改那就有必要讲清楚两种写法之间的微妙差别。第一种通过类名修改Dog.species 犬科哺乳动物这会真正修改类变量本身之后创建的新对象、以及所有从类上读取该属性的旧对象看到的都是新值。第二种通过实例修改dog1.species 猫科 # 实际上你是在给 dog1 新建了一个实例属性这个操作并没有改动类变量只是给dog1这个实例单独创建了一个名为species的属性它“遮蔽”了从类继承来的那个类变量。之后dog1.species读到的是它自己的值而dog2.species仍然是类变量里的值。新手经常在这个地方翻车某个代码片段里写了self.count 1本意是想“所有实例都统一改成 1”但由于它在实例上创建了新属性其他实例完全没变化。等调试的时候明明看到上一行给某个对象赋值了下一行另一个对象的同名属性却是原值让人一头雾水。记住一条规则在没有给实例单独赋值的情况下实例查找属性是“先找自己的字典找不到再去类的字典”。一旦你给实例赋了值它就有了自己的属性类变量就被盖住了。这跟前面说的“对象就是一个小字典”完全吻合理解了这一点遮蔽、共享、读取这些行为就都顺理成章了。5. 刚写完类最常见的报错与排查思路5.1 定位报错missing 1 required positional argument: self写类必然会遇到报错有些报错信息对新手极度不友好比如这一条TypeError: Dog.bark() missing 1 required positional argument: self很多人的第一反应是我明明调用了bark()怎么还说少参数其实原因大概率是你用类名直接调用了实例方法Dog.bark() # 报错 dog1.bark() # 正确由于bark的定义里有self参数当你用Dog.bark()调用时没有传入任何实例self没有着落自然报错。解决办法要么改成dog1.bark()要么明确传入一个实例Dog.bark(dog1)。这条报错出现时先检查自己是不是用类名调用了实例方法或是在写装饰器、回调函数时不经意地丢了实例信息。5.2 方法调用忘了加括号打印出的是一大串怪东西另一个新手高频场景print(dog1.bark)运行结果不是“汪汪”而是类似这样的输出bound method Dog.bark of __main__.Dog object at 0x...这很容易让人一头雾水但实际上 Python 在告诉你dog1.bark是一个“方法对象”它还不是“调用方法的结果”。是你自己忘了在后面加括号方法并没有真正执行。原理很简单dog1.bark是“把方法从对象上取出来”这个动作方法要真正运行需要向后调用也就是再跟一对括号dog1.bark()。如果你不小心把加括号的环节漏掉了拿到的是方法本身而不是方法的返回值。类似的还有类属性与方法的混淆——当属性名和方法名重名时也容易踩到这类坑所以命名时尽量让属性用名词、方法用动宾短语能减少很多误读。5.3 一个隐蔽的雷区Dog object is not callable这种报错信息干脆利落TypeError: Dog object is not callable。意思是你把一个对象当作函数调用了。常见的触发场景之一是创建对象时误把“类的名字”和“对象名”搞混或者某个地方变量被覆盖了class Dog: def __init__(self, name): self.name name Dog Dog(旺财) # 把类名重新绑定成了实例类从此消失 new_dog Dog(小白) # 报错Dog 对象不可调用这里的错误比较极端但真实发生。更多的是这种场景你在类内部定义了一个属性叫call然后在外面写instance.callPython 会把这个属性当作可调用对象使用结果类型对不上直接抛not callable。排查思路很固定报错说谁不可调用就先用print(type(x))去看看那个“谁”到底是个什么类型。90% 的情况是类型与预期不符——你以为它是个函数或方法其实它是别的。打印出类型的那一刻大部分问题都会迎刃而解。5.4 三查法类报错快速定位方法论最后分享一个我自己总结的排查流程记不住所有报错细节也没关系按这个顺序查基本都能定位查定义确认类的属性、方法是不是都写在了正确的位置。实例变量写到类变量区域、方法缩进错误导致不在类里面是最常见、最容易忽略的问题。看到方法里用了self但缩进比class层级少了一格那它其实已经逃出类了。查创建创建对象时参数个数、顺序有没有对上__init__的签名最常见的报错是takes 2 positional arguments but 3 were given——通常是__init__定义里漏了self。查调用调用方法的对象是不是你预期的那个实例有没有通过类名调用实例方法有没有忘了加括号属性名与用户名有没有因为大小写、拼写问题导致找不到进而触发AttributeError这三步做完绝大多数类相关的入门报错都能在几分钟内解决。我第一次接触面向对象时被self搞得几乎崩溃后来把上面的流程记熟了才建立起信心。所以别急报错不是坏事反而是你逐步理解类内部运行机制的最直接窗口。写到这里“类和对象上”的核心内容算是聊透了从为什么需要类到如何写一个类再到self的本质、类变量与实例变量的分界最后是新手阶段最常遇到的报错和排查思路。我个人的体会是学类和对象这关真正难的不是某一条语法而是思维方式的转变——把数据和行为绑在一起用图纸和实例的眼光去看待代码。一旦你用这种眼光重新审视旧代码会发现很多原本乱七八糟的逻辑都能被重新整理得井井有条。下一篇再继续聊继承、多态和__repr__等魔法方法先别急着往前赶把基础的每一个细节都亲手敲一遍才算真正拿下了。
延伸阅读

更多相关文章

2026/10/20 15:25:03

diagram-design实战:从草图到系统的高效画图方法论

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题 第一次看到 diagram-design 这个词,很多人会以为它只是一个画图工具的别名,或者某个设计模板库的代号。但真正在工程团队、产品团队里摸爬滚打过几年的人会明白,它指向…

2026/10/20 15:20:02

AIR780E驱动安装避坑指南:USB串口识别、固件烧录与AT验证

简介:合宙4G模组AIR780E的驱动程序,是一份面向物联网嵌入式开发者的底层通信中间件,专注于CAT1 4G联网和GPS/GNSS定位的硬件控制。资源包为rar压缩格式,仅2个文件、4KB大小,分为实现文件与头文件:.c文件涵盖…

2026/10/20 15:20:02

Vibe Coding:用自然语言造工具,普通人也能自动化

1. 从“会说话”到“会造工具”到底在说什么第一次听到“Vibe Coding”这个词,很多人会以为是某种新出的编程语言或者框架。其实不是。它描述的是一种正在悄悄改变普通人做事方式的范式:你不再需要先学会写代码,再去解决问题;而是…

2026/10/20 16:25:35

双栈模拟队列:用堆栈实现FIFO行为的原理与工程实践

1. 项目概述:为什么用堆栈去“假装”队列?这题不是考代码,是考思维切换 “7-22 堆栈模拟队列 (25 分)(详解夹思路)”——这个标题一出来,很多刚学数据结构的同学第一反应是懵的:堆栈(Stack)是后…

2026/10/20 16:25:35

AI赋能前端测试:从手动回归到自动化流水线的实践路径

1. 为什么前端测试会卡在手动阶段如果你所在的团队还在靠“点点点”做前端测试,我猜你已经体会过这种循环:功能迭代越来越快,手动回归越拖越长,于是测试永远落后于开发。人手不够可以加人,但真正棘手的是“加人也没用”…

2026/10/20 16:25:35

AI病历全链路设计:从生成质控到审核集成的落地指南

记录病历这事,临床医生永远嫌烦,质控科永远嫌乱。以前打过交道的某三甲医院,一个住院医生一天光敲入院记录和病程就要花两三个小时,写完还得提心吊胆等质控抽检,动不动就打回来重写。AI大模型这波起来以后,…

2026/10/20 16:25:35

国产PHY芯片替换瑞昱:PIN兼容背后的光口验证与寄存器陷阱

做网络硬件的人,这两年换芯片换出职业病了。前阵子一位做工业网关的朋友找到我,说他们板上那颗带光口的千兆PHY供应有点紧,想换成国产景略。他这块板子原设计用的是瑞昱的RTL8211FD,一颗带光口的千兆PHY,后面拖一个SFP…

2026/10/20 16:25:35

ESP32 Mixly图形化编程插件包拆解与落地指南

简介:面向ESP32初学者的图形化编程资源,将Mixly积木式编程与ESP32双核Wi-Fi/蓝牙硬件相结合,帮助入门者绕过复杂语法,通过拖拽代码块快速完成IoT原型验证与教学实验,适合高校课堂、创客教育及个人自学。资源包共53个文…

2026/10/20 16:20:34

三相PWM整流器FCS-MPC控制:Simulink仿真搭建与调试全解析

做整流器控制这些年,最常见的路子还是电压外环加电流内环、SVPWM调制那一套。直到后来接触了有限集模型预测电流控制(FCS-MPC),我才发现三相PWM整流器的控制还能这么直接——不需要调制器,不需要解耦网络,把…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑