1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
  23. Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
  24. Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
  25. Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
  26. Python 系列(二十六) Python 模块入门,从 import 到模块复用
  27. Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
  28. Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
  29. Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
  30. Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
  31. Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
  32. Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承
  33. Python 系列(三十三) 什么是类和对象?零基础理解 Python class
  34. Python 系列(三十四) Python 属性详解,搞懂类属性、实例属性和 setter
  35. Python 系列(三十五) 学会特殊方法,让对象打印出你想要的内容
  36. Python 系列(三十六) 多个对象如何共享数据?彻底搞懂类变量
  37. Python 系列(三十七) 搞懂父类、子类和方法重写,真正理解继承与多态
  38. Python 系列(三十八) 如何判断数据类型?三个内置函数一次讲明白
  39. Python 系列(三十九) 搞懂异常模型与常见异常类型,读懂程序报错
  40. Python 系列(四十) 从零学会异常处理,掌握 try、except、else 和 finally
  41. Python 系列(四十一) ValueError、TypeError 到底是什么?一文搞懂常见异常
  42. Python 系列(四十二) ValueError、TypeError 到底是什么?一文搞懂常见异常

上一篇我们学习了 Python 中的类与对象,知道了如何使用 class 定义一个类,如何通过 __init__() 初始化对象,以及 self 如何帮助我们操作对象自身的数据。

不过,学会创建对象只是第一步。

假设我们要编写一个程序,用来管理学生信息、服务器配置或者几何图形。对象中的数据不仅需要读取和修改,有时还需要进行检查,甚至根据已有数据计算出新的结果。

例如:

  • 学生的年龄不能是负数。
  • 圆形的半径必须大于 0。
  • 一个班级中的学生可以有各自的姓名和年龄,但班级名称可能是大家共享的。
  • 圆形的面积可以根据半径计算出来,不一定需要单独保存。

这些问题涉及 Python 中的属性。

本文将从实例属性与类属性的区别开始,逐步学习 @property、属性初始化、数据检查和异常捕获。我们会使用完整的代码进行演示,让每个知识点都有实际的运行结果。

从一个实际问题开始

先回顾一下上一篇学习过的类与对象。

假设我们要创建一个圆形对象,用来保存圆形的半径。

新建一个名为 circle.py 的文件,写入以下代码:

1
2
3
4
5
6
7
8
class Circle:
def __init__(self, radius):
self.radius = radius


circle = Circle(5)

print(circle.radius)

在终端中执行:

1
python circle.py

运行结果:

1
5

这里,self.radius = radius 将传入的半径保存到对象中,circle.radius 则用于读取这个值。

现在,我们尝试修改半径:

1
2
3
circle.radius = -10

print(circle.radius)

运行结果:

1
-10

程序没有报错,半径也确实被修改了。

但是,圆形的半径不应该是负数。程序虽然能够保存数据,却没有检查数据是否合理。

如果我们希望修改半径时自动检查数据,就需要一种更灵活的属性访问方式。

在了解 @property 之前,我们先把属性本身弄清楚。

认识实例属性

实例属性就是保存在某个对象中的数据,用来描述这个对象自身的状态。

例如,学生对象可以保存姓名和年龄,服务器对象可以保存主机名和 IP 地址,圆形对象可以保存半径。

来看一个学生的例子:

1
2
3
4
5
6
7
8
9
10
11
class Student:
def __init__(self, name, age):
self.name = name
self.age = age


student1 = Student("小明", 18)
student2 = Student("小红", 19)

print(student1.name)
print(student2.name)

运行结果:

1
2
小明
小红

虽然 student1 和 student2 都是由 Student 类创建的对象,但它们分别保存自己的姓名和年龄。

我们可以继续修改其中一个对象:

1
2
3
4
student1.age = 20

print(student1.age)
print(student2.age)

运行结果:

1
2
20
19

为什么修改 student1.age 不会影响 student2.age?

