十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python函数与面向对象:从参数陷阱到类与继承的实战指南

Python函数与面向对象:从参数陷阱到类与继承的实战指南 在项目里翻到一份Python学习笔记正好记到函数和面向对象这两章。说实话Python入门最容易被劝退的不是语法本身而是两个坎一个是函数参数绕来绕去搞不明白另一个是类Class和对象Object这个概念听课觉得懂了一写代码就不知道从哪下手总感觉我明明用函数也能实现啊为什么要搞这么复杂。这篇笔记我就把04函数和05面向对象这两块内容合并到一起来梳理因为我始终认为函数帮你把动作封装起来面向对象帮你把动作和它操作的数据封装在一起两者是递进关系放在一起学反而理解更快。无论你是刚装好Python还没入门还是已经能写点小脚本、想在代码组织能力上再上一个台阶这篇都值得你看完。里面所有代码我都跑过参数陷阱、作用域细节、继承多态的一些常见坑也一并标出来希望能少走弯路。1. 从无法识别cmdlet说起先把环境这块地基打好很多新手学到函数和面向对象的时候其实已经能跑通hello world了但真正停下来研究函数、类这种组织代码的语法之前有个很容易被忽略的问题你的Python运行环境到底干不干净。最近在好几个技术社群里总看到类似的求助截图里清一色是这种报错claude : 无法将“claude”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。 npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。以及类似的opencode、codex、pnpm、git的报错。看着名字五花八门但本质是同一个问题在Windows PowerShell或CMD里系统压根没在可执行文件搜索路径里找到这个命令。放到Python学习的语境里就是你敲python也弹不出交互式解释器其实就是环境变量没配对或者压根没装好。为什么我要在函数和面向对象的笔记里单独提这个事情因为函数和类的学习最大的特点就是代码块变长、修改频率高。你今天写一个函数明天改一个类后天要把代码保存成.py文件反复运行。如果连命令行里运行Python脚本都费劲学习的挫败感会成倍增加。我自己的建议是学函数和面向对象之前务必把下面三件事做扎实安装Python时勾选Add Python to PATH安装完之后在PowerShell里敲python --version验证一下能输出版本号才算通关。尽量装一个正经的IDE或编辑器我自己用的是VS Code Python扩展PyCharm社区版也很好。IDE的价值不在于好看而在于写完函数名之后能提示参数列表写类的时候能跳转到定义处这些对理解代码结构帮助极大。准备一个专门放练习脚本的目录比如D:\python_notes把这一篇笔记涉及的所有例子文件都放在里面养成一个主题一个文件的习惯。环境搞定了接下来进入正题。函数和面向对象我见过太多人把它们当成语法来学背下def、class、self这些关键字就觉得完事。但我个人的体会是它们真正的价值是思维层面的切换语法只是表象。2. 函数把重复动作打包成可调用的动作块2.1 函数解决的问题其实是你每天都在干的重复活在学习def定义函数这个动作之前先想清楚一个问题函数到底解决的是什么问题举个例子假设你要计算三个圆的面积圆的半径分别是1、2、3。如果你不写函数代码长这样r1 1 area1 3.14159 * r1 * r1 print(半径为, r1, 的圆面积是, area1) r2 2 area2 3.14159 * r2 * r2 print(半径为, r2, 的圆面积是, area2) r3 3 area3 3.14159 * r3 * r3 print(半径为, r3, 的圆面积是, area3)看起来也不复杂对吧但问题是如果这个需求从计算3个圆的面积变成计算3000个圆你就得复制3000遍而且是手动改半径值一旦后面要改动圆周率精度或者输出格式你就要在3000个位置同时改这才是真正的灾难。函数的核心价值就是把经常要做的、带输入和输出的动作封装成一块每次调用只需要给不同的参数就能复用同一套逻辑。改成函数之后的写法def calculate_circle_area(radius): # 输入半径返回圆面积 return 3.14159 * radius * radius for r in [1, 2, 3]: area calculate_circle_area(r) print(半径为, r, 的圆面积是, area)代码量少了逻辑还更清楚了。for循环负责遍历函数负责计算各干各的。2.2 定义一个函数的完整结构def、参数、函数体、返回值Python里定义一个函数语法其实就四件事def 函数名(参数1, 参数2): 这里是文档字符串docstring说明这个函数是干嘛的 # 这里是函数体写具体的逻辑 return 返回值看起来简单但每一步都有新手容易踩的细节我逐个说第一函数名要有意义用下划线分隔单词。比如calculate_circle_area就比calc、mianji、func1强得多。Python官方的PEP 8风格指南推荐函数名使用小写字母加下划线这不仅是风格问题更是可读性问题。你过两个月回头看自己的代码一个叫calculate_circle_area的函数根本不愁看不懂。第二参数可以没有也可以有多个。没有参数就用空括号def say_hello():有多个参数就用逗号分隔def add(a, b, c):。参数的本质是函数的输入接口调用者通过参数把数据传进去。第三函数体一定要缩进。Python用缩进来划分代码块这跟C、Java用花括号完全不一样。同样一个def下面缩进一致的代码才是函数体一旦缩进不对就会报IndentationError。第四返回值用return。如果不写return函数会默认返回None。这个特别容易踩坑比如很多人写了一个函数在里面算好了结果最后忘了return调用的时候拿到的全是None排查了半天才发现问题。2.3 函数调用时参数是怎么传进去的位置与关键字定义好函数之后调用的时候需要把实际值传给参数这里有两种传参方式。位置参数就是按顺序一一对应def introduce(name, age, city): print(f我叫{name}今年{age}岁来自{city}) # 位置传参按顺序对应 name小明, age18, city杭州 introduce(小明, 18, 杭州)关键字参数就是直接指定参数名# 关键字传参顺序不敏感你开心就好 introduce(city杭州, name小明, age18)我自己的习惯是参数少3个以内用位置参数没啥问题但一旦参数多起来调用的时候建议用关键字参数因为调用的地方一眼就能看出每个值代表什么含义不会出现introduce(小明, 杭州, 18)这种把城市和年龄弄混的尴尬。2.4 默认参数值好东西但有一个非常大的坑定义函数的时候可以给参数指定默认值这样调用的时候可以不传这个参数def create_user(name, age, city北京): print(f用户{name}{age}岁来自{city}) # 不传 city就用默认值北京 create_user(小红, 20) # 传了 city就用传入的值 create_user(小刚, 22, 上海)默认参数可以显著提高函数的灵活性但这个灵活性背后藏着一个Python里非常经典、几乎人人都会踩的坑——默认参数如果是可变对象比如列表、字典它只会被创建一次。看下面这段代码def add_item(item, items[]): items.append(item) return items print(add_item(苹果)) # 输出[苹果] print(add_item(香蕉)) # 期望可能是 [香蕉]实际输出[苹果, 香蕉]第二次调用的时候默认列表items已经不是空列表了而是第一次调用后留下的[苹果]。因为默认值在函数定义的时候就被创建并固定了多次调用共享的是同一个列表对象。正确做法是默认值用None在函数体里再重新赋值def add_item(item, itemsNone): if items is None: items [] items.append(item) return items这个坑我在开发时踩过一次排查了很久才意识到是默认参数共享的问题后来凡是看到别人代码里写def xxx(lst[])我都会条件反射地多说一句赶紧改掉。2.5 不定长参数*args和**kwargs到底在说什么关于参数还有一对高频出现的东西就是*args和**kwargs。很多教程把它们当成一个高级特性来讲搞得新手觉得很难但其实原理非常简单。*args用来接收任意多个位置参数函数内部它是个元组。**kwargs用来接收任意多个关键字参数函数内部它是个字典。def log_message(level, *args, **kwargs): print(日志级别, level) print(位置参数, args) print(关键字参数, kwargs) log_message(INFO, 用户登录, 请求耗时2.3秒, user_id12345, deviceiPhone)这段代码里level接收到INFO用户登录和请求耗时2.3秒被当作位置参数收进args元组user_id和device被当作关键字参数收进kwargs字典。*args和**kwargs最实用的场景是写一个函数的包装器比如装饰器里经常用到。你先知道这个语法长什么样就行后面学到装饰器、类继承的super()时还会再看到它的身影。3. 参数传值机制Python的传对象引用到底是怎么回事3.1 为什么在函数里改了列表外面的列表也变了参数传递机制是函数这块最烧脑的部分也是无数面试题爱考的点。别慌我用大白话给你拆开。先看这段代码def modify_list(lst): lst.append(4) my_list [1, 2, 3] modify_list(my_list) print(my_list) # 输出[1, 2, 3, 4]奇怪了我在函数里往lst里追加了4结果外面的my_list也变了。这是为什么不是说函数参数是复制一份进去的吗这里要理解Python的传参机制Python传参既不是典型的值传递复制一份也不是典型的引用传递传地址而是传对象引用。更通俗地说变量名是贴在对象上的标签函数调用时等于给同一个对象又贴了一个新标签lst两个标签指向的是同一个列表对象。所以你在函数里通过lst.append(4)修改的是那个对象本身外面的my_list当然会看到变化。但是下面这种操作结果又不一样def reassign_list(lst): lst [100, 200, 300] # 不是修改原对象而是把 lst 这个标签重新贴到新对象上 my_list [1, 2, 3] reassign_list(my_list) print(my_list) # 输出[1, 2, 3] 原对象没被动过区别在于lst.append(4)是修改对象内部而lst [100, 200, 300]是让lst指向一个新对象这操作动不了外面的my_list。3.2 可变对象 vs 不可变对象一张表讲明白那问题来了为什么我在函数里写count count 1外面的整数没变原因在于整数是不可变对象。对象类型是否可变典型例子不可变创建后内容不可改int、float、bool、str、tuple、frozenset可变可以修改内容list、dict、set、自定义对象注意这个不可变的含义是你看count count 1表面像是把count改成了新值实际是重新创建了一个新的整数对象count 1然后让变量名count指向这个新对象原来的那个整数对象并没有被修改。所以如果传进去的是不可变对象你在函数里怎么折腾外面都不受影响因为每次修改都等于重新绑定了一个新对象。如果传进去的是可变对象并且你调用的是修改对象内容的操作如list.append()、dict[key] value、set.add()外面的对象会跟着变。这个机制也是为什么Python面试里总喜欢问这个。理解了这个之后你就不会被为什么有时改了、有时没改困扰了。3.3 实战建议如何安全地在函数里处理列表或字典既然可变对象有这种副作用那在函数里处理外部传入的列表、字典的时候建议遵守一个原则如果不想影响外部数据先在函数内部复制一份再操作。def add_item_safe(item, itemsNone): if items is None: items [] # 防止修改到外部的原列表复制一份再操作 new_items items.copy() new_items.append(item) return new_items original [1, 2, 3] result add_item_safe(4, original) print(original) # [1, 2, 3]原列表没变 print(result) # [1, 2, 3, 4]返回的是新列表或者干脆用切片复制items[:]也行。记住函数内部的修改默认不要波及外部函数如果有输出用return明确返回。4. 变量作用域与命名空间Python函数里变量的势力范围4.1 LEGB规则找变量的顺序是这样的初学函数的时候很多人会写类似这样的代码x 10 def print_x(): print(x) print_x() # 输出 10函数内部没有定义x却能用外面的x这是因为Python在函数里寻找变量的时候有一套固定的查找顺序缩写叫LEGBLLocal当前函数内部的局部变量EEnclosing外层函数嵌套函数场景里的变量GGlobal模块全局变量BBuilt-inPython内置的变量和函数比如print、len查找顺序就是L → E → G → B一层一层往外找。所以上面的代码里函数内部没有局部x就跑到全局去找找到x 10输出10。4.2 局部变量与全局变量想改外面的值得先声明global但注意如果在函数内部给一个变量赋值情况就不一样了x 10 def change_x(): x 20 # 这里其实是创建了一个新的局部变量 x不是修改外面的全局 x change_x() print(x) # 输出 10外面没变为什么会这样因为函数内部出现了给x赋值的语句Python会认为这个x是局部变量整个函数里的x都按局部来处理。所以x 20创建了一个只活在函数内部的局部变量外面的全局x毫发无损。如果你真的想在函数内部修改全局变量需要先声明global xx 10 def change_x(): global x x 20 change_x() print(x) # 输出 20不过我个人建议在学习和日常项目中尽量少用global。大量使用全局变量会让程序的逻辑变得难以追踪——你根本不知道这个值是在哪个函数里被改掉的。更推荐的做法是函数通过参数接收数据通过return把结果交出去让数据流清晰可见。4.3 嵌套函数与nonlocal闭包概念的前奏Python允许在函数内部再定义函数这叫嵌套函数。如果内层函数想修改外层函数里的局部变量global管不到因为外层变量不是全局变量这时候需要nonlocaldef outer(): count 0 def inner(): nonlocal count count 1 return count return inner counter outer() print(counter()) # 1 print(counter()) # 2这个写法其实是闭包Closure的雏形。inner函数记住了外层函数里count这个变量即使outer已经调用结束了count的生命周期依然被inner延续着。这个模式在很多场景下都很强大比如写计数器、缓存、延迟计算等。但新手阶段不需要死磕闭包你只要理解函数内部的变量有明确的势力范围想跨层修改必须明确声明这就够了。5. 匿名函数lambda与常用内置函数代码写得短平快的技巧5.1 lambda是一句话函数Python里有一种快速定义单行函数的方式叫lambda表达式学名匿名函数。语法很简单square lambda x: x * x # 等价于 def square(x): return x * xlambda后面跟参数冒号后面跟返回值表达式。lambda通常用在那些需要传一个函数作为参数但这个函数又很简单、不值得专门def一个的场景。最典型的例子是配合sorted函数对字典列表排序students [ {name: 张三, score: 88}, {name: 李四, score: 95}, {name: 王五, score: 72}, ] # 按成绩从高到低排序 sorted_students sorted(students, keylambda s: s[score], reverseTrue) print(sorted_students)这里sorted的key参数要求传一个能从一个元素提取出排序依据的函数lambda一行搞定清晰又简洁。但lambda也有坑它只能写一个表达式不能写多行逻辑所以一旦函数体复杂起来就该老老实实用def写一个具名函数。滥用lambda反而会降低可读性。5.2 内置函数三兄弟map、filter、sortedPython内置了很多实用的高阶函数就是以函数为参数的函数。我这里重点说三个最常用的。map(function, iterable)把一个函数依次作用到可迭代对象的每个元素上返回一个迭代器numbers [1, 2, 3, 4, 5] squared list(map(lambda x: x * x, numbers)) print(squared) # [1, 4, 9, 16, 25]filter(function, iterable)把可迭代对象中能使函数返回真值的元素筛选出来numbers [1, 2, 3, 4, 5, 6] even_numbers list(filter(lambda x: x % 2 0, numbers)) print(even_numbers) # [2, 4, 6]sorted(iterable, key..., reverse...)这个前面已经演示过了它是Python里最灵活、最高频的排序函数注意它跟列表的.sort()方法的区别sorted返回一个新列表原列表不变.sort()是原地排序直接修改原列表。5.3 装饰器函数之上的函数给你的函数套一层皮肤如果只有上面这些函数这块讲得还不够解渴。Python函数还有一个非常惊艳的特性就是函数本身也是对象可以作为参数传给另一个函数也可以被返回。基于这个特性诞生了装饰器Decorator。装饰器本质是一个给函数增加额外功能的函数。比如我写了很多函数现在想统计每个函数运行耗时最笨的办法是每个函数里都加好几行计时代码但用了装饰器只需要定义一次import time def timer(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) end time.time() print(f函数 {func.__name__} 耗时 {end - start:.4f} 秒) return result return wrapper timer def slow_function(): time.sleep(1) return done slow_function() # 输出函数 slow_function 耗时 1.0003 秒这里的timer就是一个装饰器它的作用相当于执行了slow_function timer(slow_function)。这在日志记录、权限校验、缓存、性能分析等场景中极其常用。新手阶段可以先知道有这个东西等函数和面向对象都熟悉了再回来看装饰器会豁然开朗。6. 面向对象从函数管理数据到数据自带行为6.1 为什么有了函数还不够还要有类函数解决了动作复用的问题但当我们面对的数据结构越来越复杂时函数式写法会露出一个尴尬的破绽数据和操作数据的方法被拆散了。举个例子假设你要管理系统里的学生信息。用字典存一个学生的数据student1 {name: 张三, age: 18, scores: [88, 95, 72]} student2 {name: 李四, age: 19, scores: [91, 82, 88]}然后你要写一个函数计算某个学生的平均分def average_score(student): return sum(student[scores]) / len(student[scores])这样搞其实也还行逻辑没毛病。但是问题在于如果这个系统里有学生、老师、课程、班级、成绩单等多种对象每种对象都有自己的一堆操作你就要写一堆average_score(student)、get_teacher_name(teacher)、get_course_students(course)这样的函数时间一长函数满天飞数据和逻辑越来越难对应。面向对象的核心思路是把学生这个数据和学生能做哪些操作打包在一起形成一个类。学生这个类既包含属性姓名、年龄、成绩也包含方法计算平均分、打印信息。每个具体的学生叫对象或实例。用一个生活化的类比来说类是图纸对象是按照图纸造出来的实实在在的房子。图纸规定了这栋房子一定会有几个房间、几扇窗属性也规定了它能做哪些事比如开窗、锁门方法。张三、李四就是照着学生这张图纸造出来的一个个具体房子。6.2 定义一个类__init__方法决定了造出来的房子长什么样Python里定义类用class关键字class Student: def __init__(self, name, age, scores): self.name name self.age age self.scores scores def average_score(self): return sum(self.scores) / len(self.scores) def introduce(self): print(f我叫{self.name}今年{self.age}岁)然后通过类名加括号创建实例s1 Student(张三, 18, [88, 95, 72]) s2 Student(李四, 19, [91, 82, 88]) print(s1.name) # 张三 print(s1.average_score()) # 85.0 s2.introduce() # 我叫李四今年19岁这里有几个关键点必须吃透第一__init__是初始化方法。它在创建对象的时候自动调用作用是给这个对象填充初始属性。注意它的前后各有两个下划线这是Python魔法方法的标识表示这是Python内部约定俗成的特殊方法。新手最常犯的错就是把__init__拼成_init_少一个下划线或者__int__结果类能创建但属性死活初始化不了。第二self代表当前实例本身。在类里面定义方法时第一个参数永远是self它指向当前正在操作的这个对象。调用s1.average_score()的时候self就指向s1调用s2.introduce()的时候self就指向s2。很多人学不明白类就是卡在self上。你只要记住self就是我自己定义方法时写上self方法里要用本对象的属性或方法都得通过self来访问。第三括号里的参数传给__init__。Student(张三, 18, [88, 95, 72])看起来像函数调用实际上__init__会收到self新创建的对象、name张三、age18、scores[88,95,72]。6.3 类变量与实例变量的区别别让所有对象共享同一个属性定义属性时有两种写法在__init__里用self.xxx ...定义的是实例变量每个对象各有一份直接在类内部定义的是类变量所有对象共享一份。class Student: school 杭州第一中学 # 类变量所有学生共有的学校名 def __init__(self, name, age): self.name name # 实例变量每个学生的姓名不同 self.age age访问的时候都能访问到s1.school和s2.school但如果通过某个实例给school属性赋值那只是给这个实例创建了一个实例变量并不会影响类变量。这个特性跟前面函数默认参数的坑有点类似都是可变共享带来的隐患。如果你在类变量里用了一个列表然后通过实例去修改它其他实例也能看到变化这既是特性也是坑使用时要清醒。6.4 方法和函数的区别方法就是绑定在对象上的函数类里面定义的方法本质上就是函数只不过它的第一个参数是self所以调用时必须通过实例去调用除了我们后面说的staticmethod和classmethod那是给类本身用的。普通方法、静态方法、类方法这三者的区别我用一个表格总结方法类型定义方式调用方式能访问什么实例方法def method(self, ...)实例调用obj.method()可以访问实例属性和类属性最常用类方法classmethod def method(cls, ...)类或实例调用Class.method()只能访问类属性适合做备选构造函数静态方法staticmethod def method(...)类或实例调用Class.method()跟类没有数据绑定关系仅放在类里做逻辑归属新手阶段先牢牢掌握实例方法遇到这个功能不需要用到对象的任何属性时才考虑静态方法或类方法。我见过不少人一上来就炫技全写成静态方法那跟写普通函数有什么区别呢面向对象的意义就丢了。7. 封装、继承、多态面向对象三大特征的实战理解7.1 封装对外隐藏细节只暴露必要的接口封装这个词听起来很抽象其实它的核心就一句话把数据和操作数据的方法打包在一起同时对外隐藏内部实现细节只暴露一个简单的调用接口。还是用Student类举例。外部使用方只需要知道创建学生填姓名年龄成绩调用average_score()能算出平均分至于内部成绩是怎么存的、平均分公式是什么外部根本不用关心。这就是封装的好处你后面想改内部实现比如把scores从列表改成元组只要方法的对外接口不变所有调用方都不受影响。Python实现封装的手段主要有两种一是用魔法方法和命名约定。比如_name表示这是内部变量你别在外面直接用__name双下划线开头会被Python名称改写name mangling变成_ClassName__name防止意外覆盖。但Python并不是Java、C那种强私有机制它更像是一种约定轻度保护。二是用property装饰器把方法变成属性来访问这样可以在赋值和取值时增加校验逻辑class Student: def __init__(self, name, age): self._name name self._age age property def age(self): return self._age age.setter def age(self, value): if value 0 or value 150: raise ValueError(年龄不合法) self._age value s Student(张三, 18) s.age 25 # 走的是 setter会校验 print(s.age) # 走的是 getter这样外部代码写起来像直接访问属性但实际上经过了方法校验非常优雅。7.2 继承子类复用父类的基因继承是面向对象里最直观、也最容易理解错的一个特性。它就是**子类自动拥有父类的属性和方法同时还能扩展或修改**。举个例子我们要管理老师和学生两类人他们都有姓名、年龄都会介绍自己但老师还有所教科目学生还有所在班级。不写继承的话你要写两份高度重复的代码。写了继承之后class Person: def __init__(self, name, age): self.name name self.age age def introduce(self): print(f我叫{self.name}今年{self.age}岁) class Student(Person): def __init__(self, name, age, class_name): # 调用父类的初始化方法先初始化 name 和 age super().__init__(name, age) self.class_name class_name def introduce(self): # 重写父类方法但复用父类的逻辑 super().introduce() print(f我在{class_name}班) class Teacher(Person): def __init__(self, name, age, subject): super().__init__(name, age) self.subject subject s Student(张三, 18, 高三(2)班) t Teacher(王老师, 35, 数学) s.introduce() t.introduce()这里的关键是**super()**它返回父类的一个代理对象super().__init__(name, age)就是先让父类去初始化基础属性子类再补充自己的扩展属性。这是一种非常标准的写法。继承的本质好处是复用——公共的属性和逻辑只写一遍放在父类里。子类通过继承自动获取还可以通过方法重写override来改变父类方法的行为。但继承也有一个被过度使用的问题。我个人的经验是能用组合一个类持有另一个类的实例解决的问题优先别用继承。继承会让类与类之间形成强耦合的父子关系层级一旦深了三层以上就非常痛苦改父类可能影响所有子类。我见过某些代码里搞了五六层继承最后谁都改不动。新手阶段先学会标准用法就够了不急于设计复杂体系。7.3 多态同样的接口不同的行为多态这个概念用一句话说就是同一段代码处理不同类型的对象时表现出不同的行为。比如我们定义了一个函数接受一个Person类型的对象然后调用introduce()def let_them_introduce(person): person.introduce()你把Student对象传进去执行的是Student.introduce()的逻辑把Teacher对象传进去执行的是Teacher.introduce()的逻辑。调用代码完全不用修改行为却跟着对象类型不同而变化。在Python里多态甚至不需要严格的继承关系因为Python是鸭子类型——如果一个东西走路像鸭子、叫起来像鸭子那它就是鸭子。只要你传进去的对象有introduce()方法代码就能跑。这比Java那种必须继承某个接口才能多态的方式要宽松得多。对新手来说多态不用刻意设计你只要记住一个习惯写代码时尽量面向父类型或共同接口去编写调用逻辑而不是针对具体某个子类写一大堆if isinstance(...)判断。这样后续新增一个子类调用方代码不用动。8. 魔法方法与几个很容易掉的面向对象坑8.1 魔法方法让对象拥有原生类型的行为Python的类里有一批以双下划线开头和结尾的特殊方法统称魔法方法Magic Methods / Dunder Methods。它们不是让你手动调用的而是Python解释器在特定场景下自动调用。除了已经见过的__init__还有几个特别值得先学__str__和__repr__定义对象被打印或展示时的字符串形式。__str__是给用户看的__repr__是给开发者看的比如在列表里显示时。__len__定义len(obj)返回什么。__eq__定义两个对象比较相等时的逻辑。__lt__、__gt__等定义排序比较逻辑。举个最常用的例子class Student: def __init__(self, name, age, score): self.name name self.age age self.score score def __str__(self): return fStudent(name{self.name}, age{self.age}, score{self.score}) def __repr__(self): return fStudent({self.name!r}, {self.age!r}, {self.score!r}) def __eq__(self, other): if not isinstance(other, Student): return NotImplemented return self.score other.score def __lt__(self, other): return self.score other.score s1 Student(张三, 18, 88) s2 Student(李四, 19, 95) print(s1) # 调用 __str__ print(s1 s2) # True调用 __lt__不看魔法方法的话你会遇到一个常见困惑print(对象)输出的是一串看不懂的内存地址比如__main__.Student object at 0x7f9a...就是因为没有定义__str__解释器只能显示默认的对象信息。定义了__str__之后打印对象就变成你想要的样子了。8.2 常见坑一忘了给self赋值或者漏了return面向对象初学者最常见的错误我可以总结成三句话第一__init__里写了参数但没用self.xxx xxx存起来。结果创建对象倒是成功了一访问属性就报AttributeError: Student object has no attribute name。原因就是初始化的时候光顾着接收参数没把参数绑定到实例上。第二方法里忘了写self。写def introduce():然后直接在里面引用self.namePython会报n
返回列表