很多刚开始学Python的朋友都有过这种经历明明照着教程把代码一行行敲进去了回车一跑不是报TypeError就是报ValueError要不然就是数字拼成了字符、字符串永远带着引号。反复折腾几轮之后发现问题往往不在语法而在基础的数据类型没搞清楚。数据类型这种东西看起来只是变量存什么内容但实际上它会直接影响你能调用哪些方法、运算返回什么结果、甚至程序跑不跑得起来。可以说学Python的第一道门槛不是变量和函数而是数据类型。这篇内容就是专门把Python里最常用的几种基本数据类型拆开揉碎讲清楚包括数字类型、字符串、布尔值、None以及在写代码时几乎必然会遇到的类型转换问题。后面还会聊到可变与不可变这条关键分界线——它决定了你理解列表、元组、字典这些容器类型的方式。适合零基础入门的朋友也适合那些已经能写点简单脚本、但经常在类型上翻车的新手。看完之后你至少能搞清楚三件事自己的代码里每个变量到底是什么类型、不同类型之间怎么安全转换、以及为什么有些操作会报错。1. 先搞清楚为什么程序里非要分类型1.1 类型本质上是给计算机的使用说明书很多人不理解电脑运行不就是在算0和1吗为什么要分字符串、整数、浮点数这么多花样其实可以这样想你给变量赋了一个值这个值在内存里就是一串二进制数据。问题是计算机光知道这串二进制数据还不够它得知道该拿这段数据当什么来用。比如二进制序列01000001如果按整数来解析是65按字符来解析是大写字母A如果按图片颜色值来解析可能又是一个完全不同的小数。类型就是告诉Python解释器这段数据你要按什么规则去读、能做什么操作、不能做什么操作。没有这种区分数据在内存里就是一团无法解释的混乱。这也是Python被称为动态类型语言的原因。变量本身没有固定类型类型是跟着你赋的值走的。同一个变量名今天可以存整数明天可以存字符串这在Python里完全合法。好处是写起来灵活坏处是新手经常在事后才发现我这里的变量已经不是我以为的那个类型了。1.2 Python内置的基本类型清单Python官方手册把内置类型列了一大串但对入门阶段来说真正绕不开的基本类型其实就这几个整数类型int表示整数比如1、-5、1000。Python的整数理论上可以无限大不会像C语言那样有上限溢出。浮点数类型float表示带小数点的数字比如3.14、-0.5。注意中文里习惯说浮点数其实就是我们常说的小数。字符串类型str表示文本比如你好、hello。任何放在引号里的内容都是字符串。布尔类型bool只有两个值True和False专门用来表示真假。空值NoneType只有一个值None表示这里什么都没有注意它不是0也不是False。除此之外列表、元组、字典、集合也属于Python的内置数据类型但它们属于容器类型是用来装基本类型和其他数据的一组群组我放到后面再讲。1.3 用 type() 快速确认类型写代码时如果拿不准变量到底是什么类型最直接的办法就是用内置函数type()查看。我自己调试代码时非常喜欢在关键位置打印一下类型特别是新手写代码习惯性怀疑变量类型有问题是好事。a 10 b 3.14 c hello d True e None print(type(a)) # class int print(type(b)) # class float print(type(c)) # class str print(type(d)) # class bool print(type(e)) # class NoneType看到输出里的class不要慌在Python的体系里类型就是用类来实现的。type(a)返回int意味着变量a就是一个整数实例。后面你会慢慢习惯这种万物皆对象类型即类的思维方式。2. 数字三兄弟int、float、complex 的运算习惯与精度陷阱2.1 整数和浮点数在运算时的自动升级数字类型里入门最常用的是int和float。它们俩在运算时有一个很自然的规则整数和整数做运算通常得到整数但只要有一个操作数是浮点数结果就会自动变成浮点数。print(7 / 2) # 3.5除法永远返回浮点数 print(7 // 2) # 3整除取整数部分 print(7 % 2) # 1取余数 print(7 * 2) # 14整数 print(7 * 2.0) # 14.0浮点数这里有个特别容易让新手懵的地方/和//的区别。在Python 3里/做的是真正的除法哪怕两个整数相除也返回浮点数//才是整除只保留整数部分。如果你想判断一个数能否被另一个数整除看x % y 0是否成立这个写法在找数字规律时非常常用。2.2 浮点数精度问题为什么 0.1 0.2 不是 0.3这应该是Python里最出名的坑之一。运行0.1 0.2结果是0.30000000000000004。第一次见的人通常都很震惊计算机怎么还会算错原因是浮点数在内存里用的是二进制表示小数的方式而二进制无法精确表示某些十进制小数比如0.1在二进制里是一个无限循环小数存下来的时候只能截断到有限的精度。不要觉得这是Python的BugJava、JavaScript、C语言全都一样这是计算机科学的通用问题。在实际开发中如果你要处理金额、分数这类对精度要求高的数据绝对不要用float直接运算。常见做法是用Decimal模块或者把金额换算成最小的整数单位比如把元换成分。但如果是日常学习、算法练习、科学计算的一般场景浮点数的微小误差通常可以忽略影响不大。2.3 常用数字函数与运算技巧入门阶段有很多内置函数会频繁出现在这里集中列一下方便对照着用abs(x)取绝对值整数浮点数都能用。round(x, n)四舍五入保留n位小数。不过要注意Python的四舍五入是银行家舍入碰到5这种边界值时会取偶数方向。比如round(0.5)是0round(1.5)是2。pow(x, y)或x ** y幂运算。divmod(x, y)同时返回整除结果和余数比如divmod(7, 2)返回(3, 1)。bin(x)、oct(x)、hex(x)分别把整数转成二进制、八进制、十六进制字符串显示刷算法题和做嵌入式相关学习时会用到。我自己在初学阶段特别喜欢用divmod因为很多题目会同时要求商和余数一行代码就能拿到两个结果既简洁又不容易漏写。2.4 复数可以先不用太在意的类型Python里有complex复数类型比如2 3j。在数据分析、信号处理等方向会用到但新手刚入门时基本碰不上。你只需要知道如果一个数字后面带j那它就是复数比如3j是纯虚数。真到了需要用的阶段再深入学习也不迟没必要在一开始就给自己增加认知负担。3. 字符串从引号到切片Python里最全能的基本类型3.1 三种引号到底怎么选字符串大概是Python里使用频率最高的类型。定义一个字符串可以用单引号hello可以用双引号hello也可以用三引号多行文本或多行文本。许多人一开始纠结到底用哪种引号其实很简单单双引号在绝大多数场景下没有区别只要保证字符串本身如果包含某种引号时就用另一种引号包裹即可。s1 I am a programmer s2 Pythons syntax is easy s3 第一行 第二行 第三行三引号最大的价值就是多行文本。你在写函数说明文档、生成多行回复、保留排版的原样输出时用三引号可以直接换行不用在字符串里写一堆\n阅读体验好得多。3.2 索引与切片学会像切片机一样处理文本字符串本质上是一个字符序列这句话值得认真理解。因为它是序列所以可以按下标取字符也可以切割出子串。text Python print(text[0]) # P下标从0开始 print(text[-1]) # n负数表示从末尾往前数 print(text[0:3]) # Pyt切片 左闭右开 print(text[1:]) # ython从下标1到末尾 print(text[::2]) # Pto步长为2每隔一个取一个切片语法[start:stop:step]是Python里用得非常多的操作。新手最大的坑就是左闭右开text[0:3]取的是下标0、1、2不包含3。记住这句话能少踩很多坑切片包含起始位置不包含结束位置。另外负步长可以让字符串倒序输出比如text[::-1]返回nohtyP这是判断回文串时最简单的写法。3.3 字符串是不可变的这个性质很重要字符串有一个特性不可变。意思是字符串一旦创建内部的字符就不能修改。你可以重新给变量赋值但你没法改写原来那个字符串对象本身。s hello # s[0] H # 这行代码会报错 TypeError s s[0].upper() s[1:] # 正确方式是生成一个新的字符串很多初学者不理解这个特性的意义会抱怨我明明想修改一个字符串为什么还要这么麻烦其实不可变性带来的最大好处是安全。当多个地方同时用一个字符串时不需要担心某处修改会影响别处的数据。Python内部可以放心地做各种优化比如多个变量指向同一个字符串内容时可以直接复用同一块内存。在实际工作中所有处理字符串的方法比如upper()、strip()、replace()都是返回一个新字符串而不是修改原来的字符串。这一点和列表完全不同后面讲可变与不可变时我会再展开。3.4 字符串常用方法清单字符串方法非常多但入门阶段真正高频的就那么几个我按用在哪给它们分类清理整理strip()去掉首尾空白字符lower()转小写upper()转大写title()每个单词首字母大写。查找判断find(sub)找子串位置找不到返回-1startswith(prefix)判断开头endswith(suffix)判断结尾isalpha()判断是否全字母isdigit()判断是否全数字。替换拆分replace(old, new)替换子串split(sep)按分隔符拆成列表join(iterable)把列表里的字符串拼接成一个字符串。格式化f-string语法 后面单列。举一个实际场景从数据库或接口里拿到一个英文名字字符串 Alice Smith 你想判断它的姓氏是否是Smith。正确做法是先strip()去掉空格再endswith(Smith)判断。新手如果不清理空格极有可能在endswith这里返回False排查半天发现是空格问题。3.5 字符串拼接加号和 join 的关系字符串可以和字符串用拼接可以和数字相乘重复比如ab * 3得到ababab。但字符串和数字直接相加会报错age 18 message 我的年龄是 age # TypeError: can only concatenate str这种报错非常经典也直接引出了类型转换的话题。不过字符串程序里更优雅的是用f-string格式化age 18 message f我的年龄是{age}f-string 是Python 3.6以后推荐使用的格式化写法它能把变量直接嵌入字符串简洁、易读、性能也很好。初学阶段直接用它比老式的%格式化和format()方法更省心。4. 布尔值、None 和真假判断条件语句前必看的一课4.1 布尔本质上是整数的亲兄弟布尔类型只有两个值True和False。在Python里布尔其实是从整数类型派生出来的True在参与数值运算时等于1False等于0。print(True True) # 2 print(10 5) # True print(1 1.0) # True注意数字类型之间比较值相等 print(1 is 1.0) # False身份不同is比较的是内存地址和is的区别是个经典考点。比较的是值是否相等is比较的是两个变量是否指向同一个对象。日常写代码判断两个变量内容相不相等用就够了。只有当你想判断某个变量是不是None时习惯上才用is None而不是 None。这是Python社区的惯例也是PEP 8风格指南推荐的习惯。4.2 真值判断很多类型自带真假在Python里并不是只有True和False才能用于真假判断。任何对象都可以直接放在if条件里它会自动被解释为真或假。这里的规则很关键很多新手写条件判断时莫名其妙分支走错多半就是这个没掌握被判定为False的值包括None、False、数字0、空字符串、空列表[]、空字典{}、空元组()、空集合set()。除此之外的其他值都被判定为真。name if name: print(名字非空) else: print(名字为空)利用这个特性你可以少写很多显式比较。比如判断列表是否为空不用写if len(items) 0直接if items:就行。代码简洁而且语义清楚。4.3 None 不等于 False也不等于 0None表示这里没有任何值它和False、0这三个概念一定要区分清楚。打个比方False表示结果是假的0表示数量是零而None表示这个问题根本没有答案。在函数没有return时默认返回值就是None在字典里取一个不存在的键时dict.get()默认返回None。如果你把None当False来用会在某些场景下写错逻辑。不过有一点要注意None在条件判断里确实会被当成假来处理。所以如果你只是想判断某变量是否有值用if x:是没问题的但如果你要精确判断这个变量是不是None必须用if x is None:。这两类需求的结果判定范围不同前者把所有等价于假的值都过滤掉了后者只会过滤掉None。5. 类型转换为什么 input() 让无数新手摔跟头5.1 input() 返回的永远是字符串这是新手入门时的经典翻车现场。写加法计算器时大家都觉得这样没问题a input(请输入第一个数) b input(请输入第二个数) print(结果是, a b)结果输入3和4输出的不是7而是34。原因就是input()不管用户输入的是什么统统按字符串处理。字符串和字符串用连接结果自然是拼接。要解决这个问题必须显式转换a int(input(请输入第一个数)) b int(input(请输入第二个数)) print(结果是, a b)5.2 int()、float()、str() 的转换规则和边界Python提供了内置转换函数使用频率极高int(x)把字符串或浮点数转成整数。把浮点数转整数时是向下取整比如int(3.99)得到3注意不是四舍五入。float(x)把字符串或整数转成浮点数比如float(3.14)得到3.14。str(x)把其他类型转成字符串任何类型的变量都可以转因为所有对象都有字符串表示。但转换不是万能的。int(hello)会直接报ValueError因为字符串内容不是合法数字格式。int(3.14)也会报错因为整数转换函数不允许字符串带小数点。你要想把3.14变成整数得先float(3.14)再int()两步操作。同时还要注意TypeError如果传给int()的既不是数字也不是数字字符串而是一些奇怪的对象类型就可能报TypeError。区分ValueError和TypeError有个技巧值本身类型对但不合理就报ValueError类型根本不对就报TypeError。搞明白这两个报错的区别排查问题时能快不少。5.3 转换前的防御写代码前先判断能不能转实际写程序时用户未必总会按你的预期输入。如果用户输入了一个非数字字符你的int(input(...))会直接让程序崩溃。在处理用户输入时比较稳妥的写法是捕获异常或者先判断字符串的格式number_str input(请输入一个整数) if number_str.isdigit(): number int(number_str) print(输入有效数字是, number) else: print(输入无效请确保输入的是非负整数)isdigit()方法可以判断字符串是否全部由数字组成但它只能判断非负整数字符串。如果你想支持负数和小数得用正则表达式或者直接try except兜底。入门阶段先掌握isdigit()的思路即可后面学异常处理了记得把try/except加上了。5.4 慎用 eval 函数有一种偷懒的转换方式叫eval()比如eval(input(请输入表达式))它能直接把用户输入的字符串当代码执行。这非常危险因为用户如果输入__import__(os).system(rm -rf ...)这类内容程序会直接执行系统命令。在Web服务中这种写法相当于给攻击者打开大门。我看到很多仓库代码里有eval()的身影全部建议重写。千万不要图方便用它来处理用户输入。6. 可变与不可变理解基本类型和容器类型的关键分界线6.1 什么是可变什么是不可变前文提到字符串不可变而列表是可变。这个区分的具体含义是不可变类型的对象一旦创建就不能在原有对象上做修改任何修改操作都会生成一个新对象可变类型的对象可以在原地址上增删改不影响其他引用它的变量。基本类型里整数、浮点数、字符串、布尔值、None都是不可变的容器类型里列表、字典、集合是可变的元组是不可变的。一个经典案例是函数传参。把可变对象传给函数函数内部修改了它外面的原对象也会跟着变把不可变对象传给函数函数内部修改的只会是局部的新值外面的原对象不受影响。def add_one(x): x x 1 return x a 10 add_one(a) print(a) # 还是10整数不可变 def append_item(lst): lst.append(99) my_list [1, 2] append_item(my_list) print(my_list) # [1, 2, 99]列表可变函数内修改影响了外部这个差异如果不理解写复杂项目时会遇到很多诡异问题列表莫名被改了、某个变量突然变了值排查半天也找不到原因。6.2 列表和元组按需选择可变性列表和元组都属于序列容器但列表可变元组不可变。同样的元素内容存储方式上元组更轻量。什么时候选哪个我的经验是如果确定数据创建后不会变比如坐标(10, 20)、日期范围、函数的多个返回值优先用元组。如果数据需要在程序运行中被不断增删改比如待办项清单、动态集合用列表。元组有个特殊价值因为元组是不可变的它可以作为字典的键而列表不能。字典的键要求必须是可哈希的简单理解就是内容不变的对象才适合做键。元组的哈希性质让它能够充当一种稳定标识在数据处理中用来做去重和分组非常方便。6.3 字典和集合背后的哈希机制字典为什么能通过键快速取到值因为它在内部对键做了一次哈希运算并建立映射关系。这和可变不可变有什么关系很直接键在存进字典之后必须保证内容不被修改否则哈希映射就会失效。所以列表这种可变类型不能当键而整数、字符串、元组这些不可变类型可以当键。集合的原理类似它本质上是一个用哈希实现的无顺序、不重复的数据结构。所以集合里的元素也必须是可哈希的不可变类型你没法把列表放进集合但可以把元组放进去。valid {(北京, 2024-01-01), (上海, 2024-01-01)} print((北京, 2024-01-01) in valid) # True这种特性在数据去重场景里非常实用。需要唯一标识一组数据时可以考虑用元组包裹再放进集合。6.4 浅拷贝和深拷贝可变对象操作里的隐形坑因为列表可变所以把一个列表直接赋值给另一个变量它俩会指向同一个对象修改其中一个另一个也会被改动。初学者非常容易在这里踩坑a [1, 2, 3] b a # 只是复制了引用两者指向同一对象 b.append(4) print(a) # [1, 2, 3, 4]a也被改了要真正复制一份独立的列表需要调用.copy()方法或者list()构造a [1, 2, 3] b a.copy() b.append(4) print(a) # [1, 2, 3] print(b) # [1, 2, 3, 4]但.copy()是浅拷贝如果列表里的元素本身也是可变对象情况会更复杂。比如[[1, 2], [3, 4]]这种嵌套列表浅拷贝只复制了外层列表内层列表依然共享。这时就需要copy.deepcopy()做深拷贝。Python内置模块copy提供了这个方法import copy a [[1, 2], [3, 4]] b copy.deepcopy(a) b[0].append(0) print(a) # [[1, 2], [3, 4]]深拷贝后互不影响建议初学者把列表赋值不等于复制这个认知刻在脑子里这是排查很多隐性Bug的关键路径。理解了浅拷贝和深拷贝后续学习数组、DataFrame这些更复杂的数据结构时会轻松很多。6.5 遍历时别修改可变性导致的经典死循环可变类型还有一个非常常见的坑在遍历列表时尝试修改列表本身。比如你想删除列表里所有小于3的元素写了下面的代码nums [1, 2, 3, 4, 5] for num in nums: if num 3: nums.remove(num) print(nums) # 结果并不像预期结果可能会让你觉得毫无规律。原因是遍历过程依赖于列表的下标连续推进但你删除了元素后元素会前移部分元素被跳过去了。正确做法是先复制一份列表再遍历遍历和修改分离nums [1, 2, 3, 4, 5] for num in nums.copy(): if num 3: nums.remove(num) print(nums) # [3, 4, 5]或者直接用列表推导式过滤创建新列表而不是修改旧列表。养成遍历用副本过滤生成新列表的习惯可以避开大量由可变性引发的问题。7. 实操总结初学类型相关问题时值得养成的三个习惯最后聊三个我自己学了几年Python之后回头看觉得新手阶段就应该养成的习惯。每个都对应一类真实的翻车现场早建立这些意识后面能少花很多排查时间。第一个习惯是不确定类型就打印打印还不行就用type()再看一眼。很多类型相关的报错只要你在报错行之前打印一下相关变量的类型原因立刻就现形了。我刚学那会儿为了偷懒不打印结果为了一个类型问题在论坛搜了半小时浪费的时间够打印几百次了。第二个习惯是写代码前先想清楚这个变量需不需要被修改、能不能被修改。如果你拿的是用户输入默认先按字符串处理如果你要做数值运算先转换类型如果你要往函数里传容器先确认函数是否会修改容器本身。想清楚这些问题很多代码错误是可以在写出来之前就避免的。可变性这个特性理解到位了是效率工具理解不到位就是Bug制造机。第三个习惯是敢于阅读报错信息。Python的报错其实已经写得很明确了TypeError: can only concatenate str (not int) to str这句话几乎直接告诉你是字符串和整数拼接的问题。新手常见的错误是看到英文报错就不敢往下读。就我个人的经验把报错信息里的关键名词和操作动作对上号比查任何教程都快。遇到报错先读完整信息再复制到搜索引擎去搜这才是高效排错链路的正确顺序。基本数据类型就是Python这栋大楼的地基看似简单其实每个细节都值得认真对待。把这几个类型之间的关系和边界摸透了后边学流程控制、函数、面向对象都会顺畅很多。毕竟程序中绝大多数逻辑最终都落在什么类型的数据做了什么操作这一件事上。