因为 age 是实例属性,每个对象都有属于自己的属性值。

可以这样理解:

  • Student 是描述学生的类。
  • student1 和 student2 是两个不同的学生对象。
  • student1.age 和 student2.age 分别保存两个对象自己的年龄。

这也是实例属性最重要的特点:不同对象可以拥有不同的实例属性值。

认识类属性

现在换一个需求。

假设这些学生都属于同一个班级,班级名称都是“Python 入门班”。

如果每创建一个学生对象,都单独保存一次班级名称,虽然能够实现,但可能存在重复。

有没有办法让所有学生对象共享同一个班级名称?

这时候就可以考虑使用类属性。

类属性是在类的定义中、方法之外声明的属性。它属于类,可以通过类名直接访问。

修改 Student 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student:
class_name = "Python 入门班"

def __init__(self, name, age):
self.name = name
self.age = age


student1 = Student("小明", 18)
student2 = Student("小红", 19)

print(Student.class_name)
print(student1.class_name)
print(student2.class_name)

运行结果:

1
2
3
Python 入门班
Python 入门班
Python 入门班

这里的:

1
class_name = "Python 入门班"

就是类属性。

它定义在 Student 类中,而不是定义在 __init__() 方法中。

我们可以通过 Student.class_name 直接读取它,也可以通过实例 student1.class_name 读取它。

当实例本身没有同名属性时,Python 会继续从类中查找这个属性,因此能够读取到类属性。

实例属性和类属性有什么区别

把两个概念放在一起比较,就更容易理解了。

对比项目实例属性类属性
定义位置通常在 __init__() 等实例方法中通过 self 赋值在类中、方法之外定义
归属某个具体对象类本身
常见访问方式student1.nameStudent.class_name
是否适合保存每个对象不同的数据适合通常不适合
是否适合表示对象共享的信息通常不适合适合

需要注意,类属性并不是只能通过类名访问。实例在没有同名实例属性的情况下,也可以读取类属性。

不过,读取和赋值的行为并不完全相同。

修改类属性时要注意什么

先看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student:
class_name = "Python 入门班"

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


student1 = Student("小明")
student2 = Student("小红")

Student.class_name = "Python 进阶班"

print(student1.class_name)
print(student2.class_name)

运行结果:

1
2
Python 进阶班
Python 进阶班

我们通过 Student.class_name 修改了类属性。由于两个对象都没有自己的同名实例属性,因此读取时都能看到修改后的类属性值。

但如果这样操作:

1
2
3
4
5
student1.class_name = "Python 运维班"

print(student1.class_name)
print(student2.class_name)
print(Student.class_name)

运行结果:

1
2
3
Python 运维班
Python 进阶班
Python 进阶班

为什么会出现这种结果?

因为:

1
student1.class_name = "Python 运维班"

通常会为 student1 创建一个同名的实例属性,而不是修改 Student 中的类属性。

此后,读取 student1.class_name 时,Python 会优先读取这个实例自己的属性;读取 student2.class_name 时,则仍然能找到类属性。

可以用下面这张图理解属性查找过程:

flowchart TD
    A["读取 student1.class_name"] --> B{"实例中有 class_name 吗?"}
    B -->|有| C["返回实例属性"]
    B -->|没有| D["查找 Student 类属性"]
    D --> E{"类中有 class_name 吗?"}
    E -->|有| F["返回类属性"]
    E -->|没有| G["继续查找或抛出 AttributeError"]

因此,选择实例属性还是类属性,取决于数据应该属于谁。

  • 学生姓名、年龄:每个学生不同,使用实例属性。
  • 班级名称、学校名称等共享信息:可以考虑使用类属性。

实际项目中,类属性也可以保存常量或所有实例共享的配置。不过,如果属性保存的是可变对象,例如列表,多个对象可能会共享同一个列表,修改时要格外小心。

使用下划线约定表示内部属性

