目录
- 宏观视角:C 与 Python 的底层差异
- 变量机制与内存模型
- 数据类型与隐形陷阱
- 运算符与逻辑炸弹
- 函数、模块与包
- 语法规范与命名
- Pythonic 思维
一、宏观视角:C 与 Python 的底层差异
学语法之前先建立底层认知。计算机执行程序,归根结底只做两件事:处理数据(加减乘除、逻辑运算)和搬数据(从内存搬到屏幕,或者内存内部挪位置)。所有程序都是这两件事的组合。
1.1 编译型 vs 解释型
C 语言(编译型):预处理(#include把多文件组装起来)→ 编译(转机器码)→ 链接(把多个目标文件拼成可执行文件)。代码一改,必须重新编译。
Python(解释型):底层是 C 实现的 CPython。它先把源码编译成字节码,再由解释器执行。改完直接跑。
严格说"解释型"不是绝对概念,CPython 一样有编译环节,只是对使用者不可见。
这个差异的直接后果:C 的语法错误在编译期就被抓出来,Python 的很多错误要等代码真正跑到那一行才暴露。后面第四节的"死亡代码现场"就是这个机制造成的。
1.2 静态类型 vs 动态类型
C(静态类型):int a = 10;是向系统申请一块固定大小的内存,并约定这块二进制只能按整数解释。类型在编译期锁死。
Python(动态类型):a = 10不用声明,运行时自动推断。
但这里有个特别容易被误解的点:Python 是动态类型,同时是强类型。动态指的是变量不绑定类型,强类型指的是对象有明确类型、操作受严格限制。
print("1"+1)它不会像 JS 那样偷偷把"1"转成1再加。该报错就报错。
1.3 内存管理的分水岭
C 语言有指针,能直接操作内存地址。老师用辣椒打比方:有人喜欢辣椒因为辣,有人不喜欢也是因为辣。指针就是这样的特性——灵活、高效、可控,但也带来野指针、内存泄漏、数组越界。C 里申请了内存忘了free,程序越跑越慢,最后只能重启。
Python 没有指针,内存的申请和释放全自动。代价是运行速度不如 C。
1.4 类型的本质
内存底层只有二进制。类型决定两件事:占多少字节,以及这段二进制怎么解释(整数?浮点?字符?)。
同一个二进制串,按 int 解释和按 float 解释,值完全不同。int a的本质就是"申请一块空间,约定按整数解析"。
Python 里变量不直接存值,存的是对象的地址;对象自己带着类型信息。
二、变量机制与内存模型
这一节是整个基础篇最核心的底层原理,也是选择题最爱考的地方。
2.1 变量不是"盒子",是"标签"
C 的思维:变量是盒子,把 10 塞进叫 a 的盒子里。盒子的类型和大小固定。
Python 的思维:一切皆对象。执行a = 10时,Python 先在内存里建好整数对象10,然后a作为一张标签贴在这个对象上。a里面存的是对象的地址(引用)。
所以:
a=10a="hello"# 不是"把盒子里的东西换掉",而是"把标签撕下来贴到另一个对象上"旧对象如果没人引用,会被垃圾回收。
2.2 小整数缓存机制(总统套房)
x=5y=5print(id(x),id(y))# 两个地址完全相同m=289n=289print(id(m),id(n))# 通常不同原理:-5到256是使用频率最高的整数,Python 启动时就在内存里预先建好这些对象,常驻不走。老师管这叫"常住贵宾住总统套房"。你给x和y都赋 5,两张标签贴在同一个房间。
289 超出范围,不是贵宾,每次赋值都临时租新房间,所以地址不同。
这个范围是有出处的:CPython 源码里定义的[-5, 256]`。
** 这里有个坑必须说清**:网上很多笔记写257 is 257是 False,这句话只说对了一半。同一个代码块里,编译期会做常量折叠,可能让两个 257 指向同一个对象:
# 同一个代码对象内,可能为 Trueprint(257is257)# 交互式逐行输入时通常 True# 独立创建才是 Falsea=257b=257print(aisb)# 通常 False(但同一条语句里不保证)结论:不要用is比较数值。缓存机制是实现细节,不是语言规范,不要依赖。判断值相等一律用==。
2.3 is 与 ==(高频考点)
==:比较值是否相等。is:比较是不是同一个对象(内存地址相同)。
a=[1,2]b=[1,2]print(a==b)# True,值一样print(aisb)# False,两个不同的列表对象唯一推荐用is的场景:判断是否为None。
ifxisNone:# 规范写法ifx==None:# 能跑,但不规范,None 只有一个,用 is 更准确也更快2.4 三个查身份的内置函数
| 函数 | 作用 | 示例 |
|---|---|---|
id(obj) | 内存地址(身份) | id(a) |
type(obj) | 对象的类型 | type(a)→<class 'list'> |
isinstance(obj, 类型) | 是否是某类型的实例 | isinstance(a, list)→ True |
isinstance比type更好用,因为它支持继承关系的判断
2.5 动态但强类型
类型决定了对象"有什么本事":列表能 append、排序;元组不能改;字典能按键查。
三、数据类型与隐形陷阱
3.1 整数 int
Python 的整数理论上无上限,只受内存限制。C 里算 2 的 64 次方会溢出,需要自己写数组模拟大数;Python 直接算。
阿凡提棋盘放米(第 1 格 1 粒,之后每格翻倍,共 64 格):
print(2**64-1)# 18446744073709551615,Python 算得毫无压力3.2 浮点数 float(高频丢分点)
浮点数是二进制存储,十进制小数很多无法精确表示。
print(0.1+0.2)# 0.30000000000000004print(0.1+0.2==0.3)# FalseOJ 铁律:绝对不要写if a == 0.3:。要用差值判断。
ifabs(a-0.3)<1e-9:print("相等")另外,整数和浮点混合运算,结果是浮点。5 / 2永远是 2.5,不是 2。
3.3 布尔类型与真值判定
True和False是 int 的子类,True == 1、False == 0,可以直接参与算术。
在条件判断里,以下值被判为False:
False、0、0.0、0j(复数零)、None、 ""(空字符串)、[](空列表)、()(空元组)、{}(空字典)、set()(空集合)其余一切为 True。这就是if []:走 else 分支的原因。
if[]:print("不会打印")else:print("空列表是假")# 这里执行这个特性可以用来写简洁代码,比如if not lst:判断列表是否为空,比if len(lst) == 0:更 Pythonic。
3.4 字符串 str
Python没有单独的字符类型,'a'也是字符串,长度 1。
四种定界符:'...'、"..."、'''...'''、"""..."""。互相嵌套可以省掉转义。
print("他说:'你好'")# 双引号包单引号print('他说:"你好"')# 单引号包双引号原始字符串r'...':反斜杠不作转义。
print(len(r'\n'))# 2,是一个反斜杠加一个 nprint(len('\n'))# 1,是一个换行符字符串不可变:所有"修改"操作都返回新字符串,原字符串不变。
s="abc"s.upper()# 返回 "ABC"print(s)# 还是 "abc"s=s.upper()# 要赋值回去才生效这个"不可变"特性后面讲元组、列表排序时会反复用到,记住一句话:不可变对象的任何操作都不改原对象。
3.5 编码与解码(乱码的根源)
计算机只认字节。encode()把字符串转字节串,decode()把字节串还原字符串。
s="你好"b=s.encode("utf-8")print(b)# b'\xe4\xbd\xa0\xe5\xa5\xbd',6 个字节print(b.decode("utf-8"))# 你好编码和解码必须用同一套字符集,否则乱码或报UnicodeDecodeError。
补充两点老师没细说的:
- 英文字符在 ASCII、UTF-8、GBK 里编码一致,所以纯英文不会乱码。
- 汉字在 UTF-8 里占 3 字节,在 GBK 里占 2 字节。
len("你好")是 2(字符数),len("你好".encode("utf-8"))是 6(字节数),这两个长度不是一回事。
3.6 None
None是 NoneType 类型的唯一值,表示"空"、“没有”。函数没有 return 时默认返回 None。
判断用is None,不用== None。前面 2.3 讲过原因。
四、运算符与逻辑炸弹
4.1 算术运算符
| 运算符 | 含义 | 示例 | 注意 |
|---|---|---|---|
/ | 真除 | 5 / 2→2.5 | 结果永远是 float |
// | 整除(地板除) | 5 // 2→2 | -5 // 2→-3 |
% | 取余 | 5 % 2→1 | 余数符号跟除数走 |
** | 幂 | 3 ** 2→9 | 右结合 |
地板除的坑(极高频)://是"向下取整",往数轴更低的方向走,不是"去掉小数部分"。
print(5//2)# 2print(-5//2)# -3,不是 -2print(-5%2)# 1,不是 -1记法:地板除和取余是一对,满足a == (a // b) * b + (a % b)。代入a=-5, b=2验证:-3 * 2 + 1 = -5,成立。
幂运算右结合:
print(3**3**3)# 等价于 3 ** (3 ** 3) = 3 ** 27,不是 (3**3)**3**还能开方:9 ** 0.5得 3.0。pow(a, b, mod)可以算(a**b) % mod,大数取模时比先算幂再取模快得多。
4.2 赋值运算符与 ++ 陷阱
复合赋值x += 3等价于x = x + 3。
Python 没有++和--。
x=5++x# 这行不报错,但也不自增,就是 +(+x),x 还是 5--x# 同理,-(-x),x 还是 5这是从 C 转过来的同学最容易犯的错。写i++会直接 SyntaxError,写++i不报错但没效果,更隐蔽。
4.3 关系运算符与链式比较
Python 支持数学式的链式比较,C 不支持。
x=3if1<x<5:# Python 写法,等价于 1 < x and x < 5print("在区间内")// C 里必须这么写if(1<x&&x<5)C 里写1 < x < 5会先算1 < x得 1 或 0,再拿这个结果和 5 比,逻辑完全错了,而且编译器不报错。
⚠️ 条件表达式里禁止单个等号:
ifx=3:# SyntaxErrorifx==3:# 正确C 里if (x = 3)是赋值,返回值 3 为真,条件永远成立,坑了无数人。Python 直接在语法层面禁止,这是它比 C 友好的地方。
4.4 短路求值与它的返回值真相
and、or、not是 Python 的逻辑运算符(不是&&、||、!)。
短路规则:
A and B:A 为假,直接返回 A,不计算 B。A or B:A 为真,直接返回 A,不计算 B。
⚠️ 返回值真相(超高频):and和or返回的不是 True/False,而是参与运算的那个表达式的值。只有not一定返回布尔值。
print(3and5)# 5print(0and5)# 0print(3or5)# 3print(0or5)# 5print(not3)# False理解方法:and找第一个假值,找不到就返回最后一个;or找第一个真值,找不到就返回最后一个。
这个特性常被用来写默认值:
name=user_inputor"匿名"# 输入为空字符串时,取默认值4.5 死亡代码现场:短路求值导致的隐蔽报错
b=3print(b>5anda>5)# 正常输出 False,不报错!b=6print(b>5anda>5)# NameError: name 'a' is not defined第一次没报错,不是因为代码对,而是因为b > 5为假,and短路,后面的a > 5根本没执行,解释器也就没发现a不存在。
教训:测试时没报错,不代表代码没问题,可能只是你的测试数据没走到那条路径。这也是 OJ 上"本地能过、提交 WA"的常见原因之一。
4.6 为什么 C 不会这样,Python 会
这是理解两种语言的关键差异:
- C:编译期就扫描全部代码,
a未声明,编译直接失败,压根跑不起来。 - Python:不管编译(字节码)阶段,只在实际执行到那一行时才解析名字。没走到就不报。
所以 C 的语法错误是"静态暴露",Python 的很多错误是"动态暴露"。写 Python 要更依赖充分的测试覆盖。
4.7 条件顺序优化(老师讲得很实用的一个点)
既然有短路,多个条件组合时,把最容易失败的放前面,能大幅减少计算。
老师的例子:一个女子瑜伽培训班,全部是女生。门口核验身份,条件有两个——“是不是男生"和"身份证号是否在名单内”。显然性别判断又快又能筛掉大部分人。
# 好ifis_male(person)ornotin_list(person.id):reject()# 差ifnotin_list(person.id)oris_male(person):reject()数据量小感觉不出来,数据量到千万级差距就明显了。
同理,用and时把"大概率不成立"的放前面,用or时把"大概率成立"的放前面。
4.8 成员、身份与优先级
in/not in:成员测试。列表是线性扫描,集合和字典是哈希定位,后者快得多。is/is not:身份判断。
优先级从高到低:
成员访问 . > 下标 [] > 幂 ** > 正负号 > 乘除 > 加减 > 比较 > not > and > or记不住就加括号,永远不会错。
4.9 类型转换
int(5.9)# 5,直接截断,不四舍五入int("10")# 10int("0b1010",2)# 10,第二参数指定进制float("3.14")# 3.14str(123)# "123"bool("")# Falsebool("abc")# Trueint()是截断不是四舍五入:int(5.9)得 5,int(-5.9)得 -5。要四舍五入用round(),注意round(2.5)是 2 不是 3(银行家舍入),这个坑实际项目中会咬人。
字符串转数字,字符串必须是合法数字,int("abc")直接报ValueError。
五、函数、模块与包
5.1 函数:从 C 的合约思维过渡
C 的函数声明是一份"合同":输入什么类型、返回什么类型,写死在头文件里。甲方调用,乙方实现。
intadd(inta,intb);// 合约:传两个 int,返回 intPython 的函数不写类型,靠约定:
defadd(a,b):returna+b但没有类型不代表没有约束——传进去的东西能不能相加,运行时才知道。这也是为什么 Python 需要写测试。
老师的一个类比很到位:跟 AI 对话也是这样,你要说清楚输入是什么、要求是什么、期望输出什么格式。写函数就是给对方一份清晰的接口说明。
5.2 模块、包、库
- 模块:一个
.py文件。 - 包:一个文件夹,里面放多个模块,通常带
__init__.py。 - 库:可以是一个模块,也可以是多个包/模块的集合。
__init__.py的作用就是告诉 Python"这个文件夹是个包,按包来对待"。简单项目里放个空文件就行。
标准库随解释器安装(math、random、collections),第三方库要先pip install(numpy、requests)。
5.3 import 的五种写法
importmath# math.sqrt(16)frommathimportsqrt# sqrt(16)importmathasm# m.sqrt(16)frommathimportsqrtass# s(16)frommathimport*# 全部导入,命名冲突风险高,尽量别用from math import *最省事也最危险,容易覆盖你自定义的同名函数。实际项目里基本禁用。
5.4name属性
每个模块都有__name__:
- 直接运行这个文件时:
__name__ == "__main__" - 被别的文件 import 时:
__name__等于模块名
# hello.pydeftest():print("run directly")if__name__=="__main__":test()这样写的好处:直接运行python hello.py会执行test(),但别的文件import hello时不会自动执行,只提供函数。模块自测试的标准写法。
是两个下划线,不是_name_,ASR 转写和手敲都容易漏。
六、语法规范与命名
6.1 缩进就是语法
Python 用"冒号 + 缩进"界定代码块,没有花括号。同一块代码缩进必须一致,通常 4 个空格。
ifx>0:print("正数")# 属于 ifprint("结束")# 不属于 if差一个空格可能直接IndentationError,也可能逻辑跑偏但不报错——后者更可怕,老师说他曾为一个多余的空格排查很久。
混合 Tab 和空格是另一个隐形炸弹。不同编辑器 Tab 宽度不同,肉眼看着对齐,实际不一致。统一用 4 个空格。
6.2 注释
- 单行:
# 注释,习惯#后空一格。 - 多行:三引号包裹,但必须在语句外才算注释。写在函数第一行的三引号字符串是 docstring(文档字符串),不是注释,但有类似作用。
6.3 命名规则
可以用的字符:字母、数字、下划线、汉字。不能数字开头,不能有空格和标点,不能用关键字。
最容易被忽略的坑:不要用内置名当变量名。
id=55print(id(3))# TypeError: 'int' object is not callableid、type、int、str、list、dict、max、min、sum、len、input、print——这些都不能当变量名。一旦覆盖,后续调用全部失效,而且报错信息往往莫名其妙。
查关键字:
importkeywordprint(keyword.kwlist)命名风格建议:变量和函数用snake_case(小写下划线),类用PascalCase,常量用UPPER_CASE。这是 PEP 8 的约定,不是强制,但大家都这么写。
6.4 版本
Python 2 已停止维护,新代码一律用 3.x。官网是python.org(.org不是.cn,这个是老师反复强调的考点)。
同一个大版本内的小版本(3.10.1 → 3.10.2)通常只修 bug,不加功能。升级大版本时第三方库可能不兼容,所以项目要锁版本、用虚拟环境隔离。
七、OJ 实战生存指南
OJ 的判题逻辑很死板:逻辑对只能拿部分分,格式完全匹配才满分。这一节讲的是真正在考场上保命的东西。
7.1 输入格式的四种情况
情况一:一行一个数据
n=int(input())# 整数x=float(input())# 浮点name=input()# 字符串,不用转换情况二:一行多个同类型数据
# 输入:10 20a,b=map(int,input().split())# 输入:1.0,2.0(逗号分隔)x,y=map(float,input().split(","))拆解过程:input()得到整行字符串 →.split()按分隔符切成列表 →map(int, ...)批量转类型 → 序列解包给变量。
情况三:一行多个数据,但个数不定
nums=list(map(int,input().split()))不要试图用固定变量接,直接转列表,后面用len()和循环处理。
情况四:一行混合类型
# 输入:张三 95name,score=input().split()score=int(score)# 只对数字部分单独转split()出来的全是字符串,字符串部分不用管,数字部分单独转。
7.2 输出陷阱
绝对不要和机器交流。OJ 是机器阅卷,你多写一个"请输入"、多写一个"结果是",直接 0 分。
# 错print("结果是:",total)# 对print(total)小数位数必须精确控制:
print(f"{value:.2f}")# 保留两位print(f"{value:.3f}")# 保留三位:.2f会四舍五入。注意round()的银行家舍入和:.2f的行为在边界值上可能有差异,OJ 判题以:.2f为准。
7.3 全角/半角空格(真正的大坑)
OJ 对空格极其敏感。中文输入法打出来的空格是全角(占两个字符宽),英文是半角。肉眼看着差不多,提交上去格式错误。
老师的保命神技:不要去手动敲空格,直接从题目里复制样例输出,粘进代码,在前面加f,然后把数字部分换成{}包裹的变量。
# 样例输出:杨枝甘露 18# 复制整行,改成:print(f"{best_name}{best_price}")这样空格来自题目本身,绝对不会错。
如果拿不准是不是全角,可以在编辑器里选中那一行看看空格宽度是否一致。
7.4 例题精讲:计算两点间距离
题目:输入两行,每行是x,y格式的坐标(逗号分隔),输出两点距离,保留两位小数。
importmath x1,y1=map(float,input().split(","))x2,y2=map(float,input().split(","))dist=math.sqrt((x1-x2)**2+(y1-y2)**2)print(f"{dist:.2f}")几个易错点:
- 坐标可能是小数,必须用
float不是int。 - 分隔符是逗号,
split(",")一定要写参数,不写默认按空格拆。 - 数学里的中括号
[]在 Python 里不存在,表达式分组只能用圆括号()。 - 开方两种写法:
math.sqrt()或** 0.5。考试环境如果连math都不让导入,用** 0.5。
7.5 例题精讲:数据清洗与统计
题目:第一行整数n表示数据个数,第二行n个整数。过滤掉所有小于 0 的异常值,输出最大值、最小值、平均值(保留两位小数,空格分隔)。
n=int(input())data=list(map(int,input().split()))# 过滤valid=[xforxindataifx>=0]print(f"{max(valid)}{min(valid)}{sum(valid)/len(valid):.2f}")老师课堂上的真实翻车:一开始忘了过滤条件,直接算 max/min,结果和样例对不上。调了半天才发现漏了"过滤掉小于 0"这一步。
教训:题目描述里每一句话都是条件,逐句对照,别跳读。看起来很长的故事背景往往是废话,但"过滤"、“保留两位”、"取最大值"这种短句全是得分点。
另一个坑:max、sum这些是内置函数,变量名千万别叫max、min、avg。
7.6 例题精讲:存款利息
题目:三行输入——初始金额、年限、年利率。计算到期利息(不含本金),保留两位小数。
principal=float(input())years=int(input())rate=float(input())interest=principal*(1+rate)**years-principalprint(f"{interest:.2f}")这题的关键不在代码,在读懂"复利"。
老师说他一开始按单利算(本金 × 利率 × 年数),死活对不上。后来拿样例数据倒推才发现是复利(利滚利)。
- 单利:
本金 × 利率 × 年数 - 复利:
本金 × (1+利率)^年数 - 本金
遇到对不上样例的题,倒推是最快的办法:拿题目的输入和输出,代入公式反推它的计算方式,比自己猜快得多。
七、Pythonic 思维
这段是老师课上讲得最有价值的部分,我单独拎出来。
不要炫技,先找现成的。
老师原话大意:解决问题时,先想有没有现成的函数;没有就看标准库;标准库没有再找别人写好的轮子。Python 的本质不是让你从零手写,而是又快速、又简单、又稳定地利用现成的东西。想炫技证明自己强,应该去用 C++,Python 不鼓励这个。
举个例子,反转一个整数:
# C 思维:取余取整一位一位抠n=12345rev=0whilen>0:rev=rev*10+n%10n//=10# Python 思维:转字符串,切片反转,转回整数n=12345rev=int(str(n)[::-1])后者一行的效果和前者的循环完全一样。功能上都没错,但后者才是"利用了 Python 特性"的写法。
同类的还有:
sum(range(1,101))# 而不是写循环累加max(lst)# 而不是自己遍历比较[xforxindataifx>0]# 而不是 for + appenda,b=b,a# 而不是用临时变量但要注意:初学阶段先把代码写出来是最重要的,优化和理解 Pythonic 写法是第二阶段的事。老师也说了,“现在这个阶段先把代码写出来”。