W
e
l
c
o
m
e
: )

8.面向对象编程

本篇导航

本篇是 Python 面向对象的总纲 + 底层原理篇:§1~§2 编程思想与基本概念(类 / 对象 / self)→ §3 属性的添加与获取 → §4 魔术方法(__init__ / __str__ / __del__ / __call__)→ §5 实例属性与实例方法 → §6 类对象 / 类属性 / 类方法 / 静态方法 → §7 内存分析(创建过程)→ §8~§10 三种方法的存储与描述符底层原理 → §11~§12 描述符协议(属性查找优先级、__getattribute__ / __getattr__)→ §13 方法的动态性 → §14 三大特征总述 → §15~§17 封装(私有成员 / @property / 命名约定)→ §18 继承(含 MRO)→ §19 多态 → §20 方法无重载 → §21 特殊方法与运算符重载 → §22 浅拷贝与深拷贝 → §23 组合 → §24~§26 工厂模式与单例模式 → §27 综合练习。

阅读建议:§8~§12 属于「知其所以然」的进阶内容(描述符协议),初学可先跳过、只记结论;求职面试常从 §10~§12 与 §18.10(MRO)出题。

1 面向对象编程思想

1.1 什么是面向过程

传统的面向过程的编程思想总结起来就八个字——自顶向下,逐步细化。

  1. 将要实现的功能描述为一个从开始到结束、按部就班的连续「步骤」。
  2. 依次完成这些步骤;若某一步难度较大,可再细化为若干子步骤,以此类推,直至得到想要的结果。
  3. 即把要开发的系统分解为若干步骤,每个步骤对应一个函数;所有步骤完成后,系统也就开发完毕了。

举例:来教育机构报名学习这件事,可以分成哪些步骤?

开始 -> 学员提出报名,提供相关材料 -> 学生缴纳学费,获得缴费凭证 -> 教师凭借学生缴费凭证进行分配班级 -> 班级增加学生信息 -> 结束

所谓面向过程,就是把分析好的步骤依次执行。

原文勘误

原文此处写作「大家以来教育机构报名学习这件事情」,其中「以来」为衍字,已改为「大家来教育机构」。

1.2 什么是面向对象

思考:上面的整个报名过程,都有哪些动词?

提出、提供、缴纳、获得、分配、增加

有动词就一定有实现这个动作的实体!

所谓模拟现实世界,就是让编程语言在解决业务逻辑时与真实业务逻辑保持一致:任何一个动作的发生,都要有一个支配它的实体(主体)。因为在现实世界中,任何一个功能的实现,都可以看作是一个个实体在发挥各自的「功能」(能力),并在内部进行协调有序的调用。

1.3 举例:使用面向对象实现报名系统开发

☆ 第一步:分析哪些动作是由哪些实体发出的

  • 学生提出报名
  • 学生提供相关资料
  • 学生缴费
  • 机构收费
  • 教师分配教室
  • 班级增加学生信息

于是,在整个过程中,一共有四个实体:学生、机构、教师、班级!在现实中的一个具体的实体,就是计算机编程中的一个对象!

☆ 第二步:定义这些实体,为其增加相应的属性和功能

属性就是实体固有的某些特征特性信息,在面向对象的术语中,属性就是以前的变量。

比如:

一个人的属性有:身高、体重、三围、姓名、年龄、学历、电话、籍贯、毕业院校等……

一个手机的属性有:价格、品牌、操作系统、颜色、尺寸等……

功能就是实体可以完成的动作,在面向对象的术语中,功能被封装成函数或方法。

image-20210316122158360

☆ 第三步:让实体去执行相应的功能或动作

  • 学生对象提出报名
  • 学生对象提供相关资料
  • 教师对象登记学生信息
  • 学生对象缴费
  • 机构对象收费
  • 教师对象分配教室
  • 班级对象增加学生信息

1.4 面向对象编程思想迁移

以前写代码,先想到的是要实现什么功能——调用系统函数或自定义函数,然后按部就班执行。

以后写代码,首先想到的是应该由什么样的主体去实现什么样的功能,再把该主体的属性和功能统一的进行封装,最后才去实现各个实体的功能。

注意

面向对象并不是一种技术,而是一种思想,是一种解决问题的最基本的思维方式!

所以,面向对象的核心思想是:不仅仅是简单的将功能进行封装(封装成函数),更是对调用该功能的主体进行封装,实现某个主体拥有多个功能,在使用的过程中,先得到对应的主体,再使用主体去实现相关的功能!

1.5 面向对象要比面向过程好?

一个面试题:面向过程和面向对象的区别?

  1. 都可以实现代码重用和模块化编程,面向对象的模块化更深,数据也更封闭和安全。
  2. 面向对象的思维方式更加贴近现实生活,更容易解决大型的复杂的业务逻辑。
  3. 从前期开发的角度来看,面向对象比面向过程要更复杂,但是从维护和扩展的角度来看,面向对象要远比面向过程简单!
  4. 面向过程的代码执行效率比面向对象高。

1.6 面向对象编程简介

面向对象(Object Oriented Programming,OOP)编程的思想主要是针对大型软件设计而来的。面向对象编程使程序的扩展性更强、可读性更好,使得编程可以像搭积木一样简单。

面向对象编程将数据和操作数据相关的方法封装到对象中,组织代码和数据的方式更加接近人的思维,从而大大提高了编程的效率。

Python 完全采用了面向对象的思想,是真正面向对象的编程语言,完全支持面向对象的基本功能,例如:继承、多态、封装等。

Python 中,一切皆对象。我们在前面学习的数据类型、函数等,都是对象。

注意

Python 支持面向过程、面向对象、函数式编程等多种编程范式。

编程范式详解

1.7 对象的进化

Pasted-image-20250404040523

随着编程面临的问题越来越复杂,编程语言本身也在进化,从主要处理简单数据开始,随着数据变多进化数组;数据类型变复杂,进化出了结构体;处理数据的方式和逻辑变复杂,进化出了对象。

  1. 简单数据
    像 30, 40, 50.4 等这些数字,可以看做是简单数据。最初的计算机编程,都是像这样的数字。
  2. C 语言中的数组
    将同类型的数据放到一起。比如:整数数组 [20, 30, 40],浮点数数组 [10.2, 11.3, 12.4],字符串数组:["aa", "bb", "cc"]。
注意

上面的 [20, 30, 40] 不是 Python 中的列表,是 C 语言中的数组。

  1. C 语言中的结构体
    将不同类型的数据放到一起,是 C 语言中的数据结构。比如:

    struct resume{
    		int age;
    		char name[10];
    		double salary;
    	   };
    
  2. 对象
    将不同类型的数据、方法(即函数)放到一起,就是对象。比如:

    class Student:
        company = "SXT"  # 类属性
        count = 0  # 类属性
    
        def __init__(self, name, score):
            self.name = name  # 实例属性
            self.score = score
            Student.count = Student.count + 1
    
        def say_score(self):  # 实例方法
            print("我的公司是:", Student.company)
            print(self.name, '的分数是:', self.score)
    

我们前面学习的数字也是对象。比如:整数 9,就是一个包含了加法、乘法等方法的对象。

2 面向对象的基本概念

2.1 面向对象中两个比较重要概念

☆ 类

对象如何产生?又是如何规定对象的属性和方法呢?

答:在 Python 中,采用类(class)来生产对象,用类来规定对象的属性和方法!也就是说,在 Python 中,要想得到对象,必须先有类!

为什么要引入类的概念?

类本来就是对现实世界的一种模拟,在现实生活中,任何一个实体都有一个类别,类就是具有相同或相似属性和动作的一组实体的集合!所以,在 Python 中,对象是指现实中的一个具体的实体,而既然现实中的实体都有一个类别,所以,OOP 中的对象也都应该有一个类!

☆ 对象

对象,object,现实业务逻辑的一个动作实体就对应着 OOP 编程中的一个对象!

image-20210316143602528

一个对象应该具有哪些特征和特性信息,由其所属的类决定;但每个对象又可以有不同的特征和特性信息。比如我自己(人类)这个对象,名字叫老王,性别男,会写代码,会教书;另一个(人类)对象可能叫赵薇,性别女,会演戏,会唱歌。

所以:

  1. 对象使用属性(property)保存数据!
  2. 对象使用方法(method)管理数据!

☆ 类与对象举例

我们把对象比作一个「饼干」,类就是制造这个饼干的「模具」。

image-20220701174635240image-20220701174643734

我们通过类定义数据类型的属性(数据)和方法(行为),也就是说,「类将行为和状态打包在一起」。

image-20220701174658238

Pasted-image-20250404042331

Pasted-image-20250404042344

注意
  • 类:我们叫做 class。
  • 对象:我们叫做 object 或 instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。

对象是类的具体实体,一般称为「类的实例」。类看做「饼干模具」,对象就是根据这个「模具」制造出的「饼干」。

从一个类创建对象时,每个对象会共享这个类的行为(类中定义的方法),但会有自己的属性值(不共享状态)。更具体一点:「方法代码是共享的,属性数据不共享」。

image-20220701174729591

Pasted-image-20250404042811

注意

Python 中,一切皆对象。类也称为类对象,类的实例也称为实例对象。

2.2 类的定义

在 Python 中,有两种类的定义方式:Python 2(经典类)和 Python 3(新式类)。

注意

Python 3 中已经不存在经典类了,所有类都是「新式类」。没有显式继承的都默认继承 object 类。

经典类:不由任意内置类型派生出的类,称之为经典类。

class 类名:
    # 属性
    # 方法

新式类:

class 类名():
    # 属性
    # 方法

在 Python 2 及以前的版本中,只要一个内置类型位于类树的某个位置(即由内置类型派生出的类),就属于「新式类」,会获得所有「新式类」的特性;反之,不由任意内置类型派生出的类,则称为「经典类」。


「新式类」和「经典类」的区分在 Python 3 之后就已经不存在了:在 Python 3.x 之后的版本,所有的类都派生自内置类型 object(即使没有显式地继承 object 类型),即所有的类都是「新式类」。

基本语法:

class 类名:
    类体

要点如下:

  1. 类名必须符合标识符的规则;一般规定类名使用大驼峰原则。
  2. 类体中我们可以定义属性和方法。
  3. 属性用来描述数据,方法(即函数)用来描述这些数据相关的操作。

案例:一个典型的类的定义。

class Student:
    def __init__(self, name, score):  # 构造方法第一个参数必须为 self
        self.name = name  # 实例属性
        self.score = score

    def say_score(self):  # 实例方法
        print("{0}的分数是{1}".format(self.name, self.score))

if __name__ == '__main__':
    # s1 是实例对象,自动调用 __init__() 方法
    s1 = Student('张三', 80)
    s1.say_score()

2.3 类的实例化(创建对象)

类的实例化就是把抽象的事务具体为现实世界中的实体。

类的实例化就是通过类得到对象!

类只是对象的一种规范,类本身基本上什么都做不了,必须利用类得到对象,这个过程就叫作类的实例化。

类是抽象的,也称为「对象的模板」。我们需要通过类这个模板创建类的实例对象,然后才能使用类定义的功能。

我们前面说过一个 Python 对象包含三个部分:

  1. id(identity 识别码)
  2. type(对象类型)
  3. value(对象的值)

现在,可以更进一步地说,一个 Python 对象包含如下部分:

  1. id(identity 识别码)
  2. type(对象类型)
  3. value(对象的值)
    1. 属性(attribute)
    2. 方法(method)

Pasted-image-20250404043936

基本语法:

对象名 = 类名()
Tip

在其他的编程语言中,类的实例化一般是通过 new 关键字实例化生成的,但是在 Python 中,我们不需要 new 关键字,只需要 类名() 就代表创建类的实例。

Python 中实例化经历了两个阶段:

  1. 构造实例过程,实例化。通过 __new__ 魔术方法生成并返回一个实例。
类比

生产线上生成一辆车。

  1. 初始化过程,初始化。通过魔术方法 __init__ 方法完成初始化。
类比

对新造的车做出场配置,如软装,车载系统等。

案例:把 Person 类实例化为对象 p1。

# 1. 定义一个类
class Person():
    # 定义相关方法
    def eat(self):
        print('我喜欢吃零食')

    def drink(self):
        print('我喜欢喝可乐')

# 2. 实例化对象
p1 = Person()
# 3. 调用类中的方法
p1.eat()
p1.drink()

Pasted-image-20251005021835

2.4 类中的 self 关键字

self 也是 Python 内置的关键字之一,其指向了类实例对象本身。

Python 中的 self 相当于 C++ 中的 self 指针,Java 和 C# 中的 this 关键字。Python 中,self 必须为构造函数的第一个参数,名字可以任意修改。但一般遵守惯例,都叫做 self。

(图片未上传成功:Pasted-image-20250404044417)

# 1. 定义一个类
class Person():
    # 定义一个方法
    def speak(self):
        print(self)
        print('Nice to meet you!')

# 2. 类的实例化(生成对象)
p1 = Person()
print(p1)
p1.speak()

p2 = Person()
print(p2)
p2.speak()

(图片未上传成功:Pasted-image-20251005022151)

Note

一句话总结:类中的 self 就是谁实例化了对象,其就指向谁。

3 对象的属性添加与获取

3.1 什么是属性

在 Python 中,任何一个对象都应该由两部分组成:属性 + 方法。

属性即是特征,比如:人的姓名、年龄、身高、体重……都是对象的属性。

​车的品牌、型号、颜色、载重量……都是对象的属性。

对象属性既可以在类外面添加和获取,也能在类里面添加和获取。

3.2 在类的外面添加属性和获取属性

☆ 设置

对象名.属性 = 属性值

案例:

# 1. 定义一个 Person 类
class Person:
    pass

# 2. 实例化 Person 类,生成 p1 对象
p1 = Person()
# 3. 为 p1 对象添加属性
p1.name = '老王'
p1.age = 18
p1.address = '北京市顺义区京顺路99号'

☆ 获取

在 Python 中,对象的属性值我们可以通过 对象名.属性 来获取。

# 1. 定义一个 Person 类
class Person():
    pass

# 2. 实例化 Person 类,生成 p1 对象
p1 = Person()
# 3. 为 p1 对象添加属性
p1.name = '老王'
p1.age = 18
p1.address = '北京市顺义区京顺路99号'

# 4. 获取 p1 对象的属性
print(f'我的姓名:{p1.name}')
print(f'我的年龄:{p1.age}')
print(f'我的住址:{p1.address}')

(图片未上传成功:Pasted-image-20251005152858)

3.3 在类的内部获取外部定义的属性

# 1. 定义一个 Person 类
class Person:
    def speak(self):
        print(f'我的名字:{self.name},我的年龄:{self.age},我的住址:{self.address}')

# 2. 实例化 Person 类,生成 p1 对象
p1 = Person()
# 3. 添加属性
p1.name = '孙悟空'
p1.age = 500
p1.address = '花果山水帘洞'
# 4. 调用 speak 方法
p1.speak()

(图片未上传成功:Pasted-image-20251005153208)

思考

目前我们的确可以通过 对象.属性 的方式设置或获取对象的属性,但是这种设置属性的方式有点繁琐,每次定义一个对象,就必须手工设置属性,那么对象的属性能不能在实例化对象时,直接进行设置呢?

答

可以,但是需要使用魔术方法。

4 魔术方法

4.1 什么是魔术方法

在 Python 中,__xxx__() 的函数叫做魔法方法,指的是具有特殊功能的函数。

4.2 __init__() 方法(初始化方法或构造方法)

思考

人的姓名、年龄等信息都是与生俱来的属性,可不可以在创建对象的过程中就赋予这些属性呢?

答

可以,使用 __init__() 方法。

__init__() 的作用:实例化对象时,会连同其中的参数一并传给 __init__ 函数并自动执行。__init__() 的参数列表会在开头多出一项,它永远指向新建的那个实例对象,Python 语法要求这个参数必须要有,习惯命名为 self。

构造方法用于执行实例对象的初始化工作,即对象创建后,初始化当前对象的相关属性,不能有返回值,也就是只能是 return None。

__init__() 的要点如下:

  1. 名称固定,必须为:__init__()。

  2. 第一个参数固定,必须为:self。self 指的就是刚刚创建好的实例对象。

  3. 构造函数通常用来初始化实例对象的实例属性,如下代码就是初始化实例属性:name 和 score。

    def init (self, name, score):
        # 实例属性
        self.score = score
        self.name = name
    
  4. 通过 类名(参数列表) 来调用构造函数。调用后,将创建好的对象返回给相应的变量。

    s1 = Student('张三', 80)
    
  5. __init__() 方法:初始化创建好的对象。

    初始化指的是:给实例属性赋值。

  6. __new__() 方法:用于创建对象,但我们一般无需重定义该方法。

  7. 如果我们不定义 __init__ 方法,系统会提供一个默认的 __init__ 方法。如果我们定义了带参的 __init__ 方法,系统不创建默认的 __init__ 方法。

# 1. 定义一个类
class Person():
    # 初始化实例对象属性
    def __init__(self, name, age):
        # 赋予 name 属性、age 属性给实例化对象本身
        # self.实例化对象属性 = 参数
        self.name = name
        self.age = age

# 2. 实例化对象并传入初始化属性值
p1 = Person('孙悟空', 500)
# 3. 调用 p1 对象自身属性 name 与 age
print(p1.name)
print(p1.age)

(图片未上传成功:Pasted-image-20251005154635)

总结
  1. __init__() 方法,在创建一个对象时默认被调用,不需要手动调用。

  2. __init__(self) 中的 self 参数,不需要开发者传递,Python 解释器会自动把当前的对象引用传递过去。

原文勘误

第 3 点示例中的 def init (self, name, score): 缺少两侧双下划线,正确写法应为 def __init__(self, name, score):。此处保留原文以免与实际课件截图脱节,但请以 __init__ 为准。

4.3 __str__ 方法

当使用 print 输出对象的时候,默认打印对象的内存地址。如果类定义了 __str__ 方法,那么就会打印从 __str__ 中 return 的数据。

没有定义 __str__ 方法的类:

# 1. 定义一个类
class Car:
    # 首先定义一个 __init__ 方法,用于初始化实例对象属性
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    # 2. 实例化对象 c1

c1 = Car('奔驰', 'S600', '黑色')
print(c1)

(图片未上传成功:Pasted-image-20251005155013)

使用 __str__ 方法的类:

# 1. 定义一个类
class Car:
    # 首先定义一个 __init__ 方法,用于初始化实例对象属性
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    # 定义一个 __str__ 内置魔术方法,用于输出小汽车的相关信息
    def __str__(self):
        return f'汽车品牌:{self.brand},汽车型号:{self.model},汽车颜色:{self.color}'

# 2. 实例化对象 c1
c1 = Car('奔驰', 'S600', '黑色')
print(c1)

(图片未上传成功:Pasted-image-20251005155137)

总结
  1. __str__ 这个魔术方法是在类的外部,使用 print(对象) 时,自动被调用的。

  2. 在类的内部定义 __str__ 方法时,必须使用 return 返回一个字符串类型的数据。

4.4 __del__ 方法(删除方法或析构方法)

当删除对象时,Python 解释器也会默认调用 __del__ 方法。

__del__ 方法称为析构方法,用于实现对象被销毁时所需的操作。比如:释放对象占用的资源,例如:打开的文件资源、网络连接等。

Python 实现自动的垃圾回收,当对象没有被引用时(引用计数为 0),由垃圾回收器调用 __del__ 方法。

我们也可以通过 del 语句删除对象,从而保证调用 __del__ 方法。

系统会自动提供 __del__ 方法,一般不需要自定义析构方法。

class Person():
    # 构造函数 __init__
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 析构方法 __del__
    def __del__(self):
        print(f'{self} 对象已经被删除')

# 实例化对象
p1 = Person('白骨精', 100)
# 删除对象 p1
del p1

(图片未上传成功:Pasted-image-20251005155635)

思考

__del__() 方法在使用过程中,比较简单,但是其在实际开发中,有何作用呢?

答

主要用于关闭文件操作、关闭数据库连接等等。

4.5 __call__ 方法和可调用对象

定义了 __call__ 方法的对象,称为可调用对象,即该对象可以像函数一样被调用。

  1. Python 中,凡是可以将 () 直接应用到自身并执行,都称为可调用对象。
  2. 可调用对象包括自定义的函数、Python 内置函数、以及本节所讲的实例对象。
  3. 定义了 __call__ 的对象,称为可调用对象,即该对象可以像函数一样被调用。
  4. 该方法使得实例对象可以像调用普通函数那样,以 对象名() 的形式使用。

案例:测试可调用对象。

class SalaryAccount:
    """工资计算类"""

    def __call__(self, salary):
        year_salary = salary * 12
        day_salary = salary // 30
        hour_salary = day_salary // 8
        return dict(monthSalary=salary,
                    yearSalary=year_salary,
                    daySalary=day_salary,
                    hourSalary=hour_salary)

s = SalaryAccount()
# 可以像调用函数一样调用对象的 __call__ 方法
print(s(5000))

(图片未上传成功:Pasted-image-20251005160304)

案例:类装饰器的使用案例。

class MyLogDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("日志纪录...")
        return self.func(*args, **kwargs)

@MyLogDecorator  # 等价于 fun2 = MyLogDecorator(fun2)
def fun2():
    print("使用功能2")

if __name__ == '__main__':
    fun2()

(图片未上传成功:Pasted-image-20251005160437)

4.6 总结

提到魔术方法:

  1. 这个方法在什么情况下被触发。
  2. 这个方法有什么实际的作用。

__new__:创建一个类的实例。在创建对象时自动调用,其作用就是返回一个新创建的类的实例。一般我们无需重新定义此方法。

__init__:初始化方法,或称构造函数,在对象初始化时执行,主要作用是对对象进行初始化(如赋予属性)。

__str__:对象字符串方法,当我们在类的外部,使用 print 方法输出对象时被触发,其主要功能就是对对象进行打印输出操作,要求方法必须使用 return 返回字符串格式的数据。

__del__:删除方法,或称析构方法,在对象被 del 删除时触发,主要用于关闭文件、关闭数据库连接等。