回到圆形的例子。

我们可以把原来的 radius 改成 _radius:

1
2
3
4
5
6
7
8
class Circle:
def __init__(self, radius):
self._radius = radius


circle = Circle(5)

print(circle._radius)

运行结果:

1
5

这里出现了一个新的写法:_radius。

在 Python 中,属性名开头的单个下划线 _ 是一种命名约定,通常表示这个属性主要供类的内部使用,不建议外部代码直接操作。

但要注意,单个下划线并不会真正禁止外部访问。

下面的代码仍然能够运行:

1
2
3
circle._radius = -10

print(circle._radius)

运行结果:

1
-10

所以,单纯给属性名前面加上下划线,并不能自动检查或限制数据。

它更多是在提醒其他开发者:这个属性属于类的内部实现,通常应该通过类提供的接口来操作。

那么,如何让外部通过一个统一的接口读取数据,并在修改数据时执行检查呢?

这就需要用到 @property。

认识 @property 装饰器

先看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
class Circle:
def __init__(self, radius):
self._radius = radius

@property
def radius(self):
return self._radius


circle = Circle(5)

print(circle.radius)

运行结果:

1
5

和之前的代码相比,这里多了两部分:

1
2
3
@property
def radius(self):
return self._radius

先理解最下面的方法:

1
2
def radius(self):
return self._radius

它定义了一个名为 radius 的方法。调用时,这个方法会返回当前对象的 _radius。

如果没有 @property,普通方法通常需要这样调用:

1
circle.radius()

但加上 @property 后,我们可以这样访问:

1
circle.radius

不需要括号。

这是因为 @property 将这个方法变成了类似普通属性的访问接口。

当执行:

1
print(circle.radius)

Python 会调用对应的 getter 方法,并将返回值交给 print() 输出。

这里的 getter 可以理解为“获取数据的方法”。

装饰器是什么

我们刚刚使用的 @property,就是 Python 中的一个内置装饰器。

初学阶段,可以先把装饰器理解为一种能够为函数或方法增加特定行为的机制。

装饰器通常写在函数或方法定义的上一行:

1
2
3
@property
def radius(self):
return self._radius

其中,@property 会让 radius 可以通过类似属性的方式访问。

现在不需要深入装饰器的底层实现。先记住它的基本作用,以及 @property 解决了什么问题即可。

理解属性初始化为什么会调用 setter

在学习 setter 之前,先解决一个非常容易让初学者困惑的问题。

前面我们学习过:

1
2
3
class Circle:
def __init__(self, radius):
self._radius = radius

这里的 __init__() 是初始化方法。当我们执行 Circle(5) 创建对象时,传入的 5 会成为 radius 参数的值,然后通过 self._radius = radius 保存到对象中。

但如果我们希望初始化时也检查半径是否合法,就需要让初始化过程经过检查逻辑。

先理解对象创建与初始化之间的关系:

flowchart TD
    A["执行 Circle(5)"] --> B["Python 创建对象"]
    B --> C["调用 __init__(self, 5)"]
    C --> D["初始化对象的数据"]
    D --> E["返回可使用的对象引用"]

这里需要区分两个概念:

  • 创建对象:Python 为新对象建立实例。
  • 初始化对象:执行 __init__(),为这个实例设置初始状态。

日常交流中,我们经常把它们合称为“创建对象”,但它们并不是完全相同的步骤。

直接初始化内部属性

下面这种写法:

1
2
3
class Circle:
def __init__(self, radius):
self._radius = radius

会直接把传入的值保存到 _radius 中。

例如:

1
2
3
circle = Circle(-5)

print(circle._radius)

运行结果:

1
-5

因为代码没有执行任何检查,所以负数也被保存了下来。

通过属性接口初始化

现在,我们改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle:
def __init__(self, radius):
self.radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("半径必须大于 0")

self._radius = value

注意初始化方法中的这一行:

1
self.radius = radius

它与 self._radius = radius 不同。

因为类中已经定义了 radius 的 property 和 setter,所以通过 self.radius = radius 赋值时,会进入 setter 执行对应的逻辑。

这意味着,对象初始化时传入的值也会经过检查。

例如:

1
2
3
circle = Circle(5)

print(circle.radius)

运行结果:

1
5

而下面的代码:

1
circle = Circle(-5)

会抛出 ValueError,提示半径必须大于 0。

这就是为什么,在希望初始化和后续修改使用同一套验证规则时,通常会在 __init__() 中写:

1
self.radius = radius

而不是直接写:

1
self._radius = radius

前提是这个类确实已经定义了对应的 property 和 setter。

使用 setter 检查修改的数据

现在,我们正式认识 @radius.setter。

@property 负责定义属性的读取逻辑,而 @radius.setter 则可以为同一个属性定义赋值逻辑。

完整示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Circle:
def __init__(self, radius):
self.radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("半径必须大于 0")

self._radius = value


circle = Circle(5)

print("初始半径:", circle.radius)

circle.radius = 8

print("修改后的半径:", circle.radius)

运行结果:

1
2
初始半径: 5
修改后的半径: 8

我们逐步理解这段代码。

当执行:

1
circle = Circle(5)

Python 调用 __init__(),其中的 self.radius = radius 进入 setter。

在 setter 中:

1
2
if value <= 0:
raise ValueError("半径必须大于 0")

用于检查新值是否符合要求。

如果 value 小于或等于 0,就主动抛出异常,后面的赋值语句不会执行。

如果检查通过,就执行:

1
self._radius = value

把新半径保存到内部属性中。

当我们再次执行:

1
circle.radius = 8

也会进入 setter。因此,无论是初始化还是后续修改,都可以使用同一套检查规则。

为什么 setter 中不能再次写 self.radius

初学者还容易犯一个错误:在 setter 中再次给 self.radius 赋值。

例如,错误的写法是:

1
2
3
@radius.setter
def radius(self, value):
self.radius = value

因为 self.radius = value 会再次调用同一个 setter,setter 又执行 self.radius = value,于是不断重复调用,最终导致递归深度错误。

正确的做法是将实际数据保存在另一个属性中:

1
2
3
4
5
6
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("半径必须大于 0")

self._radius = value

这里,radius 是对外提供的属性接口,而 _radius 用来保存实际的半径值。

理解这个区别,才能正确使用 property 和 setter。

使用异常捕获,让程序更健壮

前面我们使用了:

1
raise ValueError("半径必须大于 0")

raise 的作用是主动抛出异常。

异常可以理解为程序执行过程中出现的一种错误情况。例如,用户输入了不合理的数据,或者程序无法完成某个操作。

但是,如果异常一直向外传播而没有被处理,程序就可能中断。

在实际应用中,我们通常希望提示用户错误原因,并允许用户重新输入,而不是让整个程序直接结束。

这就需要学习 try 和 except。

认识 try 和 except

先看一个简单的例子:

1
2
3
4
5
try:
number = int("hello")
print(number)
except ValueError:
print("输入内容无法转换为整数")

运行结果:

1
输入内容无法转换为整数

这里发生了什么?

int("hello") 试图将字符串 "hello" 转换为整数,但这个字符串不是合法的整数表示,因此 Python 抛出了 ValueError。

由于这行代码位于 try 代码块中,程序会寻找能够处理这个异常的 except ValueError。

找到后,执行:

1
print("输入内容无法转换为整数")

程序就可以继续执行异常处理逻辑,而不必因为这个错误直接退出。

可以先记住:

  • try:放置可能发生异常的代码。
  • except:指定遇到某类异常时如何处理。
  • raise:主动抛出异常。

raise 和 except 的作用不同:一个负责报告错误,另一个负责处理错误。

实战练习:让用户输入合法的圆形半径

