news 2026/10/24 11:24:54

Python类深度拆解:__init__、self与实例化核心原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python类深度拆解:__init__、self与实例化核心原理

写 Python 写到一定阶段,几乎每个人都会在“类”这里卡一下。函数用得再熟,一见到class关键字、__init__、self这些概念,还是容易懵——我当年也是这样。这篇文章就专门来拆一拆类的基本构造与实例化这件事。我会从设计思路讲起,逐行解析__init__和self,再用一个完整可跑的记账本例子走一遍实例化全流程,最后把新手常见的几类报错和排查思路列出来。适合刚学完函数、正准备跨进面向对象门槛的读者,也适合已经写过一些类、但总觉得有些细节没吃透的朋友。

1. 类怎么设计:写 class 前先想清楚三件事

1.1 不用类的代码为什么会越写越别扭

先看一个我经常用来给新手演示的例子。假设要写一个文本记账工具,记录每笔交易并统计分类总额。如果完全用函数和字典来实现,代码大概长这样:

transactions = [] def add_transaction(transactions, amount, category, date): transactions.append({ "amount": amount, "category": category, "date": date }) def total_by_category(transactions, category): return sum( t["amount"] for t in transactions if t["category"] == category )

这段代码能跑,问题也不大。但它有一个隐患:交易数据是普通字典,字段名全靠写代码的人“自觉”。只要有一次手抖,把"amount"写成"Amount",程序不会报错,统计结果却会悄悄出错。这类 bug 最折磨人,因为它不报红、不崩溃,纯粹是数据对不上。

类要解决的,就是把这个“靠自觉维护格式”的问题变成“由结构强制约束”。定义一个Transaction类之后,每次创建交易对象,它天生就带着amount、category、date这些属性;对它的操作也统一由类里的方法负责。数据和行为收拢到同一个模板里,代码结构会清楚很多,也更好维护。

1.2 设计阶段的三板斧:数据、行为、归属

写class关键字之前,我会先在注释里把三个问题列出来,想清楚了再动手:

  • 这个对象代表什么东西?它需要保存哪些数据?
  • 这个对象能做什么动作?对外提供哪些方法?
  • 哪些数据是所有实例共享的,哪些是每个实例独有的?

用前面的记账场景来回答:

  • 数据:金额amount、分类category、日期date
  • 行为:能新增交易、能按分类统计、能输出所有交易信息
  • 共享与独有:分类常量这类信息可以共享;每笔交易的金额、日期则是实例独有的

这三个问题答完,类的基本骨架就已经出来了。很多人的类写得别扭,不是因为语法不熟,而是根本没想清楚这个对象到底要承担什么责任。设计不是玄学,就是这种在最开始多花两分钟的思考。

1.3 类名的约定:细节里藏着可读性

Python 对命名有一套约定俗成的规范,语法不强制,但社区基本都遵守:

  • 类名用CapWords风格,也就是每个单词首字母大写,比如Transaction、BankAccount,方便和snake_case的函数名区分
  • 类内的方法和属性用小写加下划线,比如total_by_category
  • 不要用list、dict、str这类内建名称做类名或变量名,否则容易覆盖掉原本的行为

这些习惯看着是小事,等代码量上来、需要别人 review 时,能省掉大量沟通成本。我自己见过把类名写成bankaccount的代码,能跑,但让阅读者很难一眼判断它是一个类还是别的什么。命名不是形式主义,是代码可读性的一部分。

2. 核心语法拆解:init和 self 到底是怎么回事

2.1init:严格说它不是构造函数

很多教程把__init__称为构造函数,这个叫法其实不严谨。Python 里真正负责分配内存、创建空白对象的是__new__;__init__的作用是初始化,也就是把传入的参数填到实例上。所以你可以把__init__理解为“新对象出生后的配置环节”。

最简单的类长这样:

class Transaction: def __init__(self, amount, category, date): self.amount = amount self.category = category self.date = date

使用时直接传参,不需要手动调用__init__:

t = Transaction(12.5, "餐饮", "2024-05-01")

这里有个新手常犯的误解:以为Transaction(...)就是“调用__init__方法”。实际上,当执行Transaction(12.5, "餐饮", "2024-05-01")时,Python 会自动完成三步——创建一个新对象、调用__init__初始化它、把初始化完成的对象返回出来。你写的Transaction(...)只是在触发这个过程。所以不要在业务代码里显式调用t.__init__(...),这不是它的使用场景。

2.2 self:每个方法都要知道“我是谁”

self是新手最容易卡的点。你肯定会疑惑:为什么定义方法时第一个参数是self,调用时却不用传?

我的理解角度是这样的:当你在实例上调用t.doubled_amount()时,Python 会自动把t本身作为第一个参数传入doubled_amount(self)。也就是说,这里的self指的就是“当前这个实例对象”,方法必须知道“我是谁”,才能访问属于我的属性。