__call__:让一个对象成为可调用对象。在使用 对象名() 时触发,自动执行 __call__ 中的代码。让对象可以像函数那样被调用。

魔法方法详解

5 实例属性和实例方法

5.1 实例属性

实例属性是从属于实例对象的属性,也称为实例变量。其使用有如下几个要点:

  1. 实例属性一般在 __init__() 方法中通过如下代码定义:

    self.实例属性名 = 初始值
    
  2. 在本类的其他实例方法中,也是通过 self 进行访问:

    self.实例属性名
    
  3. 创建实例对象后,通过实例对象访问:

# 创建对象,调用 __init__() 初始化属性
obj = 类名()

# 可以给已有属性赋值,也可以新加属性
obj.实例属性名 = 值

案例:实例属性测试。

class Student:
    def __init__(self, name, score):
        self.name = name  # 增加 name 属性
        self.score = score  # 增加 score 属性

    def say_score(self):
        self.age = 18  # 增加 age 属性
        print(f"{self.name} 的分数是 {self.score}")

s1 = Student("张三", 80)
s1.say_score()
print(f"{s1.name} 的年龄是 {s1.age}")
s1.salary = 3000  # s1 对象增加 salary 属性
s2 = Student("李四", 90)
s2.say_score()
print(f"{s2.name} 的年龄是 {s2.age}")

(图片未上传成功:Pasted-image-20251005161833)

易错点

s1.age 能被打印出来,是因为上一行 s1.say_score() 已经在方法内部给 self.age 赋了值;而 s1.salary = 3000 则是在类的外部为对象动态新增属性。二者都不在 __init__ 中定义,因此 s2.age 与 s1.salary 互不影响。

5.2 实例方法

实例方法是从属于实例对象的方法。实例方法的定义格式如下:

def 方法名(self[, 形参列表]):
    函数体

方法的调用格式如下:

对象.方法名([实参列表])

要点:

  1. 定义实例方法时,第一个参数必须为 self。和前面一样,self 指当前的实例对象。
  2. 调用实例方法时,不需要也不能给 self 传参。self 由解释器自动传参。

案例:实例方法测试。

class Student:
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score

    def say_score(self):
        print(f"{self.name} 的分数是 {self.score}")

a = Student("小龙女", 18, 90)
a.say_score()

(图片未上传成功:Pasted-image-20251005162744)

函数和实例方法的区别

  1. 都是用来完成一个功能的语句块,本质一样。
  2. 实例方法调用时,通过对象来调用。实例方法从属于特定实例对象,普通函数没有这个特点。
  3. 直观上看,实例方法定义时需要传递 self,函数不需要。

实例对象的方法调用本质

(图片未上传成功:image-20220701185245323)

class Student:
    def __init__(self, name, age, score):
        self.name = name
        self.age = age
        self.score = score

    def say_score(self):
        print(f"{self.name} 的分数是 {self.score}")

a = Student("小龙女", 18, 90)
a.say_score()
# 本质上是
Student.say_score(a)

(图片未上传成功:Pasted-image-20251005162431)

其他操作

  1. dir(obj) 可以获得对象的所有属性、方法。
  2. obj.__dict__ 对象的实例属性字典。
  3. isinstance(对象, 类型) 判断对象是不是指定类型。
class Student:
    def __init__(self, name, score):
        self.name = name  # 增加 name 属性
        self.score = score  # 增加 score 属性

    def say_score(self):
        self.age = 18  # 增加 age 属性
        print("{0}的分数是{1}".format(self.name, self.score))

stu = Student("小龙女", 90)
# dir(obj) 可以获得对象的所有属性、方法
print(f"dir方法:{dir(stu)}")
# obj.__dict__ 对象的属性字典
print(f"__dict__属性:{stu.__dict__}")
# isinstance(对象, 类型)  判断“对象”是不是“指定类型”
print(f"isinstance方法:{isinstance(stu, Student)}")

"""
运行结果:
dir方法:['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', 'name', 'say_score', 'score']

__dict__属性:{'name': '小龙女', 'score': 90}

isinstance方法:True
"""

(图片未上传成功:Pasted-image-20250404053149)

说明

上面 dir() 的输出中出现了 __firstlineno__ 与 __static_attributes__,这是 Python 3.12 起新增的类属性,旧版本(≤3.11)不会有这两项,属正常的版本差异。

6 类对象、类属性、类方法、静态方法

6.1 类对象

前面讲了类定义的格式,class 类名:。实际上,当解释器执行 class 语句时,就会创建一个类对象。

案例:测试类对象的生成。

class Student:
    # 空语句
    pass

print(type(Student))
print(id(Student))

Stu2 = Student
s1 = Stu2()
print(s1)

(图片未上传成功:Pasted-image-20251005163317)

可以看到,实际上生成了一个变量名就是类名 Student 的对象;通过赋值给新变量 Stu2 也能实现相关调用,说明确实创建了类对象。

注

pass 为空语句,表示什么都不做,只作为一个占位符存在。写代码时,如果暂时不知道往方法或类中加入什么,可以先用 pass 占位,后期再补上。

6.2 类属性

类属性是从属于类对象的属性,也称为类变量。由于类属性从属于类对象,所以可以被所有实例对象共享。

类属性的定义方式:

class 类名:
    类变量名 = 初始值

在类中或者类的外面,我们可以通过:类名.类变量名 来读写。

案例:类属性的使用测试。

class Student:
    # 类属性
    company = "古墓派"
    # 类属性
    count = 0

    def __init__(self, name, score):
        # 实例属性
        self.name = name
        self.score = score
        Student.count = Student.count + 1

    def say_score(self):
        # 实例方法
        print("我的门派是:", Student.company)
        print(self.name, "的分数是:", self.score)

# s1 是实例对象,自动调用 __init__()方法
s1 = Student("小龙女", 99)
s1.say_score()
print(f"一共创建 {Student.count} 个 Student 对象")
print("=" * 40)
s1 = Student("杨过", 80)
s1.say_score()
print(f"一共创建 {Student.count} 个 Student 对象")

(图片未上传成功:Pasted-image-20251005164307)

6.3 类方法

类方法是从属于类对象的方法。类方法通过装饰器 @classmethod 来定义。

格式如下:

@classmethod
def 类方法名(cls[, 形参列表]):
    函数体

要点如下:

  1. @classmethod 必须位于方法上面一行。
  2. 第一个参数 cls 必须有;cls 指的就是类对象本身。
  3. 调用类方法格式:类名.类方法名(参数列表)。参数列表中,不需要也不能给 cls 传值。
  4. 类方法中访问实例属性和实例方法会导致错误。
  5. 子类继承父类方法时,传入 cls 是子类的类对象,而非父类的类对象。

案例:类方法使用测试。

class Student:
    # 类属性
    company = "古墓派"

    # 类方法
    @classmethod
    def print_company(cls, person):
        print(f"{cls.company}--{person}")

Student.print_company("小龙女")
Student.print_company("杨过")

(图片未上传成功:Pasted-image-20251005164812)

6.4 静态方法

Python 中允许定义与类对象无关的方法,称为静态方法。

既不操作类属性,也不操作实例属性的方法可以考虑定义成静态方法。

静态方法和在模块中定义普通函数没有区别,只不过静态方法放到了类的名字空间里面,需要通过类调用。

(图片未上传成功:Pasted-image-20250404065559)

静态方法通过装饰器 @staticmethod 来定义。

格式如下:

@staticmethod
def 静态方法名([形参列表]):
    函数体

要点如下:

  1. @staticmethod 必须位于方法上面一行。
  2. 调用静态方法格式:类名.静态方法名(参数列表)。
  3. 静态方法中访问实例属性和实例方法会导致错误。

案例:静态方法使用测试。

class Student:
    # 类属性
    company = "SXT"

    @staticmethod
    def add(a, b):
        # 静态方法
        print(f"{a} + {b} = {a + b}")
        return a + b

Student.add(20, 30)

(图片未上传成功:Pasted-image-20251005165137)

7 内存分析实例对象和类对象创建过程(重要)

以下面代码为例,分析整个创建过程,以便更深刻地掌握面向对象的概念:

class Student:
    # 类属性
    company = "尚学堂"
    # 类属性
    count = 0

    def __init__(self, name, score):
        # 实例属性
        self.name = name
        self.score = score
        Student.count = Student.count + 1

    # 实例方法
    def say_score(self):
        print("我的公司是:", Student.company)
        print(self.name, '的分数是:', self.score)

# s1 是实例对象,自动调用 __init__()方法
s1 = Student('高淇', 80)
s1.say_score()
print(f'一共创建 {Student.count} 个 Student 对象')
print("=" * 40)
# s2 是实例对象,自动调用 __init__()方法
s2 = Student('张三', 70)
s2.say_score()
print(f'一共创建 {Student.count} 个 Student 对象')

(图片未上传成功:Pasted-image-20251005165615)

(图片未上传成功:Pasted-image-20241215135501)

注意

上图中栈的顺序应该反过来,先执行的在下面,后执行的在上面。

8 实例方法的存储

先看一个案例:

def func():
    pass

class Demo:
    def test(self):
        pass

if __name__ == '__main__':
    demo1 = Demo()
    demo2 = Demo()

    print(demo1.test is demo2.test)
    print('=' * 20)
    print(demo1.test)
    print(demo2.test)
    print('=' * 20)
    print(func)

(图片未上传成功:file-20260729155325420)

问

我们通过 is 去比较两个对象的同一个实例方法时,发现返回的是 False,难道每个实例对象都重新存储一份实例方法吗?

并且使用 print 函数打印实例方法时输出 <bound method Demo.test of <__main__.Demo object at 0x000001C312D76F50>>。什么是 bound method?

答

Python 中实例方法的代码本身只在类对象中存储一份,所有实例共享;上例看到的「两个实例方法内存地址不同、is 返回 False」,是因为拿到的不是原始函数,而是 Python 动态生成的绑定方法(bound method)对象,每次通过实例访问方法都会生成独立的绑定对象。

8.1 方法代码:类中唯一存储,实例共享

在 Python 里,类的方法本质上是定义在类的命名空间(类.__dict__)中的普通函数对象,所有实例都不会单独复制这份函数代码。

验证:

class MyClass:
    def my_method(self):
        pass

a = MyClass()
b = MyClass()

# 方法不在实例自身的属性字典中
print("my_method" in a.__dict__)  # False
print("my_method" in b.__dict__)  # False

# 方法只存在于类的属性字典中
print("my_method" in MyClass.__dict__)  # True

# 类直接访问方法,永远是同一个对象
print(MyClass.my_method is MyClass.my_method)  # True

实例访问方法时,会沿着「实例自身属性 → 类属性 → 父类属性」的链条查找,最终在类中找到这个函数。但 Python 在这里通过描述符协议做了一层封装,这就是地址不同的核心原因。

8.2 为什么地址不同:绑定方法的生成机制

Python 的函数对象实现了描述符协议(内置 __get__ 方法),当通过 实例.方法 的方式访问时,不会直接返回原始函数,而是会调用函数的 __get__ 方法,生成一个新的绑定方法对象。

绑定方法对象封装了两个核心信息:

  1. 底层的原始函数(通过 __func__ 属性访问,也就是类里存的那一份)
  2. 当前的实例对象(通过 __self__ 属性访问,即方法里的 self)

调用绑定方法时,它会自动把绑定的实例作为第一个参数 self 传入原始函数——所以 a.my_method() 本质等价于 MyClass.my_method(a)。

代码验证

class MyClass:
    def my_method(self):
        pass

if __name__ == '__main__':
    a = MyClass()
    b = MyClass()

    a_method = a.my_method
    b_method = b.my_method

    # 两个绑定方法对象是不同的实例 → is 返回 False
    print(a_method is b_method)  # False

    # 但它们的底层原始函数是同一个(类中的那份)
    print(a_method.__func__ is b_method.__func__)  # True
    print(a_method.__func__ is MyClass.my_method)  # True

    # 绑定的实例分别对应 a 和 b
    print(a_method.__self__ is a)  # True
    print(b_method.__self__ is b)  # True

    # 同一个实例连续两次访问方法,得到的也是两个不同的绑定方法对象
    m1 = a.my_method
    m2 = a.my_method
    print(m1 is m2)  # False,每次访问都会新建绑定对象

甚至同一个实例连续两次访问方法,得到的也是两个不同的绑定方法对象:

m1 = a.my_method
m2 = a.my_method
print(m1 is m2)  # False,每次访问都会新建绑定对象
补充说明

如果直接写两行 print(id(a.my_method)) 可能看到相同地址,这是因为第一个绑定方法创建后立即被销毁,内存地址被第二个对象复用了;只要同时持有两个引用,地址一定不同。

8.3 这样设计的意义

  1. 内存高效:方法的字节码与逻辑只在类中存一份,实例数量再大也不会重复占用内存,极大节省空间。
  2. 语义自然:通过绑定方法自动注入 self,让方法调用符合面向对象的书写习惯,无需手动传入实例。
  3. 统一机制:基于描述符协议,@classmethod、@staticmethod 也用相同的底层逻辑实现,只是 __get__ 的行为不同。

8.4 最终总结

  • 存储层面:实例方法的代码仅在类中存在一份,所有实例共享,没有重复拷贝。
  • 访问层面:通过实例访问时,Python 会动态创建绑定方法对象来封装「实例 + 原始函数」,因此不同实例(甚至同实例多次访问)得到的对象内存地址不同,is 判断为 False。
  • 如果要判断「是不是同一个方法的实现」,应该比较它们的 __func__ 属性,而非直接比较绑定方法本身。

9 类方法和类的静态方法的存储

类方法和静态方法的底层可执行代码都只在类对象中存储唯一一份,所有实例完全共享,不会为每个实例创建独立副本。二者与实例方法的核心区别,在于访问时的描述符包装行为不同——是否生成绑定对象、绑定到哪个对象,这也决定了它们的内存地址表现差异。

9.1 类方法(@classmethod)

1. 存储方式

被 @classmethod 装饰的原始函数,会被包装成一个 classmethod 描述符对象,仅存储在类的属性字典(类.__dict__)中。

  • 所有实例的 __dict__ 中都不会复制这份方法,实例访问时会沿继承链向上查找类中的定义。
  • 原始函数本身只存在一份,保存在 classmethod 对象的 __func__ 属性中。

2. 访问机制(描述符行为)

classmethod 实现了描述符协议的 __get__ 方法:

  • 无论通过 类.方法 还是 实例.方法 访问,都会触发 __get__ 并生成一个绑定到类的绑定方法(bound method)对象。
  • 绑定方法封装了两个核心:底层原始函数(__func__)、绑定的类对象(__self__,即方法的 cls 参数)。
  • 调用绑定方法时,会自动将绑定的类作为第一个参数 cls 注入原始函数。

3. 内存地址与 is 判断

每次访问类方法时,Python 都会创建一个新的绑定方法对象。因此:

  • 同一个类连续两次访问类方法,is 返回 False
  • 不同实例访问类方法,is 也返回 False

但所有绑定方法的底层 __func__ 都指向同一个原始函数,代码逻辑完全共享。

4. 代码验证

class MyClass:
    @classmethod
    def cls_method(cls):
        pass

if __name__ == '__main__':
    a = MyClass()
    b = MyClass()

    # 实例自身不存储方法
    print("cls_method" in a.__dict__)  # False

    # 存储在类中
    print("cls_method" in MyClass.__dict__)  # True
    # 类字典中是 classmethod 描述符对象
    print(type(MyClass.__dict__["cls_method"]))  # <class 'classmethod'>

    # 每次访问都生成新的绑定方法对象
    m1 = MyClass.cls_method
    m2 = MyClass.cls_method
    m3 = a.cls_method
    m4 = b.cls_method
    print(m1 is m2)  # False
    print(m1 is m3)  # False
    print(m3 is m4)  # False

    # 底层原始函数是同一份
    print(m1.__func__ is m2.__func__)  # True
    print(m1.__func__ is MyClass.__dict__["cls_method"].__func__)  # True

    # 绑定的对象始终是类本身
    print(m1.__self__ is MyClass)  # True
    print(m2.__self__ is MyClass)  # True

9.2 静态方法(@staticmethod)

1. 存储方式

被 @staticmethod 装饰的原始函数,会被包装成一个 staticmethod 描述符对象,同样仅存储在类的属性字典中,实例不持有副本。

  • 原始函数保存在 staticmethod 对象的 __func__ 属性中,全局唯一。

2. 访问机制(描述符行为)

staticmethod 也实现了 __get__ 方法,但它的逻辑非常简单:不做任何绑定操作,直接返回原始函数对象。

  • 无论通过类访问还是通过任意实例访问,最终拿到的都是同一个原始函数。
  • 不会自动注入 self 或 cls 参数,本质就是一个挂载在类命名空间下的普通工具函数。

3. 内存地址与 is 判断

因为始终返回同一个原始函数对象,所以:

  • 类访问和实例访问的结果 is 为 True
  • 不同实例访问的结果 is 也为 True
  • 连续多次访问的结果始终是同一个对象

4. 代码验证

class MyClass:
    @staticmethod
    def static_method():
        pass

if __name__ == '__main__':
    a = MyClass()
    b = MyClass()

    # 实例自身不存储方法
    print("static_method" in a.__dict__)  # False

    # 存储在类中
    print("static_method" in MyClass.__dict__)  # True
    # 类字典中是 staticmethod 描述符对象
    print(type(MyClass.__dict__["static_method"]))  # <class 'staticmethod'>

    # 所有访问方式都返回同一个原始函数
    print(MyClass.static_method is a.static_method)  # True
    print(a.static_method is b.static_method)  # True
    print(MyClass.static_method is MyClass.__dict__["static_method"].__func__)  # True
原文勘误

原文此行注释写作 print("static_method" in MyClass.__dict__) # False,实际 staticmethod 描述符对象就存在类的属性字典里(下一行的 type(...) 已证明这一点),运行结果应为 True。原文注释与代码自相矛盾,已修正为 # True。

9.3 三种方法的核心差异对比

方法类型 类 __dict__ 中存储形式 通过类访问返回对象 通过实例访问返回对象 自动注入参数 同方式两次访问 is 结果 底层代码是否共享
实例方法 普通函数对象 原始函数 绑定实例的绑定方法 self 类访问:True;实例访问:False 是
类方法 classmethod 描述符 绑定类的绑定方法 绑定类的绑定方法 cls False 是
静态方法 staticmethod 描述符 原始函数 原始函数 无 True 是

9.4 最终总结

  1. 存储层面:实例方法、类方法、静态方法的代码都仅在类中存在一份,所有实例共享,这是 Python 面向对象实现的核心内存优化。
  2. 访问层面:
    • 实例方法:仅通过实例访问时生成绑定到实例的新对象;
    • 类方法:任何访问方式都会生成绑定到类的新对象;
    • 静态方法:不生成绑定对象,始终返回原始函数。
  3. 如果要判断「是否为同一份方法实现」,统一比较 __func__ 属性是最准确的方式。

10 实例方法、类方法和静态方法的底层原理

  • Python 的实例方法并没有独立的「特殊类型」,它本质就是定义在类命名空间中的普通函数。self 参数的自动绑定、「实例方法属于实例」的直观感受,完全由「函数对象自带的描述符协议(__get__ 方法)」实现——这也是整个 Python 方法体系的底层基石。
    代码本身只在类中存储唯一一份,所有实例共享;每次通过实例访问方法时,都会动态生成一个新的绑定方法对象,负责自动注入 self。

  • 类方法与静态方法的底层核心,是 Python 的描述符协议(Descriptor Protocol)。二者本质都是「包装了原始函数的描述符对象」,通过自定义 __get__ 方法的逻辑,修改了函数被访问时的行为——一个绑定到类,一个完全不绑定。

10.1 什么是描述符

只要一个类实现了 __get__、__set__、__delete__ 三个方法中的至少一个,它的实例就称为描述符。当描述符被放在类的属性字典中时,访问这个属性会触发描述符对应方法的执行,而非直接返回属性值。

其中 __get__ 方法的签名是固定的:

def __get__(self, instance, /, owner=None):
    # self: 描述符对象本身
    # instance: 访问该属性的实例(通过类访问时为 None)
    # owner: 描述符所属的类

Python 中所有的方法机制(实例方法、类方法、静态方法、属性 property),全部基于这套协议实现。

10.2 普通函数本身就是描述符

我们平时写的实例方法,本质就是定义在类中的普通函数。Python 的函数对象默认实现了 __get__ 方法,这是实例方法自动绑定 self 的根源。

Python 中一切皆对象,所有用 def 定义的函数,都是内置类型 types.FunctionType 的实例。而 FunctionType 类默认实现了 __get__ 方法,这意味着:普通函数天然就是一个非数据描述符。

非数据描述符属性

函数仅实现 __get__,没有 __set__/__delete__,属于非数据描述符,优先级低于实例同名属性。

验证:

def demo():
    pass

print(hasattr(demo, "__get__"))  # True,函数自带描述符能力
print(type(demo))  # <class 'function'>

这是实例方法自动绑定 self 的核心前提——不需要额外装饰器,函数本身就具备描述符的全部能力。

10.3 实例方法两种访问场景的不同行为

函数的 __get__ 方法会根据访问方式的不同,返回完全不同的结果,这就是「类访问和实例访问表现不同」的根源:

  • 通过类访问(类.方法名):__get__ 的 instance 参数为 None,直接返回原始函数本身;
  • 通过实例访问(实例.方法名):__get__ 接收到实例对象,会生成并返回一个绑定方法对象(types.MethodType)。

types.FunctionType 对应逻辑的伪代码:

class FunctionType:
    def __init__(self, func):
        self.__func__ = func

    # 原生 function.__get__ 的等价逻辑
    def __get__(self, instance, /, owner=None):
        if instance is None:
            # 类直接访问,返回原函数
            return self.__func__
        # 实例访问,生成绑定方法,自动把 instance 作为第一个参数
        return types.MethodType(self.__func__, instance)

这就是上一节实例方法底层的运行逻辑。而 @classmethod 和 @staticmethod,就是两个内置的描述符类,它们改写了 __get__ 的行为,从而实现了不同的调用规则。

