02_Python数据类型:数字、字符串、布尔值与None一次讲清楚
Python数据类型:数字、字符串、布尔值与None一次讲清楚
小明刚刚写了人生中第一个 Python 程序,兴冲冲地准备写第二个。他写下了这样一行代码:
age = input("请输入你的年龄:")
print("明年你就" + str(age + 1) + "岁了")
运行,输入 20,回车——
TypeError: can only concatenate str (not "int") to str
小明愣住了:我明明输入的是 20,20 + 1 不是等于 21 吗?怎么就报错了?
这个问题,每一个 Python 初学者都会遇到。原因很简单:你输入的是数字 20,但 Python 拿到手的是字符串 “20”。在 Python 眼里,“20” 和 20 是完全不同的两种东西——就像你钱包里的 20 块钱和你写在纸上的"20元"不是一回事。
这篇文章就来把 Python 里最基本的数据类型彻底讲清楚。别小看它——数据类型是整栋 Python 大楼的地基,地基歪了,后面学到什么都觉得不对劲。
数据是有"形状"的
想象你在收拾房间。T恤放衣柜,书放书架,零食放茶几——你不会把冰激凌塞进书柜,也不会把《Python入门》扔进冰箱。编程里的数据类型也是一样的道理:不同类型的数据有不同的"形状",适合做不同的事。
20 # 这是一个整数,适合计数、做数学运算
19.99 # 这是一个小数,适合表示价格、成绩
"Hello" # 这是一串文字,适合表示名字、地址、消息
True # 这是开关,只有开和关两种状态
None # 这是"空",表示啥也没有
Python 聪明的地方在于:你不需要提前声明类型。写 age = 20,Python 自动就知道 age 是个整数。写 name = "小明",Python 自动就知道 name 是个字符串。这叫做动态类型——类型是数据本身的属性,不是变量的属性。
但这也埋下了一个坑:因为不需要声明,很多初学者就误以为 Python"没有类型"。大错特错。Python 不但有类型,而且类型检查很严格——整数和字符串不能直接相加,就是最好的例子。