如果方法定义时少写了self,比如:

class Transaction: def __init__(self, amount): self.amount = amount def doubled(): # 忘了写 self return self.amount * 2

调用Transaction(10).doubled()会报:

TypeError: doubled() takes 0 positional arguments but 1 was given

这个报错信息不说人话,但意思很直白:你调用时悄悄传了一个实例进去,可这个方法定义的时候没留接收位置。这个报错几乎每个人都会遇到一次,遇到别慌,记住“实例方法的第一个参数必须是 self”就行了。

2.3 实例属性与类属性:一个赋值就可能踩坑

这一步藏着 Python 类最经典的坑:实例属性和类属性完全是两回事。

class BankAccount: bank_name = "某某银行" # 类属性,所有实例共享 def __init__(self, balance): self.balance = balance # 实例属性,每个实例独有

创建两个账户:

a1 = BankAccount(100) a2 = BankAccount(200) print(a1.bank_name) # 某某银行 print(a2.bank_name) # 某某银行 print(a1.balance) # 100 print(a2.balance) # 200

bank_name写在类体里,是类属性,所有实例共享同一份。balance在__init__里通过self.balance赋值,是实例属性,每个实例各存各的。

最经典的坑出现在你试图给某个实例的类属性赋值时:

a1.bank_name = "别的银行" print(a2.bank_name) # 某某银行,没变

为什么a2没跟着变?因为给a1.bank_name赋值时,Python 不是修改类上的属性,而是给a1这个实例创建了一个新的实例属性,把原本的类属性遮蔽了。看起来是改了共享值,实际没改到源头。如果真的想改类的共享属性,应该写BankAccount.bank_name = "别的银行"。

2.4 可变默认值:一个让实例互相污染的隐形陷阱

这个坑值得单独拿出来说,因为它太常见了。不要用可变对象做默认参数:

class TaskList: def __init__(self, tasks=[]): # 有问题的写法 self.tasks = tasks

如果你创建两个实例,都不传tasks:

t1 = TaskList() t2 = TaskList() t1.tasks.append("写代码") print(t2.tasks) # ['写代码']

两个实例共享了同一个默认列表。原因是def __init__(self, tasks=[])里的[]在方法定义时只创建一次,之后每次调用都用那同一个列表对象。解决办法是默认值传None,在方法体内再新建:

class TaskList: def __init__(self, tasks=None): self.tasks = [] if tasks is None else tasks

这个坑不仅限于类,普通函数的可变默认参数也一样,只不过类实例的共享会让问题变得更隐蔽。我的习惯是:任何可变对象都不要直接放在默认参数位置。

3. 实例化过程实操:跟着一个记账本例子走一遍

3.1 一个能跑的完整类

把前面的设计思路落地,写一个完整的记账本。这里有两个类:一个负责单笔交易,一个负责整个账簿。

class Transaction: def __init__(self, amount, category, date): self.amount = amount self.category = category self.date = date def info(self): return f"{self.date} [{self.category}] {self.amount:.2f}" class Ledger: def __init__(self): self.transactions = [] def add(self, amount, category, date): self.transactions.append(Transaction(amount, category, date)) def total_by_category(self, category): return sum( t.amount for t in self.transactions if t.category == category ) def all_info(self): return "\n".join(t.info() for t in self.transactions)

使用起来:

ledger = Ledger() ledger.add(12.5, "餐饮", "2024-05-01") ledger.add(45, "交通", "2024-05-02") ledger.add(30, "餐饮", "2024-05-03") print(ledger.total_by_category("餐饮")) # 42.5 print(ledger.all_info()) # 2024-05-01 [餐饮] 12.50 # 2024-05-02 [交通] 45.00 # 2024-05-03 [餐饮] 30.00

这里Ledger的self.transactions列表里装的每个元素都是Transaction实例。职责划分很清楚:单笔交易的行为归Transaction,账簿的整体逻辑归Ledger,谁也不越界。

3.2 实例化的时候,Python 到底做了什么

当你写下ledger = Ledger(),Python 内部其实做了三件事:

  1. 分配内存,创建一个空白的Ledger对象
  2. 调用Ledger.__init__(这个空白对象),往它身上挂transactions属性
  3. 把初始化完成的这个对象返回,绑定到变量名ledger上

整个流程平时看不见,但理解它特别有用。比如,你就能明白ledger是一个实例对象,而Ledger本身是一个类;为什么isinstance(ledger, Ledger)返回True;为什么type(ledger) is Ledger也为True。类就是模板,实例是模板生产出来的具体产品。

3.3 多实例的独立性:谁的数据就是谁的

继续用记账本做实验:

ledger_a = Ledger() ledger_b = Ledger() ledger_a.add(10, "购物", "2024-05-04") print(len(ledger_b.transactions)) # 0