10.4 绑定方法(MethodType)的作用

types.MethodType 是专门的绑定方法类型,它封装了两个核心数据:

  • __func__:底层的原始函数(类中存储的那一份)
  • __self__:绑定的实例对象,也就是方法中的 self

当调用绑定方法时,它会自动把 __self__ 作为第一个参数传入 __func__,因此无需手动传 self。

等价关系:

obj.method(arg1, arg2)
# 完全等价于
Class.method(obj, arg1, arg2)

模拟 types.MethodType 核心功能的伪代码:

class MethodType:
    def __init__(self, func, instance):
        self.__func__ = func  # 底层原始函数
        self.__self__ = instance  # 绑定的实例对象(即 self)

    def __call__(self, *args, **kwargs):
        # 调用绑定方法时,自动把实例作为第一个参数传入原函数
        return self.__func__(self.__self__, *args, **kwargs)

10.5 完整的属性查找流程

调用 obj.my_method() 方法时,Python 的完整执行链路:

  1. 在实例自身的 obj.__dict__ 中查找 my_method,找不到;
  2. 向上查找类的属性字典 type(obj).__dict__,找到原始函数对象;
  3. 检测到该对象实现了 __get__ 描述符协议,自动调用 my_method.__get__(obj, type(obj));
  4. 得到绑定方法对象,执行调用,自动把 obj 作为 self 注入原始函数。

10.6 实例方法手写简易实现

下面用纯 Python 代码还原这套机制,完全复刻原生实例方法的行为,包含核心的描述符与绑定方法逻辑。

步骤 1:实现简易绑定方法类

模拟 types.MethodType 的核心功能:封装函数与实例,调用时自动注入 self。

class SimpleMethodType:
    def __init__(self, func, instance):
        self.__func__ = func  # 底层原始函数
        self.__self__ = instance  # 绑定的实例对象(即 self)

    def __call__(self, *args, **kwargs):
        # 调用绑定方法时,自动把实例作为第一个参数传入原函数
        return self.__func__(self.__self__, *args, **kwargs)

步骤 2:实现实例方法描述符

模拟原生 function 类的 __get__ 行为,复刻「类访问返回原函数、实例访问返回绑定方法」的逻辑。

class InstanceMethod:
    def __init__(self, func):
        self.__func__ = func  # 包装的原始函数

    def __get__(self, instance, owner_cls):
        # instance: 访问属性的实例,通过类访问时为 None
        # owner_cls: 描述符所属的类
        if instance is None:
            # 类直接访问 → 返回原始函数
            return self.__func__
        # 实例访问 → 返回绑定方法对象
        return SimpleMethodType(self.__func__, instance)

步骤 3:放入类中验证效果

我们用手写的描述符包装函数,效果和原生类定义的实例方法完全一致。

class MyClass:
    # 等价于原生写法:def say_hello(self, name): ...
    def say_hello(self, name):
        print(f"你好 {name},我是实例 {id(self)}")

    # 手动用描述符包装函数,模拟 Python 解释器的默认行为
    say_hello = InstanceMethod(say_hello)

if __name__ == '__main__':
    # 测试验证
    a = MyClass()
    b = MyClass()

    # 1. 类访问 → 返回原始函数
    print('====1. 类访问 → 返回原始函数====')
    print(f'MyClass.say_hello: {MyClass.say_hello}')  # <function MyClass.say_hello at ...>
    print(f'type(MyClass.say_hello): {type(MyClass.say_hello)}')  # <class 'function'>

    # 2. 实例访问 → 返回自定义绑定方法
    print('\n====2. 实例访问 → 返回自定义绑定方法====')
    m_a = a.say_hello
    m_b = b.say_hello
    print(f'type(m_a): {type(m_a)}')  # <class '__main__.SimpleMethodType'>

    # 3. 底层函数共享,绑定的实例不同
    print('\n====3. 底层函数共享,绑定的实例不同====')
    print(f'm_a.__func__ is MyClass.say_hello: {m_a.__func__ is MyClass.say_hello}')  # True
    print(f'm_a.__self__ is a: {m_a.__self__ is a}')  # True
    print(f'm_b.__self__ is b: {m_b.__self__ is b}')  # True

    # 4. 正常调用,self 自动注入
    print('\n====4. 正常调用,self 自动注入====')
    a.say_hello("小明")  # 你好 小明,我是实例 2537401243728
    b.say_hello("小红")  # 你好 小红,我是实例 2537401241424

    # 5. 每次访问都会生成新的绑定对象
    print('\n====5. 每次访问都会生成新的绑定对象====')
    print(f'a.say_hello is a.say_hello: {a.say_hello is a.say_hello}')  # False

(图片未上传成功:file-20260812122355493)

10.7 类方法(@classmethod)的底层原理

1. 本质:classmethod 描述符类

@classmethod 不是什么魔法语法,它本质是一个内置的 classmethod 类装饰器(C 语言实现),接收一个函数作为参数,返回一个 classmethod 描述符对象。

最终类的 __dict__ 中存储的,不是原始函数,而是这个 classmethod 描述符实例。原始函数被保存在描述符的 __func__ 属性中。

2. 核心逻辑:__get__ 方法的实现

classmethod 的 __get__ 逻辑非常明确:无论通过实例还是类访问,永远把「类本身」作为绑定对象,返回一个绑定到类的 method 对象。

它的行为可以用 Python 代码等价模拟:

import types

class MyClassmethod:
    def __init__(self, func):
        self.__func__ = func  # 保存原始函数

    def __get__(self, instance, /, owner=None):
        # instance: 访问属性的实例(可能为 None)
        # owner: 描述符所属的类
        # 忽略 instance,永远绑定到 owner_cls(类本身)
        # 优先使用传入的 owner;没传就通过实例推导类
        if owner is None:
            if instance is None:
                # 实例和类都没提供,无法完成绑定,直接报错
                raise TypeError(
                    "classmethod __get__() requires at least an instance or owner class"
                )
            owner = type(instance)
        return types.MethodType(self.__func__, owner)

types.MethodType(func, obj) 就是 Python 底层的绑定方法构造器,它会把 obj 绑定为 func 的第一个参数,调用时自动传入。

3. 验证:手写装饰器与内置效果一致

class Test:
    @MyClassmethod
    def test_cls(cls):
        print(f"当前类:{cls}")

if __name__ == '__main__':
    # 效果和内置 @classmethod 完全一致
    a = Test()
    a.test_cls()  # 输出:当前类:<class '__main__.Test'>
    Test.test_cls()  # 输出:当前类:<class '__main__.Test'>

    m1 = Test.test_cls
    m2 = a.test_cls
    print(f'm1 is m2: {m1 is m2}')  # False:每次访问都会生成新的绑定方法对象
    print(f'm1.__func__ is m2.__func__: {m1.__func__ is m2.__func__}')  # True:底层原始函数共享
    print(f'm1.__self__ is Test: {m1.__self__ is Test}')  # True:绑定的对象永远是类本身

(图片未上传成功:file-20260812123402473)

4. 调用过程

当调用 a.test_cls() 时:

  1. 在 a.__dict__ 中找不到 test_cls,去类 Test.__dict__ 中查找;
  2. 找到 classmethod 描述符对象,触发它的 __get__(a, Test);
  3. __get__ 返回绑定了 Test 类的 method 对象;
  4. 调用这个 method 对象,自动把 Test 作为第一个参数 cls 传入原始函数。

10.8 静态方法(@staticmethod)的底层原理

1. 本质:staticmethod 描述符类

和 classmethod 同理,@staticmethod 也是一个内置描述符类,包装原始函数后存入类的属性字典。

2. 核心逻辑:__get__ 方法的实现

staticmethod 的 __get__ 是三者中最简单的:不做任何绑定操作,无论通过实例还是类访问,直接返回原始函数本身。

等价 Python 模拟:

class MyStaticmethod:
    def __init__(self, func):
        self.__func__ = func  # 保存原始函数

    def __get__(self, instance, /, owner_cls=None):
        # 直接返回原始函数,不绑定任何对象
        return self.__func__

3. 验证:手写装饰器效果

class Test:
    @MyStaticmethod
    def test_static():
        print("静态方法执行")

if __name__ == '__main__':
    a = Test()
    a.test_static()  # 正常执行
    Test.test_static()  # 正常执行

    print(f'Test.test_static is a.test_static: {Test.test_static is a.test_static}')  # True:永远返回同一个原始函数
    print(f'"test_static" in a.__dict__: {"test_static" in a.__dict__}')  # False:实例不存储副本

(图片未上传成功:file-20260812123950521)

原文勘误

原文此行写作 print('Test.test_static is a.test_static: {Test.test_static is a.test_static}'),漏写了 f 前缀,这样 {} 不会被替换、会被原样打印出来。已补上 f。

4. 调用过程

当调用 a.test_static() 时:

  1. 同样在类中找到 staticmethod 描述符,触发 __get__;
  2. __get__ 直接返回原始函数,没有任何绑定;
  3. 调用函数时不会自动注入 self 或 cls,和调用普通函数完全一致。

10.9 三种方法的底层对比

三者的差异,本质只是描述符 __get__ 方法的实现不同,原始代码都只在类中存储一份。

方法类型 类中存储的对象 __get__ 行为 返回对象类型 自动注入参数
实例方法 普通函数(自带描述符) 实例访问:绑定实例;类访问:返回原函数 绑定方法 / 原函数 self
类方法 classmethod 描述符 永远绑定到所属的类 绑定方法 cls
静态方法 staticmethod 描述符 永远直接返回原始函数,不做绑定 原始函数 无

10.10 为什么要用描述符实现?

这套设计是 Python「统一属性访问模型」的体现:

  • 所有属性访问(数据、方法、计算属性)都走同一套查找逻辑,解释器无需为方法单独做特殊处理;
  • 扩展性极强:除了内置的三种方法,你可以自定义描述符实现任意属性访问逻辑,比如 @property 也是基于描述符实现的。

10.11 综合案例

下面是一份完整可运行的整合代码,将实例方法、类方法、静态方法的手写描述符实现放在一起,并附带详细的验证测试,可以直观对比三者的底层逻辑差异。

import types

# ============================================================
# 第一步:实现绑定方法类(模拟 types.MethodType)
# ============================================================
class SimpleMethodType:
    """模拟 Python 内置的绑定方法对象 types.MethodType"""

    def __init__(self, func, instance):
        self.__func__ = func  # 底层原始函数
        self.__self__ = instance  # 绑定的对象(实例或类)

    def __call__(self, *args, **kwargs):
        """调用时自动把绑定对象作为第一个参数注入原始函数"""
        return self.__func__(self.__self__, *args, **kwargs)

# ============================================================
# 第二步:手写三种描述符(分别对应三种方法)
# ============================================================

class InstanceMethod:
    """
    模拟原生普通函数的描述符行为 → 对应【实例方法】
    原生 function 类型天生就自带这套 __get__ 逻辑
    """

    def __init__(self, func):
        self.func = func

    def __get__(self, instance, /, owner=None):
        if instance is None:
            # 通过类访问 → 返回原始函数
            return self.func
        # 通过实例访问 → 返回绑定到实例的方法
        return SimpleMethodType(self.func, instance)

class MyClassmethod:
    """
    模拟内置 classmethod 描述符 → 对应【类方法】
    """

    def __init__(self, func):
        self.__func__ = func

    def __get__(self, instance, /, owner=None):
        # 优先使用传入的 owner;没传就通过实例推导类
        # 只有手动调用 __get__ 才可能会没有传入 owner
        # 解释器调用会自动传入 instance 和 owner
        if owner is None:
            if instance is None:
                # 实例和类都没提供,无法完成绑定,直接报错
                raise TypeError(
                    "classmethod __get__() requires at least an instance or owner class"
                )
            owner = type(instance)
        # 永远绑定到类本身,无视是实例访问还是类访问
        return SimpleMethodType(self.__func__, owner)

class MyStaticmethod:
    """
    模拟内置 staticmethod 描述符 → 对应【静态方法】
    """

    def __init__(self, func):
        self.__func__ = func

    def __get__(self, instance, /, owner=None):
        # 永远直接返回原始函数,不做任何绑定
        return self.__func__

# ============================================================
# 第三步:测试类 —— 同时放入三种手写描述符方法
# ============================================================
class Demo:
    # ---------- 1. 手写实例方法 ----------
    @InstanceMethod
    def instance_method(self, msg):
        return f"[实例方法] self={id(self)}, msg={msg}"

    # ---------- 2. 手写类方法 ----------
    @MyClassmethod
    def cls_method(cls, msg):
        return f"[类方法] cls={cls.__name__}, msg={msg}"

    # ---------- 3. 手写静态方法 ----------
    @MyStaticmethod
    def static_method(msg):
        return f"[静态方法] msg={msg}"

# ============================================================
# 第四步:全方位验证测试
# ============================================================
if __name__ == "__main__":
    a = Demo()
    b = Demo()

    print("=" * 60)
    print("一、实例方法验证")
    print("=" * 60)
    print(f"类访问返回类型:{type(Demo.instance_method)}")  # function
    print(f"实例访问返回类型:{type(a.instance_method)}")  # SimpleMethodType
    print(f"两实例方法 is 相等:{a.instance_method is b.instance_method}")  # False
    print(f"底层函数是否共享:{a.instance_method.__func__ is b.instance_method.__func__}")  # True
    print(f"调用测试:{a.instance_method('hello')}")
    print()

    print("=" * 60)
    print("二、类方法验证")
    print("=" * 60)
    print(f"类访问返回类型:{type(Demo.cls_method)}")  # SimpleMethodType
    print(f"实例访问返回类型:{type(a.cls_method)}")  # SimpleMethodType
    print(f"类访问与实例访问 is 相等:{Demo.cls_method is a.cls_method}")  # False
    print(f"绑定对象是否都是类:{Demo.cls_method.__self__ is Demo}")  # True
    print(f"两实例方法底层函数共享:{a.cls_method.__func__ is b.cls_method.__func__}")  # True
    print(f"调用测试:{a.cls_method('world')}")
    print()

    print("=" * 60)
    print("三、静态方法验证")
    print("=" * 60)
    print(f"类访问返回类型:{type(Demo.static_method)}")  # function
    print(f"实例访问返回类型:{type(a.static_method)}")  # function
    print(f"类与实例访问 is 相等:{Demo.static_method is a.static_method}")  # True
    print(f"两实例访问 is 相等:{a.static_method is b.static_method}")  # True
    print(f"调用测试:{a.static_method('test')}")

(图片未上传成功:file-20260730101114705)

核心结论对照

方法类型 类 __dict__ 中存储 通过类访问返回 通过实例访问返回 自动注入参数 多次访问 is 结果 代码是否共享
实例方法 InstanceMethod 描述符 原始函数 绑定实例的方法对象 self 类访问:True;实例访问:False 是
类方法 MyClassmethod 描述符 绑定类的方法对象 绑定类的方法对象 cls False 是
静态方法 MyStaticmethod 描述符 原始函数 原始函数 无 True 是

补充说明

  1. 原生 Python 中,实例方法不需要额外的描述符类包装,因为 function 类型本身就实现了 __get__,行为和上面的 InstanceMethod 完全一致。
  2. 原生的 classmethod 和 staticmethod 是 C 语言实现的内置类型,性能更高,但逻辑和我们手写的 Python 版完全等价。
  3. 这套机制的本质就是描述符协议——通过 __get__ 方法拦截属性访问,在访问时动态决定返回什么对象,从而实现了「自动传参」的语法糖。

11 __get__ 自动调用时的赋值问题

思考

上一节中三类 __get__ 的实现中,__get__ 自动调用时 instance 和 owner 是怎么被赋值的?

答

这两个参数是由 Python 的属性访问协议(__getattribute__)在触发描述符时自动注入的,本质是我们写 x.attr 这个「点运算符」时,Python 解释器在后台完成的参数传递。

11.1 触发入口:__getattribute__ 方法

所有 Python 对象的属性访问(无论是 实例.属性 还是 类.属性),都会先调用该对象所属类的 __getattribute__ 方法:

  • 实例访问属性 → 调用 类.__getattribute__(实例, 属性名)
  • 类访问属性 → 调用 元类.__getattribute__(类, 属性名)

__getattribute__ 是 Python 属性查找的核心入口,它会沿着 MRO 链查找属性;如果找到的属性是一个实现了 __get__ 的描述符,就会自动调用描述符的 __get__ 方法,并按固定规则传入 instance 和 owner 两个参数。

11.2 参数赋值的统一规则

当执行 x.attr 且 attr 是描述符时,__get__(self, instance, owner) 的三个参数赋值规则为:

  1. self:描述符对象本身,也就是 attr 对应的描述符实例。
  2. instance:发起属性访问的主体。
    • 如果 x 是类的实例:instance = x(就是这个实例本身)
    • 如果 x 是类本身:instance = None(没有实例参与)
  3. owner:发起访问的主体对应的类。
    • 如果 x 是类的实例:owner = type(x)(实例所属的类)
    • 如果 x 是类本身:owner = x(就是这个类自身)

简单总结:instance 是访问者(实例或 None),owner 是访问者对应的类。

11.3 四类常见场景的具体赋值

结合继承场景,我们可以覆盖所有常见情况:

场景 1:实例访问自身类定义的描述符

class A:
    attr = 描述符()

a = A()
a.attr  # 触发 __get__(self, instance=a, owner=A)
  • instance = 实例 a
  • owner = 类 A

场景 2:类直接访问自身定义的描述符

class A:
    attr = 描述符()

A.attr  # 触发 __get__(self, instance=None, owner=A)
  • instance = None(无实例参与)
  • owner = 类 A

场景 3:子类访问父类继承的描述符

class A:
    attr = 描述符()

class B(A):
    pass

B.attr  # 触发 __get__(self, instance=None, owner=B)
  • instance = None
  • owner = 子类 B(而非定义描述符的父类 A)
Note

这就是类方法继承后,cls 会自动变成子类的核心原因:类方法绑定的是 owner,子类访问时 owner 就是子类。

场景 4:子类实例访问父类继承的描述符

class A:
    attr = 描述符()

class B(A):
    pass

b = B()
b.attr  # 触发 __get__(self, instance=b, owner=B)
  • instance = 子类实例 b
  • owner = 子类 B

11.4 代码验证:直观打印参数

我们可以写一个调试用的描述符,直观看到不同场景下的参数值:

class DebugDescriptor:
    def __get__(self, instance, owner):
        print("=" * 40)
        print(f"描述符自身: {self}")
        print(f"instance 参数: {instance}")
        print(f"owner 参数: {owner}")
        print(f"owner 类名: {owner.__name__ if owner else '无'}")
        return "访问完成"

# 父类定义描述符
class Parent:
    test = DebugDescriptor()

# 子类继承
class Child(Parent):
    pass

if __name__ == "__main__":
    print("1. 实例访问父类描述符")
    p = Parent()
    p.test

    print("\n2. 父类直接访问描述符")
    Parent.test

    print("\n3. 子类访问继承的描述符")
    Child.test

    print("\n4. 子类实例访问继承的描述符")
    c = Child()
    c.test

(图片未上传成功:file-20260812125308279)

运行后可以清晰看到每个场景下 instance 和 owner 的具体值,和上面的规则完全一致。

11.5 对应三类方法的参数使用方式

回到我们之前手写的三种描述符,它们正是利用这两个参数实现了不同的绑定逻辑:

11.5.1 实例方法(InstanceMethod)

def __get__(self, instance, owner):
    if instance is None:
        return self.__func__  # 类访问:直接返回原函数
    return SimpleMethodType(self.__func__, instance)  # 实例访问:绑定实例
  • 核心判断 instance is None 区分「类访问」和「实例访问」
  • owner 参数在此处没有直接使用

11.5.2 类方法(MyClassmethod)

def __get__(self, instance, owner):
    return SimpleMethodType(self.__func__, owner)  # 永远绑定 owner
  • 完全忽略 instance,永远绑定 owner
  • 这就是为什么无论实例还是类访问,最终绑定的都是类,且继承时自动适配子类

11.5.3 静态方法(MyStaticmethod)

def __get__(self, instance, owner):
    return self.__func__  # 两个参数都不用,直接返回原函数
  • 完全忽略两个参数,不做任何绑定
原文勘误

原文本节三个小节标题编号误写为 13.1 / 13.2 / 13.3(本章是第 11 章),已修正为 11.5.1 / 11.5.2 / 11.5.3。

11.6 补充说明

  • 描述符必须定义在类的属性字典中才会触发 __get__;如果直接把描述符放在实例自身的 __dict__ 里,不会触发描述符协议。
  • owner 参数的设计主要是为了支持继承场景,让描述符能感知到当前是哪个类在访问,从而实现类方法、属性等机制的多态行为。

12 描述符协议

描述符协议的核心本质是 Python 内置的一套属性访问拦截机制:当一个实现了特定魔法方法的对象(描述符)作为「类的属性」存在时,所有对该属性的读取、赋值、删除操作,都会被描述符对应的钩子方法拦截,从而实现完全自定义的属性行为。

这套协议是 Python 面向对象体系的底层基石——实例方法、类方法、静态方法、@property、__slots__ 甚至 ORM 框架的字段映射,全部基于描述符实现。

12.1 描述符的定义与分类

1. 核心协议方法

只要一个类实现了以下三个方法中的任意一个,它的实例就称为描述符对象:

方法 触发时机 签名
__get__(self, instance, owner) 读取属性值时触发 访问 obj.attr 时自动调用
__set__(self, instance, value) 给属性赋值时触发 执行 obj.attr = value 时自动调用
__delete__(self, instance) 删除属性时触发 执行 del obj.attr 时自动调用

三个参数的统一含义:

  • self:描述符对象本身
  • instance:发起属性访问的实例对象(通过类直接访问时为 None)
  • owner:描述符所属的类(即定义该描述符属性的类/子类)

2. 两大分类(优先级不同)

根据实现的方法不同,描述符分为两类,属性查找时的优先级天差地别:

  • 数据描述符:实现了 __set__ 或 __delete__ 的描述符。
    • 典型代表:@property、__slots__ 对应的成员描述符
  • 非数据描述符:只实现了 __get__,没有实现 __set__/__delete__
    • 典型代表:普通函数(实例方法)、@classmethod、@staticmethod

