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 的类与对象,也通过 Linux 服务器管理案例认识了继承与多态。

我们知道,不同类型的对象可以拥有不同的行为,子类还可以继承父类的属性和方法。

但在实际编程中,还有一个很常见的问题:当程序拿到一个对象时,怎么知道它是什么类型?怎么判断它是不是某个类的实例?又该如何判断两个类之间有没有继承关系?

比如,我们编写一个服务器管理程序,读取了一份配置文件,却发现本来应该是整数的端口号变成了字符串。程序应该如何发现这个问题?

再比如,我们想判断一台服务器是不是 WebServer,或者判断 WebServer 是否继承了 Server,又该怎么做?

Python 已经为我们准备了几个实用的内置函数:type()、isinstance() 和 issubclass()。

这篇文章就从零开始,通过实际代码把它们讲明白。

为什么需要判断数据类型

先看一个日常工作中的例子。

假设我们正在编写服务器管理程序,需要记录服务器名称、IP 地址和端口号。

1
2
3
4
5
6
7
server_name = "web01"
server_ip = "192.168.1.10"
server_port = 8080

print(server_name)
print(server_ip)
print(server_port)

运行结果:

1
2
3
web01
192.168.1.10
8080

这里有三个数据:

  • server_name 保存的是服务器名称,例如 web01。
  • server_ip 保存的是 IP 地址,例如 192.168.1.10。
  • server_port 保存的是端口号,例如 8080。

虽然它们都是通过 = 保存数据,但数据的类型并不相同。

服务器名称和 IP 地址都是字符串,端口号则是整数。

为什么要关心类型?

因为不同类型的数据通常适合不同的操作。例如,整数可以进行数值计算,字符串主要用于保存和处理文本,列表则可以保存多个数据。

如果程序没有弄清楚数据的类型,就可能执行不符合预期的操作,甚至报错。

因此,我们先来学习第一个函数:type()。

使用 type() 查看数据类型

type() 是 Python 的内置函数,可以用来查看一个对象的类型。

这里先解释一下什么是内置函数。

你可以把内置函数理解为 Python 已经准备好的工具。我们不需要自己编写它们,只要按照规定的方式调用,就可以完成相应的操作。

例如,之前使用过的 print() 可以输出信息,input() 可以读取用户输入,而 type() 可以查看对象的类型。

新建一个文件 type_demo.py,写入下面的代码:

1
2
3
4
5
6
7
8
9
server_name = "web01"
server_port = 8080
cpu_usage = 75.5
is_running = True

print(type(server_name))
print(type(server_port))
print(type(cpu_usage))
print(type(is_running))

运行程序:

1
python type_demo.py

输出结果:

1
2
3
4
<class 'str'>
<class 'int'>
<class 'float'>
<class 'bool'>

我们逐个理解。

  • <class 'str'> 表示字符串类型。
  • <class 'int'> 表示整数类型。
  • <class 'float'> 表示浮点数类型,也就是可以表示小数的数值类型。
  • <class 'bool'> 表示布尔类型,主要用于表示真或假。

为什么输出结果中会出现 class?

这里的 class 表示类。Python 中,对象都有自己的类型,而类型通常由类来描述。现在不必深入研究类的内部机制,只需要知道:type() 返回的结果可以帮助我们确认对象的直接类型。

type() 的实际使用场景

只查看几个变量的类型,是为了帮助我们认识这个函数。在实际编程中,type() 还可以用来排查数据问题。

例如,我们从配置文件或其他程序中读取了服务器端口号。本来期望得到整数 8080,实际拿到的却是字符串 "8080"。

这两种数据看起来很相似,但类型不同。

1
2
3
4
server_port = "8080"

print(type(server_port))
print(server_port + "0")

运行结果:

1
2
<class 'str'>
80800

为什么没有得到数字 8080 加上数字 0 的结果?

因为 server_port 保存的是字符串 "8080",而不是整数 8080。这里的 + 执行的是字符串拼接,把两个字符串连接起来,所以结果变成了 "80800"。