现在,我们把属性、setter 和异常捕获组合起来,编写一个可以交互的程序。

程序需要完成以下任务:

  • 让用户输入圆形半径。
  • 检查输入是否为数字。
  • 检查半径是否大于 0。
  • 如果输入不合法,提示错误原因。
  • 如果输入合法,显示半径和圆形面积。

请将下面的代码保存为 circle.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import math


class Circle:
def __init__(self, radius):
self.radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if not isinstance(value, (int, float)):
raise TypeError("半径必须是数字")

if value <= 0:
raise ValueError("半径必须大于 0")

self._radius = value

@property
def area(self):
return math.pi * self._radius ** 2


try:
radius = float(input("请输入圆形的半径:"))

circle = Circle(radius)

print("圆形半径:", circle.radius)
print("圆形面积:", round(circle.area, 2))

except ValueError as error:
print("输入错误:", error)

except TypeError as error:
print("数据类型错误:", error)

运行程序:

1
python circle.py

当输入:

1
请输入圆形的半径:5

运行结果:

1
2
圆形半径: 5.0
圆形面积: 78.54

这里的 round(circle.area, 2) 表示将面积结果四舍五入到小数点后两位,方便阅读。

测试不同的输入

输入正常的半径

输入 5,程序计算面积并正常结束。

输入负数

输入 -3,程序会提示:

1
输入错误: 半径必须大于 0

这是因为 Circle(-3.0) 初始化对象时执行 self.radius = radius,调用 setter 后触发了 ValueError。

输入非数字内容

输入 hello,程序会提示类似下面的信息:

1
输入错误: could not convert string to float: 'hello'

这次异常不是 setter 抛出的,而是:

1
float(input("请输入圆形的半径:"))

在将字符串转换成浮点数时发生的。

这个例子非常值得理解:同一个 try 代码块中,可能出现不同位置、不同原因的异常。只要异常类型能够匹配相应的 except,就可以进行处理。

在这里,ValueError 既可能来自 float() 的转换失败,也可能来自 setter 中对半径范围的检查。

为什么要捕获具体的异常类型

我们分别使用了:

1
2
3
4
5
except ValueError as error:
print("输入错误:", error)

except TypeError as error:
print("数据类型错误:", error)

as error 表示将捕获到的异常对象保存到变量 error 中,这样就可以读取异常信息。

在这个示例中,由于 float() 已经将用户输入转换成数字,因此输入 hello 时会在转换阶段产生 ValueError,程序不会执行到创建 Circle 对象的步骤。

另外,当前程序只要求用户输入一次。如果输入不合法,程序会显示错误信息并结束。后续学习循环时,我们可以进一步改进它,让用户能够反复输入,直到输入正确为止。

使用 @property 计算属性值

@property 不仅可以控制属性的读取和修改,还可以用于计算一个值。

例如,圆形的面积可以根据半径计算。我们不一定要在创建对象时单独保存面积,因为只要知道半径,就可以得到面积。

圆形面积的计算公式是:

S = π × r2
S 表示面积,π 是圆周率,r 表示半径

这里使用 HTML 的 <sup> 标签显示平方,不依赖 KaTeX 或 Markdown 数学公式。

下面把计算逻辑加入类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import math


class Circle:
def __init__(self, radius):
self.radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if not isinstance(value, (int, float)):
raise TypeError("半径必须是数字")

if value <= 0:
raise ValueError("半径必须大于 0")

self._radius = value

@property
def area(self):
return math.pi * self._radius ** 2


circle = Circle(5)

print("半径:", circle.radius)
print("面积:", round(circle.area, 2))

运行结果:

1
2
半径: 5
面积: 78.54

这里出现了两个新知识点。

import math 用于导入 Python 内置的数学模块。通过 math.pi,我们可以使用圆周率的近似值。

self._radius ** 2 中的 ** 是乘方运算符,表示半径的平方。