12.2 核心触发原理:属性查找的完整流程

描述符之所以能生效,根源在于 Python 所有的「点号属性访问」(obj.attr),都会先调用 obj 所属类的 __getattribute__ 方法。这个方法是属性查找的唯一入口,描述符的拦截逻辑就内嵌在这个方法的执行流程中。

1. 属性查找的优先级顺序

__getattribute__ 会严格按照以下顺序查找属性,找到即返回,后面的步骤不再执行:

  1. 数据描述符:先去类的 __dict__ 中找属性,如果是数据描述符,直接调用它的 __get__ 方法返回结果
  2. 实例自身属性:去实例的 __dict__ 中查找同名属性
  3. 非数据描述符:再去类的 __dict__ 中找属性,如果是非数据描述符,调用它的 __get__ 方法返回结果
  4. 类的普通属性:类的 __dict__ 中普通的非描述符属性
  5. 父类属性:沿着 MRO 继承链向上重复上述流程
  6. 全部失败 → 抛出 AttributeError
  7. 外层捕获异常,尝试调用 __getattr__ → 触发 __getattr__(如果类定义了的话)
一句话总结优先级

数据描述符 > 实例属性 > 非数据描述符 > 类普通属性

这也是为什么「实例可以覆盖方法,但覆盖不了 property」的底层原因:

  • 实例方法是非数据描述符,优先级低于实例属性,所以实例 __dict__ 里的同名属性会直接覆盖方法
  • @property 是数据描述符,优先级高于实例属性,所以哪怕实例里有同名属性,也会优先走描述符逻辑

2. 伪代码模拟 __getattribute__

我们可以用 Python 伪代码还原 __getattribute__ 的核心逻辑,直观看到描述符是如何被触发的:

def object_getattribute(instance, attr_name):
    """模拟 Python 内置 object.__getattribute__ 的执行流程"""
    cls = type(instance)
    class_attr = None

    # 1. 先从类的 MRO 链中查找属性
    for base in cls.__mro__:
        if attr_name in base.__dict__:
            attr = base.__dict__[attr_name]

            # 2. 判断是不是数据描述符(同时有 __get__ 和 __set__/__delete__)
            if (hasattr(attr, '__get__') and
                    (hasattr(attr, '__set__') or hasattr(attr, '__delete__'))):
                # 是数据描述符 → 直接调用 __get__,优先级最高
                return attr.__get__(instance, cls)

            # 先存下来,后面如果实例里找不到再用
            class_attr = attr
            break

    # 3. 去实例自身的 __dict__ 里找
    if attr_name in instance.__dict__:
        return instance.__dict__[attr_name]

    # 4. 实例里没找到,再处理类属性
    if class_attr is not None:
        # 判断是不是描述符(有 __get__ 就行,非数据描述符)
        if hasattr(class_attr, '__get__'):
            # 调用描述符的 __get__,传入 instance 和 owner
            return class_attr.__get__(instance, cls)
        # 普通类属性,直接返回
        return class_attr

    # 5. 都找不到,抛出属性不存在异常(之后会尝试 __getattr__)
    raise AttributeError(f"'{cls.__name__}' object has no attribute '{attr_name}'")

这就是 __get__ 方法中 instance 和 owner 两个参数的来源:__getattribute__ 在调用描述符的 __get__ 时,自动把当前实例和所属类作为参数传了进去。

3. 类.属性 访问时的 __getattribute__

思考

使用 类.属性名 访问属性时,还是触发 类.__getattribute__(obj, "attr") 方法吗?

答

并不是。

使用 Cls.attr 方式访问属性时,触发的是 type(Cls).__getattribute__(元类的 __getattribute__),不是类自己的 __getattribute__;

区分两条调用路径:

  1. 实例访问 obj.attr

    obj = Demo()
    obj.attr
    # 调用:object.__getattribute__(obj, "attr")
    # __get__(instance=obj, owner=Demo)
    
  2. 类访问 Demo.attr

    Demo.attr
    # Demo 本身也是对象,它的类型是元类 type
    # 调用:type.__getattribute__(Demo, "attr")
    # __get__(instance=None, owner=Demo)
    
注意

误区:以为 Demo.attr 走 Demo.__getattribute__,并不是。

obj.__getattribute__ 是实例的方法;对类对象本身,它的属性查找走元类的 __getattribute__。

Cls.attr 的完整查找走(type.__getattribute__),不是实例的 object.__getattribute__。

类.属性 访问时有两类描述符,所在位置不一样:

  1. 定义在普通类 Demo 的 __dict__ 里:Demo.attr = MyDesc()
    • 访问:Demo.attr
    • 调用 __get__(instance=None, owner=Demo)
  2. 定义在元类 Meta 的 __dict__ 里:元类的属性,类对象 Demo 是元类的实例
    • 访问:Demo.meta_attr
    • 此时 Demo 就是元类的实例对象,调用 __get__(instance=Demo, owner=Meta)
① type.__getattribute__ 伪代码
关键点区分

数据/非数据描述符的区分逻辑,只作用于实例对象读取属性 obj.attr,用来决定:实例 __dict__ 里面同名 key 是否可以覆盖描述符。

  • 实例读属性:
    • 数据描述符:优先级 > 实例 __dict__,优先走描述符 __get__
    • 非数据描述符:优先级 < 实例 __dict__,实例字典有同名就直接返回字典的值,跳过 __get__
  • 类读属性 Cls.attr:完全没有这套「实例字典覆盖」逻辑,因此根本不需要区分数据/非数据描述符。只要有 __get__ 就调用。
  1. 在元类 MRO 查找出来的属性(存在于元类的 __dict__ 中):只要对象实现了 __get__,不管是数据描述符还是非数据描述符,都会调用 attr.__get__(instance=Cls, owner=meta_cls)
注意

这里 instance 参数传入的是这个类本身(因为类就是元类的实例),owner 是元类。元类查找路径下,不会区分数据/非数据描述符!不做 has_set 判断!只要有 __get__ 就调用。

  1. 当属性不在元类 MRO,落到本类/父类 MRO(普通类的 __dict__):拿到这个属性对象后:
    • 如果有 __get__,依然调用 attr.__get__(instance=None, owner=Cls)
    • 同样:不分数据描述符、非数据描述符,只要有 __get__ 就执行

逻辑顺序:元类 MRO → 当前类及父类 MRO → AttributeError

def type_getattribute(cls, attr_name):
    """
    模拟 type.__getattribute__(cls, attr_name)
    :param cls: 被访问属性的类对象(元类的实例)
    :param attr_name: 属性名字符串
    """
    meta_cls = type(cls)  # 获取该类的元类,一般是 type

    # ========== 第一阶段:遍历元类的 MRO(元类继承链) ==========
    for meta_base in meta_cls.__mro__:
        if attr_name in meta_base.__dict__:
            attr = meta_base.__dict__[attr_name]
            # 元类域找到属性:只要有 __get__ 就触发描述符,不检查 __set__
            if hasattr(attr, "__get__"):
                # instance=cls:类本身就是元类的实例;
                # owner=meta_base:定义该属性的元类
                return attr.__get__(instance=cls, owner=meta_base)
            else:
                # 普通对象,直接返回
                return attr

    # ========== 第二阶段:元类 MRO 未命中,遍历普通类自身及父类 MRO ==========
    for base_cls in cls.__mro__:
        if attr_name in base_cls.__dict__:
            attr = base_cls.__dict__[attr_name]
            # 普通类字典上的描述符:类访问时 instance=None
            if hasattr(attr, "__get__"):
                return attr.__get__(instance=None, owner=cls)
            else:
                return attr

    # ========== 第三阶段:整条链路都找不到,抛出 AttributeError ==========
    raise AttributeError(f"type object '{cls.__name__}' has no attribute '{attr_name}'")
项目 type_getattribute(cls, name)(类.属性) object_getattribute(obj, name)(实例.属性)
查找起点 先元类 MRO,再普通类 MRO 直接普通类 MRO
描述符 instance 参数 元类域:instance=cls;普通类域:instance=None instance=obj
是否区分数据/非数据描述符 ❌ 不区分,只要有 __get__ 就调用 ✅ 区分;数据描述符优先于实例 __dict__
是否读取实例 __dict__ ❌ 不会,没有实例对象 ✅ 会,作为中间优先级
② 关键结论
  1. 类.属性 访问,不代表 instance 一定是 None。
    • 如果该属性来自普通类的类字典:instance = None
    • 如果该属性来自元类的类字典:当前这个类对象就是元类的实例,instance = 类对象
  2. instance=None 仅仅代表:描述符所在类的实例没有参与本次访问,也就是:拿类直接读取本类上面定义的描述符。
  3. __set__/__delete__ 只有在对实例赋值/删除才会触发。
注意
  • Demo.attr = xxx:这是对类对象本身赋值,调用元类的 __setattr__,不会调用描述符的 __set__。
  • 描述符的 __set__(instance, value) 中的 instance 永远是实例对象,类对象赋值不会进入描述符协议。
  1. 元类描述符的行为,完全复用标准描述符协议:
注意

元类的实例就是普通类对象,对元类属性赋值 Demo.y = xxx,会触发元类数据描述符的 __set__(instance=Demo, value=xxx)。

class MyDesc:
    def __get__(self, instance, owner):
        print(f"[__get__] instance={instance!r}, owner={owner!r}")
        return self

    def __set__(self, instance, value):
        print(f"[__set__] instance={instance!r}, value={value!r}")

class Meta(type):
    meta_field = MyDesc()  # 元类上的描述符

class Demo(metaclass=Meta):
    cls_field = MyDesc()  # 普通类上的描述符

if __name__ == "__main__":
    print("=== Demo.cls_field (普通类上描述符) ===")
    print(Demo.cls_field)

    print("\n=== Demo.meta_field (元类上描述符) ===")
    print(Demo.meta_field)

    print("\n=== 对元类描述符赋值 Demo.meta_field = 123 ===")
    Demo.meta_field = 123
    print(Demo.meta_field)

    print("\n=== 对普通类描述符赋值 Demo.cls_field = 123 ===")
    # 不会触发 __set__,直接覆盖 Demo.__dict__["cls_field"]。
    Demo.cls_field = 123
    print(Demo.cls_field)

(图片未上传成功:file-20260902115229599)

  • 可以看到:Demo.meta_field = 123 成功触发了描述符的 __set__,因为 Demo 是元类 Meta 的实例。

  • 对比:Demo.cls_field = 123,不会触发 __set__,直接覆盖 Demo.__dict__["cls_field"]。

4. __getattr__ 执行流程

def __getattr__(self, name: str):
    # name:你访问的属性名字符串

触发条件:

  1. 执行 obj.name
  2. object.__getattribute__(obj, name) 查找失败 → 调用 obj.__getattr__(name)

__getattr__ 不会被属性查找流程主动调用,它是「兜底后备」;只有 __getattribute__ 完整查找全部路径、最终找不到属性、抛出 AttributeError 之前,才会尝试调用 __getattr__。

整体执行顺序伪代码(obj.attr):

try:
    # 1. object.__getattribute__ 完整执行标准查找流程
    #    数据描述符 → 实例__dict__ → 非数据描述符 → 类/父类普通属性
    return object.__getattribute__(obj, "attr")
except AttributeError:
    # 2. 标准查找全部失败,进入后备逻辑
    if hasattr(obj, "__getattr__"):
        return obj.__getattr__("attr")
    raise  # 仍然没有__getattr__,向外抛出异常

关键点:

  1. __getattribute__:主查找逻辑(内置在 object 里,实现描述符优先级规则);
  2. __getattr__:兜底回调,仅在【标准查找全部失败】时触发;
  3. 只要 __getattribute__ 找到属性,永远不会走到 __getattr__。
注意

如果重写 __getattribute__,手动抛出 AttributeError,才会触发 __getattr__;

如果重写 __getattribute__ 内部直接返回值,则不会进入 __getattr__。

案例演示:

# --------------------------
# 1. 定义两类描述符
# --------------------------
class DataDescriptor:
    """数据描述符:同时实现 __get__ + __set__/__delete__"""

    def __get__(self, instance, owner):
        print("【命中】数据描述符 __get__")
        return "来自【数据描述符】"

    def __set__(self, instance, value):
        print("【命中】数据描述符 __set__")

class NonDataDescriptor:
    """非数据描述符:只实现 __get__"""

    def __get__(self, instance, owner):
        print("【命中】非数据描述符 __get__")
        return "来自【非数据描述符】"

# --------------------------
# 2. 测试类:同时包含各类属性 + __getattr__
# --------------------------
class TestCls:
    # 类属性:数据描述符
    data_attr = DataDescriptor()
    # 类属性:非数据描述符
    non_data_attr = NonDataDescriptor()
    # 普通类属性(无描述符)
    normal_cls_attr = "普通类属性值"

    def __getattr__(self, name):
        print(f"【兜底】进入 __getattr__,属性名:{name}")
        return f"__getattr__兜底值({name})"

if __name__ == '__main__':
    # --------------------------
    # 测试 1:访问数据描述符属性 data_attr
    # --------------------------
    print("===== 测试 1:访问 data_attr(数据描述符) =====")
    obj = TestCls()
    # 先不修改实例字典
    print(f'obj.data_attr:{obj.data_attr}')

    print('----实例插入同名属性----')
    # 强行往实例 __dict__ 插入同名 key
    obj.__dict__["data_attr"] = "实例字典里的 data_attr"
    print(f'obj.data_attr:{obj.data_attr}')
    # ✅ 结论:数据描述符优先级 > 实例__dict__,依然走描述符
    print('✅ 结论:数据描述符优先级 > 实例__dict__,依然走描述符')

    # --------------------------
    # 测试 2:访问非数据描述符 non_data_attr
    # --------------------------
    print("\n===== 测试 2:访问 non_data_attr(非数据描述符) =====")
    obj2 = TestCls()
    print(f'obj2.non_data_attr:{obj2.non_data_attr}')

    print('----实例插入同名属性----')
    # 实例写入同名属性
    obj2.__dict__["non_data_attr"] = "实例字典里的non_data_attr"
    print(f'obj2.non_data_attr:{obj2.non_data_attr}')
    # ✅ 结论:实例__dict__ > 非数据描述符,直接读取实例字典,不再触发描述符
    print('✅ 结论:实例__dict__ > 非数据描述符,直接读取实例字典,不再触发描述符')

    # --------------------------
    # 测试 3:普通类属性
    # --------------------------
    print("\n===== 测试 3:普通类属性 normal_cls_attr =====")
    obj3 = TestCls()
    print(f'obj3.normal_cls_attr:{obj3.normal_cls_attr}')
    print('----实例插入同名属性----')
    obj3.__dict__["normal_cls_attr"] = "实例覆盖普通类属性"
    print(f'obj3.normal_cls_attr:{obj3.normal_cls_attr}')
    print('✅ 结论:实例__dict__ > 类属性,直接读取实例字典,不再触发类属性')

    # --------------------------
    # 测试 4:访问完全不存在的属性,触发 __getattr__
    # --------------------------
    print("\n===== 测试 4:访问不存在属性 unknown_attr =====")
    obj4 = TestCls()
    print(obj4.unknown_attr)
    print('✅ 结论:访问完全不存在的属性,触发 __getattr__')

    # --------------------------
    # 测试 5:存在描述符时,永远不会走到 __getattr__
    # --------------------------
    print("\n===== 测试 5:再次验证:只要找到属性,__getattr__ 不会执行 =====")
    obj5 = TestCls()
    # 命中数据描述符,不会触发 __getattr__
    print(obj5.data_attr)
    print('✅ 结论:存在描述符时,永远不会走到 __getattr__')

(图片未上传成功:file-20260817145942590)

重要结论

只要存在数据描述符,并且命中,流程直接终止,根本不会执行 __getattr__!

① 和描述符体系的区别
__getattribute__ __getattr__
触发时机 每次访问属性必然最先执行 仅标准查找失败后触发
是否默认存在 所有对象都继承自 object,原生自带 需要手动定义才存在
职责 实现完整查找规则(描述符优先级) 兜底、动态属性
风险 手写极易无限递归 相对安全

描述符(__get__)是属性查找过程内部的一环;

__getattr__ 是整个查找流程全部失败后的外部回调。

层级关系:

obj.attr
    ↓
调用 obj.__getattribute__("attr")
        ↓
        执行内置查找逻辑(数据描述符 → 实例 dict → 非数据描述符…)
        ↓
        找到 → 返回;找不到 → 抛 AttributeError
    ↓
捕获异常 → 调用 __getattr__(如有)
② 经典坑点:无限递归(重写 __getattribute__)

如果手动重写 __getattribute__,千万不要直接 self.xxx,会无限递归:

class Demo:
    def __getattribute__(self, name):
        print("进入自定义getattribute")
        # ❌ 错误!再次触发 __getattribute__,死循环
        return self.name

# 正确写法:使用 object.__getattribute__
class Demo:
    def __getattribute__(self, name):
        print("进入自定义getattribute")
        try:
            return object.__getattribute__(self, name)
        except AttributeError:
            # 可以在这里自定义逻辑,或者继续抛出触发 __getattr__
            raise
③ 总结
  • __getattr__ 不属于描述符协议内部逻辑,是独立的异常兜底机制;
  • 数据描述符优先级最高,只要命中,直接返回,不会触发 __getattr__;
  • 只有整条 MRO 链查找完毕,完全找不到属性,抛出 AttributeError 时,才会尝试执行 __getattr__(属性名);
  • __getattribute__ 是属性查找主入口,掌控所有描述符调度;__getattr__ 只是最后的安全网。

12.3 __set__ 与 __delete__ 的触发

赋值和删除操作的逻辑类似,同样在 __setattr__ 和 __delattr__ 中拦截:

  • 执行 obj.attr = value 时,会先去类中查找该属性:
    • 如果是数据描述符,直接调用描述符的 __set__ 方法,不会写入实例 __dict__
    • 如果不是数据描述符,直接把键值对写入实例自身的 __dict__
  • 执行 del obj.attr 时逻辑同理:数据描述符触发 __delete__,否则删除实例 __dict__ 中的键
注意

数据描述符判定在读取链路和赋值/删除链路语义不一样

  • ✅ 赋值/删除链路(__setattr__/__delattr__):只要有 __set__ 或 __delete__ 就是数据描述符,不需要 __get__,控制赋值/删除优先级。
  • ✅ 读取链路(__getattribute__)中,想要在读取时拥有高于实例字典的优先级,必须同时具备 __get__ + (__set__ 或 __delete__)。

1. 伪代码模拟 __setattr__

def object_setattr(instance, attr_name, value):
    """模拟 Python 内置 object.__setattr__ 的执行流程"""
    cls = type(instance)

    for base in cls.__mro__:
        if attr_name in base.__dict__:
            attr = base.__dict__[attr_name]
            # 有 __set__/__delete__ 即为数据描述符
            if hasattr(attr, '__set__') or hasattr(attr, '__delete__'):
                # 判断数据描述符是否有 __set__ 方法
                if hasattr(attr, '__set__'):
                    attr.__set__(instance, value)
                    return
                else:
                    raise AttributeError('__set__')
            break

    # 不是数据描述符,直接添加实例属性
    instance.__dict__[attr_name] = value

2. 伪代码模拟 __delattr__

def object_delattr(instance, attr_name):
    """模拟 Python 内置 object.__delattr__ 的执行流程"""
    cls = type(instance)

    for base in cls.__mro__:
        if attr_name in base.__dict__:
            attr = base.__dict__[attr_name]
            # 有 __set__/__delete__ 即为数据描述符
            if hasattr(attr, '__set__') or hasattr(attr, '__delete__'):
                # 判断数据描述符是否有 __delete__ 方法
                if hasattr(attr, '__delete__'):
                    attr.__delete__(instance)
                    return
                else:
                    raise AttributeError('__delete__')
            break

    # 不是数据描述符,直接删除实例属性
    if attr_name in instance.__dict__:
        del instance.__dict__[attr_name]
    else:
        raise AttributeError(f"'{cls.__name__}' object has no attribute '{attr_name}'")

3. 类.属性 访问时的行为

注意

__set__/__delete__ 只有在对实例赋值/删除时才会触发。

  • Demo.attr = xxx:这是对类对象本身赋值,调用元类的 __setattr__,不会调用描述符的 __set__。
  • 描述符的 __set__(instance, value) 中的 instance 永远是实例对象,类对象赋值不会进入描述符协议。
class DataDesc:
    def __get__(self, instance, owner):
        print(f"__get__ instance={instance}")
        return self

    def __set__(self, instance, value):
        print(f"__set__ trigger, instance={instance}, value={value}")

class Demo:
    x = DataDesc()

if __name__ == '__main__':
    d = Demo()
    d.x = 10  # ✅触发 DataDesc.__set__(d, 10)

    # ❗直接改写类属性,不会触发 DataDesc.__set__,直接把 Demo.x 覆盖掉!
    Demo.x = 99
    # 99,描述符直接被抹掉
    print(Demo.x)

4. 总结

  1. 一个对象只有 __set__(无 __get__):
    • ✔ 赋值:是数据描述符,压制实例 __dict__,调用 __set__。
    • ✔ 读取:不触发描述符协议,实例 __dict__ 优先级更高。
  2. 非数据描述符:仅有 __get__;实例 __dict__ 永远可以覆盖它。
  3. property(带 setter):同时具备 __get__ + __set__;读写链路都识别为数据描述符,读写都走描述符协议。
  4. 两套判定标准,不要混为一谈:写的优先级判定和读的优先级判定条件并不相同,很多教程直接混淆这一点。

12.4 数据描述符测试

测试代码:

class OnlySet:
    """测试只有 __set__ 是不是描述符"""

    def __set__(self, instance, value):
        print(f'OnlySet.__set__ 被调用')
        print(f'instance: {instance}')
        print(f'value: {value}')
        print('=' * 20)

class OnlyDelete:
    """测试只有 __delete__ 是不是描述符"""

    def __delete__(self, instance):
        print(f'OnlyDelete.__delete__ 被调用')
        print(f'instance: {instance}')
        print('=' * 20)