如果我们希望执行数值计算,就需要先确认数据的类型,再根据实际情况进行转换。

1
2
3
4
5
6
7
8
server_port = "8080"

print(type(server_port))

server_port = int(server_port)

print(type(server_port))
print(server_port + 1)

运行结果:

1
2
3
<class 'str'>
<class 'int'>
8081

这里的 int() 用于把可以转换为整数的字符串转换成整数。

第一次执行 type(server_port),结果是 str;经过 int() 转换后,再查看类型,结果就变成了 int。

最后,server_port + 1 执行的是整数加法,所以结果为 8081。

这个例子说明:当程序的计算结果不符合预期时,使用 type() 查看数据类型,是一种简单而实用的排查方法。

不过要注意,int() 并不是对所有字符串都能成功转换。如果字符串中包含不能解析为整数的内容,例如 "eight",转换就会产生异常。后续学习异常处理时,我们会介绍如何处理这种情况。

使用 type() 检查配置数据

再来看一个更接近实际运维工作的例子。

假设程序需要读取服务器配置,并且要求端口号必须是整数。我们可以先用 type() 检查它。

1
2
3
4
5
6
server_port = 8080

if type(server_port) == int:
print("端口号类型正确")
else:
print("端口号类型错误")

运行结果:

1
端口号类型正确

这里的 == 用于比较两边是否相等。因此,type(server_port) == int 就是在判断变量的直接类型是否为 int。

因为 server_port 保存的是整数,所以条件成立,程序输出“端口号类型正确”。

如果把变量改成:

1
server_port = "8080"

那么判断结果就会变成 False,程序会输出“端口号类型错误”。

这就是 type() 的一种使用场景:在调试程序或检查数据时,确认数据的直接类型是否符合预期。

不过,type() 并不是所有类型检查场景的首选。尤其是在面向对象编程中,我们经常需要考虑继承关系。接下来就要认识一个更灵活的函数:isinstance()。

使用 isinstance() 判断对象属于什么类型

先来看一个问题。

前面我们学习过继承,知道子类可以继承父类。

假设有一个 Server 父类和一个 WebServer 子类:

1
2
3
4
5
6
7
8
9
class Server:
pass


class WebServer(Server):
pass


web01 = WebServer()

这里有三个需要理解的名称:

  • Server 是父类。
  • WebServer 是继承 Server 的子类。
  • web01 是通过 WebServer 创建的对象。

现在,我们想回答两个问题:

  • web01 是不是 WebServer 类型的对象?
  • web01 是不是 Server 类型的对象?

第一个问题很好理解。既然 web01 是通过 WebServer 创建的,那么它当然属于 WebServer 类型。

但第二个问题呢?

虽然 web01 是通过 WebServer 创建的,而不是直接通过 Server 创建的,但 WebServer 继承了 Server。因此,web01 也属于 Server 这个父类所代表的类型体系。

这时就可以使用 isinstance()。

1
2
3
4
5
6
7
8
9
10
11
12
class Server:
pass


class WebServer(Server):
pass


web01 = WebServer()

print(isinstance(web01, WebServer))
print(isinstance(web01, Server))

运行结果:

1
2
True
True

两次判断的结果都是 True。

原因是 isinstance() 不仅可以判断对象是否属于指定的类型,还会考虑这个对象是否属于该类型的子类。

换句话说,判断 web01 是否属于 Server 时,Python 会考虑 WebServer 与 Server 之间的继承关系。

这里要特别注意:isinstance() 检查的是对象,而不是直接检查两个类之间的继承关系。

它的基本写法是:

1
isinstance(对象, 类型或类)

例如:

1
2
3
print(isinstance("web01", str))
print(isinstance(8080, int))
print(isinstance([], list))

输出结果:

1
2
3
True
True
True

为什么第三行是 True?

因为 [] 表示一个空列表,而 list 是 Python 中列表对应的类型。

因此,isinstance([], list) 就是在询问:这个空列表是不是一个列表对象?

答案当然是 True。

type() 和 isinstance() 有什么区别

刚刚我们已经认识了两个函数,它们都能帮助我们判断类型,但判断方式有所不同。