再看下面这段代码:

1
2
3
@property
def area(self):
return math.pi * self._radius ** 2

这里定义了一个名为 area 的计算属性。读取 circle.area 时,Python 会执行方法中的计算逻辑,并返回结果。

因此,我们写的是:

1
print(circle.area)

而不是:

1
print(circle.area())

由于面积由当前半径计算得到,如果半径发生变化,再次读取面积时就会得到新的结果。

例如:

1
2
3
circle.radius = 10

print(round(circle.area, 2))

运行结果:

1
314.16

这就是计算属性的用途:外部代码可以像读取普通属性一样获取结果,而类内部负责完成计算。

当前示例中的 area 只有 getter,没有 setter,所以不能直接通过 circle.area = ... 来设置面积。

用一张图理解属性访问

前面涉及了实例属性、类属性、读取、赋值、检查和计算。我们可以把圆形对象的属性访问过程整理成一张图:

flowchart TD
    A["访问 circle.radius"] --> B["调用 getter"]
    B --> C["读取 self._radius"]
    C --> D["返回半径"]

    E["设置 circle.radius"] --> F["调用 setter"]
    F --> G{"数据类型正确?"}
    G -->|否| H["抛出 TypeError"]
    G -->|是| I{"半径大于 0?"}
    I -->|否| J["抛出 ValueError"]
    I -->|是| K["更新 self._radius"]

    L["访问 circle.area"] --> M["根据当前半径计算面积"]
    M --> N["返回计算结果"]

记住三个主要过程:

  • 读取 circle.radius:调用 getter,获取内部保存的半径。
  • 设置 circle.radius:调用 setter,检查数据后再决定是否修改。
  • 读取 circle.area:执行计算逻辑,返回面积。

而类属性与实例属性的区别,则在于数据是属于某个对象,还是由类提供共享的属性值。

什么时候应该使用 @property

并不是所有属性都需要 @property。如果一个属性只是保存普通数据,不需要额外检查或计算,直接使用实例属性通常就足够了。

使用场景示例是否适合使用 @property
保存普通数据学生姓名通常不需要
修改前检查数据圆形半径必须大于 0适合
读取时计算结果根据半径计算面积适合
读取数据时需要额外逻辑根据当前状态返回结果可以考虑

最重要的原则是:当一个值的读取、修改或计算需要额外逻辑时,可以考虑使用 @property,让外部代码继续使用简洁的属性访问方式。

同时还要记住,单个下划线只是 Python 的命名约定,并不是真正的访问权限控制。

写在最后

这篇文章从圆形和学生的例子出发,逐步学习了 Python 属性的不同用法。

现在,你应该能够理解:

  • 实例属性:保存某个对象自己的数据,例如学生姓名和年龄。
  • 类属性:定义在类中,可以为实例提供共享的属性值。
  • 单个下划线:通常表示内部使用的属性,但不会禁止外部访问。
  • @property:让方法能够以类似普通属性的方式读取。
  • @radius.setter:为属性增加赋值逻辑,可以在修改数据时进行检查。
  • 属性初始化:__init__() 中的 self.radius = radius 可以调用 setter,让初始值也经过检查。
  • raise 和 try...except:前者用于抛出异常,后者用于捕获和处理异常。
  • 计算属性:读取时执行计算,例如根据半径计算圆形面积。

建议你动手完成下面几个练习:

  • 将圆形的半径改成 0,观察程序会发生什么。
  • 将半径输入改成 hello,观察异常来自哪里。
  • 在 Student 类中增加一个类属性 school_name,创建两个学生对象并观察它们如何读取这个属性。
  • 修改圆形半径后,再次读取面积,验证计算属性是否使用了最新的数据。

下一篇,我们将继续学习 Python 的特殊方法,看看为什么有些方法的名字前后都有双下划线,以及它们如何让对象支持更多 Python 原生操作。