class Demo:
    only_set = OnlySet()
    only_delete = OnlyDelete()

给 only_set 赋值:

demo.only_set = 10
print(f'demo.__dict__: {demo.__dict__}')
print(f'demo.only_set: {demo.only_set}')

(图片未上传成功:file-20260825233250302)

从结果可以看出 only_set 属性被当成了描述符对象进行赋值,触发了 OnlySet 的 __set__ 方法,并没有为对象添加一个 only_set 实例属性。

上面的结果在访问 only_set 属性时直接获取到了 OnlySet 对象,但 OnlySet 并没有设置 __get__ 方法,所以猜想访问 only_set 属性时可能将其看做了普通类属性。

下面为 demo 对象强行添加 only_set 实例属性:

demo.__dict__['only_set'] = 'only_set'
print(f'demo.__dict__: {demo.__dict__}')
print(f'demo.only_set: {demo.only_set}')

(图片未上传成功:file-20260825233908185)

从结果可以看出成功为 demo 对象添加了一个实例属性 only_set,并且在访问 only_set 属性时直接获取到了实例属性的值。结合上面一个测试的结果,可以得出结论:

结论

对于没有定义 __get__ 的数据描述符属性,在访问属性时会将其作为普通类属性。

此时再次给 only_set 属性赋值,还会触发描述符协议吗?

demo.only_set = 'new only_set'
print(f'demo.__dict__: {demo.__dict__}')
print(f'demo.only_set: {demo.only_set}')

(图片未上传成功:file-20260825234605698)

从结果可以看出赋值操作时成功触发了描述符协议,但在取值时还是没有触发描述符协议,且实例属性未被更改。

测试手动删除 only_delete 属性会触发描述符协议吗?

del demo.only_delete

(图片未上传成功:file-20260825234943917)

从结果可以看出成功触发了 OnlyDelete 的 __delete__ 方法。

尝试给 only_delete 属性赋值:

try:
    demo.only_delete = 10
except AttributeError as e:
    print('赋值失败')
    print(f'AttributeError: {e}')
else:
    print('赋值成功')
    print(f'demo.__dict__: {demo.__dict__}')
    print(f'demo.only_delete: {demo.only_delete}')

(图片未上传成功:file-20260826000732147)

从结果看 only_delete 属性的赋值操作触发了描述符协议,并没有新增一个实例属性,但是由于 OnlyDelete 没有 __set__ 方法,所以赋值操作出错。

下面强行添加 only_delete 实例属性:

demo.__dict__['only_delete'] = 'only_delete'
print(f'demo.__dict__: {demo.__dict__}')
print(f'demo.only_delete: {demo.only_delete}')

(图片未上传成功:file-20260826001140929)

从结果可以看出成功为 demo 对象添加了一个实例属性 only_delete,并且因为 OnlyDelete 未定义 __get__ 方法,所以在取值时没有触发描述符协议,直接获取到了实例属性。

再次给 only_delete 属性赋值:

try:
    demo.only_delete = 10
except AttributeError as e:
    print('赋值失败')
    print(f'AttributeError: {e}')
else:
    print('赋值成功')
    print(f'demo.__dict__: {demo.__dict__}')
    print(f'demo.only_delete: {demo.only_delete}')

(图片未上传成功:file-20260826003105450)

依旧赋值失败,并没有去改变实例属性的值,结合上一个测试结果可以得出结论:

结论

数据描述符属性赋值时必然会触发描述符协议,如果没有定义 __set__ 方法会报错:AttributeError: __set__。

再次测试删除操作:

del demo.only_delete
print(f'demo.__dict__: {demo.__dict__}')
print(f'demo.only_delete: {demo.only_delete}')

(图片未上传成功:file-20260826003751937)

从结果可以看出同名实例属性并不会影响数据描述符删除协议的响应。

测试删除 only_set 属性:

try:
    del demo.only_set
except AttributeError as e:
    print('删除失败')
    print(f'AttributeError: {e}')
else:
    print('删除成功')

(图片未上传成功:file-20260826004421624)

从结果可以看出,在执行删除操作时 only_set 被当成了数据描述符,结合上面测试的结果可以得出结论:

结论

数据描述符属性手动执行删除操作时必然会触发描述符协议,如果没有定义 __delete__ 方法会报错:AttributeError: __delete__。

完整代码:

class OnlySet:
    """测试只有 __set__ 是不是描述符"""

    def __set__(self, instance, value):
        print(f'OnlySet.__set__ 被调用')
        print(f'instance: {instance}')
        print(f'value: {value}')
        print('=' * 20)

class OnlyDelete:
    """测试只有 __delete__ 是不是描述符"""

    def __delete__(self, instance):
        print(f'OnlyDelete.__delete__ 被调用')
        print(f'instance: {instance}')
        print('=' * 20)

class Demo:
    only_set = OnlySet()
    only_delete = OnlyDelete()

if __name__ == '__main__':
    demo = Demo()

    # 测试只有 __set__ 是不是描述符
    print('====1. 测试只有 __set__ 是不是描述符====')
    print('--测试赋值')
    demo.only_set = 10
    print(f'demo.__dict__: {demo.__dict__}')
    print(f'demo.only_set: {demo.only_set}')

    print('\n--强行添加 only_set 实例属性')
    demo.__dict__['only_set'] = 'only_set'
    print(f'demo.__dict__: {demo.__dict__}')
    print(f'demo.only_set: {demo.only_set}')

    print('\n--再次为 only_set 赋值')
    demo.only_set = 'new only_set'
    print(f'demo.__dict__: {demo.__dict__}')
    print(f'demo.only_set: {demo.only_set}')

    # 测试只有 __delete__ 是不是描述符
    print('\n====2. 测试只有 __delete__ 是不是描述符====')
    print('--测试删除')
    del demo.only_delete

    print('\n--测试给 only_delete 赋值')
    try:
        demo.only_delete = 10
    except AttributeError as e:
        print('赋值失败')
        print(f'AttributeError: {e}')
    else:
        print('赋值成功')
        print(f'demo.__dict__: {demo.__dict__}')
        print(f'demo.only_delete: {demo.only_delete}')

    print('\n--强行添加 only_delete 实例属性')
    demo.__dict__['only_delete'] = 'only_delete'
    print(f'demo.__dict__: {demo.__dict__}')
    print(f'demo.only_delete: {demo.only_delete}')

    print('\n--再次测试给 only_delete 赋值')
    try:
        demo.only_delete = 10
    except AttributeError as e:
        print('赋值失败')
        print(f'AttributeError: {e}')
    else:
        print('赋值成功')
        print(f'demo.__dict__: {demo.__dict__}')
        print(f'demo.only_delete: {demo.only_delete}')

    print('\n--再次测试删除')
    del demo.only_delete
    print(f'demo.__dict__: {demo.__dict__}')
    print(f'demo.only_delete: {demo.only_delete}')

    print('\n====3. 测试只有 __set__ 方法描述符的删除操作====')
    try:
        del demo.only_set
    except AttributeError as e:
        print('删除失败')
        print(f'AttributeError: {e}')
    else:
        print('删除成功')

(图片未上传成功:file-20260826004912388)

1. 总结

  • 数据描述符属性执行访问操作时不一定会触发描述符协议,如果没有定义 __get__ 方法,不会直接报错,而是会自动按属性查找的优先级顺序往下找。
  • 数据描述符属性执行赋值操作时必然会触发描述符协议,如果没有定义 __set__ 方法会报错:AttributeError: __set__。
  • 数据描述符属性手动执行删除操作时必然会触发描述符协议,如果没有定义 __delete__ 方法会报错:AttributeError: __delete__。

12.5 属性查找优先级验证

我们用两段代码直观验证「数据描述符 vs 非数据描述符」的优先级差异。

1. 非数据描述符:被实例属性覆盖

普通函数(实例方法)是典型的非数据描述符,优先级低于实例属性:

class Demo:
    def func(self):
        return "实例方法执行了"

if __name__ == '__main__':
    d = Demo()
    # 输出:类中的方法(此时走非数据描述符逻辑)
    print(d.func())

    # 给实例自身添加同名属性
    d.func = "实例中的属性"

    # 实例属性优先级更高,直接覆盖了方法
    print(d.func)  # 输出:实例中的属性

(图片未上传成功:file-20260817152317316)

2. 数据描述符:覆盖实例属性

@property 是典型的数据描述符,优先级高于实例属性:

class Demo:
    @property
    def val(self):
        return "描述符返回的值"

if __name__ == '__main__':
    d = Demo()
    # 输出:描述符返回的值
    print(d.val)

    # 尝试给实例同名属性赋值
    # AttributeError: property 'val' of 'Demo' object has no setter
    d.val = "强行赋值"

(图片未上传成功:file-20260817152555243)

哪怕强行通过 __dict__ 写入实例属性,读取时依然优先走数据描述符:

d.__dict__["val"] = "实例里的值"
# 数据描述符优先级更高
print(d.val)  # 输出:描述符返回的值
# 直接访问字典才能拿到
print(d.__dict__["val"])  # 输出:实例里的值

(图片未上传成功:file-20260817154116538)

12.6 描述符的状态存储:常见误区

描述符对象是类属性,所有实例共享同一个描述符实例。因此绝对不能把每个实例的私有数据存在描述符自身的 self 上,否则会出现实例间数据互相覆盖的问题。

1. 错误写法(数据冲突)

class BadDescriptor:
    def __get__(self, instance, owner):
        # 数据存在描述符自己身上
        return self.value

    def __set__(self, instance, value):
        # 所有实例共用同一个 value
        self.value = value

class Demo:
    attr = BadDescriptor()

if __name__ == '__main__':
    a = Demo()
    b = Demo()
    a.attr = 100
    # 输出:100 → b 的值被 a 覆盖了,完全错误
    print(b.attr)

(图片未上传成功:file-20260817154712307)

2. 正确写法(数据隔离)

把每个实例的数据存在实例自身的 __dict__ 上,用属性名作为键:

class GoodDescriptor:
    def __init__(self, attr_name):
        self.attr_name = attr_name  # 记录要存储的属性名

    def __get__(self, instance, owner):
        if instance is None:
            return self  # 类访问时返回描述符自身
        return instance.__dict__.get(self.attr_name)

    def __set__(self, instance, value):
        instance.__dict__[self.attr_name] = value  # 数据存在实例上

class Demo:
    attr = GoodDescriptor("attr")

if __name__ == '__main__':
    a = Demo()
    b = Demo()
    a.attr = 100
    b.attr = 200
    print(a.attr)  # 100
    print(b.attr)  # 200 → 实例数据完全隔离,正确

(图片未上传成功:file-20260817155458411)

12.7 设计意义

描述符协议的核心价值在于统一了 Python 的属性访问模型:

  1. 机制统一:普通数据属性、方法、计算属性、类方法、静态方法,全部走同一套属性查找流程,解释器无需为不同语法单独做特殊处理
  2. 扩展性极强:开发者可以通过自定义描述符,实现任意复杂的属性逻辑(类型校验、懒加载、数据校验、ORM 字段映射等),完全不侵入业务类的代码
  3. 语法糖底层支撑:Python 中绝大多数面向对象的语法糖,本质都是描述符的封装,理解描述符后就能看透这些语法的本质

13 方法的动态性

Python 是动态语言,可以动态地为类添加新的方法,或者动态地修改类已有的方法。

案例:测试方法的动态性。

class Person:
    def __init__(self, name):
        self.name = name

    def work(self):
        print(f"{self.name} 努力上班!")

def play_game(self):
    print(f"{self.name} 玩游戏")

# s 就是为了在调用时接收 self 参数
def work2(s):
    print(f"{s.name} 好好工作,努力上班!")

Person.play = play_game
Person.work = work2

p = Person("小龙女")
p.play()
p.work()

(图片未上传成功:Pasted-image-20251005170708)

我们可以看到,Person 动态的新增了 play_game 方法,以及用 work2 替换了 work 方法。

14 面向对象三大特征介绍

Python 是面向对象的语言,也支持面向对象编程的三大特性:封装(隐藏)、继承、多态。

(图片未上传成功:Pasted-image-20251006130133)

面向对象编程的核心思想是将现实世界的事物抽象为对象,对象由「属性(数据)」和「方法(行为)」组成。封装、继承、多态是 OOP 的三大核心特性,共同实现了代码的复用性、安全性、扩展性与可维护性。

14.1 封装(Encapsulation)

1. 核心概念

封装是将对象的属性和方法捆绑为一个独立的整体,并尽可能隐藏内部实现细节,仅对外暴露有限的访问接口,外部只能通过指定的接口与对象交互。

封装的核心是信息隐藏:把该保护的数据藏起来,把必要的操作入口暴露出去。

简单来说,封装就是隐藏对象的属性和实现细节,只对外提供必要的方法。相当于将细节封装起来,只对外暴露相关调用方法。

2. 实现方式

通过访问权限控制实现,主流编程语言通常定义三级访问权限:

  • public(公有):对外完全开放,外部可直接访问
  • protected(保护):仅类内部和子类可访问
  • private(私有):仅类内部可访问,外部和子类都无法直接访问

Python 没有严格的语法级权限控制,通过命名约定实现:

  • 无前缀:公有成员,如 name
  • 单下划线 _ 开头:约定为保护成员,如 _age,提示外部不要随意访问
  • 双下划线 __ 开头:私有成员,如 __id_card,Python 会通过「名字重整」机制修改属性名,外部无法直接访问

3. 封装的优势

  • 数据安全:通过接口控制属性的读写,可加入校验逻辑,避免非法数据
  • 降低耦合:外部只依赖公开接口,内部实现逻辑修改不影响外部调用
  • 简化使用:使用者无需了解内部细节,只需调用接口即可完成操作

14.2 继承(Inheritance)

1. 核心概念

继承是让子类(派生类)获得父类(基类 / 超类)的属性和方法,同时子类可以新增自己的属性和方法,或者重写父类已有的方法。

继承体现了类之间的 is-a 层次关系,核心目的是代码复用,避免重复编写相同逻辑。

2. 分类

  • 单继承:一个子类只继承一个父类(Java 等语言强制单继承)
  • 多继承:一个子类可以同时继承多个父类(Python 支持多继承)

3. 核心操作

  • 子类继承父类后,默认拥有父类所有非私有属性和方法
  • 方法重写(Override):子类可以定义与父类同名的方法,覆盖父类的实现
  • super():在子类中调用父类的方法,常用于初始化父类属性

4. 继承的优势

  • 代码复用:公共逻辑写在父类,子类直接复用,减少冗余代码
  • 层次清晰:体现事物的从属关系,符合人类认知逻辑
  • 易于扩展:新增子类时,只需编写差异化逻辑,无需从零开发
注意

行业同时存在「组合优于继承」的设计原则——过度深的继承层级会提升代码耦合度,复杂场景下更推荐用对象组合实现复用。

14.3 多态(Polymorphism)

1. 核心概念

多态指同一个行为,作用于不同的对象时,可以产生完全不同的执行结果。简单说就是「一个接口,多种实现」。

多态的本质是:程序运行时才确定具体调用哪个对象的方法,而非编译时确定。

生活中这样的例子比比皆是:同样是休息方法,人不同休息方法不同。张三休息是睡觉,李四休息是玩游戏,程序员休息是「敲几行代码」。

2. 两种常见形态

  1. 运行时多态(动态多态):最典型的多态形式,静态语言中依赖「继承 + 方法重写 + 父类引用指向子类对象」实现。
    Python 作为动态语言,还支持「鸭子类型」:不强制要求继承关系,只要对象拥有对应的方法,就可以被调用,即「走起来像鸭子、叫起来像鸭子,就可以当鸭子用」。
  2. 编译时多态(静态多态):典型代表是方法重载,同一个类中存在多个同名方法,但参数个数 / 类型不同。Python 不支持语法级别的方法重载(同名方法会被覆盖),可通过默认参数模拟类似效果。

3. 多态的优势

  • 扩展性强:新增一个子类时,无需修改原有调用逻辑,符合软件设计的「开闭原则」
  • 灵活性高:面向接口编程,大幅降低代码耦合度
  • 简化代码:不用为每种对象编写单独的调用逻辑

14.4 三者的关系总结

  • 封装是基础:将数据与行为打包,隐藏细节、暴露接口,是对象的基本组织形式
  • 继承是纵向扩展:在封装的基础上,实现类与类之间的代码复用与层级关系
  • 多态是横向灵活:在继承的基础上,实现同一接口的多种实现,提升程序扩展性

三者相辅相成,共同构成了面向对象编程的核心骨架。

思考

为什么说封装是面向对象三大特征中基础呢?

解析

封装之所以是三大特征的基础,核心原因是:封装构建了面向对象编程的基本单元(类与对象),而继承、多态都是在这个单元之上实现复用与扩展的高级特性。具体可以从三个维度展开:

  1. 封装是「对象」概念的本质体现:面向对象的核心是用「对象」组织代码,而封装的作用就是把数据(属性)和操作数据的行为(方法)打包成一个独立、有边界的整体,再通过访问权限控制隐藏内部细节、暴露对外接口。没有封装,代码就只是零散的变量和函数,不存在真正的「类」和「对象」,继承和多态就失去了作用的载体,根本无法实现。
  2. 封装支撑了继承的语法与语义:继承的本质是子类复用、扩展父类的成员,而父类的属性和方法正是通过封装才形成了结构清晰的类结构。同时,封装的访问控制规则(例如 protected 成员对子类开放、对外部隐藏)直接定义了继承场景下的成员可见性,是继承机制的核心组成部分。
  3. 封装是多态的逻辑基础:无论是编译时多态(方法重载)还是运行时多态(方法重写、接口实现),都依赖于「方法属于类 / 对象」这一封装的结果。运行时多态的核心思想——「统一调用接口,不同内部实现」——本身就是封装思想的延伸:对外隐藏实现差异,只暴露一致的调用方式。
面试回答

封装是面向对象三大特征的基础,是继承与多态的前提和载体,核心原因有三点:

  1. 构建基本单元:封装将数据(属性)与行为(方法)打包为类与对象,划定边界并控制访问权限。没有封装就不存在独立的类 / 对象单元,继承和多态就失去了作用载体。
  2. 支撑继承机制:继承的本质是子类复用、扩展父类成员,而父类的成员结构正是封装的产物;同时封装的访问控制规则(如 protected 权限),直接定义了继承场景下的成员可见性。
  3. 构成多态前提:多态的核心是「统一调用接口,差异化内部实现」,本身就是封装思想的延伸;无论是方法重载还是重写,都依赖「方法隶属于类 / 对象」这一封装结果。

一句话总结:封装解决「对象怎么构成」的底层问题,是面向对象的地基;继承和多态是上层的复用与扩展特性,必须建立在封装之上。

15 面向对象的三大特性——封装

15.1 私有属性和私有方法(实现封装)

(图片未上传成功:Pasted-image-20250404072535)

Python 对于类的成员没有严格的访问控制限制,这与其他面向对象语言有区别。

关于私有属性和私有方法,有如下要点:

  1. 命名约定实现
    • 无前缀:公有成员(public),如 name
    • 单下划线 _ 开头:约定为保护成员(protected),如 _age,提示外部不要随意访问
    • 双下划线 __ 开头:私有成员(private),如 __id_card,Python 会通过「名字重整」机制修改属性名,外部无法直接访问
  2. 类内部可以访问私有属性(方法)。
  3. 类外部不能直接访问私有属性(方法)。
  4. 类外部可以通过 对象._类名__私有属性(方法)名 访问私有属性(方法)。
注意

方法本质上也是属性!只不过是可以通过 () 执行而已。

所以,此处讲的私有属性和公有属性,也同时讲解了私有方法和公有方法的用法。

如下测试中,同时也包含了私有方法和公有方法的例子。

案例:测试私有属性、私有方法。

from rich import print

class Employee:
    # 私有类属性。通过 dir 可以查到 _Employee__company
    __company = "古墓派"

    def __init__(self, name, age):
        self.name = name
        # 私有实例属性
        self.__age = age

    def say_company(self):
        # 类内部可以直接访问私有属性
        print(f"我的门派是:{Employee.__company}")
        print(f"{self.name} 的年龄是:{self.__age}")
        print(self.__work())

    # 私有实例方法 通过 dir 可以查到 _Employee__work
    def __work(self):
        return f"{self.name} 正在练武!"

if __name__ == '__main__':
    p1 = Employee("小龙女", 18)
    print('====1. 访问对象的公有属性和公有方法====')
    # 访问公有属性
    print(f'公有属性 name:{p1.name}')
    print('使用公有函数:')
    # 访问公有函数
    p1.say_company()

    print('\n====2. dir 查看函数的所有属性====')
    # 通过 dir 可以查到属性:_Employee__age
    print(dir(p1))

    print('\n====3. 无法直接访问私有属性和私有方法====')
    try:
        # 直接访问私有属性,报错
        print(f'访问实例私有属性 __age:{p1.__age}')
    except AttributeError as e:
        print('无法通过 __age 访问实例私有属性')
        print(e)

    try:
        # 直接访问私有方法,报错
        print(f'访问实例私有方法 __work:{p1.__work}')
    except AttributeError as e:
        print('无法通过 __work 访问实例私有属性')
        print(e)

    print('\n====4. Python 没有严格的访问控制限制====')
    print('可以通过以下方式访问实例的私有属性和私有方法(不推荐使用)')
    # 通过这种方式可以直接访问到私有属性。
    print(f'通过 _Employee__age 获取私有属性 __age:{p1._Employee__age}')
    print(f'通过 _Employee__work 调用私有方法 __work:{p1._Employee__work()}')

(图片未上传成功:file-20260731100115765)

原文勘误

原文此处写作 print('公有属性 name:{p1.name}'),漏写了 f 前缀,{} 不会被替换、会原样打印。已补上 f。

从打印的 Employee 对象所有属性我们可以看出。私有属性 age 在实际存储时是按照 _Employee__age 这个属性来存储的。这也就是为什么我们不能直接使用 __age 而可以使用 _Employee__age 的根本原因。

思考

