提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 面向对象之类和对象
-
- 定义类
- 类的操作
- __init()__方法
- self
- 属性
-
- 第一幕:共享的图纸(查找原则)
- 第二幕:修改公共图纸(牵一发而动全身)
- 第三幕:震撼反转!"赋值即创建"(私有财产的诞生)
- [🌟 终极总结(防坑口诀)](#🌟 终极总结(防坑口诀))
- 实例属性
- 方法
- 总结
-
-
- [⚠️ 避坑指南:极其容易犯错的 6 大暗坑](#⚠️ 避坑指南:极其容易犯错的 6 大暗坑)
-
- [💣 暗坑 1:实例属性"屏蔽"类属性(赋值即创建)](#💣 暗坑 1:实例属性“屏蔽”类属性(赋值即创建))
- [💣 暗坑 2:在 `init` 中忘记加 `self.`](#💣 暗坑 2:在
__init__中忘记加self.) - [💣 暗坑 3:动态给实例添加方法时,忘了 `types.MethodType`](#💣 暗坑 3:动态给实例添加方法时,忘了
types.MethodType) - [💣 暗坑 4:以为 `del p` 会立刻触发 `del`](#💣 暗坑 4:以为
del p会立刻触发__del__) - [💣 暗坑 5:误用 `slots` 以为能锁死整个类](#💣 暗坑 5:误用
__slots__以为能锁死整个类) - [💣 暗坑 6:类属性如果是"可变类型",会导致全员污染!](#💣 暗坑 6:类属性如果是“可变类型”,会导致全员污染!)
-
面向对象之类和对象
面向过程编程(Procedural Programming)和面向对象编程(OOP)是两种不同的编程范式,它们在软件开发中都有广泛的应用。
Python是一种混合型的语言,既支持面向过程的编程,也支持面向对象的编程。
面向过程的编程是一种以过程为中心的编程方式,主要关注解决问题的步骤,并将这些步骤写成函数或方法。
面向过程举例
想象一下,你要做一顿美味的晚餐。在面向过程编程的思维下,你会把整个做饭的过程拆分成一系列的步骤。
python
def buy():
print("去超市购买食材。")
def wash():
print("清洗蔬菜。")
def cut():
print("切菜。")
def cook():
print("开始烹饪。")
def serve():
print("上菜啦!")
buy()
wash()
cut()
cook()
serve()
上面就是一个典型的面向过程的程序,我们把整个做饭的过程分解成了一个个函数,每个函数完成一个特定的任务,然后按照顺序依次调用这些函数,就可以完成做晚餐的任务啦。这种方式非常直接,适合一些简单的任务,它注重的是程序的流程和步骤。
但是呢,当我们的程序变得越来越复杂,会出现什么问题呢?比如说,我们现在想做不同类型的菜,有些菜可能不需要洗菜,有些菜可能不需要切菜,或者你要同时做几道菜,那我们的代码就会变得越来越长,越来越乱,而且上面的代码步骤是没有通用性的。
面向对象举例
python
用面向对象的思想实现上面的做菜功能
class Dish:
def __init__(self, name):
self.name = name
def prepare(self):
pass
class Salad(Dish):
def prepare(self):
print(f"为 {self.name} 购买食材。")
print(f"清洗 {self.name} 的蔬菜。")
print(f"切 {self.name} 的蔬菜。")
class Stew(Dish):
def prepare(self):
print(f"为 {self.name} 购买食材。")
print(f"切 {self.name} 的肉。")
print(f"烹饪 {self.name}。")
class Soup(Dish):
def prepare(self):
print(f"为 {self.name} 购买食材。")
print(f"煮 {self.name}。")
salad = Salad("蔬菜沙拉")
stew = Stew("炖肉")
soup = Soup("西红柿鸡蛋汤")
salad.prepare()
stew.prepare()
soup.prepare()
在这里,我们创建了一个 Dish 类,它就像是一个菜的模板。然后我们创建了 Salad、Stew 和 Soup 这些子类,它们都继承自 Dish 类。每个子类都有自己的 prepare 方法,这个方法描述了如何准备这道菜。
这样,我们可以看到面向对象编程的优势啦 首先,我们把相关的数据(比如菜的名字)和操作(比如准备菜的过程)都封装在了一个类里面,这叫做 "封装"。而且,不同类型的菜可以有自己独特的准备方法,我们可以根据需要去修改或扩展这些方法,而不会影响其他类。这就像是每个菜都有自己的制作过程。
还有,当我们想要添加新的菜品时,我们只需要创建一个新的子类,定义它自己的 prepare 方法就好,不需要修改原来的代码。
类(Class)
类描述了所创建的对象共同的属性(是什么)和方法(能做什么),属性和方法统称为类的成员。
类是对大量对象共性的抽象
类是创建对象的模板
类是客观事物在人脑中的主观反映
对象(Object)
在自然界中,只要是客观存在的事物都是对象
类是抽象的,对象是类的实例(Instance),是具体的。
一个对象有自己的状态(属性)、行为(方法)和唯一的标识(本质上指内存中所创建的对象的地址)。
定义类
python
class 类名:
"""类说明文档"""
类体
类名一般使用大驼峰命名法。
类体中可以包含类属性(也叫类变量)、方法、实例属性(也叫实例变量)等。
定义一个人的类,包含 init () 方法、eat() 方法和 drink() 方法。

python
class Person:
"""人的类"""
home = "earth"#类属性
def __init__(self):
self.age = 0#实例属性
def eat(self):
print("eating...")
def drink(self):
print("drinking...")
类的操作
类支持两种操作,成员引用和实例化。
成员引用
类名.成员名
python
class Person:
"""人的类"""
home = "earth"
def __init__(self):
self.age = 0
def eat(self):
print("eating...")
def drink(self):
print("drinking...")
home = Person.home # 获取一个字符串
eat_function = Person.eat # 获取一个函数对象
doc = Person.__doc__ # 获取类的说明文档
print(home) # earth
print(eat_function) # <function Person.eat at 0x00000232C8230F40>
print(doc) # 人的类
实例化
变量名 = 类名()
python
class Person:
"""人的类"""
home = "earth"
def __init__(self):
self.age = 0
def eat(self):
print("eating...")
def drink(self):
print("drinking...")
p = Person() # 创建一个对象
print(p.home) # earth
print(p.age) # 0
p.eat() # eating...
p.drink() # drinking...
__init()__方法
init () 是一个特殊的方法,也被称作构造函数。init () 方法的主要作用是在创建类的对象时,对对象的属性进行初始化。当你使用类名创建一个新的对象时,Python 会自动调用 init () 方法,并将新创建的对象作为第一个参数(通常命名为 self)传递给它。
self:这是一个约定俗成的参数名,它代表类的实例对象本身。在方法内部,通过 self 可以访问和修改对象的属性。
init () 方法不是必需的。如果类中没有定义 init () 方法,Python 会使用默认的构造函数,该构造函数不执行任何操作。
init() 方法只能返回 None,不能返回其他值。如果尝试返回其他值,会引发 TypeError 异常
python
class Person:
"""人的类"""
home = "earth"
def __init__(self, name):
self.name = name
p = Person("张三") # 创建一个对象
print(p.name) # 张三
self
self代表类的实例自身。调用实例方法时,实例对象会作为第一个参数被传入。因此,我们调用p.eat()时就相当于调用了Person.eat§。
python
class Person:
"""人的类"""
home = "earth"
def __init__(self, name):
self.name = name
def eat(self):
print("eating...")
def drink(self):
print("drinking...")
p = Person("张三") # 创建一个对象
p.eat() # eating...
Person.eat(p) # eating...
通过self在类中调用类的实例属性和实例方法
python
class Person:
"""人的类"""
home = "earth"
def __init__(self, name):
self.name = name
def eat(self):
print("eating...")
def drink(self):
print("drinking...")
def eat_and_drink(self):
print(self.name) # 在类中调用name
self.eat() # 在类中调用eat()方法
self.drink() # 在类中调用drink()方法
p = Person("张三") # 创建一个对象
p.eat_and_drink()
属性
类属性 :也叫类变量。在类中方法外定义的属性。
1)通过 类名.属性名 或 实例名.属性名 访问
python
class Person:
"""人的类"""
home = "earth" # 定义类属性
print(Person.home) # 通过类名访问类属性
p1 = Person() # 创建一个实例对象
print(p1.home) # 通过实例名访问类属性,(如果实例没有覆盖这个类属性的值)
2)通过 类名.属性名 添加与修改类属性
python
class Person:
"""人的类"""
Person.home = "earth" # 添加类属性
print(Person.home) # earth
Person.home = "mars" # 修改类属性
print(Person.home) # mars
若使用 实例名.属性名 则会创建或修改实例属性,因此不建议类属性和实例属性同名。若是修改某一个实例属性,则不会影响其他的实例属性
python
class Person:
"""人的类"""
home = "earth"
p1 = Person()
p2 = Person()
print(Person.home) # earth
print(p1.home) # earth
print(p2.home) # earth
print("通过 类名.属性名 修改 类属性")
Person.home = "mars"
print(Person.home) # mars
print(p1.home) # mars
print(p2.home) # mars
print("通过 实例名.属性名 会创建 实例属性")
p1.home = "venus"
print(Person.home) # mars
print(p1.home) # venus
print(p2.home) # mars
3)所有该类的实例共享同一个类属性
python
class Person:
"""人的类"""
home = "earth" # 定义类属性,所有实例共享
p1 = Person() # 创建一个实例对象
p2 = Person() # 创建另一个实例对象
print(p1.home) # earth
print(p2.home) # earth
Person.home = "mars" # 修改类属性
print(p1.home) # mars
print(p2.home) # mars
这段代码是 Python 面向对象(OOP)中最经典、最容易把新手绕晕、也是面试中必考的"连环坑"!
要彻底看懂它,你只需要掌握 Python 内部一个极其霸道且聪明的机制:属性查找的"就近原则(MRO 的变体)"和"赋值即创建"原则。
我们继续用**"造人图纸(类)"和 "活生生的人(实例)"的比喻,外加一个"户口本"**的设定,来完美破解这个谜团。
第一幕:共享的图纸(查找原则)
python
class Person:
home = "earth" # 1. 这是一个写在"人类图纸"右上角的公共属性
p1 = Person() # 造出张三 (p1)
p2 = Person() # 造出李四 (p2)
print(Person.home) # earth (直接看图纸,没毛病)
print(p1.home) # earth
print(p2.home) # earth
【核心原因】:为什么 p1.home 也是 earth?
当 Python 看到 p1.home 时,它的内心戏是这样的:
- 先看
p1(张三)自己的"个人户口本(实例字典__dict__)"里有没有home这个记录。 - 找了一圈,发现张三是个穷光蛋,自己的户口本是空的。
- Python 说:"没关系,我去你出生的那张**公共图纸(类
Person)**上帮你找找看。" - 在图纸上找到了
home = "earth"。 - 于是,Python 把图纸上的
earth读出来,借给张三用。
结论 1: 实例自己没有这个属性时,会向上级(类)借用 。这就是为什么 p1 和 p2 都能读出 earth。
第二幕:修改公共图纸(牵一发而动全身)
python
print("通过 类名.属性名 修改 类属性")
Person.home = "mars" # 2. 我们拿笔,把图纸上的 earth 划掉,改成了 mars
print(Person.home) # mars
print(p1.home) # mars
print(p2.home) # mars
【核心原因】:为什么 p1 和 p2 都跟着变了?
因为在这个阶段,p1 和 p2 依然是穷光蛋,他们自己的户口本里依然没有 home。
当他们再次去读 p1.home 时,依然要去图纸上借。
而图纸上的字已经被改成了 mars,所以他们读出来的自然就全都变成了 mars。
结论 2: 修改类属性,会瞬间影响所有还在"借用"该属性的实例。
第三幕:震撼反转!"赋值即创建"(私有财产的诞生)
python
print("通过 实例名.属性名 会创建 实例属性")
p1.home = "venus" # 3. 极其关键的一步!
print(Person.home) # mars (图纸没变)
print(p1.home) # venus (张三变了!)
print(p2.home) # mars (李四没变)
【核心原因】:这到底发生了什么魔法?
这句代码 p1.home = "venus" 触发了 Python 中一条铁律:
👉 一旦遇到等号(=),并且左边是具体的实例(p1),Python 绝对不会去修改类图纸!而是直接在 p1(张三)自己的户口本里,强制新建一条个人记录!
此时,Python 的内心戏变了:
- 你要给
p1赋值home = "venus"。 - 我才不管图纸上有没有
home,我直接在张三(p1)的私有口袋里塞进去一个home="venus"。 - 从这一刻起,张三不再是穷光蛋了,他拥有了自己私有的
home属性!
接下来,见证"就近原则"的威力:
- 当再次执行
print(p1.home)时,Python 先找张三的口袋。找到了! 里面有他私有的venus。既然自己有,就绝对不会再去图纸上借了 。所以打印出venus。(这叫"实例属性屏蔽了类属性")。 - 当执行
print(p2.home)时,李四(p2)依然是个穷光蛋,口袋空空,只能继续去图纸上借。图纸上依然是mars。 - 当执行
Person.home时,图纸上的字从来没被动过,依然是mars。
🌟 终极总结(防坑口诀)
背下这两句话,你就能在面向对象的坑里横着走:
- 读取属性(读):先找自己(实例),自己没有,再找亲爹(类)。
- 赋值属性(写):不管亲爹有没有,只要用了
实例.属性 = 值,永远只给自己(实例)新建/修改私有财产,绝对不连累亲爹!
实例属性
也叫实例变量。在类__init__方法中定义的属性。通过 self.属性名定义。
1)通过 实例名.属性名 访问
python
class Person:
"""人的类"""
def __init__(self, name, age):
self.name = name # 定义实例属性
self.age = age # 定义实例属性
p1 = Person("张三", 18) # 创建一个实例对象
print(p1.name, p1.age) # 张三 18
p2 = Person("李四", 81) # 创建一个实例对象
print(p2.name, p2.age) # 李四 81
print(Person.name) # 报错
2)通过 实例名.属性名 添加与修改实例属性
python
class Person:
"""人的类"""
pass
p1 = Person() # 创建一个实例对象
p1.name = "张三" # 添加实例属性
p1.age = 18 # 添加实例属性
print(p1.name, p1.age) # 张三 18
p1.age = 25 # 修改实例属性
print(p1.name, p1.age) # 张三 25
3)每个实例独有一份实例属性
python
class Person:
"""人的类"""
def __init__(self, name):
self.name = name # 定义实例属性
self.age = 0 # 定义实例属性
p1 = Person("张三") # 创建一个实例对象
print(p1.name, p1.age) # 张三 0
p1.age = 18 # 修改p1的age属性
print(p1.name, p1.age) # 张三 18
p2 = Person("李四") # 创建另一个实例对象
print(p2.name, p2.age) # 李四 0
方法
Python的类中有三种方法:实例方法、静态方法、类方法。
1. 普通的【实例方法】(Instance Method)
- 长相 :头上没有任何
@装饰器。 - 特征 :第一个参数永远是
self(代表活生生的人/具体的实例)。 - 作用:专门用来操作属于那个实例自己的私有财产(实例属性)。
- 代码*:
python
class Person:
"""人的类"""
home = "earth"
def __init__(self, name):
self.name = name
def instance_method(self):
print(self.name, self.home, Person.home)
p = Person("张三")
p.instance_method() # 张三 earth earth,此时p中没有home实例属性,会去查找home类属性
Person.home = "venus" # 修改类属性
p.home = "mars" # 定义实例属性
p.instance_method() # 张三 mars venus
为了防止你混淆,我顺便把另外两个加了 @ 的方法给你做个"剧透"和对比,你一看秒懂:
2. 真正的【类方法】(Class Method)
-
长相 :头上必须戴一顶帽子
@classmethod。 -
特征 :第一个参数不能叫
self了,行规要求必须叫cls(代表 Class,也就是那张"图纸"本身)。 -
作用 :既然第一个参数是图纸,说明这个方法是专门用来修改公共图纸的,跟具体的张三李四没关系。
-
代码模样 :
pythonclass Person: home = "earth" @classmethod def change_home(cls, new_home): cls.home = new_home # 直接修改全人类的图纸!
3. 特殊的【静态方法】(Static Method)
-
长相 :头上戴着帽子
@staticmethod。 -
特征 :它是个"孤儿"。它的括号里既不需要
self,也不需要cls。 -
作用 :它其实就是一个普普通通的函数,只是因为逻辑上跟这个类有点关系,所以被硬塞到了类的肚子里(方便管理)。它既不碰张三的私有财产,也不改人类的公共图纸。
-
代码模样 :
pythonclass Person: @staticmethod def say_hello(): # 里面空空如也,没有 self 也没有 cls print("Hello, 无论你是谁,不管是在地球还是火星!")
🌟 终极防坑总结
- 头上什么都不戴 ,第一个参数是
self👉 【实例方法】(最常用,操作具体对象)。 - 头上戴
@classmethod,第一个参数是cls👉 【类方法】(操作类图纸)。 - 头上戴
@staticmethod,不需要默认参数 👉 【静态方法】(寄人篱下的普通函数)。
在类外定义方法
并非必须在类定义中进行方法定义,也可以将一个函数对象赋值给一个类内局部变量。
python
# 在类外定义的函数
def f1(self, x, y):
print(x & y)
class C:
f = f1
C().f(6, 13) # 4
过程类似于下面
- 定义函数 f1
↓ - 创建类 C,把 f1 赋值给 C.f
↓ - C() 创建实例(临时对象)
↓ - instance.f(6, 13)
↓ - Python 自动转换为: C.f(instance, 6, 13)
↓ - 实际调用: f1(instance, 6, 13)
↓ - 执行函数体: print(6 & 13)
↓ - 输出: 4
特殊方法
方法名中有两个前缀下划线和两个后缀下划线的方法为特殊方法,也叫魔法方法。上文提到的 init () 就是一个特殊方法。这些方法会在进行特定的操作时自动被调用。
几个常见的特殊方法:
1)new ()
对象实例化时第一个调用的方法。
2)init ()
类的初始化方法。
3)del ()
对象的销毁器,定义了当对象被垃圾回收时的行为。使用 del xxx 时不会主动调用 del () ,除非此时引用计数==0。
4)str ()
定义了对类的实例调用 str() 时的行为。
5)repr ()
定义对类的实例调用 repr() 时的行为。 str() 和 repr() 最主要的差别在于目标用户。 repr() 的作用是产生机器可读的输出(大部分情况下,其输出可以作为有效的Python代码),而 str() 则产生人类可读的输出。
6)getattribute ()
属性访问拦截器,定义了属性被访问前的操作。
python
import sys
class Person:
count = 0 # 类属性(统计实例数量)
def __new__(cls, *args, **kwargs):
print(f"1. __new__() 被调用")
instance = super().__new__(cls)
return instance
def __init__(self, name, age):
print(f"2. __init__() 被调用: name={name}, age={age}")
self.name = name
self.age = age
Person.count += 1
def __str__(self):
return f"{self.name}, {self.age}岁"
def __repr__(self):
return f'Person("{self.name}", {self.age})'
def __getattribute__(self, item):
print(f"正在访问属性: {item}")
return super().__getattribute__(item)
def __del__(self):
print(f"3. __del__() 被调用: {self.name} 被销毁")
Person.count -= 1
# 创建对象
print("=== 创建对象 ===")
p1 = Person("张三", 25)
print("\n=== 访问属性 ===")
print(p1.name)
print("\n=== 字符串表示 ===")
print(f"str(p1) = {str(p1)}")
print(f"repr(p1) = {repr(p1)}")
print("\n=== 增加引用 ===")
p2 = p1
print(f"引用计数: {sys.getrefcount(p1) - 1}") # 减去 getrefcount 自己的引用
print("\n=== 删除一个引用 ===")
del p2
print(f"引用计数: {sys.getrefcount(p1) - 1}")
print("\n=== 删除最后一个引用 ===")
del p1
print("\n=== 程序结束 ===")
动态给对象添加属性
python
class Person:
def __init__(self, name=None):
self.name = name
p = Person("张三")
print(p.name) # 张三
p.age = 18
print(p.age) # 18
动态给类添加属性
python
class Person:
def __init__(self, name=None):
self.name = name
p = Person("张三")
print(p.name) # 张三
Person.age = 0
print(p.age) # 0
动态给实例添加方法
1)添加普通方法
python
class Person:
def __init__(self, name=None):
self.name = name
def eat():
print("吃饭")
p = Person("张三")
p.eat = eat
p.eat() # 吃饭
2)添加实例方法
给对象添加的实例方法只绑定在当前对象上,不对其他对象生效,而且需要传入 self 参数。需要使用 types.MethodType(方法名,实例对象) 来添加实例方法。
python
import types
class Person:
def __init__(self, name=None):
self.name = name
def eat(self):
print(f"{self.name}在吃饭")
p = Person("张三")
p.eat = types.MethodType(eat, p)
p.eat() # 张三在吃饭
动态给类添加方法
给类添加的方法对它的所有对象都生效,添加类方法需要传入 cls 参数,添加静态方法则不需要。
python
class Person:
home = "earth"
def __init__(self, name=None):
self.name = name
# 定义类方法
@classmethod
def come_from(cls):
print(f"来自{cls.home}")
# 定义静态方法
@staticmethod
def static_function():
print("static function")
Person.come_from = come_from
Person.come_from() # 来自earth
Person.static_function = static_function
Person.static_function() # static function
动态删除属性与方法
del 对象.属性名
delattr(对象,属性名)
__slots__限制实例属性与实例方法
Python允许在定义类的时候,定义一个特殊的 slots 变量,来限制该类的实例能添加的属性。使用 slots 可以限制添加实例属性和实例方法,但类属性、类方法和静态方法还可以添加。__slots__仅对当前类生效,对其子类无效。
python
import types
class Person:
__slots__ = ("name", "age", "eat")
def __init__(self, name=None):
self.name = name
def eat(self):
print(f"{self.name}在吃饭")
def drink(self):
print(f"{self.name}在喝水")
p = Person("张三")
# 添加实例属性
p.age = 10
print(p.age) # 10
# 添加实例方法
p.eat = types.MethodType(eat, p)
p.eat() # 张三在吃饭
# 添加实例属性
p.weight = 100 # AttributeError: 'Person' object has no attribute 'weight'
# 添加实例方法
p.drink = type.MethodType(drink, p) # AttributeError: type object 'type' has no attribute 'MethodType'
总结
- 宏观思维(面向过程 vs 面向对象)
- 面向过程 :像是一份"菜谱",一步步按顺序执行(
buy -> wash -> cut -> cook)。 - 面向对象 :像是一个"世界",创造不同的实体(对象),每个实体带着自己的数据(属性)和动作(方法)互相交互。核心优势是封装、继承、多态。
- 面向过程 :像是一份"菜谱",一步步按顺序执行(
- 灵魂伴侣(类与对象 &
self)- 类 是图纸,对象是按图纸造出来的实物。
self的本质 :它绝不是类,它是对象本身 的化身。p.eat()在底层就是Person.eat(p)。
- 属性与方法(静态与动态)
- 属性 :分为全人类共享的类属性 (写在图纸上)和个人独有的实例属性 (写在个人的户口本
__dict__里)。 - 方法 :实例方法 (传
self)、类方法 (加@classmethod传cls)、静态方法 (加@staticmethod什么都不传)。
- 属性 :分为全人类共享的类属性 (写在图纸上)和个人独有的实例属性 (写在个人的户口本
- Python 特有的黑魔法(动态特性 & 魔法方法)
- 动态特性:Python 允许在程序运行期间,随时给类或对象打"猴子补丁"(动态添加/删除属性和方法)。
- 魔法方法 :双下划线包围的方法(如
__init__,__del__),它们会在特定的生命周期被系统自动触发。
⚠️ 避坑指南:极其容易犯错的 6 大暗坑
面向对象是 Python 中最容易出 Bug 的地方,以下 6 个坑,每一个都是面试常客和实战灾难!
💣 暗坑 1:实例属性"屏蔽"类属性(赋值即创建)
这是笔记中最重要的部分,也是无数人抓狂的地方。
❌ 错觉: 以为修改实例的属性,就是在修改类的属性。
python
class Person:
home = "earth"
p1 = Person()
p1.home = "mars" # 致命错觉:以为把全人类的老家都改成火星了
print(Person.home) # 实际输出: earth (图纸根本没变!)
✅ 铁律: p1.home = "mars" 触发了**"赋值即创建"**。它并没有修改类属性,而是在 p1 自己的口袋里新建了一个私有的 home。要修改全人类的老家,必须严格写:Person.home = "mars"。
💣 暗坑 2:在 __init__ 中忘记加 self.
❌ 错误代码:
python
class Person:
def __init__(self, name):
name = name # 报错隐患:这只是个局部变量!函数一结束就销毁了
p = Person("张三")
print(p.name) # 报错:AttributeError,对象没有 name 属性
✅ 避坑: 想要把数据永久绑在对象身上,必须像贴狗皮膏药一样,死死贴在 self 上:self.name = name。
💣 暗坑 3:动态给实例添加方法时,忘了 types.MethodType
当给类 动态添加方法时,直接赋值 Person.eat = eat 是没问题的。
但是当你给具体的对象 动态添加方法时,直接赋值会出大问题!
❌ 错误代码:
python
def eat(self):
print(f"{self.name}在吃饭")
p = Person("张三")
p.eat = eat # 强行赋值
p.eat() # 报错:eat() 缺少1个必需的位置参数 'self'
✅ 避坑: 必须使用 types.MethodType 把函数和对象强行绑定 在一起,Python 才会自动把 p 当作 self 传进去:
p.eat = types.MethodType(eat, p)
💣 暗坑 4:以为 del p 会立刻触发 __del__
❌ 错觉: 以为写了 del,对象就立刻死亡并执行遗言 __del__。
python
p1 = Person("张三")
p2 = p1 # p2 也指向张三 (引用计数为2)
del p1 # 错觉:以为这里会打印出 __del__ 里的销毁信息
✅ 避坑: del 只是剪断了一根牵着气球的绳子。只有当所有绳子都被剪断 (引用计数为 0 时),气球飞走,才会真正触发 __del__。
💣 暗坑 5:误用 __slots__ 以为能锁死整个类
❌ 错觉: 以为加了 __slots__ = ("name", "age"),这个类就彻底不能加别的属性了。
✅ 避坑: __slots__ 有两个致命的弱点:
- 它只限制实例属性 ,不限制类属性!(你依然可以执行
Person.weight = 100)。 - 它不被子类继承 !如果你写了个子类继承自
Person,子类依然可以随便加属性,除非子类自己也写一遍__slots__。
💣 暗坑 6:类属性如果是"可变类型",会导致全员污染!
这结合了上一节"函数传参"的知识点。如果类属性是个列表,通过实例去修改它,会发生恐怖的连锁反应。
⚠️ 危险代码:
python
class Dog:
tricks = [] # 这是一个类属性,全体狗共享
dog1 = Dog()
dog2 = Dog()
# 注意:这里用的是 .append(),不是 =。它没有创建私有属性,而是顺藤摸瓜改了图纸上的列表!
dog1.tricks.append("翻滚")
print(dog2.tricks) # 输出: ['翻滚'] <-- dog2 突然自己学会了翻滚!
✅ 避坑: 永远不要把列表、字典这种可变类型 设为类属性(除非你真的想让全体实例共享一个数据池)。正确的做法是把它们放在 __init__ 里,作为 self.tricks = [] 实例属性。