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):程序的开关

布尔类型只有两个值:TrueFalse。注意首字母必须大写——写成 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

  • 00.0(数字零)
  • ""(空字符串)、[](空列表)、{}(空字典)
  • None
  • False 本身

其他一切(非零数字、非空字符串、非空列表等等)都是 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(假值)

  • False00.00j(复数零)
  • ""(空字符串)、[](空列表)、()(空元组)、{}(空字典)、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 前三个月遇到的所有报错里,大概有一半跟类型有关——TypeErrorValueErrorAttributeError……如果你能一眼认出"哦,这是把字符串当数字用了",你定位问题的速度比不知道类型的人快五倍。

第二,读懂别人的代码需要类型意识。 当你调用一个别人写的函数,你怎么知道它返回什么?文档可能会写,但你也可以这样推测:

def get_student_score(name):
    # 函数名包含 "score" 和 "student"
    # 很可能是通过 name 查分数
    # 返回什么?大概率是 float 或 int
    pass

有类型意识的人看函数名就能猜出参数和返回值的大致类型,这能大幅提高阅读代码的速度。

第三,写代码时类型对了,Bug 直接减半。 很多 bug 的根源是:“我以为这是个数字,结果传进来的是个字符串”。如果你写每一行代码时都清楚当前变量的类型,这种低级错误就不会发生。

动手做

  1. 定义一个变量存你的名字,再定义一个存你的年龄,用 type() 打印它们各自的类型。然后把年龄改成字符串类型,再次打印类型——感受动态类型的灵活性。
  2. 计算 0.1 + 0.2 == 0.3 的结果,再用 math.isclose(0.1 + 0.2, 0.3) 计算一次。理解为什么前者是 False 而后者是 True。
  3. 写一个程序,让用户输入两个数字(用 input()),把它们相加并输出。注意:先别转类型,看看报什么错——然后加上 float() 转换,再运行。
  4. 创建一个字符串 " Python ",先后用 .strip().upper().replace("P", "J") 处理它,每次处理后打印结果。观察每个方法返回的是新字符串还是修改了原字符串。

本篇要点回顾

  • Python 有五大数据类型:int、float、str、bool、NoneType
  • 整数任意大,浮点数不要用 == 比较——用 round()math.isclose()
  • 字符串不可变,"修改"其实是创建新字符串;单双引号没区别
  • input() 返回的永远是 str,想当数字用必须 int()float() 转换
  • 布尔值有短路求值,真值测试覆盖了零值、空值、空容器和 None
  • None 表示"没有值",检查用 is None 不要用 == None
  • isinstance()type() == 更好,以后写判断类型时优先用它
  • 变量是标签,对象是箱子——可变对象共享时要当心

下一篇我们进入变量、注释和输入输出的世界——f-string 到底有多好用,print 有哪些隐藏参数,变量命名有什么门道。

更多推荐