ledger_a记账,ledger_b毫无感觉。因为每次Ledger()都会执行__init__,生成一个全新的self.transactions列表。实例的意义就在这:用同一张模板,生成无数个互不干扰的对象。

但如果把列表放到类属性里,画风突变:

class BadLedger: transactions = [] # 注意:这是类属性 def add(self, item): self.transactions.append(item) a = BadLedger() b = BadLedger() a.add("买了杯咖啡") print(b.transactions) # ['买了杯咖啡']

列表变成共享的了。这就是 2.3 节那个类属性和实例属性问题直接导致的后果。所以,凡是要保存“每个对象自己的数据”,一定要在__init__里用self.xxx初始化,不要图省事直接写到类体里。这一点我踩过不止一次,尤其是从写脚本转过来的同学特别容易顺手写错。

3.4 方法调用背后的绑定机制

除了ledger.add(20, "学习", "2024-05-05")这种常见写法,Python 其实还允许绕一下:

ledger = Ledger() Ledger.add(ledger, 20, "学习", "2024-05-05")

效果和ledger.add(...)完全等价。区别在于,第一种写法是 Python 帮你做了绑定,把ledger隐式传给self;第二种是显式把ledger作为第一个参数传进去。

这个机制也解释了为什么类内部调用另一个方法时,必须写成self.method(),而不能直接写method()。普通方法在类内部并不会“自动记得自己属于哪个实例”,只有通过实例访问,绑定才会生效。我见过不少人写类方法时直接调另一个方法名,结果收到一个NameError,其实都是对绑定机制不够熟悉。

4. 新手必看:类相关的常见报错与排查思路

4.1 TypeError: method() takes 0 positional arguments but 1 was given

这是我看到的最高频报错,九成和self有关。比如:

class Person: def __init__(self, name): self.name = name def greet(): # 少写 self return f"Hello, {self.name}"

调用Person("A同学").greet(),报错就是TypeError: greet() takes 0 positional arguments but 1 was given。

原因前面已经讲过:实例方法调用时,实例本身一定会被当作第一个参数传进去。排查这类报错,先数一数定义时参数个数和调用时传参个数。定义时没写self,调用却报“多了一个参数”,基本就是少self;如果定义写了self,调用却报“参数不够”,多半是没用实例调用,直接用类名调用了。

4.2 类属性神秘共享,实例之间互相污染

这个现象用上面的BadLedger演示过。现实里更隐蔽的版本是:“我明明在__init__里写了self.tasks = tasks,为什么两个实例还是互相影响?”——通常就是 2.4 节那个可变默认参数[]搞的鬼。排查思路很简单:所有需要独立的属性都在__init__里重新赋值,不直接依赖默认参数里的可变对象。另外,如果某个值本身就是只读常量,比如分类枚举、固定配置,用类属性反而更合适,不必矫枉过正。

4.3init里写 return 会直接报错

这个坑比较隐蔽,但遇到一次就记住了。__init__是初始化器,Python 规定它不能显式返回非None的值:

class Demo: def __init__(self): return 42 # 报错

运行直接抛TypeError: __init__() should return None。因为__init__不负责创建对象,它没有返回值给调用方。平时大家一般不会主动去 return,但如果你在__init__的分支逻辑里不小心写了return some_value,就会踩到。记住:__init__里要么不写 return,要么就写裸return用来提前结束。

4.4 高频问题速查表

报错或现象常见原因处理思路
TypeError: method() takes 0 positional arguments but 1 was given实例方法定义缺self给方法补上self参数
TypeError: __init__() should return None__init__里返回了非 None 值去掉 return 或改成裸 return
两个实例的数据互相影响用类属性存可变对象,或可变默认参数在__init__里用self.xxx初始化
打印实例得到<__main__.Transaction object at 0x...>没有定义__repr__或__str__添加这两个方法,见第 5 章
加了@classmethod还是报错第一个参数名写成了self类方法第一个参数应写成cls
访问self.xxx报AttributeError__init__里没有赋值这个属性检查初始化逻辑,确认赋值语句执行过

这张表不是权威清单,是我这几年看报错总结出来的高频项。遇到问题先判断是“定义阶段的问题”还是“调用阶段的问题”,再对症下手,比盲目试错要快得多。

5. 几个让类更好用的扩展小技巧

5.1 加上repr和str,调试不再看内存地址

新手打印实例,看到的往往是<__main__.Transaction object at 0x7f1234567890>,除了一串地址什么信息都没有,调试起来特别痛苦。给类加上一个__repr__就能瞬间改观:

class Transaction: # 前面省略属性初始化逻辑 def __repr__(self): return ( f"Transaction(amount={self.amount}, " f"category='{self.category}', date='{self.date}')" )