由以上代码运行可知,私有属性不能在类的外部被直接访问。但是出于种种原因,我们想在外部对私有属性进行访问,该如何操作呢?

答

我们可以定义一个专门的访问接口(函数),专门用于实现私有属性的访问。

15.2 私有属性设置与访问接口

在 Python 中,一般定义函数名 get_xx 用来获取私有属性,定义 set_xx 用来修改私有属性值。

class Girl:
    def __init__(self, name, age):
        self.name = name
        self.__age = age

    # 公共方法:提供给外部的访问接口
    def get_age(self):
        # 本人访问:允许直接访问
        # 外人访问:加上限制条件
        return self.__age

    # 公共方法:提供给外部的设置接口
    def set_age(self, age):
        self.__age = age

if __name__ == '__main__':
    girl = Girl("小龙女", 18)
    # 通过接口获取私有属性
    print(f"{girl.name} {girl.get_age()} 岁了!!")
    # 通过接口设置私有属性
    girl.set_age(19)
    # 通过接口获取私有属性
    print(f"{girl.name} {girl.get_age()} 岁了!!")

(图片未上传成功:Pasted-image-20251005175556)

15.3 封装性到底有何意义

  1. 以面向对象的编程思想进行项目开发。

  2. 封装数据属性:明确的区分内外,控制外部对隐藏的属性的操作行为(过滤掉异常数据)。

    class People:
        def __init__(self, name, age):
            self.__name = name
            self.__age = age
    
        def tell_info(self):
            print(f"{self.__name} {self.__age} 岁了")
    
        # 对私有属性的访问接口
        def set_info(self, name, age):
            if not isinstance(name, str):
                print("修改失败:名字必须是字符串类型")
                return
            if not isinstance(age, int):
                print("修改失败:年龄必须是数字类型")
                return
            # 过滤异常数据
            if not 0 <= age <= 125:
                print("修改失败:年龄必须在 0~125 之间")
                return
    
            self.__name = name
            self.__age = age
    
    if __name__ == '__main__':
        p = People("小龙女", 25)
        p.tell_info()
    
        p.set_info("杨过", 18)
        p.tell_info()
    
        p.set_info(123, 35)
        p.tell_info()
    
        p.set_info("郭靖", 135)
        p.tell_info()
    

    (图片未上传成功:file-20260731101631086)

  3. 私有方法封装的意义:降低程序的复杂度。

    class ATM:
        def __card(self):
            print('插卡')
    
        def __auth(self):
            print('用户认证')
    
        def __input(self):
            print('输入取款金额')
    
        def __print_bill(self):
            print('打印账单')
    
        def __take_money(self):
            print('取款')
    
        # 定义一个对外提供服务的公共方法
        def withdraw(self):
            self.__card()
            self.__auth()
            self.__input()
            self.__print_bill()
            self.__take_money()
    
    atm = ATM()
    atm.withdraw()
    

    (图片未上传成功:Pasted-image-20251005180430)

15.4 @property 装饰器

@property 可以将一个方法的调用方式变成属性调用。

下面是一个简单的示例,让大家体会一下这种转变。

案例:简单测试 @property。

class Employee:
    @property
    def salary(self):
        return 30000

emp1 = Employee()
print(emp1.salary)
print(type(emp1.salary))

# 报错:TypeError: 'int' object is not callable
# emp1.salary()

# @property 修饰的属性,如果没有加 setter 方法,则为只读属性。
# 此处修改报错:AttributeError: can't set attribute
# emp1.salary = 1000

(图片未上传成功:Pasted-image-20251005225733)

(图片未上传成功:Pasted-image-20251005230603)

(图片未上传成功:Pasted-image-20251005230236)

Tip

@property 主要用于帮助我们处理属性的读操作、写操作。

使用案例

对于某一个属性,我们可以直接通过:

# 读取 salary 属性
emp.salary

# 设置 salary 属性
emp.salary = 30000

如上可以实现属性的读操作、写操作。但是,这种做法不安全。比如,我需要限制薪水必须为 1~10000 的数字。这时候,我们就需要通过 getter、setter 方法来处理。

# 方法一:使用 getter 和 setter 方法
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.__salary = salary

    # salary 属性的 getter 方法
    def get_salary(self):
        return self.__salary

    # salary 属性的 setter 方法
    def set_salary(self, salary):
        if 0 < salary < 1000000:
            self.__salary = salary
        else:
            print("薪水录入错误!薪水只能在 0~1000000 之间")

if __name__ == '__main__':
    emp = Employee("小龙女", 50000)
    print(f"月薪为 {emp.get_salary()}, 年薪为{emp.get_salary()}")
    print("=========更改薪水=========")
    emp.set_salary(200999)
    print(f"月薪为 {emp.get_salary()}, 年薪为{emp.get_salary()}")
    print("=========更改薪水=========")
    emp.set_salary(-200)
    print(f"月薪为 {emp.get_salary()}, 年薪为{emp.get_salary()}")

(图片未上传成功:Pasted-image-20251005232942)

上面的方式使用函数来操作属性,我们还可以通过 property 和 setter 装饰器来像操作属性那样进行处理。

# 方法二:使用 @property 和 setter 装饰器
class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.__salary = salary

    @property
    # 相当于 salary 属性的 getter 方法
    def salary(self):
        return self.__salary

    # @ 后面的名字一定要和 @property 修饰的函数名保持一致
    @salary.setter
    # 相当于 salary 属性的 setter 方法
    def salary(self, salary):
        if 0 < salary < 1000000:
            self.__salary = salary
        else:
            print("薪水录入错误!薪水只能在 0-1000000 之间")

if __name__ == '__main__':
    emp = Employee("小龙女", 50000)
    print(f"月薪为 {emp.salary}, 年薪为{emp.salary}")
    print("=========更改薪水=========")
    emp.salary = 200999
    print(f"月薪为 {emp.salary}, 年薪为{emp.salary}")
    print("=========更改薪水=========")
    emp.salary = -200
    print(f"月薪为 {emp.salary}, 年薪为{emp.salary}")

(图片未上传成功:Pasted-image-20251005235005)

15.5 属性和方法命名总结

_xxx:保护成员,不能用 from module import * 导入,只有类对象和子类对象能访问这些成员。

__xxx__:系统定义的特殊成员。

__xxx:类中的私有成员,只有类对象自己能访问,子类对象也不能访问。(但,在类外部可以通过 对象名._类名__xxx 这种特殊方式访问。Python 不存在严格意义的私有成员)。

原文勘误

原文写作 `对象名._类名 __xxx`,标识符中间多了一个空格,易被误读为两个标识符。正确的写法是 对象名._类名__xxx(_类名 与 __xxx 之间无空格),已修正。

注意

再次强调,方法和属性都遵循上面的规则。

15.6 类编码风格

  1. 类名首字母大写,多个单词之间采用大驼峰原则。
  2. 实例名、模块名采用小写,多个单词之间采用下划线隔开。
  3. 每个类,应紧跟文档字符串,说明这个类的作用。
  4. 可以用空行组织代码,但不能滥用。在类中,使用一个空行隔开方法;模块中,使用两个空行隔开多个类。

16 封装行业规范

思考

@property 和 get_xx、set_xx 都可以实现属性的封装,那这两种方式的差别是什么?什么情况下该使用 @property 来封装属性?什么时候该使用 get_xx 和 set_xx 来封装属性?

16.1 核心底层原理

Python 没有真正严格私有变量(_var 约定私有,__var 只是名称改写),两种方案本质都是封装、校验、拦截读写,只是语法风格、适用场景、语言定位不同:

  1. @property / @xxx.setter:Python 原生语法糖,属性式访问(obj.xxx 读写)
  2. get_xxx() / set_xxx(val):传统 Java/C++ 风格,方法调用式访问(obj.get_xxx())

16.2 什么时候用 @property + @setter(优先推荐,Pythonic)

1. 适用场景

  1. 对外表现得像普通属性,读写逻辑简单
    只是加少量校验、类型转换、计算衍生值,不需要复杂业务操作。

    class Student:
        def __init__(self, age):
            self._age = age
    
        @property
        def age(self):
            # 读属性拦截
            return self._age
    
        @age.setter
        def age(self, val):
            # 写属性拦截,做校验
            if not isinstance(val, int):
                raise TypeError("年龄必须是整数")
            if 0 < val < 150:
                self._age = val
            else:
                raise ValueError("年龄范围0~150")
    
    if __name__ == '__main__':
        s = Student(18)
        # 像普通属性一样读取,不用括号
        print(s.age)  # 18
        # 直接赋值,自动走校验逻辑
        s.age = 20
        print(s.age)  # 20
    
  2. 只读属性(只写 @property,不写 setter)
    只允许读取、禁止外部修改,非常常用(计算属性、固定内置值)

    @property
    def full_name(self):
        # 由 first_name + last_name 动态计算,无法赋值
        return f"{self._first} {self._last}"
    
  3. 平滑兼容历史代码(从普通变量升级带逻辑)
    项目早期直接用 obj.age,后续需要加校验,改用 @property 后外部调用代码完全不用改,零侵入兼容。

    • 原来:obj.age = 10
    • 加 @property 后:外部代码写法不变,内部新增校验逻辑
  4. 数据模型、ORM、Pydantic、GUI 组件、简单实体类
    Django/ORM 模型、dataclass、数据库映射类大量使用 @property,贴合 Python 属性访问习惯。

  5. 衍生计算属性(无底层存储变量)
    不依赖独立私有变量,实时计算返回结果,只能读不能写。

2. @property 优势

  • 语法简洁自然,符合 Python 「简洁优雅」 设计哲学
  • 调用无括号,和原生变量体验完全一致
  • 代码可读性高,区分「数据属性」和「操作方法」
  • 支持 delattr(@xxx.deleter) 拦截删除操作

3. 局限

  • 不适合多参数写入,setter 只能接收一个值
  • 复杂业务流程、多步骤操作写在 setter 里会臃肿混乱

16.3 什么时候用 get/set(Java 风格)

1. 适用场景

  1. set 需要传入多个参数
    @setter 仅支持单个入参,多参数只能用 set 方法:

    class User:
        def __init__(self):
            self._info = {}
    
        # 多参数赋值,setter做不到
        def set_info(self, name, phone, addr):
            self._info["name"] = name
            self._info["phone"] = phone
            self._info["addr"] = addr
    
        def get_info(self):
            return self._info.copy()
    
  2. 修改属性时伴随复杂业务逻辑
    比如:数据库写入、接口请求、日志记录、缓存刷新、事务操作、多对象联动,大量业务代码不适合塞进装饰器:

    def set_balance(self, money):
        # 大量复杂逻辑:加锁、流水记录、数据库更新、通知推送
        if money < 0:
            raise ValueError("余额不能负数")
    
        self._lock.acquire()
    
        try:
            self._balance += money
            self._save_account_log()
            self.db.update(self.id, balance=self._balance)
            self._send_notify()
        finally:
            self._lock.release()
    
  3. 团队跨语言协作,开发人员习惯 Java/Go 后端
    前后端、跨语言项目统一使用 get/set 规范,降低沟通成本。

  4. 需要附加额外控制参数(如是否跳过校验、是否同步数据库)

    # 第二个控制参数,setter 无法实现
    def set_score(self, score, skip_check: bool = False):
        if not skip_check and not 0 <= score <= 100:
            raise ValueError
        self._score = score
    
  5. 大型 SDK、底层工具库,严格遵循标准封装规范
    框架底层为了接口标准化,统一使用 get/set,区分「数据读写接口」和「业务方法」。

  6. 需要返回自定义状态、错误码,而非直接抛异常
    get/set 函数可以返回元组 (ok, data),装饰器属性赋值只能抛异常。

2. get/set 方法优势

  • 支持多入参、可选控制参数
  • 适合承载复杂业务流程,代码分层清晰
  • 兼容面向对象静态语言开发习惯

3. 缺点

  • 调用繁琐:obj.get_age()/obj.set_age(20)
  • 如果后期需要增加校验,所有业务调用处都要改代码,无法平滑兼容
原文勘误

原文本节两个小节标题编号误写为 18.2 / 18.3(本章是第 16 章),已修正为 16.3.2 / 16.3.3。

16.4 清晰选择判断标准(速查表)

场景 推荐方案
读写逻辑简单,仅类型/范围校验 @property + @setter
只读、动态计算属性 仅 @property
需要多参数赋值、控制开关参数 get_xx/set_xx
修改时触发数据库、网络、复杂事务逻辑 get_xx/set_xx
旧代码直接点访问,不想改动外部调用 @property
团队主要使用 Java/C++,统一编码规范 get_xx/set_xx
ORM、数据实体、简单模型类 @property
底层工具 SDK、标准化对外接口 get_xx/set_xx

16.5 补充规范最佳实践

  1. 不要混合使用两套方案
    同一个私有变量不要同时写 @property 和 get_age(),会造成代码混乱。
  2. 私有变量统一单下划线 _attr
    两种封装方案底层都用 _变量 约定私有,双下划线 __ 尽量少用(名称改写易踩坑)。
  3. 复杂项目分层建议:
    • 实体模型层:全部使用 @property
    • 业务服务层:复杂数据修改统一用 get/set 方法
  4. 特殊场景:dataclass 搭配 @property
    dataclass 生成构造函数,搭配 @property 做字段校验是 Python 标准写法。

16.6 反面示例(不推荐写法)

  1. setter 塞入几百行业务逻辑(可读性极差)

    # 坏写法:setter 逻辑过重
    @age.setter
    def age(self, v):
        # 数据库、加锁、日志、推送一堆代码
        ...
    
  2. 简单属性硬写 get/set 冗余代码

    # 冗余,完全可以用 @property 简化
    def get_age(self):
        return self._age
    
    def set_age(self, v):
        self._age = v
    

17 约定私有/内部变量

先纠正概念:Python 没有「真正私有/保护」,只有命名约定 + 名称改写

17.1 三个下划线规范的官方定义(PEP8 行业统一规范)

1. 单下划线前缀

单下划线前缀 _var:约定私有/内部变量(最常用)

含义:仅供类内部使用,外部代码不要直接访问,只是人为约定,语法不拦截。

  • 外部完全能访问:obj._age 不会报错
  • 导入时单独写 from module import * 不会导入 _var 变量
  • 适用:实例属性、方法、内部工具函数

2. 双下划线前缀

双下划线前缀 __var:名称改写(name mangling),不是严格私有

Python 解释器会自动重命名:self.__name → _类名__name

class A:
    def __init__(self):
        self.__name = "test"

a = A()
print(a._A__name)  # 依然能访问,只是改了名字,锁不住
# a.__name 会直接报错

设计初衷:避免子类重名覆盖父类属性,不是用来做「私有封装」。

缺陷:

  1. 外部换个名字照样能读写,无法真正隐藏;
  2. 继承场景极易踩坑,父子类都写 __var 会生成两个完全独立变量;
  3. 可读性差,调试、序列化、ORM 映射都会出麻烦;
  4. 搭配 @property/setter 会写大量冗余代码。

3. 单下划线后缀

单下划线后缀 var_:规避关键字(如 class_、id_)

4. 前后双下划线

前后双下划线 __var__:系统魔法方法(__init__/__str__),禁止自定义

17.2 为什么封装 property 统一用 _var,不用 __var?(行业通用规范)

1. __var 的设计目的和业务封装需求完全不匹配

__var 是为继承防重名,不是做「外部禁止访问」的封装;

而我们用 @property 的目的是:对外暴露干净的 obj.age,内部存一份底层存储字段,只需要人为约束外部不要碰底层字段,不需要解释器强制改名。

2. 调试、扩展、继承更友好

  • 单下划线 _age 一眼看懂:内部存储字段,我只是不建议外部直接改;
  • 双下划线 __age 调试打印对象时看不到原名,子类扩展极易冲突。

示例继承踩坑:

class Parent:
    def __init__(self):
        self.__num = 10

class Child(Parent):
    def __init__(self):
        super().__init__()
        self.__num = 20  # 生成 _Child__num,不会覆盖父类 _Parent__num

if __name__ == '__main__':
    c = Child()
    print(c._Parent__num)  # 10
    print(c._Child__num)  # 20
    # 两个完全独立变量,大概率不是开发者想要的效果

如果用 _num,父子类共用同一个属性,逻辑更符合日常业务。

3. 社区、PEP8、主流框架统一约定

Django、SQLAlchemy、Pydantic、FastAPI、标准库全部统一规范:

  • 对外暴露的访问入口:obj.age(@property)
  • 底层真实存储字段:self._age(单下划线,约定内部私有)
    几乎没有正规项目会用 __var 配合 @property。

4. __var 会破坏 property 简洁性

# 不推荐双下划线写法,麻烦且无收益
class User:
    def __init__(self, age):
        self.__age = age

    @property
    def age(self):
        return self.__age

    @age.setter
    def age(self, v):
        self.__age = v

这里 __age 的名称改写完全多余,徒增理解成本,没有任何封装收益。

17.3 概念澄清:Java/C++ vs Python 命名语义差异

  1. Java:

    • private:语法强制外部不可访问;
    • protected:仅子类、同包访问;
      语言层面有访问权限控制。
  2. Python(无语法级权限):
    行业约定语义重新对应:

    • _var ≈ Java private/protected(日常封装首选,约定内部使用)
    • __var ≈ 特殊防重名语法,不对应 Java private,业务开发极少使用
提醒

很多初学者混淆:以为 __var 是私有、_var 是保护,这套对应逻辑不适用于 Python 社区规范。

17.4 什么时候才需要用 __var?

仅一种场景:你明确要防止子类意外覆盖父类同名属性。

例:框架底层基类,预留内部变量,担心子类随便定义同名字段覆盖逻辑:

class BasePlugin:
    def __init__(self):
        self.__plugin_id = uuid.uuid4()  # 不希望子类不小心覆盖

# 子类就算写 self.plugin_id 也不会冲突;写 self.__plugin_id 会新建变量

普通业务实体、数据模型、业务类永远不需要双下划线。

17.5 总结标准编码规范(生产环境通用)

  1. 普通封装、配合 @property/get_xx:底层存储变量统一 _attr(单下划线约定私有);
  2. 只有基类需要隔离子类重名冲突,才少量使用 __attr;
  3. 永远不要用 __attr 做常规属性封装;
  4. 对外只提供 obj.attr(property)或 get_attr(),团队约定禁止直接读写 self._attr;
  5. 记住核心逻辑:Python 靠开发者约定实现封装,不靠语法强制隐藏,单下划线是社区公认的「内部私有」标记。

18 面向对象三大特征——继承

(图片未上传成功:Pasted-image-20251006130153)

继承是面向对象程序设计的重要特征,也是实现「代码复用」的重要手段。

如果一个新类继承自一个设计好的类,就直接具备了已有类的特征,就大大降低了工作难度。已有的类,我们称为父类或者基类,新的类,我们称为子类或者派生类。

(图片未上传成功:Pasted-image-20251006130240)

18.1 什么是继承

我们接下来来聊聊 Python 代码中的继承:类是用来描述现实世界中同一组事务的共有特性的抽象模型,但是类也有上下级和范围之分,比如:生物 => 动物 => 哺乳动物 => 灵长型动物 => 人类 => 黄种人。

从哲学上说,就是共性与个性之间的关系,比如:白马和马!所以,我们在 OOP 代码中,也一样要体现出类与类之间的共性与个性关系,这里就需要通过类的继承来体现。简单来说,如果一个类 A 使用了另一个类 B 的成员(属性和方法),我们就可以说 A 类继承了 B 类,同时这也体现了 OOP 中代码重用的特性!

(图片未上传成功:Pasted-image-20251006130449)

18.2 继承的基本语法

Python 支持多重继承,一个子类可以继承多个父类。继承的语法格式如下:

class 子类类名(父类1[, 父类2, ...]):
    类体

如果在类定义中没有指定父类,则默认父类是 object 类。也就是说,object 是所有类的父类,里面定义了一些所有类共有的默认实现,比如:__new__ 方法。

假设 A 类要继承 B 类中的所有属性和方法(私有属性和私有方法除外)。

class B:
    pass

class A(B):
    pass

a = A()
原文勘误

原文此处写作 clss A(B):,clss 是 class 的拼写错误,会导致 SyntaxError,已修正为 class。

另外,原文紧随其后还有两行伪代码 a.B中的所有公共属性、a.B中的所有公共方法,它们不是合法 Python 语句,会让整个代码块无法运行,已移出代码块,见下方说明。

继承后,A 类的实例可以调用从 B 继承来的所有公共属性与公共方法,即伪代码所表达的:

a.B中的所有公共属性
a.B中的所有公共方法

案例:Person 类与 Teacher、Student 类之间的继承关系。

class Person:
    def eat(self):
        print('i can eat food!')

    def speak(self):
        print('i can speak!')

class Teacher(Person):
    pass

class Student(Person):
    pass

teacher = Teacher()
teacher.eat()
teacher.speak()
print("=" * 40)

student = Student()
student.eat()
student.speak()

(图片未上传成功:Pasted-image-20251006130909)

18.3 与继承相关的几个概念

  • 继承:一个类从另一个已有的类获得其成员的相关特性,就叫作继承!
  • 派生:从一个已有的类产生一个新的类,称为派生!

很显然,继承和派生其实就是从不同的方向来描述的相同的概念而已,本质上是一样的!

  • 父类:也叫作基类,就是指已有被继承的类!
  • 子类:也叫作派生类或扩展类!
  • 扩展:在子类中增加一些自己特有的特性,就叫作扩展,没有扩展,继承也就没有意义了!
  • 单继承:一个类只能继承自一个其他的类,不能继承多个类,单继承也是大多数面向对象语言的特性!
  • 多继承:一个类同时继承了多个父类,(C++、Python 等语言都支持多继承)。

18.4 单继承

单继承:一个类只能继承自一个其他的类,不能继承多个类。这个类会具有父类的属性和方法。

基本语法:

# 1、定义一个共性类(父类)
class Person:
    pass

# 2、定义一个个性类(子类)
class Teacher(Person):
    pass

案例:比如汽车可以分为两种类型(汽油车、电动车)。

# 1、定义一个共性类(车类)
class Car:
    def run(self):
        print('I can run')