看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Server:
pass


class WebServer(Server):
pass


web01 = WebServer()

print(type(web01) == WebServer)
print(type(web01) == Server)

print(isinstance(web01, WebServer))
print(isinstance(web01, Server))

运行结果:

1
2
3
4
True
False
True
True

为什么会出现这样的区别?

type(web01) == Server 的结果是 False,因为 web01 的直接类型是 WebServer,而不是 Server。

但是,isinstance(web01, Server) 的结果是 True,因为 WebServer 继承了 Server。

可以这样记忆:

函数或写法主要作用
type(obj)获取对象的直接类型
type(obj) == SomeClass判断对象的直接类型是否就是指定的类
isinstance(obj, SomeClass)判断对象是否属于指定的类或其子类

在实际编程中,如果你只关心对象的直接类型,可以使用 type()。如果你关心对象是否属于某个类型体系,通常优先考虑 isinstance()。

bool 为什么是 int 的子类

现在来看一个有点出人意料的结果:

1
2
3
print(type(True))
print(isinstance(True, int))
print(issubclass(bool, int))

输出结果:

1
2
3
<class 'bool'>
True
True

可能有人会疑惑:True 明明表示真,为什么会被认为是整数类型?

这里需要先区分两个概念:

  • type(True) 返回 bool,说明 True 的直接类型是布尔类型。
  • isinstance(True, int) 返回 True,说明布尔值也属于 int 的类型体系。
  • issubclass(bool, int) 返回 True,说明 bool 是 int 的子类。

bool 是 int 的子类,但布尔值的直接类型仍然是 bool。 这两个结论并不矛盾。

这是 Python 的一个类型设计特点:布尔类型只有两个值,True 和 False。在数值运算中,它们分别具有数值 1 和 0 的行为,因此布尔类型继承自整数类型。

我们可以通过运算观察这个特点。

1
2
3
4
print(True + 1)
print(False + 1)
print(True == 1)
print(False == 0)

输出结果:

1
2
3
4
2
1
True
True

为什么会这样?

因为在 Python 中:

  • True 在数值运算中相当于 1。
  • False 在数值运算中相当于 0。

所以:

  • True + 1 相当于 1 + 1,结果为 2。
  • False + 1 相当于 0 + 1,结果为 1。
  • True == 1 的结果是 True。
  • False == 0 的结果是 True。

bool 的运算边界:可以参与数值运算,不代表什么都能算

理解了上面的结果之后,还需要知道一个重要边界:布尔值可以参与某些数值运算,但它并不等同于普通整数,也不是所有运算都可以随意混用。

我们先看加法。

1
2
print(True + 10)
print(False + 10)

输出结果:

1
2
11
10

这是因为加法运算会把 True 和 False 当作数值 1 和 0 处理。

那么,能不能把布尔值直接和字符串拼接呢?

1
print(True + "1")

这段代码会产生 TypeError,因为 + 的两边分别是布尔值和字符串,Python 不会自动把它们转换成同一种类型。

如果想把布尔值转换成文字,可以显式使用 str():

1
print(str(True) + "1")

输出结果:

1
True1

这里 str(True) 将布尔值转换为字符串 "True",然后 + 执行字符串拼接。

再来看一个运算结果容易让初学者误解的例子:

1
2
print(True / 2)
print(False / 2)

输出结果:

1
2
0.5
0.0

这两次运算都能执行,是因为布尔值在数值运算中分别相当于 1 和 0。除法 / 会返回浮点数,因此结果分别是 0.5 和 0.0。

但如果把布尔值放进一个需要严格整数语义的业务场景,就要格外小心。

例如,我们正在检查服务器的端口号:

1
2
3
server_port = True

print(isinstance(server_port, int))

输出结果:

1
True

这看起来似乎很奇怪。端口号明明应该是一个整数,为什么布尔值也通过了检查?

原因正是 bool 继承了 int。因此,isinstance(server_port, int) 不仅接受普通整数,也接受布尔值。

如果业务明确要求必须是普通整数,而不能是 True 或 False,可以这样判断:

1
2
3
4
5
6
server_port = True

if isinstance(server_port, int) and not isinstance(server_port, bool):
print("端口号类型符合要求")
else:
print("端口号类型不符合要求")

输出结果:

1
端口号类型不符合要求

这里的 and 表示两个条件都必须成立,not 表示对后面的判断结果取反。

程序先判断 server_port 是否属于整数类型体系,再判断它是否不是布尔类型。由于 server_port 是 True,第二个条件不成立,所以最终进入 else 分支。

如果你只需要判断一个值是真是假,就应该直接使用布尔值,而不是把它当作数字来处理。例如,服务器是否运行可以使用 True 或 False 表示;端口号则应该使用符合要求的整数。

这个例子也提醒我们:类型判断要结合业务需求,不能只看某个函数返回了 True,就认为数据一定符合业务要求。

使用 issubclass() 判断类之间的继承关系

前面我们用 isinstance() 判断对象属于什么类型。

那么,如果手上没有对象,只知道两个类的名称,想判断它们之间有没有继承关系,应该怎么办?

例如:

  • WebServer 是否继承了 Server?
  • DatabaseServer 是否继承了 Server?
  • Server 是否继承了 WebServer?

这时就可以使用 issubclass()。

先看一个完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Server:
pass


class WebServer(Server):
pass


class DatabaseServer(Server):
pass


print(issubclass(WebServer, Server))
print(issubclass(DatabaseServer, Server))
print(issubclass(Server, WebServer))

运行结果:

1
2
3
True
True
False

我们逐个理解。

第一行:

1
issubclass(WebServer, Server)

是在判断 WebServer 是否为 Server 的子类。

因为 WebServer 继承了 Server,所以结果是 True。

第二行:

1
issubclass(DatabaseServer, Server)

同样是在检查继承关系。由于 DatabaseServer 也继承了 Server,结果仍然为 True。

第三行:

1
issubclass(Server, WebServer)

这次把父类和子类的位置交换了。

Server 是父类,而 WebServer 是子类,所以 Server 不是 WebServer 的子类,结果为 False。

issubclass() 的基本写法是:

1
issubclass(子类, 父类)

这里的两个参数都应该是类,而不是通过类创建出来的普通对象。

例如,下面的写法就是错误的:

1
2
3
4
5
6
7
8
9
10
11
class Server:
pass


class WebServer(Server):
pass


web01 = WebServer()

print(issubclass(web01, Server))

运行时会产生 TypeError,因为 web01 是一个对象,不是类。

记住这个区别,就不容易把两个函数混淆:

  • isinstance():判断对象是否属于某个类型。
  • issubclass():判断一个类是否是另一个类的子类。

用一张图串起三个函数

学到这里,我们已经认识了三个函数。可以用下面的关系图帮助记忆:

flowchart TD
    A["需要判断类型或继承关系"] --> B{"手上有什么?"}
    B -->|"想查看对象的直接类型"| C["使用 type()"]
    B -->|"想检查对象属于什么类型体系"| D["使用 isinstance()"]
    B -->|"想判断两个类的继承关系"| E["使用 issubclass()"]
    C --> F["获取对象的直接类型"]
    D --> G["检查对象是否属于指定类型或其子类"]
    E --> H["检查类之间的继承关系"]

例如:

1
type(web01)

获取 web01 的直接类型。

1
isinstance(web01, Server)

判断 web01 是否属于 Server 或其子类。

1
issubclass(WebServer, Server)

判断 WebServer 是否为 Server 的子类。

三者的用途不同,但都能帮助我们更清楚地理解程序中的对象和类。

动手实践:完成服务器类型检查程序

概念理解之后,我们通过一个完整的小任务巩固所学知识。

任务目标

编写一个名为 server_type_check.py 的程序,完成以下要求:

  • 定义 Server 父类。
  • 定义继承 Server 的 WebServer 子类。
  • 创建一个 WebServer 对象。
  • 使用 type() 查看对象的直接类型。
  • 使用 isinstance() 判断对象是否属于 WebServer 和 Server。
  • 使用 issubclass() 验证 WebServer 与 Server 之间的继承关系。
  • 再创建一个普通的 Server 对象,比较两个对象的判断结果。
  • 尝试检查端口号,并理解为什么布尔值可能通过普通的整数类型检查。