图2-1 Python 五种基本数据类型:int、float、str、bool、NoneType 的特征对比和典型使用场景。
整数(int):比你想象的能装
Python 的整数有个让其他语言羡慕的特性:理论上没有上限。
在 C 语言里,int 类型通常是 32 位的,最大值只有 21 亿左右。超过就溢出了。Java 也是。但 Python 的 int 可以表示任意大的整数——只要你电脑内存够用:
# 普通使用
age = 20
count = 1000
year = 2025
# Python 的整数有多大?来算个阶乘试试
import math
print(math.factorial(100))
# 输出一个 158 位的数字,毫无压力
这是因为 Python 内部使用了"大整数"实现——当数字超过一定范围,它会自动扩展存储空间。你不用关心这些实现细节,只需要知道:在 Python 里做数学运算,基本不用担心"溢出"这回事。
整数的运算也很直接:
a = 10
b = 3
print(a + b) # 13 加法
print(a - b) # 7 减法
print(a * b) # 30 乘法
print(a / b) # 3.333... 除法(结果一定是浮点数)
print(a // b) # 3 整除(向下取整)
print(a % b) # 1 取余
print(a ** b) # 1000 幂运算
注意 / 和 // 的区别:/ 永远给你浮点数结果,哪怕两个整数能整除;// 是整除,结果向下取整。
print(5 / 2) # 2.5
print(5 // 2) # 2
print(-5 // 2) # -3(注意!向下取整,不是向零取整)
最后一个结果很多人会猜 -2,实际上是 -3。因为 -2.5 向下取整是 -3。这是 Python 整除的一个小陷阱,初学的时候知道有这回事就行。
浮点数(float):那个不精确的小数
浮点数用来表示带小数点的数字。成绩、价格、温度、坐标——凡是需要精度的场景都会用到。
score = 95.5
price = 19.99
pi = 3.1415926535
但你很快就会踩到一个经典大坑:
print(0.1 + 0.2)
# 你期望:0.3
# 实际输出:0.30000000000000004
这是什么鬼?0.1 + 0.2 不等于 0.3?Python 算错了吗?
不是 Python 的错。这是二进制浮点数表示法的固有缺陷——几乎所有编程语言都有这个问题。在二进制里,0.1 是一个无限循环小数(就像十进制里 1/3 = 0.3333… 无限循环一样)。计算机用有限的比特位存储它,只能存近似值,加在一起自然有微小的误差。
这个误差在大多数场景下可以忽略,但如果你在写金融程序,涉及金额比较,就会出大问题:
# 这样比较可能失败
price = 0.1 + 0.2
if price == 0.3:
print("价格正确")
else:
print("价格不对") # 这条会执行!
怎么解决?几种方案:
# 方案1:使用 round() 四舍五入
print(round(0.1 + 0.2, 2)) # 0.3
# 方案2:使用 math.isclose() 判断近似相等
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
# 方案3:使用 Decimal(适合金融计算)
from decimal import Decimal
print(Decimal('0.1') + Decimal('0.2')) # 0.3
初学阶段你不需要记住所有方案。只要记住一件事:不要直接用 == 比较两个浮点数是否相等。用 round() 或 math.isclose()。

图2-2 动态类型变量指向示意:变量名只是标签,可以随时重新贴到不同类型的对象上;有类型的是对象,不是变量名。
浮点数的另一个小知识:科学计数法。当数字特别大或特别小时,Python 会自动用科学计数法显示:
print(0.000000123) # 1.23e-07
print(1230000000.0) # 1.23e+09
你也可以手动使用:
avogadro = 6.022e23 # 阿伏伽德罗常数
electron_mass = 9.109e-31 # 电子质量
字符串(str):一切文字信息的家
字符串是 Python 里你可能使用最频繁的数据类型。名字、地址、消息、文件路径、网页内容——凡是文字,都是字符串。
基本写法
name = "小明" # 双引号
city = '北京' # 单引号,效果完全一样
message = "It's OK" # 内容里有单引号时,外层用双引号
path = '他说:"你好"' # 内容里有双引号时,外层用单引号
# 多行文字用三引号
intro = """Python 是一门优雅的语言。
它简单易学,功能强大。
我正在学习它。"""
Python 里单引号和双引号没有区别,怎么方便怎么来。这一点比某些语言(比如 PHP、Shell)处理得更灵活。
字符串不可变——这是一个重要的概念
很多初学者在这里困惑:我明明可以重新赋值,为什么说"不可变"?
name = "小明"
name = "小红" # 这不是修改了字符串吗?
没有。你没有修改原来的字符串 "小明",而是让变量 name 指向了一个全新的字符串 "小红"。原来的 "小明" 还存在内存里的某个角落(直到 Python 的垃圾回收把它清理掉)。
理解这个区别很重要,因为后面你会学到:
name = "Python"
name[0] = "J" # 报错!TypeError
# 字符串不能通过下标修改其中的某一个字符
如果你想"修改"一个字符串,只能创建一个新的:
name = "Python"
name = "J" + name[1:] # "Jython"

图2-3 字符串不可变示意:修改变量指向与修改字符串本身是两种完全不同的操作;字符串一旦创建就不能改动。
字符串的基本操作
# 拼接
greeting = "你好," + "世界" # "你好,世界"
# 重复
line = "-" * 30 # "------------------------------"
# 长度
len("Python") # 6
len("你好") # 2(注意:一个汉字算一个字符)
# 成员检查
"Py" in "Python" # True
"Java" in "Python" # False
# 索引(从0开始)
s = "Python"
print(s[0]) # 'P'
print(s[-1]) # 'n'(负数从右边倒数)
# 切片(取一部分)
print(s[0:2]) # 'Py'(从0到2,不包含2)
print(s[2:]) # 'thon'(从2到最后)
这些操作看起来很基础,但几乎所有复杂的文本处理都是在它们之上构建的。后面第 9 篇会专门深入讲字符串的高级用法——格式化、正则、编码问题等等。
一个实用技巧:字符串的常用方法
name = " Xiao Ming "
print(name.strip()) # "Xiao Ming" 去掉两端空白
print(name.upper()) # " XIAO MING " 全大写
print(name.lower()) # " xiao ming " 全小写
print(name.replace("Ming", "Hong")) # " Xiao Hong " 替换
print(name.split(" ")) # ['', '', 'Xiao', 'Ming', '', ''] 按空格分割
现在你不需要背这些方法。用到的时候用 dir("任意字符串") 查看所有可用方法,help(str.strip) 看具体用法。
布尔值(bool):程序的开关
布尔类型只有两个值:True 和 False。注意首字母必须大写——写成 true 会报错。
is_student = True
has_passed = False
is_adult = age >= 18 # 比较运算的结果也是布尔值
布尔值主要用于条件判断:
if is_adult and has_ticket: # and:两边都为真才为真
print("您可以入场")
if is_student or is_teacher: # or:只要一边为真就是真
print("可以享受折扣")
if not is_blocked: # not:取反
print("账号正常")
一个容易被忽视的点:在 Python 里,每个值都有"真值"或"假值"。以下这些在条件判断中会被当成 False:
0、0.0(数字零)""(空字符串)、[](空列表)、{}(空字典)NoneFalse本身
其他一切(非零数字、非空字符串、非空列表等等)都是 True。
name = ""
if not name:
print("你还没输入名字") # 空字符串被视为 False,这里会执行
scores = [85, 92]
if scores:
print(f"共有 {len(scores)} 条成绩") # 非空列表被视为 True
这种"真值检验"的写法在 Python 里非常常用,比写 if len(scores) > 0: 更符合 Python 风格。
None:一个特殊的"空"
None 是 Python 里表示"没有值"的方式。它不是 0,不是空字符串,不是 False——它就是"无"。
result = None # 表示 result 现在没有任何值
# 常见的检查方式
if result is None:
print("还没有结果")
# 不要用 == 来比较 None,用 is
# result is None 推荐
# result == None 不推荐(可能被某些特殊情况干扰)
什么时候会用到 None?
- 函数的返回值,表示"我没有东西可以返回"
- 变量的初始值,表示"还没被赋值"
- 数据库查询结果,表示"没有找到"
type() 和 isinstance()——确认类型的两个工具
不确定一个变量是什么类型?用 type():
print(type(20)) # <class 'int'>
print(type(3.14)) # <class 'float'>
print(type("Hello")) # <class 'str'>
print(type(True)) # <class 'bool'>
print(type(None)) # <class 'NoneType'>
这在调试时特别有用。比如你的程序报错了,你不确定某个变量到底是什么类型,print(type(x)) 一下,真相大白。
但如果要判断"这个变量是不是某个类型",推荐用 isinstance():
age = 20
print(isinstance(age, int)) # True
print(isinstance(age, float)) # False
print(isinstance(age, (int, float))) # True 可以一次检查多个类型
为什么 isinstance() 比 type() == int 更好?因为它能正确处理继承关系(面向对象篇会讲到),而且更简洁。
类型转换:在数字和文字之间搭桥
回到开头小明的问题:input() 拿到的永远是字符串。要把它当数字用,必须转换类型。
# 字符串 → 数字
age_str = input("年龄:") # 用户输入 "20",得到字符串 "20"
age_int = int(age_str) # 转换成整数 20
age_float = float(age_str) # 转换成浮点数 20.0
# 数字 → 字符串
price = 19.99
price_str = str(price) # "19.99"
# 任意值 → 布尔值
print(bool(0)) # False
print(bool(1)) # True
print(bool("")) # False
print(bool("Hi")) # True
类型转换不是万能的——你没法把 "abc" 转成整数:
int("abc") # ValueError: invalid literal for int()
所以在转换之前,最好确认一下数据是否"干净"。后面学异常处理时会讲更优雅的方式,现在先用 isdigit() 做个简单判断:
user_input = input("请输入一个数字:")
if user_input.isdigit():
number = int(user_input)
print(f"你输入的数字是 {number}")
else:
print("输入的不是纯数字,请重试。")
深入研究浮点精度——为什么 0.1 + 0.2 ≠ 0.3
前面简单提了一下浮点误差问题。这里我们掰开揉碎来看看,这到底是怎么回事,以及在实际编程中你需要记住什么。
二进制怎么表示小数
整数转二进制很简单:5 就是 101,10 就是 1010。但小数呢?
十进制里,0.5 是 1/2,很干净。0.2 是 2/10,也很干净。但在二进制里要表示 0.1,你会发现:2 进制下 0.1 是一个无限循环小数(0.0001100110011...),就像十进制里 1/3 = 0.3333… 一样。
计算机只能存有限的位数(Python 的浮点数使用 IEEE 754 双精度,64 位),因此必须截断。截断就意味着近似。近似就意味着误差。
实际影响
# 经典的例子
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
# 看似没问题的运算也可能有问题
print(1.1 + 2.2) # 3.3000000000000003
# 循环累加会放大误差
total = 0.0
for i in range(1000):
total += 0.1
print(total) # 99.9999999999... 不是 100.0
生活中的类比
想象你有一把尺子,上面只有厘米刻度,没有毫米。你要量一个 5.3 厘米的东西——你只能看到"大约 5 厘米"。量一个东西可能有 ±0.5cm 的误差。计算机存浮点数也是类似的道理:它用有限的比特位去"刻度"数轴,没法精确表示每一个实数。
几个你应该养成的习惯
# 比较浮点数时,给一个容忍范围
EPSILON = 1e-9 # 非常小的数
if abs(a - b) < EPSILON:
print("可以认为相等")
# 或者直接用 math.isclose
import math
if math.isclose(a, b, rel_tol=1e-9):
print("可以认为相等")
# 处理金额时,用整数表示"分"而不是用浮点数表示"元"
price_cents = 1999 # 19.99 元 = 1999 分
tax_cents = price_cents * 8 // 100 # 8% 的税,精确到分
这些技巧不用一次全记住。你只需要记住一句话:浮点数有误差,比较时留点余地。
深入聊聊字符串——那些你可能不知道的事
转义字符
有些字符没法直接打出来——比如换行、制表符、引号本身。这时需要用反斜杠加一个特殊字符:
print("第一行\n第二行") # \n 是换行
print("姓名\t年龄\t成绩") # \t 是制表符(Tab)
print("他说:\"你好\"") # \" 是双引号本身
print('路径是 C:\\Users\\小明') # \\ 是反斜杠本身
如果不想要转义——比如你写文件路径,反斜杠特别多,每个都要写 \\ 太累了——可以加一个 r 前缀,叫做"原始字符串":
print(r"C:\Users\小明\Documents") # 不需要每个反斜杠都转义
字符串格式化:不止 f-string
f-string 是 Python 3.6 引入的,目前最推荐。但了解一下其他方式也有好处——你在老代码里会看到它们:
name = "小明"
age = 20
# f-string(推荐)
print(f"{name}今年{age}岁")
# format() 方法(老代码里常见)
print("{}今年{}岁".format(name, age))
print("{0}今年{1}岁".format(name, age))
# % 格式化(更老的写法,源自 C 语言)
print("%s今年%d岁" % (name, age))
三种写法效果一样,但 f-string 最直观。后面第 3 篇会深入展开 f-string 的所有高级用法。
字符串的不可变性——再深入一点
s = "Hello"
# s[0] = "h" # 会报错!TypeError: 'str' object does not support item assignment
# 正确的做法:创建一个新字符串
s = "h" + s[1:] # "hello"
# 所有的字符串方法都不会修改原字符串,而是返回一个新字符串
original = " Python "
trimmed = original.strip()
print(original) # " Python " ——原字符串没变!
print(trimmed) # "Python" ——新字符串
这个设计不是 Python 故意为难你。不可变对象有很多好处:线程安全、可以作为字典的键、不怕意外修改。等你学到字典和集合的时候会体会更深。
字符串常用的二十几个方法速览
不需要背,但可以先混个眼熟。用的时候 dir("test") 查一下就行:
s = "Hello World"
# 大小写
s.upper() # "HELLO WORLD"
s.lower() # "hello world"
s.title() # "Hello World"
s.capitalize() # "Hello world"
s.swapcase() # "hELLO wORLD"
# 查找与替换
s.find("World") # 6 (找到返回位置,找不到返回 -1)
s.index("World") # 6 (找不到会报错)
s.count("l") # 3 (统计出现次数)
s.replace("World", "Python") # "Hello Python"
s.startswith("He") # True
s.endswith("ld") # True
# 判断
s.isalpha() # False(有空格)
s.isdigit() # False
"123".isdigit() # True
s.isspace() # False
# 分割与拼接
s.split(" ") # ['Hello', 'World']
" ".join(['Hello', 'Python']) # "Hello Python"
这就像你在厨房里看刀具——不需要一次性记住每把刀怎么用,但你知道"大概有这些功能",需要的时候再去拿。
布尔值深入:逻辑的本质
比较运算符的完整清单
a, b = 10, 3
print(a == b) # False 等于
print(a != b) # True 不等于
print(a > b) # True 大于
print(a < b) # False 小于
print(a >= b) # True 大于等于
print(a <= b) # False 小于等于
# Python 特有的链式比较
x = 5
print(1 < x < 10) # True,等价于 1 < x and x < 10
print(60 <= x <= 100) # False
链式比较是 Python 的一个语法糖,其他语言里你通常只能写 1 < x && x < 10。它不光好读,而且效率更高——因为中间的表达式只计算一次。
逻辑运算符与短路求值
Python 的逻辑运算有一个重要特性叫"短路求值":
# and 短路:左边为 False 时,右边根本不执行
False and print("这行不会打印") # 什么都不会输出
# or 短路:左边为 True 时,右边根本不执行
True or print("这行也不会打印") # 什么都不会输出
短路求值不只是性能优化,它还能帮你写出更简洁的代码:
# 如果 name 非空,就取 name;否则用默认值
display_name = name or "匿名用户"
# 如果用户已登录且余额充足,才发起扣款
if is_logged_in and balance >= amount:
process_payment()
# 如果 is_logged_in 是 False,后面 balance >= amount 根本不会执行,省了一次比较
真值测试的完整规则
前面提到 Python 里每个值都有"真"或"假"的属性。完整规则如下:
以下被视为 False(假值):
False、0、0.0、0j(复数零)""(空字符串)、[](空列表)、()(空元组)、{}(空字典)、set()(空集合)None
其他所有情况都是 True(真值)——包括 " "(空白字符串)、[0](包含元素 0 的列表)、"False"(注意:这是字符串,不是布尔值!)。
一个常见的陷阱:
# 错误示范
user_input = input("请输入:") # 用户直接回车,得到 ""
if user_input:
print(f"你输入了:{user_input}")
else:
print("你没有输入任何内容")
# 注意区分 "" 和 " " —— 后者有个空格,是真值!
None 的更多使用场景
None 有些微妙的地方值得了解:
# 函数没有 return 语句时,默认返回 None
def do_something():
print("做了一些事")
result = do_something() # 输出"做了一些事"
print(result) # None —— 函数没有明确返回值,就返回 None
# 检查返回值时注意
def find_student(name):
# 模拟查找学生
if name == "小明":
return {"name": "小明", "score": 90}
# 找不到,返回什么?None!
return None
student = find_student("小红")
if student is None:
print("没找到这个学生")
else:
print(f"成绩是:{student['score']}")
None 做默认参数也是一个常见模式:
def greet(name, greeting=None):
if greeting is None:
greeting = "你好"
print(f"{greeting},{name}!")
greet("小明") # "你好,小明!"
greet("小红", greeting="欢迎") # "欢迎,小红!"
注意这里用 is None 而不是 == None。绝大多数情况下两者效果一样,但 is None 更快,且不会被可能复写了 __eq__ 方法的对象干扰。
类型转换的边界情况
类型转换不是"想转就转",每种转换有自己的规则和边界:
# int() 可以接受浮点数——但会截断小数部分
print(int(3.9)) # 3 ——注意!不是四舍五入,是直接截断
print(int("42")) # 42 ——纯数字字符串可以转
# int("42.5") # 报错!有小数点不行
# int("abc") # 报错!不是数字
# float() 能接受字符串中的小数点
print(float("3.14")) # 3.14
print(float("42")) # 42.0
print(float(42)) # 42.0
# str() 几乎什么都能转
print(str(42)) # "42"
print(str(3.14)) # "3.14"
print(str(True)) # "True"
print(str(None)) # "None"
print(str([1, 2, 3])) # "[1, 2, 3]"
# bool() 多数情况符合直觉,但有些容易误判
print(bool(0)) # False
print(bool(0.0)) # False
print(bool("")) # False
print(bool([])) # False
print(bool({})) # False
print(bool(None)) # False
print(bool(42)) # True
print(bool("False")) # True —— 非空字符串,所以是真值!
print(bool(" ")) # True —— 空格也是字符
print(bool([0])) # True —— 列表非空
最后一个特别容易踩的坑:bool("False") 是 True!因为 "False" 是非空字符串,而所有非空字符串的真值都是 True。
整数的高级玩法——进制转换和内置函数
除了加减乘除,Python 的整数还有一些你可能用到的功能。
进制表示
Python 支持直接写二进制、八进制和十六进制:
# 不同进制的表示
bin_num = 0b1010 # 二进制,十进制是 10
oct_num = 0o17 # 八进制,十进制是 15
hex_num = 0xFF # 十六进制,十进制是 255
print(bin_num, oct_num, hex_num) # 10 15 255
进制转换用内置函数:
num = 42
print(bin(num)) # '0b101010' 转为二进制字符串
print(oct(num)) # '0o52' 转为八进制字符串
print(hex(num)) # '0x2a' 转为十六进制字符串
# 从字符串按指定进制解析
print(int('101010', 2)) # 42 从二进制字符串解析
print(int('FF', 16)) # 255 从十六进制字符串解析
print(int('17', 8)) # 15 从八进制字符串解析
大部分时候你不需要用这些,但处理颜色值(比如网页里的 #FF6600)、权限标志位、网络协议时它们很常见。
几个常用的数值函数
# 绝对值
print(abs(-10)) # 10
print(abs(-3.14)) # 3.14
# 四舍五入
print(round(3.14159, 2)) # 3.14
print(round(3.5)) # 4(注意:Python 使用银行家舍入,.5 舍入到最近的偶数)
print(round(4.5)) # 4(所以 3.5 → 4,但 4.5 → 4)
# 最大值和最小值
print(max(3, 8, 1, 6)) # 8
print(min(3, 8, 1, 6)) # 1
# 求和
print(sum([1, 2, 3, 4])) # 10
round() 的"银行家舍入"(四舍六入五成双)需要特别注意:3.5 舍入到 4,但 4.5 也舍入到 4。这是为了在大批量数据处理中减少统计偏差。如果你需要传统的四舍五入,可以用 Decimal。
再看一个综合例子——把今天学的串起来
不写个完整例子总觉得空空的。这个例子不复杂,但它覆盖了 int、float、str、bool、None 五种类型,以及类型转换和条件判断:
def analyze_number():
"""让用户输入一个数字,分析它的各种属性"""
user_input = input("请输入一个数字:").strip()
# 先判断有没有输入
if not user_input:
print("你什么都没输入,分析个寂寞。")
return None
# 尝试把输入转成浮点数
try:
num = float(user_input)
except ValueError:
print(f"'{user_input}' 不是有效的数字,请重试。")
return None
print(f"\n===== 数字 {num} 的分析报告 =====")
# 判断正负
if num > 0:
print("类型:正数")
elif num < 0:
print("类型:负数")
else:
print("类型:零")
# 判断是否为整数
is_integer = (num == int(num))
print(f"是整数:{is_integer}")
# 如果是整数,额外给一些分析
if is_integer:
int_num = int(num)
is_even = (int_num % 2 == 0)
print(f"是偶数:{is_even}")
print(f"二进制:{bin(int_num)}")
print(f"十六进制:{hex(int_num)}")
# 取绝对值和四舍五入
print(f"绝对值:{abs(num)}")
print(f"四舍五入到两位:{round(num, 2)}")
return {
"value": num,
"is_integer": is_integer,
"is_positive": num > 0
}
# 运行试试
result = analyze_number()
if result:
print(f"\n分析完成,结果摘要:{result}")
你可以把这个代码保存成 number_analyzer.py 跑一下,输入不同的数字看看输出。里面的 try/except 语法现在看不懂没关系,下一篇第 12 篇会详细讲。
动态类型的利与弊——一个程序员的真实感受
动态类型让 Python 写起来非常快——你不用先规划好每个变量的类型,想到了直接写。对于快速开发、原型验证、数据分析,这简直就是开了加速器。
但它也有代价。最典型的问题:你一直以为某个变量存的是数字,结果跑了 50 行之后发现它是个字符串——报错的行和真正出错的行离了很远,排查起来费劲。
# 这种代码不会报错,但可能会给你制造隐秘的 bug
data = 100 # 整数
data = "一百" # 现在变成字符串了,Python 完全不管
data = [1, 2, 3] # 又变成列表了
真实案例:类型问题引发的三个经典Bug
理论讲完了,来看几个真实编程中因为类型问题踩的坑。这些不是编出来的例子——每个学 Python 的人几乎都遇到过。
Bug 1:input 拿到的是字符串
# 一个计算圆面积的程序(错误版本)
radius = input("请输入圆的半径:")
area = 3.14 * radius ** 2
# TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'
这个前面讲过,但值得再说一遍——因为实在太多人犯。input() 返回的一定是字符串。解决:
radius = float(input("请输入圆的半径:"))
area = 3.14 * radius ** 2
print(f"圆的面积是:{area:.2f}")
Bug 2:条件判断中的类型陷阱
# 一个判断用户是否输入了数字的程序(有bug的版本)
user_input = input("请输入一个数字:")
if user_input:
number = int(user_input)
print(f"数字是:{number}")
else:
print("你没输入任何东西")
# 如果用户输入 0 呢?
# user_input = "0","0" 是非空字符串,所以 if "0" 为 True
# number = 0,正常。没问题。
#
# 但如果用户输入的是空格呢?
# user_input = " "," " 也是非空字符串,if " " 为 True
# 然后 int(" ") 报错:ValueError: invalid literal for int()
正确处理方式:
user_input = input("请输入一个数字:").strip()
if user_input: # 去掉了空白后非空
try:
number = int(user_input)
print(f"数字是:{number}")
except ValueError:
print("你输入的不是有效的整数")
else:
print("你没有输入任何东西")
Bug 3:可变对象做默认参数
这个 bug 稍微超前一点(涉及列表,后面第 7 篇会讲),但太经典了,先给你种个印象:
# 一个"添加学生到列表"的函数(错误版本)
def add_student(name, student_list=[]):
student_list.append(name)
return student_list
# 第一次调用
class_a = add_student("小明")
print(class_a) # ['小明'] ——正常
# 第二次调用(没传第二个参数)
class_b = add_student("小红")
print(class_b) # 你期望 ['小红'],实际是 ['小明', '小红']!
print(class_a) # 而且 class_a 也变成了 ['小明', '小红']!
为什么会这样?因为 Python 的默认参数是在函数定义时只创建一次,之后的每次调用都共用同一个列表对象。这里 student_list=[] 中的空列表被反复复用了。
正确的写法:
def add_student(name, student_list=None):
if student_list is None:
student_list = [] # 每次调用都创建新列表
student_list.append(name)
return student_list
这是 Python 面试中最高频的陷阱题之一。暂时不理解没关系,学到第 7 篇和第 10 篇时会豁然开朗。
理解 Python 的类型系统——变量是标签,对象是箱子
很多从 C/Java 转过来的学习者会带着"变量就是容器"的思维定式。在 Python 里,更准确的比喻是:变量是贴在对象上的标签。
a = [1, 2, 3] # 创建一个列表对象,把标签 "a" 贴在上面
b = a # 不是复制列表!是把标签 "b" 也贴到同一个列表上
b.append(4) # 通过 b 修改列表
print(a) # [1, 2, 3, 4] —— a 也变了!因为 a 和 b 指向同一个对象
这个特性对可变对象(列表、字典、集合)影响很大,对不可变对象(数字、字符串、元组)影响不大——因为不可变对象不会被修改,每次"修改"其实是创建一个新对象。
a = "hello"
b = a # b 也指向 "hello"
b = b + " world" # b 现在指向新字符串 "hello world",a 还是指向 "hello"
print(a) # "hello" —— a 没变,因为字符串不可变
刚开始理解这些确实有点绕。每多写一个月的 Python,你就能多理解一分。现在只需要记住核心结论:不可变类型"改不动",可变类型"共享时要当心"。
知道类型到底有什么用——给你三个你在乎的理由
说了这么多理论,你可能在想:“我就随便写写,类型错了报个错,我改就是了,搞这么清楚干嘛?”
给你三个实打实的理由:
第一,类型错误是最常见的报错。 你写 Python 前三个月遇到的所有报错里,大概有一半跟类型有关——TypeError、ValueError、AttributeError……如果你能一眼认出"哦,这是把字符串当数字用了",你定位问题的速度比不知道类型的人快五倍。
第二,读懂别人的代码需要类型意识。 当你调用一个别人写的函数,你怎么知道它返回什么?文档可能会写,但你也可以这样推测:
def get_student_score(name):
# 函数名包含 "score" 和 "student"
# 很可能是通过 name 查分数
# 返回什么?大概率是 float 或 int
pass
有类型意识的人看函数名就能猜出参数和返回值的大致类型,这能大幅提高阅读代码的速度。
第三,写代码时类型对了,Bug 直接减半。 很多 bug 的根源是:“我以为这是个数字,结果传进来的是个字符串”。如果你写每一行代码时都清楚当前变量的类型,这种低级错误就不会发生。
动手做
- 定义一个变量存你的名字,再定义一个存你的年龄,用
type()打印它们各自的类型。然后把年龄改成字符串类型,再次打印类型——感受动态类型的灵活性。 - 计算
0.1 + 0.2 == 0.3的结果,再用math.isclose(0.1 + 0.2, 0.3)计算一次。理解为什么前者是 False 而后者是 True。 - 写一个程序,让用户输入两个数字(用
input()),把它们相加并输出。注意:先别转类型,看看报什么错——然后加上float()转换,再运行。 - 创建一个字符串
" Python ",先后用.strip()、.upper()、.replace("P", "J")处理它,每次处理后打印结果。观察每个方法返回的是新字符串还是修改了原字符串。
本篇要点回顾:
- Python 有五大数据类型:int、float、str、bool、NoneType
- 整数任意大,浮点数不要用
==比较——用round()或math.isclose() - 字符串不可变,"修改"其实是创建新字符串;单双引号没区别
input()返回的永远是str,想当数字用必须int()或float()转换- 布尔值有短路求值,真值测试覆盖了零值、空值、空容器和 None
None表示"没有值",检查用is None不要用== Noneisinstance()比type() ==更好,以后写判断类型时优先用它- 变量是标签,对象是箱子——可变对象共享时要当心
下一篇我们进入变量、注释和输入输出的世界——f-string 到底有多好用,print 有哪些隐藏参数,变量命名有什么门道。
更多推荐
所有评论(0)