# 2、定义汽油车
class GasolineCar(Car):
    pass

# 3、定义电动车
class ElectricCar(Car):
    pass

bwm = GasolineCar()
bwm.run()

byd = ElectricCar()
byd.run()

(图片未上传成功:Pasted-image-20251006131520)

18.5 单继承特性:传递性

在 Python 继承中,如 A 类继承了 B 类,B 类又继承了 C 类。则根据继承的传递性,则 A 类也会自动继承 C 类中所有属性和方法(公共)。

class C:
    def func(self):
        print('我是C类中的相关方法func')

class B(C):
    pass

class A(B):
    pass

a = A()
a.func()
print(a)

(图片未上传成功:Pasted-image-20251006131729)

18.6 编写面向对象代码中的常见问题

问题 1

在定义类时,其没有遵循类的命名规则。

答

在 Python 中,要遵循一定的命名规范:首字母必须是字母或下划线,其中可以包含字母、数字和下划线,而且要求其命名方式采用大驼峰。

例如:

  • 电动汽车:ElectricCar

  • 父类:Father

  • 子类:Son

原文勘误

原文示例类名写作 EletricCar,Eletric 是 Electric(电动的)的拼写错误,已修正为 ElectricCar。本节后文 EletricCar / run_with_eletric / eletric 同理一并修正。

问题 2

父类一定要继承 object 么?Car(object)

答

在 Python 面向对象代码中,所有类都继承了 object 类,就算不写也会,因为默认情况下,Python 中的所有类都继承自 object。

(图片未上传成功:image-20210318104228654)

问题 3:打印属性和方法时,都喜欢用 print。

class Person():
    def __init__(self, name):
        self.name = name

    def speak(self):
        print('i can speak')

# 创建对象,打印属性和方法
p = Person('Tom')
print(p.name)
p.speak()

18.7 多继承

什么是多继承?

Python 是少数支持多继承的编程语言之一。所谓多继承,就是允许一个类同时继承自多个类。

语法格式:

class 子类类名(父类 1[,父类 2,...]):
    类体

(图片未上传成功:image-20210318105709652)

基本语法:

class B:
    pass

class C:
    pass

class A(B, C):
    pass

a = A()

继承后,A 类的实例可同时获得 B、C 两个父类的公共成员,即伪代码所表达的:

a.B中的所有属性和方法
a.C中的所有属性和方法

案例:汽油车、电动车 => 混合动力汽车(汽车 + 电动)。

class GasolineCar(object):
    def run_with_gasoline(self):
        print('I can run with gasoline')

class ElectricCar(object):
    def run_with_electric(self):
        print('I can run with electric')

class HybridCar(GasolineCar, ElectricCar):
    pass

tesla = HybridCar()
tesla.run_with_gasoline()
tesla.run_with_electric()

(图片未上传成功:Pasted-image-20251006132506)

注意

虽然多继承允许我们同时继承自多个类,但是实际开发中,应尽量避免使用多继承,因为如果两个类中出现了相同的属性和方法就会产生命名冲突。

案例:多重继承。

(图片未上传成功:Pasted-image-20251006132556)

class A:
    def aa(self):
        print("aa")

class B:
    def bb(self):
        print("bb")

class C(B, A):
    def cc(self):
        print("cc")

c = C()
c.aa()
c.bb()
c.cc()

(图片未上传成功:Pasted-image-20251006132712)

18.8 子类扩展:重写父类属性和方法

扩展特性:继承让子类获得父类的所有公共属性和方法;但如果仅止于此,继承就没有实际意义了——子类应当在继承之后,还有一些自己的属性和方法。

什么是重写?

重写也叫作覆盖,就是当子类成员与父类成员名字相同的时候,从父类继承下来的成员会重新定义!

此时,通过子类实例化出来的对象访问相关成员的时候,真正起作用的是子类中定义的成员!

  1. 成员继承:子类继承了父类除构造方法之外的所有成员。
注意

Python 中没有严格的访问控制,私有属性其实也会继承。私有属性、私有方法被继承时,名称为 _类名__属性名。

  1. 方法重写:子类可以重新定义父类中的方法,这样就会覆盖父类的方法,也称为重写。

上面单继承例子中 Animal 的子类 Cat 和 Dog 继承了父类的属性和方法,但是我们狗类 Dog 有自己的叫声 汪汪叫,猫类 Cat 有自己的叫声 喵喵叫,这时我们需要对父类的 call() 方法进行重构。如下:

class Animal:
    def eat(self):
        print('I can eat')

    def call(self):
        print('I can call')

class Dog(Animal):
    pass

class Cat(Animal):
    pass

dog = Dog()
dog.eat()
dog.call()
print("=" * 40)

cat = Cat()
cat.eat()
cat.call()

(图片未上传成功:Pasted-image-20251006133759)

Dog、Cat 子类重写父类 Animal 中的 call 方法。

class Animal:
    def eat(self):
        print('I can eat')

    # 公共方法
    def call(self):
        print('I can call')

class Dog(Animal):
    # 重写父类的 call 方法
    def call(self):
        print('I can wang wang wang')

class Cat(Animal):
    # 重写父类的 call 方法
    def call(self):
        print('I can miao miao miao')

dog = Dog()
dog.eat()
dog.call()
print("=" * 40)

cat = Cat()
cat.eat()
cat.call()

(图片未上传成功:Pasted-image-20251006134134)

思考

重写父类中的 call 方法以后,此时父类中的 call 方法还在不在?

答

还在,只不过是在其子类中找不到了。

因为类方法的调用顺序,当我们在子类中重构父类的方法后,Cat 子类的实例先会在自己的类 Cat 中查找该方法,当找不到该方法时才会去父类 Animal 中查找对应的方法。

注意

Python 3.12 引入了 @override 装饰器,可以用来指定该方法是用来覆盖基类方法的。

程序会检查被装饰的方法是否在基类中存在。如果被装饰的方法并未在基类中定义,程序会报错,从而帮助我们发现错误。

18.9 super() 调用父类属性和方法

super():调用父类属性或方法。完整写法为 super(当前类名, self).属性 或 super(当前类名, self).方法();在 Python 3 以后版本中,调用父类的属性和方法只需使用 super().属性 或 super().方法名() 即可。

super() 代表父类的定义,不是父类对象。即 super().方法名 相当于 父类名.方法名。

定义子类时,如果重写 __init__ 方法后还想使用父类 __init__ 中定义的属性,必须在其构造函数中调用父类的构造函数。

关于构造函数:

  1. 子类不重写 __init__,实例化子类时,会自动调用父类定义的 __init__。
  2. 子类重写了 __init__ 时,实例化子类,就不会调用父类已经定义的 __init__。
  3. 如果重写了 __init__ 时,要使用父类的构造方法,可以使用 super 关键字 super().__init__(参数列表),也可以使用如下格式调用:父类名.__init__(self, 参数列表)。

调用格式如下:

父类名.__init__(self, 参数列表)
# 或
super().__init__(参数列表)
注意

构造函数中包含调用父类构造函数。根据需要,不是必须。

子类并不会自动调用父类的 __init__(),我们必须显式地调用它。

案例:Car 汽车类、GasolineCar 汽油车、ElectricCar 电动车。

class Car:
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    def run(self):
        print('I can run')

class GasolineCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)

    def run(self):
        print('I can run with gasoline')

class ElectricCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)
        # 电池属性
        self.battery = 70

    def run(self):
        print(f'I can run with electric,remain:{self.battery}')

bwm = GasolineCar('宝马', 'X5', '白色')
bwm.run()

tesla = ElectricCar('特斯拉', 'Model S', '红色')
tesla.run()

(图片未上传成功:Pasted-image-20251006135543)

18.10 MRO 属性或 MRO 方法:方法解析顺序

Python 支持多继承,如果父类中有相同名字的方法,在子类没有指定父类名时,解释器将从左向右按顺序搜索。

MRO(Method Resolution Order):方法解析顺序,我们可以通过类的属性 类名.__mro__ 或类的方法 类名.mro() 获得类的层次结构,方法解析顺序也是按照这个类的层次结构寻找的。

class Car(object):
    def __init__(self, brand, model, color):
        self.brand = brand
        self.model = model
        self.color = color

    def run(self):
        print('i can run')

class GasolineCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)

    def run(self):
        print('i can run with gasoline')

class ElectricCar(Car):
    def __init__(self, brand, model, color):
        super().__init__(brand, model, color)
        # 电池属性
        self.battery = 70

    def run(self):
        print(f'i can run with electric,remain:{self.battery}')

print("=" * 20 + "GasolineCar" + "=" * 20)
print(GasolineCar.__mro__)
print(GasolineCar.mro())
print("=" * 20 + "ElectricCar" + "=" * 20)
print(ElectricCar.__mro__)
print(ElectricCar.mro())

(图片未上传成功:Pasted-image-20251006140518)

说明

由 MRO 方法解析顺序可知:在类的继承中,当某个类创建的对象调用属性或方法时,先在自身类中查找,找到即直接使用、停止后续查找;未找到则继续向上一级继承的类查找,找到即使用;仍找不到就继续向上……直到 object 类。这就是 Python 类继承中的方法解析顺序。

小结

综上 object 类是所有类的基类(因为这个查找关系到 object 才终止)。

案例:多继承 MRO 测试。

class A:
    def aa(self):
        print("aa")

    def say(self):
        print("say AAA!")

class B:
    def bb(self):
        print("bb")

    def say(self):
        print("say BBB!")

class C(B, A):
    def cc(self):
        print("cc")

c = C()
# 打印类的层次结构
print(C.mro())
# 解释器寻找方法是"从左到右"的方式寻找,此时会执行 B 类中的 say()
c.say()

(图片未上传成功:Pasted-image-20251006140830)

18.11 object 根类

(图片未上传成功:Pasted-image-20251006140918)

object 类是所有类的父类,因此所有的类都有 object 类的属性和方法。我们显然有必要深入研究一下 object 类的结构。对于我们继续深入学习 Python 很有好处。

dir() 查看对象属性

为了深入学习对象,我们先学习内置函数 dir(),它可以让我们方便地看到指定对象所有的属性。

案例:查看对象所有属性以及和 object 进行比对。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say_age(self):
        print(f"{self.name} 的年龄是 {self.age}")

obj = object()
print(dir(obj))

s2 = Person("小龙女", 19)
print(dir(s2))

执行结果:

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'say_age']

从上面我们可以发现这样几个要点:

  1. Person 对象增加了六个属性:

    __dict__
    __module__
    __weakref__
    age
    name
    say_age
    
  2. object 的所有属性,Person 类作为 object 的子类,显然包含了所有的属性。

  3. 我们打印 age、name、say_age,发现 say_age 虽然是方法,实际上也是属性。只不过,这个属性的类型是 method 而已。

    age <class 'int'>
    name <class 'str'>
    say_age <class 'method'>
    

重写 __str__() 方法

  1. object 有一个 __str__() 方法,用于返回一个对于对象的描述,内置函数 str(对象),调用的就是 __str__()。
  2. __str__() 经常用于 print() 方法,帮助我们查看对象的信息。__str__() 可以重写。
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        """将对象转化成一个字符串描述,一般用于 print 方法"""
        return f"名字是:{self.name},年龄是 {self.age}"

p = Person("小龙女", 19)
print(p)

(图片未上传成功:Pasted-image-20251006141736)

19 面向对象三大特征——多态

19.1 什么是多态

多态指的是一类事物有多种形态。

定义:多态(polymorphism)是指调用同一个方法,但由于调用对象不同可能会产生不同的行为。

类比

现实生活中,同一个行为,具体实现会完全不同。

比如:同样是调用人「吃饭」的方法,中国人用筷子吃饭,英国人用刀叉吃饭,印度人用手吃饭。

(图片未上传成功:Pasted-image-20251006150242)

关于多态要注意以下 2 点:

  1. 多态是方法的多态,属性没有多态。
  2. 多态的存在有 2 个必要条件:
    • 继承
    • 方法重写
Tip

首先定义一个父类,其可能拥有多个子类对象。当我们调用一个公共方法时,传递的对象不同,则返回的结果不同。

多态:可以基于继承也可以不基于继承(可以借由模板方法等方式实现多态,但是继承是一种非常方便且严谨的实现多态的方式)。

好处:调用灵活,有了多态,更容易编写出通用的代码,做出通用的编程,以适应需求的不断变化!

19.2 多态原理图

(图片未上传成功:image-20210318122417602)

公共接口 service 就是多态的体现,随着传入水果对象的不同,能返回不同的结果。

19.3 多态代码实现

class Fruit(object):
    # 公共方法
    def makejuice(self):
        print('I can make juice')

class Apple(Fruit):
    def makejuice(self):
        print('I can make apple juice')

class Banana(Fruit):
    def makejuice(self):
        print('I can make banana juice')

class Orange(Fruit):
    def makejuice(self):
        print('I can make orange juice')

class Peach(Fruit):
    def makejuice(self):
        print('I can make peach juice')

apple = Apple()
banana = Banana()
orange = Orange()
peach = Peach()

for i in (apple, banana, orange, peach):
    i.makejuice()

(图片未上传成功:Pasted-image-20251006150818)

20 方法没有重载

20.1 重载与重写

重载(Overloading)与重写(Overriding,也叫覆盖)是面向对象编程中实现多态性的两种核心机制,二者都基于「同名方法实现不同逻辑」的设计思想,但适用场景、语法规则、执行阶段和设计目的有本质区别。

1. 重载(Overloading)

指在同一个作用域内(通常是同一个类,也可包含父子类继承场景),存在多个方法名相同,但参数列表不同的方法。

  • 参数列表不同:参数的类型、个数、顺序至少有一项不同。
  • 与返回值类型、访问修饰符、抛出的异常无关。
  • 属于编译期多态(静态多态):编译阶段就可以根据参数列表确定要调用的具体方法。

2. 重写(Overriding)

指在父子类继承关系(或接口与实现类)中,子类重新实现父类中已有的、可访问的方法。

  • 方法名、参数列表必须与父类完全一致。
  • 属于运行期多态(动态多态):程序运行时,根据对象的实际类型(而非引用类型)决定调用哪个版本的方法。
原文勘误

本节内容描述的是 Java / C++ 等静态语言的「重载 vs 重写」规则(如 public/protected、final、@Override、编译期分派)。Python 不支持语法级方法重载,也没有 private/final/@Override,阅读时请以此区分——Python 的实际行为见 §20.5。

20.2 核心区别对比

对比维度 重载(Overload) 重写(Override)
适用范围 同一个类中,或父子类继承的同一作用域下 必须存在父子类继承/接口实现关系
方法签名要求 方法名相同,参数列表必须不同 方法名、参数列表必须完全相同
返回值类型 无要求,不能作为重载的判断依据 必须与父类一致,或是父类返回值的子类(协变返回类型)
访问修饰符 无限制,可任意修改 子类方法的访问权限 不能比父类更严格(如父类 public,子类不能为 protected)
异常声明 无限制,可任意抛出 子类抛出的异常范围 不能大于父类(可抛出父类异常的子类,或不抛出)
多态类型 编译期多态(静态分派) 运行期多态(动态分派)
调用时机 编译时根据参数类型/个数确定调用版本 运行时根据对象的实际类型确定调用版本
核心目的 同名方法适配不同参数输入场景,提升易用性 子类定制化修改/扩展父类已有逻辑,实现行为差异化

20.3 二者的联系

  • 同属多态的实现方式:二者都是面向对象多态特性的体现,通过复用方法名简化代码、提升程序的灵活性与可读性。
  • 方法名复用:核心前提都是方法名相同,通过不同的维度区分方法版本。
  • 可以共存:子类可以同时对父类的某个方法进行重写,也可以新增同名不同参的方法形成重载,二者互不冲突。
  • 都遵循方法命名规则:都必须符合语言的方法命名规范,不能通过方法体差异来区分重载/重写。

20.4 常见易错点与注意事项

  • 只有返回值不同不构成重载
    方法签名不包含返回值,仅返回值不同的同名方法会直接编译报错,无法构成重载。
  • 参数列表不同的「重写」实际是重载
    如果子类定义了和父类同名但参数不同的方法,本质是子类新增了一个重载方法,而非重写,不具备运行期多态特性。
  • 这些方法不能被重写
    • private 私有方法:子类不可见,子类同名方法是全新方法,不是重写。
    • static 静态方法:子类同名静态方法属于「方法隐藏」,不是重写,不支持动态分派。
    • final 修饰的方法:语法层面禁止被重写。
  • @Override 注解的作用
    它是编译期检查注解,用于标记该方法是重写父类方法。如果不符合重写规则(参数不一致、方法名错误等),编译会直接报错,避免手写失误。

20.5 Python 没有重载

在其他语言中,可以定义多个重名的方法,只要保证方法签名唯一即可。方法签名包含 4 个部分:方法名、参数数量、参数类型和参数顺序。

Python 中,方法的参数没有声明类型(调用时确定参数的类型),参数的数量也可以由可变参数控制。因此,Python 中是没有方法的重载的。定义一个方法即可有多种调用方式,相当于实现了其他语言中的方法的重载。

如果我们在类体中定义了多个重名的方法,只有最后一个方法有效。

建议

不要使用重名的方法!Python 中方法没有重载。

案例:Python 中没有方法的重载。定义多个同名方法,只有最后一个有效。

class Person:
    def say_hi(self):
        print("hello")

    def say_hi(self, name):
        print(f"{name},hello")

if __name__ == '__main__':
    p1 = Person()
    p1.say_hi("小龙女")
    # 不带参,报错:
    # TypeError: Person.say_hi() missing 1 required positional argument: 'name'
    p1.say_hi()

(图片未上传成功:file-20260731133041135)

21 特殊方法和运算符重载

21.1 特殊方法

Python 的运算符实际上是通过调用对象的特殊方法实现的。比如:

a = 20
b = 30
c = a + b
d = a.__add__(b)

print("c =", c)
print("d =", d)

(图片未上传成功:file-20260731134304811)

常见的特殊方法统计如下:

方法 说明 例子
__init__ 构造方法 对象创建:p = Person()
__del__ 析构方法 对象回收
__repr__,__str__ 打印,转换 print(a)
__call__ 函数调用 a()
__getattr__ 点号运算 a.xxx
__setattr__ 属性赋值 a.xxx = value
__getitem__ 索引运算 a[key]
__setitem__ 索引赋值 a[key] = value
__len__ 长度 len(a)

每个运算符实际上都对应了相应的方法,统计如下:

运算符 特殊方法 说明
+ __add__ 加法
- __sub__ 减法
<,<=,== __lt__,__le__,__eq__ 比较运算符
>,>=,!= __gt__,__ge__,__ne__ 比较运算符
|,^,& __or__,__xor__,__and__ 或、异或、与
<<,>> __lshift__,__rshift__ 左移、右移
*,/,%,// __mul__,__truediv__,__mod__,__floordiv__ 乘、浮点除、模运算(取余)、整数除
** __pow__ 指数运算

我们可以重写上面的特殊方法,即实现了运算符的重载。

# 测试运算符的重载
class Person:
    def __init__(self, name):
        self.name = name

    # 重写加法
    def __add__(self, other):
        if isinstance(other, Person):
            return f"{self.name}--{other.name}"
        else:
            return "不是同类对象,不能相加"

    # 重写乘法
    def __mul__(self, other):
        if isinstance(other, Person):
            return f"{self.name} * {other.name}"
        else:
            return "不是同类对象,不能相乘"

p1 = Person("赵敏")
p2 = Person("周芷若")
print(p1 + p2)
print(p1 + 1)
print("=" * 40)

print(p1 * 3)
print(p1 * p2)

(图片未上传成功:Pasted-image-20251006152851)

思考

Python 中的运算符重载实际上是通过重写实现的吧?为什么不叫运算符的重写?

答
  1. 语法层面:运算符重载 = 重写父类魔法方法
    所有运算符对应的 __add__、__sub__ 都是object 基类自带的内置方法,你在自定义类里重新实现,本质就是方法重写(override)。
  2. 但术语不叫「运算符重写」,统一叫运算符重载(operator overloading),是行业历史命名 + 语义区分两层原因。

1. 历史术语来源:C++ 先定义了两套概念

最早区分 overload/override 是静态编译语言(C++/Java):

  • Overload(重载):同一作用域,同名函数,参数不同,适配多种调用形式

    int add(int a);
    int add(int a, int b); // 函数重载
    Complex operator+(Complex a, Complex b);  // 运算符重载
    

    C++ 里运算符重载本质是全局/类内同名运算符函数多版本,属于 overload 范畴,所以命名固化为 运算符重载。

  • Override(重写/覆盖):父子继承,子类改写父类已有虚函数。

Python 只是沿用了计算机通用术语体系,不会为动态语言单独造新词。

2. 语义区分:两个行为解决完全不同的问题

  1. 普通方法重写:面向继承多态
    目标:替换父类原有逻辑,遵循里氏替换。

    class Animal:
        def speak(self): ...
    
    class Dog(Animal):
        def speak(self): ...  # 重写 override
    

    关注点:继承、父子替换、运行时子类覆盖父类行为。

  2. 运算符重载:面向运算符多态

    目标:让 + - * [] == 这类内置符号,适配自定义对象。

    class Num:
        def __add__(self, other):  # 重写 object 的 __add__
            return Num(self.val + other.val)
    
    a + b  # 自动调用 a.__add__(b)
    

虽然底层是重写魔法方法,但业务语义是:给同一个运算符 + 增加一套新的运算规则。

  • 运算符 + 本身有原生实现(数字相加、字符串拼接);
  • 你新增了「自定义 Num 对象相加」的规则;
  • 同一个运算符,多种实现逻辑 → 完美贴合 overload(重载) 的字面含义:多套负载、多种实现。

如果叫「运算符重写」会产生歧义:

  1. 会让人误以为是覆盖 / 删除原有的 + 数字、字符串逻辑,但实际上只是新增一套对象分支,原有功能完全保留;
  2. 无法区分两种场景:
    • 场景 A:子类改父类普通方法 → 重写 override
    • 场景 B:给运算符新增自定义对象运算规则 → 运算符重载 overload