编写完整代码

创建 server_type_check.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
40
class Server:
def __init__(self, name, ip):
self.name = name
self.ip = ip


class WebServer(Server):
pass


# 创建一个 WebServer 对象
web01 = WebServer("web01", "192.168.1.10")

# 创建一个普通的 Server 对象
server01 = Server("server01", "192.168.1.5")


print("=== 查看对象的直接类型 ===")
print(type(web01))
print(type(server01))

print("\n=== 判断对象属于什么类型 ===")
print("web01 是 WebServer:", isinstance(web01, WebServer))
print("web01 是 Server:", isinstance(web01, Server))
print("server01 是 WebServer:", isinstance(server01, WebServer))
print("server01 是 Server:", isinstance(server01, Server))

print("\n=== 判断类之间的继承关系 ===")
print("WebServer 继承 Server:", issubclass(WebServer, Server))
print("Server 继承 WebServer:", issubclass(Server, WebServer))

print("\n=== 检查端口号 ===")
server_port = True

print("端口号属于 int 类型体系:", isinstance(server_port, int))

if isinstance(server_port, int) and not isinstance(server_port, bool):
print("端口号类型符合要求")
else:
print("端口号类型不符合要求")

运行程序

打开终端,进入文件所在目录,执行:

1
python server_type_check.py

如果你的环境使用 python3 命令,则运行:

1
python3 server_type_check.py

预期输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=== 查看对象的直接类型 ===
<class '__main__.WebServer'>
<class '__main__.Server'>

=== 判断对象属于什么类型 ===
web01 是 WebServer: True
web01 是 Server: True
server01 是 WebServer: False
server01 是 Server: True

=== 判断类之间的继承关系 ===
WebServer 继承 Server: True
Server 继承 WebServer: False

=== 检查端口号 ===
端口号属于 int 类型体系: True
端口号类型不符合要求

对照输出理解结果

先看 type(web01) 的结果:

1
<class '__main__.WebServer'>

这表示 web01 的直接类型是 WebServer。__main__ 是 Python 在直接运行的程序中经常使用的模块名称,这里不需要深入研究它。

再看:

1
isinstance(web01, Server)

结果是 True,因为 WebServer 继承了 Server。

而:

1
isinstance(server01, WebServer)

结果为 False,因为 server01 是直接通过 Server 创建的对象,并不会因为存在一个 WebServer 子类,就自动变成 WebServer 对象。

最后,issubclass(WebServer, Server) 返回 True,证明子类与父类之间的继承关系确实存在。

端口号检查部分则验证了另一个知识点:True 属于 int 的类型体系,但如果业务不允许把布尔值当作普通整数使用,就需要额外排除 bool。

如果你能解释这些结果为什么出现,就已经掌握了本篇最重要的内容。

写在最后

这一篇,我们学习了 Python 中三个非常实用的内置函数。

type() 用来获取对象的直接类型,适合在调试和排查数据问题时确认对象究竟是什么类型;isinstance() 用来判断对象是否属于指定的类型或其子类;issubclass() 则用来判断类之间的继承关系。

我们还通过 bool 继承 int 的例子,理解了为什么布尔值可以参与部分数值运算,以及为什么不能因此就把布尔值当作普通整数使用。

到这里,Python 类与对象这一章的基础内容就告一段落了。从定义类、创建对象,到理解属性、方法、继承、多态,再到判断类型与继承关系,我们已经逐步建立起面向对象编程的基本认识。

但程序运行过程中,并不总是一帆风顺。

例如,用户输入了错误的数据、程序尝试打开一个不存在的文件,或者调用某个操作时出现问题,程序可能会产生异常。

下一章,我们将学习 Python 的异常处理,认识程序出错时会发生什么,以及如何使用 try、except 等语句,让程序更稳健地处理意外情况。