先给结论:这篇文章是给刚接触 Python 3 的人写的。为什么我要把"基本数据类型"拆成系列来讲?因为数据类型是 Python 一切计算的根基,但大多数教程喜欢一口气把所有类型堆给你,结果新手看完之后脑子里只剩一堆名词,真正写代码时照样不知道怎么选。这篇【1】先讲最核心的两大类——数值类型和字符串,把它们的底层逻辑、常见陷阱和实操写法一次说透。
1. 为什么数据类型是第一道绕不过去的坎
在所有编程语言里,数据类型回答的都是同一个问题:这块数据在内存里是什么形态、能做什么操作。Python 作为一个"动态类型"语言,变量本身不绑定类型,同一个变量可以先存整数再存字符串,这在带来灵活性的同时,也让很多新手养成了坏习惯——根本不关心数据到底是什么类型,直到某行代码报错才回头查。
我见过太多这样的场景:某个初学者写了个计算平均分的程序,从 input() 拿到的分数直接参与除法,结果 Python 告诉他字符串不能除以数字。这时候他才意识到,原来键盘输入进来的东西默认是字符串。所以说,在 Python 里可以不显式声明类型,但你心里必须时刻知道每个变量当前是什么类型。
从数据模型的角度看,Python 3 里的基本数据类型其实分两大类:一种是不可变类型,包括整数(int)、浮点数(float)、复数(complex)、布尔值(bool)、字符串(str)、元组(tuple);另一种是可变类型,包括列表(list)、字典(dict)、集合(set)等。这篇先讲不可变类型里的数值和字符串,因为它们是最基础、也最容易被忽略细节的部分。
我建议你开一个 Python 交互式环境(终端里输入 python3),跟着这篇文章的每一段代码敲一遍。只看不练是学不会 Python 的,敲的过程中你会产生很多"为什么",这些疑问本身就是最好的老师。
2. 数值类型:整数、浮点数、布尔值和复数
2.1 整数 int:无界是个大优势
很多从 Java 或 C 转过来的朋友,刚接触 Python 的 int 时都会有点不适应——Python 3 的整数没有长度限制。你在 Java 里 long 最多 2 的 63 次方减 1,在 C 里要考虑 int 和 long 的字节数,但在 Python 里你可以直接写:
big_num = 10 ** 100 print(big_num)这行代码会打印出一个 101 位的巨大数字,Python 并不会溢出。它采用的是大整数实现,当数值超过机器字长时自动切换到任意精度模式。这对日常业务来说非常省心,你永远不用操心"这个数会不会溢出"。
但省心的地方也藏着坑。Python 2 里整数除法是整除,5 / 2 结果是 2;Python 3 里除法统一返回浮点数,5 / 2 结果是 2.5。想要整除必须用双斜杠:
print(5 / 2) # 2.5 print(5 // 2) # 2 print(int(5 / 2)) # 2- 运算符是整除,它向下取整,注意负数的情况——7 // 2 是 3,但 -7 // 2 是 -4,因为 -3.5 向下取整是 -4。很多人在这里第一次碰到"向下取整"和"向零取整"的区别,这也是面试常见考点。
还有进制表示,Python 里可以在数字前面加前缀来切换进制:0b 开头是二进制,0o 开头是八进制,0x 开头是十六进制。处理内存地址、位运算、文件权限这类底层场景时很常用。转换方法也顺手记一下:
print(bin(10)) # 0b1010 print(oct(10)) # 0o12 print(hex(10)) # 0xa print(int('10', 8)) # 8,把八进制字符串转成十进制整数最后一个 int() 接收两个参数时,第二个参数是基数,这个在写解析逻辑时非常好用。
2.2 浮点数 float:精度问题不是 bug,是存储方式决定的
浮点数在 Python 里就是 C 语言的 double,64 位双精度。它最大的特点是:没法精确表示所有十进制小数。你在交互环境里输入 0.1 + 0.2,会得到 0.30000000000000004。我第一次看到这个结果时也怀疑过 Python 是不是算错了,后来去查了文档才知道,这是 IEEE 754 标准下的正常现象,几乎所有编程语言都这样。
0.1 在二进制里是一个无限循环小数,计算机只能截取一段有限位数的近似值来存,所以计算时自然会引入误差。如果你做的是金额计算或者需要精确小数的场景,千万不要直接用 float,应该用 decimal 模块:
from decimal import Decimal print(Decimal('0.1') + Decimal('0.2')) # 0.3注意 Decimal 的参数我用了字符串,不是数字 0.1。因为如果写成 Decimal(0.1),它会把 float 的近似值转进来,结果还是那个 0.30000000000000004。这是很多人踩过的坑。
浮点数判断相等也有讲究。不要写 if a == b,因为两个 float 计算后的结果可能是 0.1 的近似值和 0.1 的近似值之间差一点点。稳妥的做法是判断差值是否小于一个极小值:
if abs(a - b) < 1e-9: print("相等")科学计算里这个阈值通常叫 epsilon,选多少取决于你的精度要求。另外,格式化浮点数输出时,f-string 的写法最顺手:
pi = 3.14159265358979 print(f"{pi:.2f}") # 3.142.3 布尔值 bool:它是整数的一个子类
布尔值只有 True 和 False,但有个很多新手不知道的事实——在 Python 里 bool 是 int 的子类,True 等于 1,False 等于 0。这意味着 True + True 等于 2,True == 1 返回 True。这个设计是为了兼容早期 Python 的习惯,但现在一般不建议依赖这个特性去写算术运算,代码可读性太差了。
不过布尔值的"真值判断"倒是值得专门说。Python 里任何对象都可以直接当布尔值用,判断规则是:默认情况下,空的东西是 False,非空的是 True。比如空字符串 "" 是 False,非空字符串 "abc" 是 True;空列表 [] 是 False;数字 0 和 0.0 是 False,其他数字都是 True;None 永远是 False。
这带来一个实用的化简写法。判断一个列表是否为空,新手会写:
if len(my_list) == 0: print("列表是空的")老手直接写:
if not my_list: print("列表是空的")因为空列表是 False,取反就是 True,效果完全一样。这属于 Python 的惯用法(idiomatic style),等你读别人的代码时会非常常见。条件表达式里也可以用链式比较这种 Python 特有的写法:
age = 25 if 18 <= age < 30: print("青年")这种写法在其他语言里通常要写成 age >= 18 and age < 30,Python 把它简化成了一个直观的数学不等式,可读性好很多。
2.4 复数 complex:Python 是少数内置复数类型的语言
复数在 Python 里是原生支持的,写法是实部加虚部,虚部用 j 表示:
c = 3 + 4j print(c.real) # 3.0 print(c.imag) # 4.0j 是虚数单位,数学里通常用 i,Python 采用了电气工程领域的习惯。做信号处理、控制理论、数值计算的人会经常用到,比如用复数表示旋转向量、频域信号。如果只是普通业务开发,知道有这个东西就行,用到的时候再查文档。
3. 字符串 str:不可变的字符序列
3.1 四种写法怎么选
Python 3 里的字符串是 Unicode 字符串,也就是说它能存世界上几乎所有语言的字符,这比 Python 2 时代的天坑改善了太多。字符串的定界符有四种:单引号、双引号、三个单引号、三个双引号。
普通字符串用单引号和双引号都行,关键是字符串本身包含哪种引号。比如要表示 I'm a boy,用双引号包最省心;要用字符串中包含青"你好"这种内容,就用单引号包:
s1 = "I'm a boy" s2 = '他说:"你好"'如果两种引号都出现在字符串里,就得用转义字符反斜杠:
s3 = 'I\'m a boy, and she said "hello"'多行字符串用三个引号。它的原始用途是 docstring(函数文档),但很多人也拿它来写长文本模板:
message = """第一行 第二行 第三行"""注意多行字符串会保留换行符和缩进,如果你不希望字符串里包含开头的换行,可以在开头直接用反斜杠,或者用 textwrap.dedent 来处理缩进。这个细节在写命令行工具、生成报告文本时很实用。
3.2 切片:字符串操作里最该练熟的动作
字符串是字符序列,所以它支持索引和切片。索引从 0 开始,这个所有语言都一样;负索引从 -1 开始,表示从右往左数。切片语法是 s[start:stop:step],左闭右开——包含 start,不包含 stop。
记住"左闭右开"是减少 bug 的关键。很多人写 s[0:5] 以为取到第 5 个字符,其实取的是第 0 到第 4 个,一共 5 个字符。这个设计其实有其一致性考虑:因为 s[:n] 表示前 n 个字符,跟内置函数 len() 配合得很自然;两个切片 s[a:b] 和 s[b:c] 拼接起来就是 s[a:c],不会重复也不会漏。
实用的切片例子我列几个:
s = "abcdef" print(s[0]) # 'a' print(s[-1]) # 'f' print(s[1:3]) # 'bc' print(s[:3]) # 'abc',前三个 print(s[3:]) # 'def',从索引3到结尾 print(s[::-1]) # 'fedcba',反转字符串最后一个 s[::-1] 是反转字符串的经典写法,step 为 -1 表示从右往左取值。很多算法题里判断回文串,直接 return s == s[::-1] 一行搞定。
3.3 字符串不可变到底意味着什么
字符串是不可变对象,这意味着你不能修改已有字符串的某个字符。s[0] = 'x' 会直接报 TypeError。你可能觉得这个限制很烦,但它有一个大好处:字符串可以作为字典的键,可以被哈希(hashable),在多线程环境里共享也不用担心被修改。哈希的前提就是对象不可变,如果字符串可变,它作为 dict 键的哈希值就得跟着变,整个查找结构就崩了。
既然不能原地修改,那"修改字符串"实际上是生成一个新字符串。比如想替换字符:
s = "hello" s = s.replace("l", "L") # 变成 "heLLo"replace 返回的是新字符串,如果原字符串还在别处被引用,它会保持原样。搞清楚这一点,你才不会在函数返回字符串时犯迷糊。
字符串拼接也有性能讲究。少量拼接用 + 就行;循环里大量拼接,用 join 而不是 +=:
parts = [] for i in range(100): parts.append(str(i)) result = ",".join(parts)因为字符串不可变,每次 += 都会创建新字符串、复制旧内容,循环 1000 次就是 1000 次复制,效率很低。而 join 一次性申请内存、一次性拼接,性能差距在数据量大时非常明显。这属于那种"不遇到性能问题不会主动改"的细节,但面试和代码评审里经常被问到。
3.4 format 和 f-string:字符串格式化的发展脉络
早期写 Python 的人用 % 占位符,后来出了 format 方法,Python 3.6 之后又引入了 f-string。我的建议是:直接学 f-string,别在 % 上花太多时间。
f-string 的基本写法是在字符串前加 f 或 F,然后用花括号 {} 写表达式:
name = "张三" score = 92.5 print(f"{name} 的得分是 {score:.1f}")花括号里可以直接放变量,可以放函数调用,可以放运算表达式,甚至能做简单的格式化控制。:.1f 表示保留一位小数。想输出花括号本身就用双花括号转义:
print(f"{{}}") # 输出 {}format 方法依然存在于大量老代码里,所以你要能看懂它,但写新代码用 f-string 更简洁。还有一个不太起眼但很实用的细节:f-string 里也可以嵌套格式控制,比如动态指定保留位数:
precision = 3 value = 3.14159 print(f"{value:.{precision}f}") # 3.1423.5 常用方法速查:不是让你背,是让你知道有这些工具
字符串方法非常多,我不建议新手去背,但你至少要看一遍、知道"有这么个东西",等需要时再查文档。我按使用频率帮你划重点:
- 大小写类:upper()、lower()、title()、capitalize()、swapcase()
- 查找类:find() 返回子串第一次出现的索引,找不到返回 -1;index() 找不到会抛异常;count() 统计出现次数
- 判断类:startswith()、endswith()、isalpha()、isdigit()、isspace()。注意 isdigit() 对 "123" 返回 True,对 "12.3" 返回 False,因为 12.3 不是整数型数字格式
- 清洗类:strip() 去除首尾空白或指定字符;lstrip()、rstrip() 只去一边
- 拆分与合并:split() 按指定分隔符拆成列表,rsplit() 从右往左拆;join() 是它的逆操作
- 替换与翻译:replace("旧", "新", 次数);translate() 用于批量字符映射
有个经典坑是 split() 不传参数时的行为:不传参数时它按空白字符(空格、换行、制表符)拆分,并自动压缩连续的分隔符;传了参数则严格按该字符串拆分。比如 "a b c".split() 得到 ['a', 'b', 'c'],而 "a b c".split(" ") 得到 ['a', 'b', '', 'c'],中间会留空字符串。这个细节在处理读取文本文件的场景里经常让人栽跟头。
4. 类型转换:int()、float()、str() 的边界条件
4.1 从字符串到数值
int() 可以把合法数字字符串转成整数,但两个细节容易出错:
print(int("10")) # 10 print(int("10.5")) # ValueError print(int(" 10 ")) # 10,会自动去掉首尾空白 print(int("0x0A", 16)) # 10,按16进制解析int("10.5") 会报错,因为 int() 不负责处理小数格式字符串;你如果确实想把"10.5"这个字符串变成整数 10,得先转 float 再转 int:
num = int(float("10.5")) # 10float() 的容忍度高一些,float("10.5") 和 float("1e3") 都合法,前者 10.5,后者 1000.0。这里面其实涉及两个层面的转换:字符串到浮点数,浮点数的精度损失同样存在,float("0.1") 得到的也是一个近似值,只是它打印出来看起来是 0.1 而已。
4.2 从数值到字符串
str() 直接把数值转成字符串,适合拼接场景。f-string 其实内部也是调用了 str()。如果你想控制格式,比如保留两位小数,用 f-string 或者 format 更容易,str() 没有格式控制参数。
4.3 bool 参与转换的特殊性
bool 是 int 的子类,所以 bool 到 int 的转换是隐式合法的:int(True) 得到 1。但反过来,你很少需要把整数转成 bool——直接用它做条件判断就够了。唯一需要注意的场景是你想要一个"整数 0/1"来表示开关状态,但原始数据是字符串"True"或"False",这时候直接 bool("False") 得到的是 True,因为非空字符串都是 True。正确做法是:
flag = "False" flag_bool = flag == "True"这个坑我在配置文件解析时踩过不止一次,写配置解析工具时特别容易遇到。
5. 新手最常踩的五个坑,以及事后总结的检查习惯
说着说着就到收尾了。前面每个小节都穿插了一些坑,但你实际写代码时还是会以更综合的方式碰到它们。我把自己带新人时最常见的问题集中列出来,对照着自查一遍,很多报错就能少犯:
第一个坑是类型不分。所有 input() 拿到的值都是字符串,如果你要算平均数,记得先 float() 或 int() 转换。这是新手报错频率最高的原因之一。
第二个坑是除法结果理解错误。Python 3 的 / 永远是浮点除,// 才是整数除。想从 7 个苹果里平均分给 3 个人,要算"每人几个"用 //,要算"每人占多少比例"用 /。
第三个坑是忘记字符串不可变。调用了 upper() 或者 replace() 却没用变量接住返回值,结果发现字符串根本没变。要记住:不可变类型的方法基本都会返回新对象,你需要接收这个返回值。
第四个坑是索引和切片越界。单个索引 s[100] 越界会抛 IndexError,但切片 s[1:100] 不报错,它最多取到字符串末尾。这种"切片更宽容"的行为有时候是好意,有时候反而掩盖了逻辑错误,你需要自己判断。
第五个坑是浮点数比较。这个前面说过,判断两个 float 是否相等,要用差值绝对值小于某个阈值的方式,别直接用 ==。
我在教新人的时候经常建议他们养成两个习惯:第一,在交互环境里用 type() 查看变量类型,不确定类型就先看一眼;第二,写代码前先问自己"这个变量现在是什么类型,我要进行的操作支持这种类型吗"。别小看这两个习惯,大多数类型相关的报错都能靠它们提前发现。
到这里,数值类型和字符串就聊完了。下一篇【2】里,我打算讲列表、元组和字典这组容器类型,它们是业务代码里最常用的数据结构,到时候我们继续。