每次带新人学Python我都会先让他们做一道题写个程序输入两个整数输出它们的和。这道题只用第一章的知识就够但年年有人被卡住——有人忘了把input()的结果转成数值两个字符串直接拼成了35有人不知道print()还能控制分隔符和结尾输出结果挤成乱麻有人写完一个小功能全堆在一个文件里想复用只能复制粘贴。教过几轮之后我越来越确信教程把注释、数值类型、输出函数、输入函数、格式化输出、模块排在一章绝不是随便选的这是一套最小区块的闭环——旁白注释、材料数值、入口输入、出口输出、排版格式化、扩展模块。这篇文章把六个知识点逐个拆开结合我实际写脚本时踩过的坑一次讲透。刚装好Python、准备从抄Hello World走向能自己写点小工具的初学者直接按顺序读就行有点基础的同志可以跳到自己薄弱的小节。1. 为什么第一课要把这六个知识点捆在一起1.1 所有程序本质上都是同一套动作我写了这么多年脚本越来越觉得一个道理不管业务多复杂代码跑起来无非就是三件事——拿数据进来算一算把结果交出去。第一课里的input()就是拿进来的动作print()就是交出去的动作数值类型决定运算时数据的材质格式化输出决定交付内容的排版模块则是把你已经写好的动作打包带走下个程序接着用。这套关系用最小案例就能串起来# 输入半径输出圆面积 r float(input(请输入半径)) s 3.14159 * r * r print(半径为, r, 的圆面积约为, s)初学者花十分钟把这段跑通第一课的六个知识点就都在手里了input读入、float强转、数值运算、print输出旁边的#是注释将来还能把面积计算封装成函数放进模块。这也是为什么我强烈建议第一课学完马上动手写这种两行小程序而不是把时间花在抄各种复杂的示例上。1.2 第一课知识点会以什么方式反复出现拿盖房子打比方的话注释是图纸上的说明文字输入输出是房屋的大门数值类型是砖和水泥的规格格式化输出是把材料码得整整齐齐的工地管理模块则是工厂里预先浇筑好的预制件——只要你愿意随时可以整体搬过来用。这三者的出现频率完全不同注释写代码的每一分钟都在写只是不一定用#。好的变量名、函数名本身就是注释。输入输出只要程序需要跟人交互就一定会用到即便做后端服务、做爬虫日志输出本质也是print。数值类型任何数据计算都绕不开尤其是整数和浮点数混用的场景坑最多。模块程序规模一旦超过两百行拆模块就变成刚需这个技能越早掌握越省事。1.3 我推荐的学习顺序如果让我重新排这六个知识点的学习顺序顺序是这样先print和input因为它们立刻就有反馈跑一次就能看到结果这种正反馈对新手特别重要然后学数值类型因为输入的数据总要拿来做运算接着学格式化输出因为变量一多直接print会有1 2 3这种不明不白的输出需要排版最后学模块它是文件级别的组织方式等你的代码超过一个屏幕再引入都不迟。这个顺序也顺便解释了为什么教程第一课内容多但并非每个点都要一次吃透先跑通再回头精读才是我的经验。2. 注释写给未来那个一脸懵的自己2.1 单行注释很简单但多行注释其实是个假概念Python里的注释用#开头从#到行尾结束解释器直接忽略。这是最标准、最保险的做法没有什么操作难度# 这行不会被解释器执行 name 张三 # 行尾也可以加注释那三个双引号或单引号包起来的内容呢很多人管它叫多行注释严格来说不准确。Python官方并没有真正的多行注释语法你用三引号写一段文字解释器会当它是一个没有被赋值的字符串表达式创建之后立刻丢弃。大多数时候确实起到了不执行的效果所以大家叫它多行注释也说得过去。但有个区别一定要知道三引号放在函数或者类里面的第一行会被Python当作文档字符串docstring跑help()或者module.__doc__的时候会显示出来。这跟普通注释完全是两回事。如果你只是想把一大段话临时屏蔽掉用三引号没问题如果这段文字是想写给未来的调用者看那就得写在函数的第一行用docstring的形式。def circle_area(r): 根据半径r计算圆面积r为数值类型返回浮点数结果 return 3.14159 * r * r2.2 什么才值得写注释把为什么留给后人把怎么样留给代码我在实际写代码和带新人时最常说的一句话是注释的重点不是解释代码做了什么而是解释代码为什么要这么做。代码本身就能告诉你它做了什么注释的增量价值在于那些代码里看不出来的前提、约束和决策。对比一下这两段# 坏注释代码本身已经说清楚的事再重复一遍就是噪音 # 把x加1 x x 1 # 好注释解释了一个代码里看不出来的原因 # 偏移量从1开始是为了跳过CSV的表头行 x x 1那到底什么时候该写注释我自己的标准是这四种情况函数、类、模块的开头用docstring说明用途、参数、返回值和需要注意的坑一段逻辑有多个候选项你选了其中一个注释里说清楚为什么排除另外几个使用了一个不常见的外部库或者魔法数字注释里给出出处或换算方式临时禁用某段代码要加TODO标记并记录删除条件。其余的日常代码我倾向于不写注释而是把变量名、函数名命名得足够直白让代码本身会说话。好多老手说的代码即注释指的就是这个。2.3 最离谱的注释事故注释也会过期有一年我接手一个老项目几千行代码只找到一行注释写的是不要动这段代码会炸。我小心翼翼绕开它排查了好几天最后发现那行注释保护的是一段早被另一个同事用if False注释掉的死代码。换句话说注释本身已经成了代码里最危险的陷阱——它承诺的东西根本不存在却让所有人都不敢去验证。这个事故给我留下一个习惯注释必须和代码同步更新。代码改了注释没改比没有注释更坑因为它给你一份错误的地图让你朝着错误的方向找问题。所以在Code Review里我看到注释和代码行为不符会直接打回不管代码逻辑本身对不对。这条规则我建议初学者从第一天就建立起来否则后面写半年代码就会面对自己留下的过期注释。3. 数值类型整数、浮点数与隐藏的精度陷阱3.1 Python的数值家族比你想的多一位成员Python 3里常见的数值类型有四种int整数、float浮点数、complex复数、bool布尔值。看起来简单实际上已经藏了好几个新手必踩的坑。类型写法示例底层说明主要坑点int42、-7、0x1F3.x中整数任意大不担心溢出与float混合运算时自动转floatfloat3.14、1e5IEEE 754双精度精度丢失0.1 0.2 ! 0.3complex12j实部虚部日常业务代码极少用到boolTrue、False是int的子类True 1False 0最反直觉的是bool属于int子类这件事。我第一次发现是因为一个同事写的统计代码sum([True, False, True, False, True])他以为会报错结果运行结果是3。因为bool在参与数学运算时会被当成1和0。知道了这点以后看到if x True这种写法就该意识到可以直接写if x因为等于真和是真的在这个语言里是一回事。int类型在Python 3里不限制长度你可以算10**100这种大数它不会像C语言的int那样溢出。这一点在处理时间戳、大数据ID的时候特别省心。但要注意int只要和float做一次混合运算结果就会变成float比如10 / 4得到的是2.5而不是2这是后面除法的坑。3.2 除法的三个兄弟/、//、%该选谁刚开始学Python的人最不习惯的一点就是它有三个除法相关的运算符。它们的区别直接改变程序结果/真除法不管能不能整除结果都是浮点数。10 / 4得到2.5。//整除向下取整结果类型取决于操作数。10 // 4得到2。%取余结果是整除之后的余数。10 % 4得到2。这里有个很多人会踩的坑Python的//是向下取整不是向零取整。C、Java里的整数除法是向零取整比如-7 / 2在C里是-3但在Python里-7 // 2的结果是-4因为-4比-3更小向下取整取的是更小的那个数。同样-7 % 2在Python里结果是1而不是-1。这个差异在写涉及负数索引、时间差计算、方向判断的逻辑时特别容易翻车。实际业务里我最常用//和%的场景是时间换算。比如把秒数转成时:分:秒total 3661 hour total // 3600 minute total % 3600 // 60 second total % 60 print(f{hour:02d}:{minute:02d}:{second:02d})另一个常见场景是分页total_pages (total_items page_size - 1) // page_size这个公式比total_items // page_size更稳因为它能保证多出来的一条也要占一页。3.3 浮点数0.1 0.2 不是0.3这是IEEE 754不是bug我几乎每周都会在某个群或者论坛看到有人贴这个问题Python的0.10.2为什么等于0.30000000000000004是不是Python算错了然后下面一堆人开始怀疑人生。事实是几乎所有主流编程语言都存在这个现象因为0.1这个十进制数在二进制里是无限循环小数跟十进制的1/3一样永远写不完。计算机只能用有限位存储必然产生舍入误差。print(0.1 0.2) # 输出 0.30000000000000004这个误差该怎么处理看场景。日常展示用格式化输出把结果变成用户能看的小数位数就够了print(f{(0.1 0.2):.2f}) # 输出 0.30涉及金额计算或者对精度有硬性要求的场景用decimal.Decimalfrom decimal import Decimal result Decimal(0.1) Decimal(0.2) print(result) # 输出 0.3注意Decimal传进去的是字符串不是浮点数因为浮点数本身已经脏了再传进去等于把误差带进来。工程上大多数应用其实不需要用Decimal误差通常在10的负16次方级别对日常计算完全无感。搞清楚什么时候要精算、什么时候可以忽略比看到0.10.2就方重要得多。4. 输入与输出程序与人的双向对话通道4.1 print()远不止打印两个字那么简单很多教程把print()一句话带过导致好多人以为它就是把括号里的东西显示出来。实际上print()在Python里是一个完整的函数签名是这样的print(*objects, sep , end\n, filesys.stdout, flushFalse)初学者最不关心、但实际最常用的两个参数是sep和end。sep是多个打印对象之间的分隔符默认是一个空格。如果你想打印一行逗号分隔的数据不用自己去拼字符串print(2025, 04, 01, sep-) # 输出 2025-04-01 print(a, b, c, sep,) # 输出 a,b,cend是打印结束时的结尾字符默认是换行符\n。我在写命令行小工具、做进度条或者实时日志的时候经常把end设成空字符串配合回车符\r实现原地刷新的效果import time for i in range(101): print(f\r进度{i}%, end, flushTrue) time.sleep(0.05)这段代码的关键除了end还有flushTrue。默认情况下print的内容是攒在缓冲区里等缓冲区满了或者程序结束时才真正写出来这在实时输出场景里会导致你根本看不到进度。flushTrue强制每次print都立刻刷新到屏幕上。我在写日志和长时间跑批脚本时不写flush的话经常在卡死的时候看不到最后一条输出非常头疼所以后来所有涉及进度的print我都会加上flushTrue。4.2 input()它永远返回字符串这句话价值一百分input()函数的作用是从终端读取一行用户输入括号里可以放一个提示字符串先显示给用户再等待输入。问题在于不管用户输入的是什么它返回的类型永远是字符串。这就是那句所有输入都是字符串的实际含义。如果不做类型转换input()拿到的5是一个字符串5 3的结果是53不是8。所以典型的第一课作业——输入两个数求和——最容易翻车的地方就在这。正确的做法是显式转换a int(input(请输入第一个整数)) b int(input(请输入第二个整数)) print(两数之和是, a b)但这里有一个更隐蔽的坑如果用户在终端里乱输入了一个非数字比如abcint(abc)会直接抛出ValueError程序当场崩溃。我在一次帮同事处理批量导入数据的脚本里就遇到这种情况数据文件稍微有点脏某个字段混进一个字母整个task跑到一半中断。所以工程代码里对输入要做防御性处理while True: try: a int(input(请输入第一个整数)) break except ValueError: print(输入不合法请重新输入一个整数)这个try-except结构第一次看可能觉得啰嗦但它才是生产环境能扛住用户手滑的写法。学完第一课就可以开始习惯它。4.3 一次输入多个值被问得最多的input场景很多人刚开始学的时候都会问用户在地铁上想在终端里一次输入两个数用空格隔开代码该怎么写这就是split()和map()登场的时刻。最经典的竞赛式写法是一行代码搞定a, b map(int, input().split()) print(a b)拆开看input().split()会把输入字符串按空格拆成一个列表map(int, ...)把列表里的每个元素都转成整数然后解包给a和b。这是一行输入多个数的标准答案刷题网站和在线判题系统里几乎全是这种写法。但工程代码我建议稍微多写两行做一下健壮性处理。比如用户多敲了一个数或者输入空行上面那行代码会直接报错data input(请输入两个整数用空格分隔).split() if len(data) ! 2: print(输入格式不对请重新运行) else: a, b map(int, data) print(两数之和是, a b)练基本功的时候先把一行版跑明白理解它每一步做了什么然后再改成稳健版两种都会了才算真正掌握输入处理的套路。5. 格式化输出三种写法怎么选5.1 %占位符老代码的通用语言见得到要能看懂Python里历史最悠久的格式化方式是%占位符基本语法是把字符串里的%s、%d、%f这些占位符用括号里的元组依次替换name 张三 score 95.5 print(%s 的得分是 %.2f % (name, score))这里的%s表示字符串%d表示整数%f表示浮点数.2f表示保留两位小数。如果你要输出一个百分号本身需要写%%转义rate 0.976 print(成功率是 %.1f%% % (rate * 100)) # 输出 成功率是 97.6%为什么现在我不太推荐新手用这种方式写新代码原因有两个一是占位符和变量离得远代码一长就不知道%s对应哪个值二是%的格式控制符记起来麻烦容易在.2f和:.2f之间来回试错。但无论如何都要能看懂它因为老项目和某些日志框架里还在大量使用你随时可能在维护代码时遇到。5.2 str.format()中间的过渡方案灵活性确实有str.format()是Python 2.6引入的改进版格式化它把占位符写成大括号{}用位置参数、关键字参数或字典来传值# 位置参数 print({} 的得分是 {:.2f}.format(name, score)) # 编号参数 print({0} 的得分是 {1:.2f}.format(name, score)) # 关键字参数 print({name} 的得分是 {score:.2f}.format(namename, scorescore)) # 字典解包 info {name: 张三, score: 95.5} print({name} 的得分是 {score:.2f}.format(**info))相比%占位符.format()已经有很大的可读性提升特别是关键字参数一眼就能看出占位符怎么映射。它在Python 3.6之前的时代是绝对的主流很多库的源码、历年的培训视频、技术问答帖里都是这种写法。但它的缺点是写起来啰嗦尤其是多次使用同一个变量的时候要么重复传入要么用编号手动对应。如果你维护的代码里大量使用.format()可以继续用写新代码我建议直接往下看f-string。5.3 f-string写起来最像人话的现代选择f-string是Python 3.6引入的格式化方式也是我日常写代码的默认选择。它的核心特点是在字符串前面加个f然后直接在字符串里用{}嵌入表达式Python会当场计算name 张三 score 95.5 print(f{name} 的得分是 {score:.2f})除了基础的变量嵌入f-string最爽的地方是支持任意表达式还能直接写格式控制符a, b 15, 7 print(f{a} {b} {a b}) # 表达式直接算 print(f{12.3456:8.2f}) # 宽度8保留两位小数 print(f{name:10}|) # 左对齐宽度10 print(f{score:8.2f}) # 右对齐 print(f{9876543:,}) # 千分位分隔 print(f{42:06d}) # 6位数字不足补零f-string还经常用来对齐表格输出。我之前写过一个命令行小工具要输出一堆键值对就是靠f{key:20} {value:10}实现的代码短效果还整齐。三种方式的对比我直接整理成表格式化方式可读性性能兼容性我的建议%占位符一般较快Python 2.x起读懂老代码用str.format()较好较慢Python 2.6起维护旧项目时用f-string最强最快Python 3.6日常新代码首选关于性能我实测过同样一段日志拼接f-string比.format()快不少。原因也很简单f-string在编译期就把表达式内联成直接求值而.format()是运行时按字符串模板动态解析。不过大部分业务代码不至于被这个差异卡住选f-string最重要的理由是可读性和写起来顺手。6. 模块从这一课开始Python就从语法课变成了积木课6.1 三种import方式怎么选这里最容易埋雷模块的入门用法无非三种写法# 方式一导入整个模块用的时候加前缀 import math print(math.sqrt(16)) # 方式二从模块里直接导入具体名字 from math import sqrt print(sqrt(16)) # 方式三给模块起别名 import math as mt print(mt.sqrt(16))三种方式没有绝对的对错但我在实战中踩过很多名字冲突的坑所以总结出两条经验。第一条优先用import math这种带前缀的写法因为math.sqrt一眼就能看出来函数的出处别人读代码也清楚。直接from math import sqrt虽然写起来短但如果你自己的代码里恰好也定义了一个叫sqrt的函数后定义的会覆盖先导入的运行时行为瞬间变成一场灾难。第二条别名主要用来缩短特别长的模块名。比如import pandas as pd、import numpy as np这是社区约定俗成的缩写你直接写全名反而没人看得懂。给自带模块起缩写别名倒也行但别太随意否则别人接手时会一头雾水。6.2 import背后到底发生了什么很多人学模块只知道import就是导入但实际上这个动作背后有三步理解了它很多诡异现象都可以解释。第一步Python会先去sys.modules这个字典里查一下这个模块是不是已经导入过。如果已经导入过直接复用不会重复执行。所以你在两个文件里分别import同一个模块模块顶层的代码只会执行一次。记住这一点后面遇到为什么我改了模块代码但运行没变化就知道需要重启解释器或者用importlib.reload()。第二步Python按sys.path的顺序去文件系统里找模块对应的.py文件。sys.path通常包含当前脚本所在目录、标准库目录、环境变量PYTHONPATH指定的目录、以及site-packages第三方库目录。这里有个经典坑如果你给自己的文件起名叫math.py再写import mathPython会优先在当前目录找到它然后你的代码就加载了你自己写的math.py标准库math那一堆函数全没了。这种文件命名和标准库冲突的问题排查起来非常隐蔽。第三步找到文件后Python会从头到尾完整执行一遍这个.py文件的代码把执行结果打包成一个模块对象。注意是完整执行这意味着如果模块顶层有一行print(hello)那么任何地方import这个模块都会打印一次hello。很多新人第一次看到这个现象会以为程序里有bug其实是模块被导入时顶层的代码被执行了。这也是为什么正式的程序里模块顶层只放import、变量定义和函数定义真正要执行的代码都要放到if __name__ __main__:里面。6.3 理解__name__让同一个文件当脚本又当库if __name__ __main__:是Python里最经典的入口写法也是模块这个知识点里最实用的一招。它的原理是Python在执行一个文件时会给这个文件设置一个内置变量__name__。如果这个文件是直接通过命令python xxx.py运行的那__name__的值就是字符串__main__如果这个文件是被别的文件用import导入的那__name__的值就是模块名本身。有了这个机制你就可以让一个文件具备双重身份# utils.py def greet(name): return f你好{name} print(模块被导入时这一行会执行) if __name__ __main__: # 只有直接运行 python utils.py 时才会走到这里 print(greet(直接运行))另一个文件这么用# main.py import utils print(utils.greet(导入方式))运行python main.py你会看到模块被导入时这一行会执行也打印出来了因为import会执行utils.py整个文件。但if __name__ __main__里面的那行不会执行因为此时utils的__name__不是main。这就实现了既能被导入又能独立运行的双重能力。第一课学完我建议你立刻养成一个习惯所有脚本文件底部都加上这个判断再在里面写测试代码或入口逻辑。这跟你后面写函数入口、写单元测试、拆代码文件都直接相关收益极大。6.4 模块化的第一个小练习把一坨代码拆成积木模块这个知识点价值要到代码量上来时才体现得出来。一个文件塞三百行和十个文件各三十行维护体验完全是两个世界。模块化说白了就是拆积木把功能相近的代码放进同一个.py文件互相之间用import引用。我通常建议新人在第一课结束后做这样一个练习写一个命令行小工具箱程序要求把功能按模块拆开——一个math_utils.py放数学函数一个date_utils.py放日期判断函数一个main.py负责菜单和交互。比如math_utils.py里放一个判断奇偶的函数date_utils.py里放一个判断闰年的函数主程序让用户选择功能并输入数据然后调用对应模块的函数返回结果。这个练习做完你对import的理解会从语法层面上升到工程组织层面。代码一旦拆成模块后面学函数、学类、学第三方库安装都会顺畅很多。如果学完第一课只做一件额外的事我就推荐做这个。顺带说一句我自己的体会写了几年Python回头看90%的日常脚本逃不出这一课的框架——读入一点数据处理一下格式化地输出再顺手把工具函数装进自己的模块里。第一课看着基础但它的每一分细节都会在未来无数代码里反复用到。尤其是input返回字符串、//向下取整、f-string格式化、__name__判断入口这几个点越早踩过坑后面越省心。