Python基础 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 时,它的内心戏是这样的:

  1. 先看 p1(张三)自己的"个人户口本(实例字典 __dict__)"里有没有 home 这个记录。
  2. 找了一圈,发现张三是个穷光蛋,自己的户口本是空的
  3. Python 说:"没关系,我去你出生的那张**公共图纸(类 Person)**上帮你找找看。"
  4. 在图纸上找到了 home = "earth"
  5. 于是,Python 把图纸上的 earth 读出来,借给张三用。

结论 1: 实例自己没有这个属性时,会向上级(类)借用 。这就是为什么 p1p2 都能读出 earth


第二幕:修改公共图纸(牵一发而动全身)

python 复制代码
print("通过 类名.属性名 修改 类属性")
Person.home = "mars"  # 2. 我们拿笔,把图纸上的 earth 划掉,改成了 mars

print(Person.home)  # mars
print(p1.home)      # mars
print(p2.home)      # mars

【核心原因】:为什么 p1 和 p2 都跟着变了?

因为在这个阶段,p1p2 依然是穷光蛋,他们自己的户口本里依然没有 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 的内心戏变了:

  1. 你要给 p1 赋值 home = "venus"
  2. 我才不管图纸上有没有 home,我直接在张三(p1)的私有口袋里塞进去一个 home="venus"
  3. 从这一刻起,张三不再是穷光蛋了,他拥有了自己私有的 home 属性!

接下来,见证"就近原则"的威力:

  • 当再次执行 print(p1.home) 时,Python 先找张三的口袋。找到了! 里面有他私有的 venus。既然自己有,就绝对不会再去图纸上借了 。所以打印出 venus。(这叫"实例属性屏蔽了类属性")。
  • 当执行 print(p2.home) 时,李四(p2)依然是个穷光蛋,口袋空空,只能继续去图纸上借。图纸上依然是 mars
  • 当执行 Person.home 时,图纸上的字从来没被动过,依然是 mars

🌟 终极总结(防坑口诀)

背下这两句话,你就能在面向对象的坑里横着走:

  1. 读取属性(读):先找自己(实例),自己没有,再找亲爹(类)。
  2. 赋值属性(写):不管亲爹有没有,只要用了 实例.属性 = 值,永远只给自己(实例)新建/修改私有财产,绝对不连累亲爹!

实例属性

也叫实例变量。在类__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,也就是那张"图纸"本身)。

  • 作用 :既然第一个参数是图纸,说明这个方法是专门用来修改公共图纸的,跟具体的张三李四没关系

  • 代码模样

    python 复制代码
    class Person:
        home = "earth"
    
        @classmethod
        def change_home(cls, new_home):
            cls.home = new_home  # 直接修改全人类的图纸!

3. 特殊的【静态方法】(Static Method)

  • 长相 :头上戴着帽子 @staticmethod

  • 特征 :它是个"孤儿"。它的括号里既不需要 self,也不需要 cls

  • 作用 :它其实就是一个普普通通的函数,只是因为逻辑上跟这个类有点关系,所以被硬塞到了类的肚子里(方便管理)。它既不碰张三的私有财产,也不改人类的公共图纸。

  • 代码模样

    python 复制代码
    class 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

过程类似于下面

  1. 定义函数 f1
  2. 创建类 C,把 f1 赋值给 C.f
  3. C() 创建实例(临时对象)
  4. instance.f(6, 13)
  5. Python 自动转换为: C.f(instance, 6, 13)
  6. 实际调用: f1(instance, 6, 13)
  7. 执行函数体: print(6 & 13)
  8. 输出: 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'

总结


  1. 宏观思维(面向过程 vs 面向对象)
    • 面向过程 :像是一份"菜谱",一步步按顺序执行(buy -> wash -> cut -> cook)。
    • 面向对象 :像是一个"世界",创造不同的实体(对象),每个实体带着自己的数据(属性)和动作(方法)互相交互。核心优势是封装、继承、多态
  2. 灵魂伴侣(类与对象 & self
    • 是图纸,对象是按图纸造出来的实物。
    • self 的本质 :它绝不是类,它是对象本身 的化身。p.eat() 在底层就是 Person.eat(p)
  3. 属性与方法(静态与动态)
    • 属性 :分为全人类共享的类属性 (写在图纸上)和个人独有的实例属性 (写在个人的户口本 __dict__ 里)。
    • 方法实例方法 (传 self)、类方法 (加 @classmethodcls)、静态方法 (加 @staticmethod 什么都不传)。
  4. 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__ 有两个致命的弱点:

  1. 只限制实例属性 ,不限制类属性!(你依然可以执行 Person.weight = 100)。
  2. 不被子类继承 !如果你写了个子类继承自 Person,子类依然可以随便加属性,除非子类自己也写一遍 __slots__
💣 暗坑 6:类属性如果是"可变类型",会导致全员污染!

这结合了上一节"函数传参"的知识点。如果类属性是个列表,通过实例去修改它,会发生恐怖的连锁反应。
⚠️ 危险代码:

python 复制代码
class Dog:
    tricks = []  # 这是一个类属性,全体狗共享

dog1 = Dog()
dog2 = Dog()

# 注意:这里用的是 .append(),不是 =。它没有创建私有属性,而是顺藤摸瓜改了图纸上的列表!
dog1.tricks.append("翻滚")  

print(dog2.tricks)  # 输出: ['翻滚']  <-- dog2 突然自己学会了翻滚!

✅ 避坑: 永远不要把列表、字典这种可变类型 设为类属性(除非你真的想让全体实例共享一个数据池)。正确的做法是把它们放在 __init__ 里,作为 self.tricks = [] 实例属性。

相关推荐
郝学胜-神的一滴2 小时前
「栈与缩点的艺术」二叉树前序序列化合法性判定:从脑筋急转弯到工程实现
java·开发语言·数据结构·c++·python·算法
她说..2 小时前
Java Object类与String相关高频面试题
java·开发语言·jvm·spring boot·java-ee
Mr_Tony2 小时前
Swift 中的 Combine 框架完整指南(含示例代码 + 实战)
开发语言·swift
无心水2 小时前
22、Java开发避坑指南:日期时间、Spring核心与接口设计的最佳实践
java·开发语言·后端·python·spring·java.time·java时间处理
Hello.Reader2 小时前
双卡 A100 + Ollama 最终落地手册一键部署脚本、配置文件、预热脚本与 Python 客户端完整打包
开发语言·网络·python
vx_biyesheji00012 小时前
计算机毕业设计:Python网约车订单数据可视化系统 Django框架 可视化 数据大屏 数据分析 大数据 机器学习 深度学习(建议收藏)✅
大数据·python·机器学习·信息可视化·django·汽车·课程设计
AC赳赳老秦2 小时前
OpenClaw实战案例:用1个主控+3个Agent,实现SEO文章日更3篇
服务器·数据库·python·mysql·.net·deepseek·openclaw
cch89182 小时前
汇编VS C++:底层控制与高效开发之争
java·开发语言
智算菩萨2 小时前
PyCharm版本发展史:从诞生到AI时代的Python IDE演进历程
ide·人工智能·python·pycharm·ai编程