21.2 特殊属性

Python 对象中包含了很多双下划线开始和结束的属性,这些是特殊属性,有特殊用法。

这里我们列出常见的特殊属性:

特殊方法 含义
obj.__dict__ 对象的属性字典
obj.__class__ 对象所属的类
class.__bases__ 类的基类元组(多继承)
class.__base__ 类的基类
class.__mro__ 类层次结构
class.__subclasses__() 子类列表
from rich import print

# 测试特殊属性
class A:
    pass

class B:
    pass

class C(B, A):
    def __init__(self, nn):
        self.nn = nn

    def cc(self):
        print("cc")

class D(C, B, A):
    def __init__(self, nn, ndd):
        super().__init__(nn)
        self.ndd = ndd

    def dd(self):
        print("dd")

if __name__ == '__main__':
    d = D(3, 4)
    print(f"{'=' * 4}1. 对象的所有属性 dir(d){'=' * 4}")
    print(dir(d))
    print(f"\n{'=' * 4}2. 对象的属性字典 d.__dict__{'=' * 4}")
    print(d.__dict__)
    print(f"\n{'=' * 4}3. 对象所属的类 d.__class__{'=' * 4}")
    print(d.__class__)
    print(f"\n{'=' * 4}4. 类的基类元组(多继承) D.__bases__{'=' * 4}")
    print(D.__bases__)
    print(f"\n{'=' * 4}5. 类层次结构 D.__mro__{'=' * 4}")
    # print(D.mro())
    print(D.__mro__)
    print(f"\n{'=' * 4}6. 子类列表 A.__subclasses__(){'=' * 4}")
    print(A.__subclasses__())

运行结果:

====1. 对象的所有属性 dir(d)====
[
    '__class__',
    '__delattr__',
    '__dict__',
    '__dir__',
    '__doc__',
    '__eq__',
    '__format__',
    '__ge__',
    '__getattribute__',
    '__getstate__',
    '__gt__',
    '__hash__',
    '__init__',
    '__init_subclass__',
    '__le__',
    '__lt__',
    '__module__',
    '__ne__',
    '__new__',
    '__reduce__',
    '__reduce_ex__',
    '__repr__',
    '__setattr__',
    '__sizeof__',
    '__str__',
    '__subclasshook__',
    '__weakref__',
    'cc',
    'dd',
    'ndd',
    'nn'
]

====2. 对象的属性字典 d.__dict__====
{'nn': 3, 'ndd': 4}

====3. 对象所属的类 d.__class__====
<class '__main__.D'>

====4. 类的基类元组(多继承) D.__bases__====
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>)

====5. 类层次结构 D.__mro__====
(
    <class '__main__.D'>,
    <class '__main__.C'>,
    <class '__main__.B'>,
    <class '__main__.A'>,
    <class 'object'>
)

====6. 子类列表 A.__subclasses__()====
[<class '__main__.C'>, <class '__main__.D'>]

22 对象的浅拷贝和深拷贝

(图片未上传成功:Pasted-image-20250404084232)

22.1 变量的赋值操作

只是形成两个变量,实际还是指向同一个对象。

22.2 浅拷贝

Python 拷贝一般都是浅拷贝。拷贝时,对象包含的子对象内容不拷贝。因此,源对象和拷贝对象会引用同一个子对象。

22.3 深拷贝

使用 copy 模块的 deepcopy 函数,递归拷贝对象中包含的子对象。源对象和拷贝对象所有的子对象也不同。

# 测试对象的引用赋值、浅拷贝、深拷贝
import copy

class MobilePhone:
    def __init__(self, cpu, screen):
        self.cpu = cpu
        self.screen = screen

class CPU:
    def calculate(self):
        print("计算,算个 12345")
        print("CPU 对象:", self)

class Screen:
    def show(self):
        print("显示一个好看的画面,亮瞎你的钛合金大眼")
        print("屏幕对象:", self)

if __name__ == '__main__':
    c = CPU()
    s = Screen()
    m = MobilePhone(c, s)
    m.cpu.calculate()

    print(f"{'=' * 30}引用赋值测试{'=' * 30}")
    # 两个变量,但是指向了同一个对象
    n = m
    print('m, n:', m, n)

    print(f"{'=' * 30}浅拷贝测试{'=' * 30}")
    # m2 是新拷贝的另一个手机对象
    m2 = copy.copy(m)
    print('m, m2:', m, m2)
    m.cpu.calculate()
    # m2 和 m 拥有了一样的 cpu 对象和 screen 对象
    m2.cpu.calculate()

    print(f"{'=' * 30}深拷贝测试{'=' * 30}")
    m3 = copy.deepcopy(m)
    print('m, m3:', m, m3)
    m.cpu.calculate()
    # m3 和 m 拥有不一样的 cpu 对象和 screen 对象
    m3.cpu.calculate()

(图片未上传成功:Pasted-image-20251006160357)

内存分析:

(图片未上传成功:image-20220702162610275)

23 组合

(图片未上传成功:Pasted-image-20251006163325)

结婚就是组合。两人组合后,可以复用对方的属性和方法!

除了继承,组合也能实现代码的复用!组合核心是将其它类的对象作为本类的属性。

注意

is-a 关系,我们可以使用继承。从而实现子类拥有的父类的方法和属性。

is-a 关系指的是类似这样的关系:狗是动物,dog is animal。狗类就应该继承动物类。

has-a 关系,我们可以使用组合,也能实现一个类拥有另一个类的方法和属性。

has-a 关系指的是这样的关系:手机拥有 CPU,MobilePhone has a CPU。手机类拥有 CPU 类。

# 组合测试
class MobilePhone:
    def __init__(self, cpu, screen):
        self.cpu = cpu
        self.screen = screen

class CPU:
    def calculate(self):
        print("计算,算个 12345")

class Screen:
    def show(self):
        print("显示一个好看的画面,亮瞎你的钛合金大眼")

if __name__ == '__main__':
    c = CPU()
    s = Screen()
    m = MobilePhone(c, s)
    # 通过组合,我们也能调用 cpu 对象的方法。相当于手机对象间接拥有了 “cpu 的方法”
    m.cpu.calculate()
    m.screen.show()

(图片未上传成功:Pasted-image-20251006161157)

24 设计模式——工厂模式实现

(图片未上传成功:Pasted-image-20251006163559)

设计模式是面向对象语言特有的内容,是我们在面临某一类问题时固定的做法。设计模式有很多种,比较流行的是 GOF(Gang of Four)的 23 种设计模式。当然,没有必要全部学习,掌握几个常用的即可。

对于初学者,我们学习两个最常用的模式:工厂模式和单例模式。

常用设计模式

工厂模式实现了创建者和调用者的分离,使用专门的工厂类将选择实现类、创建对象进行统一的管理和控制。

(图片未上传成功:Pasted-image-20251006163610)

# 工厂模式

class CarFactory:
    def create_car(self, brand):
        if brand == "奔驰":
            return Benz()
        elif brand == "宝马":
            return BMW()
        elif brand == '比亚迪':
            return BYD()
        else:
            return "未知品牌,无法创建"

class Benz:
    def run(self):
        print(f"奔驰正在奔驰!!!")

class BMW:
    def run(self):
        print(f"宝马正在驰骋!!!")

class BYD:
    def run(self):
        print(f"比亚迪正在飞驰!!!")

if __name__ == '__main__':
    factory = CarFactory()

    c1 = factory.create_car("奔驰")
    c2 = factory.create_car("宝马")
    c3 = factory.create_car("比亚迪")
    c4 = factory.create_car("迈巴赫")

    print(c1)
    c1.run()
    print(c2)
    c2.run()
    print(c3)
    c3.run()
    print(c4)

(图片未上传成功:Pasted-image-20251006161838)

注意点

create_car() 里的 Benz、BMW、BYD 在函数调用时才被解析(Python 名称查找是运行时执行的),所以类定义写在工厂类后面也能正常创建对象——只要调用发生在所有类定义之后即可。

25 设计模式——单例模式

(图片未上传成功:Pasted-image-20251006163624)

单例模式是一种常见的设计模式!

单例模式(Singleton Pattern)的核心作用是确保一个类只有一个实例,并且提供一个访问该实例的全局访问点。

单例模式只生成一个实例对象,减少了对系统资源的开销。当一个对象的产生需要比较多的资源(如读取配置文件、产生其他依赖对象)时,可以产生一个单例对象并永久驻留内存中,从而极大地降低开销。

(图片未上传成功:image-20210318162425553)

应用场景:音乐播放器对象、回收站对象、打印机对象。

注意

单例模式有多种实现的方式,我们这里推荐重写 __new__() 的方法。

25.1 __new__ 方法

在 Python 中,我们目前已经学了几个魔术方法了,分别是 __init__、__str__、__del__,接下来介绍一下 __new__ 方法。

使用 类名() 创建对象时,Python 的解释器首先会调用 __new__ 方法为对象分配空间。

__new__ 是一个由 object 基类提供的内置的静态方法,主要作用有两个:

  1. 在内存中为对象分配空间。
  2. 返回对象的引用。

Python 解析器获得对象的引用后,将引用作为第一个参数,传递给 __init__ 方法。

重写 __new__ 方法的代码非常固定,一定要在函数最后写上 return super().__new__(cls) 语句,否则 Python 解释器得不到分配了空间的对象引用,就不会调用对象的初始化方法。

注意

__new__ 方法是一个静态方法,在调用时,要求将自身类信息 cls 作为参数传递到这个方法中,这个方法属于 object 类中的一个静态方法。

案例:没有使用单例模式,每次实例化对象时,都需要重新创建,开销较大。

# ==========未使用单例模式==========
# 定义一个播放器类
class MusicPlayer(object):
    # 重写 __new__() 魔术方法
    def __new__(cls, *args, **kwargs):
        print('1、开辟内存空间')
        print('2、返回实例化对象引用地址')
        return super().__new__(cls)

    def __init__(self, name):
        self.name = name

# 1. 实例 mp1 对象
mp1 = MusicPlayer('红色的高跟鞋')
print(mp1)

# 2. 实例化 mp2 对象
mp2 = MusicPlayer('春夏秋冬')
print(mp2)

(图片未上传成功:Pasted-image-20251006162607)

25.2 单例模式的代码实现

# ==========使用单例模式==========
class MySingleton:
    _obj = None
    # 是否已经初始化
    _init_flag = False

    def __new__(cls, *args, **kwargs):
        if cls._obj is None:
            print("正在开辟内存空间……")
            cls._obj = super().__new__(cls)
        return cls._obj

    def __init__(self, name):
        # 判断是否需要进行初始化操作
        if MySingleton._init_flag:
            return

        print("init....")
        self.name = name
        MySingleton._init_flag = True

if __name__ == '__main__':
    a = MySingleton("红色的高跟鞋")
    print(a)
    print(f"a.name:{a.name}")
    b = MySingleton("春夏秋冬")
    print(b)
    print(f"b.name:{b.name}")
注

类属性在内存中是一个特殊的存在,其不同于以前讲过的局部变量(局部变量当函数执行完毕后,其会被内存所销毁)。但是类属性一旦定义,除非对象以及这个类在内存中被销毁了,否则其不会自动销毁。

(图片未上传成功:Pasted-image-20251006163808)

26 工厂和单例模式结合

(图片未上传成功:Pasted-image-20251006163925)

设计模式称之为“模式”,就是一些固定的套路。我们很容易用到其他场景上,比如前面讲的工厂模式,我们需要将工厂类定义成“单例”,只需要简单的套用即可实现:

# 测试工厂模式和单例模式的整合使用
class CarFactory:
    _obj = None
    _init_flag = False

    def __new__(cls):
        if cls._obj is None:
            print('1、开辟内存空间')
            print('2、返回实例化对象引用地址')
            CarFactory._obj = super().__new__(cls)

        return CarFactory._obj

    def __init__(self):
        if CarFactory._init_flag:
            return

        print("初始化工厂……")
        CarFactory._init_flag = True

    def create_car(self, brand):
        if brand == "奔驰":
            return Benz()
        elif brand == "宝马":
            return BMW()
        elif brand == '比亚迪':
            return BYD()
        else:
            return "未知品牌,无法创建"

class Benz:
    def run(self):
        print(f"奔驰正在奔驰!!!")

class BMW:
    def run(self):
        print(f"宝马正在驰骋!!!")

class BYD:
    def run(self):
        print(f"比亚迪正在飞驰!!!")

if __name__ == '__main__':
    factory = CarFactory()
    c1 = factory.create_car("奔驰")
    c2 = factory.create_car("比亚迪")
    c1.run()
    c2.run()

    factory2 = CarFactory()
    print(factory)
    print(factory2)
    print(factory2 is factory)

(图片未上传成功:Pasted-image-20251006164838)

27 工厂模式和单例模式练习

27.1 练习一

定义发动机类 Motor、底盘类 Chassis、座椅类 Seat,车辆外壳类 Shell,并使用组合关系定义汽车类。其他要求如下:

  • 定义汽车的 run() 方法,里面需要调用 Motor 类的 work() 方法,也需要调用座椅类 Seat 的 work() 方法,也需要调用底盘类 Chassis 的 work() 方法。
class Motor:
    def work(self):
        print("发动机正在工作……")

class Chassis:
    def work(self):
        print("底盘正在工作……")

class Seat:
    def work(self):
        print("座椅正在工作……")

class Shell:
    pass

class Car:
    def __init__(self, motor, chassis, seat, shell):
        self.motor = motor
        self.chassis = chassis
        self.seat = seat
        self.shell = shell

    def run(self):
        print("汽车开始运行……")
        self.motor.work()
        self.chassis.work()
        self.seat.work()

if __name__ == '__main__':
    motor = Motor()
    chassis = Chassis()
    seat = Seat()
    shell = Shell()

    car = Car(motor, chassis, seat, shell)
    car.run()

(图片未上传成功:Pasted-image-20251006172459)

27.2 练习二

使用工厂模式、单例模式实现如下需求:

  1. 电脑工厂类 ComputerFactory 用于生产电脑 Computer。工厂类使用单例模式,也就是说只能有一个工厂对象。
  2. 工厂类中可以生产各种品牌的电脑:联想、华硕、神舟。
  3. 各种品牌的电脑使用继承实现。
  4. 父类是 Computer 类,定义了 calculate 方法
  5. 各品牌电脑类需要重写父类的 calculate 方法。
class ComputerFactory:
    _obj = None
    _init_flag = False
    _id = 1000

    def __new__(cls):
        if ComputerFactory._obj is None:
            print("开辟内存空间……")
            ComputerFactory._obj = super().__new__(cls)

        return ComputerFactory._obj

    def __init__(self):
        if ComputerFactory._init_flag:
            return

        print("初始化工厂……")
        ComputerFactory._init_flag = True

    def create_computer(self, brand):
        if brand == "联想":
            ComputerFactory._id += 1
            return Lenovo(ComputerFactory._id)
        elif brand == "华硕":
            ComputerFactory._id += 1
            return ASUS(ComputerFactory._id)
        elif brand == "神舟":
            ComputerFactory._id += 1
            return Hasee(ComputerFactory._id)
        else:
            print("未知品牌,无法创建")
            return None

class Computer:
    def __init__(self, computer_id):
        self.computer_id = computer_id

    def calculate(self):
        print(f"计算机 {self.computer_id} 正在计算……")

class Lenovo(Computer):
    def calculate(self):
        print(f"联想计算机 {self.computer_id} 正在计算……")

class ASUS(Computer):
    def calculate(self):
        print(f"华硕计算机 {self.computer_id} 正在计算……")

class Hasee(Computer):
    def calculate(self):
        print(f"神舟计算机 {self.computer_id} 正在计算……")

if __name__ == '__main__':
    factory = ComputerFactory()
    lenovo = factory.create_computer("联想")
    asus = factory.create_computer("华硕")
    hasee = factory.create_computer("神舟")
    lenovo.calculate()
    asus.calculate()
    hasee.calculate()
    print("=" * 40)

    factory1 = ComputerFactory()
    print(factory)
    print(factory1)
    print(factory1 is factory)

(图片未上传成功:Pasted-image-20251006174513)

27.3 练习三

定义一个 Employee 雇员类,要求如下:

  1. 属性有:id、name、salary。
  2. 运算符重载 +:实现两个对象相加时,默认返回他们的薪水和。
  3. 构造方法要求:输入 name、salary,不输入 id。id 采用自增的方式,从 1000 开始自增,第一个新增对象是 1001,第二个新增对象是 1002。
  4. 根据 salary 属性,使用 @property 设置属性的 get 和 set 方法。set 方法要求输入:1000~50000 范围的数字。
class Employee:
    _id = 1000

    def __init__(self, name, salary):
        Employee._id += 1
        self.id = Employee._id
        self.name = name
        self._salary = salary

    @property
    def salary(self):
        return self._salary

    @salary.setter
    def salary(self, salary):
        if 1000 <= salary <= 50000:
            self._salary = salary
        else:
            raise ValueError("salary 必须为 1000~50000 范围的数字")

    def __add__(self, other):
        if isinstance(other, Employee):
            return self.salary + other.salary
        else:
            raise TypeError("类型不同不能相加")

    def __str__(self):
        return f"{self.name} 的 id 为 {self.id},薪水为 {self.salary}。"

if __name__ == '__main__':
    emp1 = Employee("小龙女", 20000)
    emp2 = Employee("赵敏", 15000)
    print(emp1)
    print(emp2)
    print(f"emp1 + emp2:{emp1 + emp2}")
    # print(f"emp1 + 1:{emp1 + 1}")

    emp1.salary = 50000
    print(emp1.salary)
    # emp1.salary = 500000
    # print(emp1.salary)

(图片未上传成功:Pasted-image-20251006175820)

(图片未上传成功:Pasted-image-20251006175956)


28 术语表(重要英文单词)

28.1 面向对象基础

术语 全拼/说明 释义
OOP Object-Oriented Programming 面向对象编程,以「对象」为基本单元组织代码的范式
Class — 类,描述同一组事务共有特性的抽象模型,是创建对象的模板
Object / Instance — 对象 / 实例,类被实例化后得到的具体实体
Encapsulation — 封装,把属性和方法打包并隐藏内部实现,只暴露接口
Inheritance — 继承,子类获得父类成员,实现代码复用
Polymorphism — 多态,同一行为作用于不同对象产生不同结果
Attribute — 属性,对象或类所携带的数据
Method — 方法,定义在类中的函数,本质也是属性
self — 实例方法第一个参数,指当前实例对象,由解释器自动传入
cls class 类方法第一个参数,指当前类对象

28.2 魔术方法与描述符协议

术语 全拼/说明 释义
Magic Method / Dunder double underscore 双下划线包裹的特殊方法,如 __init__,由解释器在特定时机自动调用
__init__ initialization 初始化方法(构造函数),对象创建后为其赋属性
__new__ — 为对象分配内存并返回引用的静态方法,先于 __init__ 执行
__del__ destructor 析构方法,对象被 del 或引用计数归零时触发
__str__ — 返回对象的字符串描述,print() / str() 时调用
__call__ — 使实例成为可调用对象,对象名() 时触发
Descriptor Protocol — 描述符协议,实现 __get__/__set__/__delete__ 中任一即构成描述符
Data Descriptor — 数据描述符,实现了 __set__ 或 __delete__,读取优先级高于实例字典
Non-data Descriptor — 非数据描述符,仅实现 __get__,优先级低于实例字典
__get__ — 读取属性时触发,签名 __get__(self, instance, owner)
__set__ — 对实例赋值时触发,类对象赋值不触发
__delete__ — 对实例删除时触发
__getattribute__ — 属性查找唯一入口,内嵌描述符调度逻辑,所有属性访问必先经过
__getattr__ — 兜底回调,仅当标准查找全部失败、抛 AttributeError 前才尝试调用
Name Mangling — 名字重整,self.__name 被改写为 _类名__name

28.3 方法类型与继承机制

术语 全拼/说明 释义
Instance Method — 实例方法,第一个参数为 self,实例访问时生成绑定方法
Class Method — 类方法,@classmethod 装饰,第一个参数为 cls,永远绑定到类
Static Method — 静态方法,@staticmethod 装饰,不绑定任何对象,直接返回原函数
Bound Method — 绑定方法(types.MethodType),封装 __func__ 与 __self__,自动注入首个参数
MRO Method Resolution Order 方法解析顺序,多继承下「从左到右」的查找链,可用 类.__mro__ 查看
Override — 重写/覆盖,子类重新定义父类同名同参方法,属运行期多态
Overload — 重载,同作用域内同名不同参方法;Python 语法层面不支持
super() — 代表父类的定义,用于调用父类方法,Python 3 可省略参数
is-a — 「是一个」关系,用继承表达,如「狗是动物」
has-a — 「有一个」关系,用组合表达,如「手机拥有 CPU」

28.4 设计模式与拷贝

术语 全拼/说明 释义
Design Pattern — 设计模式,面对某类问题时的固定解决方案
GOF Gang of Four 《设计模式》四位作者,提出 23 种经典设计模式
Factory Pattern — 工厂模式,用专门的工厂类统一管理选择实现类与创建对象,实现创建者与调用者分离
Singleton Pattern — 单例模式,确保一个类只有一个实例并提供全局访问点
Shallow Copy — 浅拷贝(copy.copy),不拷贝子对象,源与副本共享同一子对象
Deep Copy — 深拷贝(copy.deepcopy),递归拷贝子对象,源与副本完全独立
Composition — 组合,把其它类的对象作为本类属性以复用其功能

29 系列导航

Python 基础系列
  • 1.Python 入门
  • 2.编程基础概念
  • 3.Python 中的基础数据类型
  • 4.字符串
  • 5.内建数据结构
  • 6.控制语句
  • 7.函数用法
  • 8.面向对象编程(本篇)
  • 9.异常和错误
  • 10.文件操作(IO 技术)
  • 11.模块 (module)
  • 12.正则表达式 ★★★
  • 13.并发编程
  • 14.网络编程
  • 15.Python操作MySQL

30 参考链接

分类:Python

posted @ 2026-04-11 02:00  挖掘鱼  阅读(34)  评论(0)    收藏  举报