之后repr(t)、直接打印t都会输出可读的字符串。__repr__是给开发者看的,尽量写成“可以直接复制用来重新创建对象”的格式;__str__是给终端用户看的,可以更友好。如果只定义一个,我建议优先__repr__,因为 Python 在没定义__str__时会自动用它兜底。

5.2 下划线前缀只是约定,不是权限控制

Python 没有 Java 那种private关键字,一切靠约定自觉。单下划线前缀_amount表示“内部实现细节,外部请勿直接访问”,双下划线前缀__amount会触发名称改写机制,但也只是让名称变复杂,并不是真锁死。

我自己的习惯是:内部辅助方法用单下划线开头,比如_calc_total,提醒自己和读者“这是内部细节,接口不稳定”;对外稳定接口才用普通命名。这个约定没什么魔法,但能让类的边界清晰很多。调用方看到下划线前缀,就该明白它不属于公共 API。

5.3slots:实例多到内存吃紧时的优化手段

默认情况下,每个实例都带一个__dict__字典来存属性,字典本身开销不小。如果类要产生海量实例,可以用__slots__显式声明允许的属性名:

class Point: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y

加了__slots__之后,实例不再需要__dict__,内存占用能明显下降,属性访问也会略快一点。代价是你不能给实例添加__slots__之外的新属性。日常业务代码没必要刻意用,但我确实见过有人处理上百万坐标点时靠它省下不少内存。要用的前提是:你确认这个类不需要动态扩展属性。

最后分享一点个人经验吧。刚学类的头两周,我也被self和共享属性折腾得够呛。后来发现最有效的办法不是死背规则,而是自己写小例子去验证:故意在类里放一个可变类属性,实例化两个对象,看看互相会不会影响;故意少写一个self,亲眼看看报错长什么样。这种“主动制造错误再观察错误”的练习,比看十遍教程都管用。一句话总结我的理解:类是模板,对象是数据,self是连接两者的桥梁。想通这层关系,之后学继承、多态会顺畅很多。

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

ERP升级操盘指南:从数据准备到切换上线的全流程实战

做了十几年企业信息化相关项目&#xff0c;我接到最多的需求不是“我们上一套新系统”&#xff0c;而是“帮我们把现在的ERP升级一下”。这句话听着简单&#xff0c;背后牵扯的数据、流程、接口、权限、二次开发、用户习惯&#xff0c;哪一个都能让人一个头两个大。一个ERP系统…

作者头像 李华
网站建设 2026/10/22 13:10:01

JVM大对象与数组堆内存管理优化实战

1. 大对象与数组堆内存管理的核心挑战 在JVM或类似运行时环境中&#xff0c;大对象(Large Object)通常指超过特定阈值&#xff08;如JVM中-XX:PretenureSizeThreshold参数定义&#xff09;的内存分配请求。这类对象会直接进入老年代&#xff08;Old Generation&#xff09;&…

作者头像 李华
网站建设 2026/10/19 9:00:09

上海亨得利2026年10月最新服务信息公布:客服电话、门店地址及不同品牌维修保养报价

上海表主送修腕表&#xff0c;最容易踩的坑不是价格&#xff0c;而是渠道没找对。上海亨得利2026年10月服务信息现已公布&#xff0c;本次把官方客服电话、上海两家直营门店地址、以及不同品牌维修保养的报价逻辑一次讲清。上海地区表主可直接拨打亨得利全国统一服务电话: 400-…

作者头像 李华
网站建设 2026/10/22 7:14:28

无人机航拍三维重建实战:SfM-NeRF耦合与位姿对齐

简介&#xff1a;本资源是一套面向计算机相关专业学生与初学者的无人机航拍三维场景重建完整实践方案&#xff0c;适用于毕业设计、课程设计、大作业及科研入门等场景&#xff0c;聚焦于从真实航拍图像出发构建高精度三维模型的核心任务。压缩包共54个文件&#xff0c;含41个Py…

作者头像 李华
网站建设 2026/10/23 10:58:50

SpringBoot2+Vue3校车调度系统开发实践

1. 项目概述校车调度管理系统是高校后勤管理中的重要信息化工具&#xff0c;我最近用SpringBoot2Vue3MyBatis-PlusMySQL8.0技术栈完整实现了一套解决方案。这个系统主要解决三个核心问题&#xff1a;一是校车资源与师生需求的精准匹配&#xff0c;二是调度过程的数字化管理&…

作者头像 李华
网站建设 2026/10/23 11:30:09

Go服务在128核服务器性能下降的排查与优化

1. 问题现象与初步排查当公司为技术团队配置了一台128核的高性能服务器时&#xff0c;作为开发者的我们本应欢呼雀跃。但现实却给了我们当头一棒——原本在16核机器上运行良好的Go服务&#xff0c;迁移到新环境后性能不升反降。这种反直觉的现象背后&#xff0c;往往隐藏着几个…

作者头像 李华