ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python基本数据类型全解析:从动态类型到类型转换的实用指南

Python基本数据类型全解析:从动态类型到类型转换的实用指南 很多刚开始学 Python 的朋友都有过这种经历写完几行代码信心满满地一运行结果控制台给你甩来一句TypeError: can only concatenate str (not int) to str。报错里的字符串、整数、拼接这些词你都认识但摆在面前就是想不通——我明明只是想拼一句话它怎么就翻脸了这类问题背后几乎全部指向同一个主题Python 的基本数据类型。这篇文章就把 Python 基本数据类型这件事聊透。我会从“动态类型”这个最容易被忽视的底层概念开始把数值、字符串、列表、字典、集合这些核心类型逐个拆开看再讲清楚类型转换和类型判断的完整套路。不管你是刚配置好 Python 环境准备入门的新手还是写过一阵子脚本、但对“可变不可变”“深拷贝浅拷贝”这些概念还似懂非懂的同学这篇文章都能让你少走一点弯路。好我们开始。1. 数据类型是整个 Python 学习的第一道分水岭1.1 动态类型变量只是“贴标签”不是“装东西的盒子”很多教程会把变量比作“盒子”把数据放进盒子。这个比喻在 C 语言、Java 里还算贴切但在 Python 里它可能会误导你。在 Python 里一个变量名本质上只是一个“标签”或者说一个指向对象的引用。真正有类型的是“对象”而不是“变量名”。同一个变量名可以先指向一个整数对象再指向一个字符串对象这在 Python 中完全合法a 42 print(type(a)) # class int a hello print(type(a)) # class str这就是我们常说的“动态类型”变量的类型是在运行时由它指向的对象决定的而不是在声明时固定的。Python 编译时不会告诉你a到底应该是整数还是字符串只有运行到那一行它才会去检查。这个机制的代价就是你容易在类型的“不匹配”上踩坑比如把字符串和数字做拼接或者把一个字符串传给一个需要元组的函数。理解这一点之后你会明白报错不是 Python 在耍脾气而是你让一个对象去做了它类型不允许的事情。1.2 看清“类型”和“身份”type() 和 id() 怎么用我刚学 Python 的时候老师教的第一句调试咒语就是“打印出来看看”。看什么呢最值得看的两样东西就是类型和身份type(x)返回 x 的类型id(x)返回 x 在内存中的身份编号这两个函数在排查类型问题的时候特别管用。比如你怀疑一个变量的类型不对直接print(type(x))就知道它是 str 还是 list 还是别的什么。id()则更适合判断“对象到底是不是同一个”这在后面讲不可变类型、拷贝问题时会反复用到。看一个经典的例子a [1, 2, 3] b a print(id(a)) # 两个 id 打印结果相同 print(id(b)) b.append(4) print(a) # [1, 2, 3, 4]a 也变了把b a理解为“把 a 的盒子复制一份给 b”的话上面的结果会让你完全懵掉。但换成“贴标签”的理解就顺了a和b两个标签贴在同一个列表对象上你通过任何一个标签去修改这个对象另一个标签看到的当然也是修改后的结果。1.3 一张总览图看懂 Python 内置类型的分家结构Python 基本数据类型到底有多少种说实话不用背但心里要有张地图。大类类型可变性典型用途数值int、float、complex不可变数字计算、科学计算序列str、list、tuple、rangestr/tuple/range 不可变list 可变文本、数组、坐标记录映射dict可变键值对应、查询表集合set、frozensetset 可变frozenset 不可变去重、集合运算布尔与空bool、NoneType不可变逻辑判断、空值标记注意一个容易混淆的点bool其实是int的子类True和False在某些语境下会表现得像1和0。这个细节平时没啥存在感但遇到sum([True, False, True])这种写法时结果会是2很多人会当场愣住。后面几个章节我会按这个地图逐个深入。2. 数值三兄弟int、float、complex 的边界与坑2.1 int没有上限的整数Python 最被低估的天赋如果你是从 C 语言或者 Java 转过来的第一个让你觉得“居然还有这种好事”的一定是 Python 的int。在大多数编译型语言里整数是有固定位数的比如 C 的 int 一般是 32 位Java 的 long 是 64 位超过上限就会溢出程序直接发疯。但 Python 的 int 是任意精度整数你想写多长就写多长Python 会自己扩容big 2 ** 1000 print(big)这一行代码在 C 里早就溢出了在 Python 里却能把一个 300 多位的数字完整打印出来。对初学者来说这可能只是个花活但对涉及大数运算的场景比如 RSA 加密实验、阶乘计算、精度较高的金融模拟来说直接省掉了自己实现大数库的痛苦。不过int 和 float 运算时有一个行为要记住int 和 float 混合运算结果会变成 float。比如5 / 2拿到的是2.5而不是2想要整除要用//想要余数用%。Python 在 int 和 float 之间转换时会尽量“向上提升”到精度更高的类型这是隐式转换的基本原则后面第五节会细讲。2.2 float0.1 0.2 不等于 0.3这不是 bug是固有限制几乎所有 Python 新手都会被这个经典问题吓一跳print(0.1 0.2) # 0.30000000000000004这个现象不是 Python 的 bug而是所有采用 IEEE 754 浮点数标准的语言共同的宿命。二进制没法精确表示很多十进制小数就像十进制没法用有限位数精确表示 1/3 一样。计算机里的 float 是二进制浮点数0.1 这个十进制小数在二进制里是个无限循环小数存的时候就丢了一点精度运算之后误差再累积一点最后打印出来的就是那一串 00000000000000004。如果只是做普通的数值计算这个误差通常无关痛痒。但如果你在处理金额、利率、计数这类对精度敏感的数据千万别直接用 float。实际工作里我的经验是涉及钱的计算用decimal.Decimal并明确指定精度涉及大量科学计算要求速度float 没问题但要避免用去判断浮点相等判断两个浮点数是否“差不多相等”时用abs(a - b) 1e-9这样的容差判断而不是直接a b。举个例子from decimal import Decimal price Decimal(0.1) quantity Decimal(3) total price * quantity print(total) # 0.3不会出现 0.30000000000000004注意Decimal 的构造参数建议传字符串而不是浮点数Decimal(0.1)反而会把浮点里已经丢失的精度带进去。2.3 complex 和 bool两个容易被忽略的“特殊学生”complex在基础教程里经常被一笔带过但用好了非常方便。Python 里直接写3 4j就是一个复数对象注意虚部单位是j不是ic 3 4j print(c.real) # 3.0 print(c.imag) # 4.0 print(abs(c)) # 5.0模长复数在信号处理、图像处理、量子力学模拟这些领域是常态需求Python 内置了 complex省去你安装第三方库的功夫。日常写脚本你也许用不到但记住它的存在将来遇到相关需求时就不会觉得 Python 干不了。bool更要单独说一句。因为它是int的子类所以True本质是1False本质是0。这带来一个非常实际的坑当你用isinstance(x, int)去判断一个变量是否是整数时布尔值也会返回 True。这在某些数据清洗场景下会闯祸——你明明想筛出整数结果一堆 True/False 也被当成整数放过去了。所以严格判断时要写成type(x) is int或者把bool排除掉。3. 字符串与布尔值不可变类型的“一次定终身”3.1 字符串为什么不可变先搞懂再背结论Python 里字符串是“不可变类型”意思是字符串对象一旦创建你就没法修改它的内容。你只能创建一个新的字符串再把这个新字符串赋值给变量。s hello # s[0] H # TypeError: str object does not support item assignment s s[0].upper() s[1:] # 生成新字符串 Hello print(s)很多人不理解为什么要有这种限制。其实不可变带来的好处非常实际第一字符串可以被安全地哈希。哈希要求对象在生命周期内不变字符串因此可以作为字典的键、放进集合。第二多个变量共享同一个字符串时不用担心一个地方改了影响全局。第三解释器和优化器可以在后台做缓存和复用内存更省。同样的道理也适用于tuple和frozenset。记住一个判断口诀可变的东西不能去当字典键不能进集合不可变的东西则可以。3.2 字符串操作的高频方法别只会拼字符串新手写字符串最容易的写法是到处用拼接。这种方式在代码量少时没啥问题但在循环里拼接长文本时性能会变得很难看因为每拼一次都可能创建一个新字符串。工程上常见的更优做法是把片段收集到列表里最后用join一次性合成parts [2025, 05, 20] print(-.join(parts)) # 2025-05-20字符串自带的方法里我觉得最值得优先掌握的是一张“高频清单”split()按分隔符拆成列表strip()去掉首尾空白或指定字符replace()替换子串startswith()/endswith()判断前后缀常用于文件名处理和链接判断find()/index()定位子串找不到时 find 返回 -1index 抛异常format()和 f-string格式化输出f-string 是我现在最推荐的格式化方式从 Python 3.6 开始可用写法直观name 张三 score 89.5 print(f{name} 的得分是 {score:.1f}) # 张三 的得分是 89.5花括号里可以直接放表达式还能指定格式比如:.1f表示保留一位小数。这比老式的%格式化和.format()都更贴近人的读写习惯。3.3 None、True、False别用判断“是不是 None”NoneType 只有一个值就是None。它表示“这里什么都没有”不是 0不是空字符串也不是 False。很多新手写判断时习惯写if x None:技术上能跑但更规范、更稳的写法是if x is None:。原因是 None 在 Python 里是一个单例对象全程序只有一个。用is比较的是“是不是同一个对象”对这种单例来说是语义最准确的。反过来比较的是“值相等”在某些自定义类的场景下可能被重载产生意外行为。同理判断 True/False 也建议用is True/is False只是现实中更常见的是直接if x:这种真值判断。另外记住空字符串、空列表[]、空字典{}、数字0、None、False在布尔语境下都是假值。这是 Python 真值测试的规则写 if 判断时极其常用。4. 四大容器类型list、tuple、dict、set 怎么选4.1 list 和 tuple一个可变一个不可变选型依据很清晰list和tuple在外观上极像都是有序序列都支持索引和切片。最大的区别只有一条list 可变tuple 不可变。lst [1, 2, 3] lst.append(4) # 可以 lst[0] 0 # 可以 tup (1, 2, 3) # tup.append(4) # AttributeError # tup[0] 0 # TypeError选型逻辑其实很清晰如果你需要动态增删改用 list如果你要表示一组“定下来就不该变”的数据比如一个坐标点(x, y)、一个数据库查询结果的单行记录、函数的多个返回值用 tuple 更合适。tuple 因为不可变可以作为字典键还能被打包和解包。tuple 的解包特性我特别推荐大家习惯使用point (3, 5) x, y point print(x, y) # 3 5函数返回多个值时Python 实际返回的就是一个 tuple接收的时候直接a, b func()拆开这是非常 Pythonic 的写法。有一个细节提醒tuple 的“不可变”指的是它不能增删改元素但如果 tuple 里放了 list这个 list 本身还是可变的。t ([1, 2], 3)里t[0].append(3)是合法的别被“tuple 不可变”这个结论骗了。4.2 dict哈希表背后的“键值宇宙”dict大概是 Python 里使用率最高、也最值得深入研究的一种数据类型。它的底层是哈希表给定一个键先算出它的哈希值再通过哈希值快速定位到存储位置。所以 dict 的查找速度在数据量大的时候也接近 O(1)。这也引出 dict 的核心规则键必须是可哈希的。整数、字符串、tuple元组内元素也得全部可哈希都可以当键list、dict、set 这种可变类型不行。如果你试图用列表当键Python 会立刻报错# d {[1, 2]: x} # TypeError: unhashable type: list日常操作 dict除了最基础的d[key] value和d.get(key)我建议掌握几个很能省事的 APIsetdefault(key, default)键不存在时设置默认值并返回存在则返回现有值适合做计数统计get(key, default)读取不存在的键时返回默认值而不是抛 KeyErroritems()/keys()/values()遍历时最常用dict.pop(key, default)取出并删除键适合做状态管理。举一个用 dict 做统计的例子words [apple, banana, apple, cherry] counter {} for w in words: counter[w] counter.get(w, 0) 1 print(counter) # {apple: 2, banana: 1, cherry: 1}4.3 set去重、成员判断和集合运算的三合一列表去重很多新手第一反应是写一层 for 循环再判断。Python 其实早就内置了set帮你干这件事data [3, 1, 3, 2, 1] unique list(set(data)) print(unique) # [1, 2, 3]set 和 dict 一样基于哈希表所以它也要求成员必须是可哈希类型。它的强项不只是去重还有极快的成员判断x in set的平均时间复杂度是 O(1)而x in list是 O(n)。当你在大数据集合里反复判断某个元素是否存在时把数据转成 set 往往是立竿见影的优化手段。集合运算也是 set 独有的优势a {1, 2, 3, 4} b {3, 4, 5, 6} print(a b) # 交集 {3, 4} print(a | b) # 并集 {1, 2, 3, 4, 5, 6} print(a - b) # 差集 {1, 2}两个需要注意的点第一set()是函数写法{1, 2, 3}是字面量但空的集合只能用set()写{}得到的是空 dict这个坑我见过太多次第二set 是无序的迭代顺序不保证想要既唯一又有序的最简单办法是list(dict.fromkeys(data))这个技巧在保持原顺序去重时很实用。5. 类型转换与判断从隐式到显式的完整指南5.1 隐式转换Python 帮你做的未必是你想要的隐式转换发生在你什么都没写Python 自己把一种类型转成另一种类型的时候。最常见的场景是数字类型之间的提升print(1 2.5) # 3.5int 被提升为 float print(True 2) # 3bool 被当作 int看上去很方便但隐式转换也有翻车的场景。比如字符串和数字拼接# print(结果 42) # TypeError: can only concatenate str (not int) to strPython 在这里不会帮你把数字隐式转成字符串因为它觉得“猜你意图”的风险太大。遇到这种情况正确做法是你自己显式转结果 str(42)或者更简洁地用 f-string。我的经验是依赖隐式转换越少越好。代码里如果某个值可能在不同类型之间切换最好在入口处就显式转换后面统一处理。这样虽然多写两行但排查问题时节省的时间是几倍。5.2 显式转换几个常用函数和它们的小脾气显式转换就是调用类型名当函数用常见的一组是int(x)转整数float(x)转浮点数str(x)转字符串list(x)转列表tuple(x)转元组set(x)转集合这套函数看起来简单但细节坑不少。先说int()它可以直接解析字符串形式的整数比如int(42)得到 42但int(42.5)会抛 ValueError因为字符串里的42.5不是整数格式。想从字符串转浮点数得先float(42.5)拿到 42.5再转 int 才合理。再说list()和tuple()。字符串转列表时Python 会把每个字符拆开当成一个元素print(list(hello)) # [h, e, l, l, o]如果你想把字符串按分隔符拆成多个段应该用split()而不是list()。两者看起来都在制造列表语义完全不同。布尔值转数字也是个常见的意外源int(True)是 1int(False)是 0。反过来bool()是 False而bool(False)是 True——因为非空字符串是真值。这几个对应关系如果不清楚写配置解析、表单校验的时候很容易出现“明明填了 False 却被当成 True”的诡异问题。5.3 类型检查isinstance 和 type别再用错判断一个变量是什么类型有两个函数各有各的使用场景。type(x) int这种写法做“精确匹配”是可以的但它不认继承关系。前面说过bool是int的子类所以type(True) int是 False这个结果有时正是你想要的有时不是。isinstance(x, int)则会把子类也算进去isinstance(True, int)是 True。这在大多数业务场景中是更合理的检查方式因为继承意味着“is-a”的关系。比如你定义了自定义异常捕获时用isinstance(e, MyError)就能连子类异常一起捕获。实际项目中我的习惯是校验函数参数类型、做分支逻辑时优先isinstance并且尽量给出一组可接受的类型比如isinstance(x, (int, float))需要精确识别类型本身比如处理布尔和整数的差异时才用type(x) is int更重要的是能用“鸭子类型”的地方就别硬查类型——Python 社区推崇“如果它走路像鸭子、叫起来像鸭子那就是鸭子”很多函数其实只要对象具备某个方法就能工作硬性类型检查反而会限制扩展。6. 实操中的类型坑与排查思路6.1 可变默认参数一个能让你怀疑人生的“经典 bug”Python 函数定义时允许给参数设置默认值比如def add_item(item, container[]): container.append(item) return container这段代码看起来没问题每次不传 container 时应该得到一个空列表往里加。但实际上运行两次之后你会发现上一次的结果“残留”下来了print(add_item(a)) # [a] print(add_item(b)) # [a, b]原因在于默认值[]在函数定义时就被创建了一个列表对象之后所有不传该参数的调用都会复用这同一个列表对象。这又一次印证了第一节“变量是标签”的道理你以为每次拿到的是新列表其实拿到的都是同一个对象的引用。标准解法是默认值用None在函数体内部再创建新列表def add_item(item, containerNone): if container is None: container [] container.append(item) return container可变默认参数这个坑几乎每个 Python 开发者都会踩一次。踩过之后你就记住了函数默认值建议只用不可变类型比如None、整数、字符串、tuple。6.2 浅拷贝与深拷贝为什么 list.copy() 还是不够前面用b a展示了“贴标签”的问题。那想要一份真正的副本怎么办很多人会用切片或者list.copy()a [1, 2, 3] b a.copy() b.append(4) print(a) # [1, 2, 3]这回 a 没变但如果列表里的元素本身也是可变对象事情又来了a [[1, 2], [3, 4]] b a.copy() b[0].append(99) print(a) # [[1, 2, 99], [3, 4]]a 内部还是被改了copy()和切片都属于“浅拷贝”只复制了最外层内层的子列表对象依然是被共享的。要想彻底复制所有层级的数据结构要用copy.deepcopyimport copy a [[1, 2], [3, 4]] b copy.deepcopy(a) b[0].append(99) print(a) # [[1, 2], [3, 4]]没被改动深拷贝不是免费的午餐它在大型数据结构上会明显消耗时间和内存所以也不是所有地方都该用。判断标准很简单如果你打算修改嵌套的可变对象且不希望影响原数据就用 deepcopy如果只是最外层操作浅拷贝就够。6.3 报错信息的快速定位TypeError 不是玄学最后分享一个排查类型的实用套路。当代码抛 TypeError 时不要急着改逻辑先把报错信息读完整里面有大量线索unsupported operand type(s) for : int and str意思是 int 和 str 之间不能用加号大概率是你把数字和字符串混在一起运算了list object is not callablelist 不能像函数那样被调用多半是变量名把内置函数list覆盖掉了unhashable type: listlist 不可哈希多半是拿去当字典键或放进了集合。我自己的排查三部曲是这样的先在出错的那行前后加print(type(x))把每步的类型打出来再看报错信息里提到的“参与运算的双方”各自是什么类型最后思考一下我是不是在某个地方依赖了隐式转换或者把可变对象放进了只允许不可变对象的位置。这一套流程走下来百分之八十的类型报错都能在几分钟内定位剩下的百分之二十往往就属于深拷贝、默认参数这类“看起来和类型无关本质还是对象共享”的问题了。另外写测试的时候可以刻意去验证边界类型空列表、空字符串、None、布尔值、浮点数这些“边缘值”往往是类型问题的高发区。把这些情况在本地跑一遍比等线上出 bug 再崩溃要舒服太多了。我这两年带人入门 Python发现凡是基础扎实的几乎都是先把数据类型的“底层性格”摸清楚了——哪个可变哪个不可变哪个能当键哪个不能哪种转换会静默丢精度。这些知识不炫技也不花哨但会在你写每一个循环、每一个函数、每一次数据处理时反复用到。就像盖房子先打地基看起来慢实际上是最快的路。希望你读完之后也能少踩几个我当年踩过的坑。
返回列表