Python入门语法(一)

从 C++ 到 Python:基础语法背后的设计哲学差异

前言

作为一个先学的 C++ 再碰 Python 的人,我第一次写 Python 代码的感受是"轻"------不用声明类型,不用写分号,print 甚至不需要 #includestd::。但真正让我觉得有意思的,不是这些表面的轻,而是几乎每一个基础语法点上,Python 和 C++ 都做出了截然不同的选择:

  • C++ 的整数除法会截断,Python 的 / 永远是真除法;
  • C++ 的 int 有固定的 32/64 位宽度,Python 的 int 理论上没有上限;
  • C++ 的变量类型在编译期就定死,Python 的变量在运行过程中可以随便换类型;
  • C++ 有 ++--,Python 干脆把它们砍了。

这些差异不是随便来的,背后是两套完整的设计哲学在打架:C++ 贴近机器,Python 贴近人的直觉。这篇文章就把 Python 最基础的部分过一遍------常量、变量、类型、注释、输入输出、运算符。重点不是"怎么用"(这玩意随便查文档都会),而是每一个差异背后"为什么",以及这个选择付出了什么代价。把这条线想通了,后面学 Python 的很多东西都会顺理成章。

1. 把 Python 当计算器:常量与表达式

先从最没有悬念的部分开始。Python 可以直接当成一个计算器来用:

python 复制代码
print(1 + 2 - 3)
print(1 + 2 * 3)
print(1 + 2 / 3)

运行结果:

text 复制代码
0
7
1.6666666666666665

前两行没有任何意外。第三行,如果你是从 C++ 过来的,大概率预期是 1------在 C++ 里 2 / 3 是两个整数相除,小数部分被截断,结果是 0,于是 1 + 0 = 1。但 Python 给出的是 1.666.../ 在 Python 里做的永远是数学意义上的除法,不会截断。

这其实是 Python 3 有意为之的设计。Python 2 时代,/ 对整数的行为和 C++ 一样是截断的,后来社区意识到这个"继承自 C"的行为对使用者太反直觉了------日常认知里 1 除以 2 就是 0.5,凭什么是 0?于是 Python 3 把 / 改成了真除法,需要整数结果的时候另有运算符(后面讲)。C++ 保留截断除法也有它的道理:整数除法可以直接翻译成一条机器指令,贴近硬件。两种选择,一个贴近机器,一个贴近数学,谁也没错,但你要是带着 C++ 的肌肉记忆来写 Python,就会掉坑里。

在这段代码里顺便把几个术语说清楚。所谓的表达式 (expression),就是 1 + 2 - 3 这样可以进行运算的算式;算式的运算结果,称为表达式的返回值123 这种写死的数值,称为字面值常量+ - * / 这些,称为运算符(或者叫操作符)。

print 是 Python 的内置函数,负责把内容输出到控制台,现在先把它当黑盒用就行,函数的概念后面会详细讲。

有了这些,来做个练习:给定四个分数 67.5, 89.0, 12.9, 32.2,求平均数。一行搞定:

python 复制代码
print((67.5 + 89.0 + 12.9 + 32.2) / 4)

输出 50.400000000000006。等一下,说好的 50.4 呢,怎么还带个尾巴?这个尾巴先卖个关子,讲到浮点数的时候会回来解释它------它不是 bug,而是一类非常经典的问题。

到目前为止一切都很美好。但接下来一个需求,就能让"不使用变量"的写法原形毕露。

2. 变量:内存宿舍楼里的房间

2.1 为什么需要变量

现在把需求升级:求这四个分数的方差。方差的计算过程是:取每一项减去平均值,计算平方,再求和,最后除以 (项数 - 1)。

注意这个计算过程------从第二步开始就要反复用到"平均值",但平均值是第一步算出来的中间结果。中间结果存哪?总不能人肉把 50.4 抄进去吧?这个时候就需要变量了:

python 复制代码
# 计算 4 个数字 67.5, 89.0, 12.9, 32.2 的方差
avg = (67.5 + 89.0 + 12.9 + 32.2) / 4
total = (67.5 - avg) ** 2 + (89.0 - avg) ** 2 + (12.9 - avg) ** 2 + (32.2 - avg) ** 2
result = total / 3
print(result)

运行结果是 1173.2866666666666

顺带认识一个新运算符:** 在 Python 里表示乘方,x ** 2 就是求平方。求方差这个场景刚好用得上。

avgtotalresult 就是三个变量。变量这个概念有点像计算器上的 M 键------把中间结果存起来,后面随时取用。区别在于,计算器的 M 键一般只能存一个数,而 Python 里可以创建任意多个变量,想存多少存多少。

2.2 变量到底是什么:盒子,还是标签?

一个很常用的类比:可以把内存想象成一栋宿舍楼,楼里有很多房间,每个房间都能存放数据;衡量内存的一个重要指标就是空间大小,比如 16GB 的内存,数字越大,能存放的数据(变量)就越多。

这个类比对建立直觉很好用,但如果从 C++ 的视角深挖一层,会发现 Python 的变量和 C++ 的变量,在"是什么"这个层面上就有本质区别。

在 C++ 里,int a = 10; 意味着:编译器划出一块 4 字节的内存,这块内存的名字是 a,类型是 int,从生到死都改不了。变量就是一个盒子,类型是盒子的形状,数据装在盒子里,盒子里装什么必须一开始就说清楚。

那 Python 的变量是盒子吗?做个实验就知道了:

python 复制代码
a = 10
print(type(a))   # <class 'int'>
a = 'hello'
print(type(a))   # <class 'str'>

在 C++ 里这是铁定的编译错误------盒子都定形了,怎么可能先装 int 再装字符串?但 Python 里这段代码跑得好好的,a 的类型从 int 变成了 str

实际上,Python 的变量根本不是盒子,而是一张标签 :名字 a 本身不装数据,它只是一个指向对象的引用。类型、值、大小,这些信息都长在对象身上,不长在名字上。所谓赋值,就是把标签贴到某个对象上:
#mermaid-svg-9nMBTwbWL5JD5tS8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9nMBTwbWL5JD5tS8 .error-icon{fill:#552222;}#mermaid-svg-9nMBTwbWL5JD5tS8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9nMBTwbWL5JD5tS8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .marker.cross{stroke:#333333;}#mermaid-svg-9nMBTwbWL5JD5tS8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9nMBTwbWL5JD5tS8 p{margin:0;}#mermaid-svg-9nMBTwbWL5JD5tS8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .cluster-label text{fill:#333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .cluster-label span{color:#333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .cluster-label span p{background-color:transparent;}#mermaid-svg-9nMBTwbWL5JD5tS8 .label text,#mermaid-svg-9nMBTwbWL5JD5tS8 span{fill:#333;color:#333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .node rect,#mermaid-svg-9nMBTwbWL5JD5tS8 .node circle,#mermaid-svg-9nMBTwbWL5JD5tS8 .node ellipse,#mermaid-svg-9nMBTwbWL5JD5tS8 .node polygon,#mermaid-svg-9nMBTwbWL5JD5tS8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9nMBTwbWL5JD5tS8 .rough-node .label text,#mermaid-svg-9nMBTwbWL5JD5tS8 .node .label text,#mermaid-svg-9nMBTwbWL5JD5tS8 .image-shape .label,#mermaid-svg-9nMBTwbWL5JD5tS8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-9nMBTwbWL5JD5tS8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9nMBTwbWL5JD5tS8 .rough-node .label,#mermaid-svg-9nMBTwbWL5JD5tS8 .node .label,#mermaid-svg-9nMBTwbWL5JD5tS8 .image-shape .label,#mermaid-svg-9nMBTwbWL5JD5tS8 .icon-shape .label{text-align:center;}#mermaid-svg-9nMBTwbWL5JD5tS8 .node.clickable{cursor:pointer;}#mermaid-svg-9nMBTwbWL5JD5tS8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .arrowheadPath{fill:#333333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9nMBTwbWL5JD5tS8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9nMBTwbWL5JD5tS8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9nMBTwbWL5JD5tS8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9nMBTwbWL5JD5tS8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-9nMBTwbWL5JD5tS8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9nMBTwbWL5JD5tS8 .cluster text{fill:#333;}#mermaid-svg-9nMBTwbWL5JD5tS8 .cluster span{color:#333;}#mermaid-svg-9nMBTwbWL5JD5tS8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9nMBTwbWL5JD5tS8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9nMBTwbWL5JD5tS8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-9nMBTwbWL5JD5tS8 .icon-shape,#mermaid-svg-9nMBTwbWL5JD5tS8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9nMBTwbWL5JD5tS8 .icon-shape p,#mermaid-svg-9nMBTwbWL5JD5tS8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-9nMBTwbWL5JD5tS8 .icon-shape .label rect,#mermaid-svg-9nMBTwbWL5JD5tS8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9nMBTwbWL5JD5tS8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9nMBTwbWL5JD5tS8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9nMBTwbWL5JD5tS8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 执行 a = 'hello'
a
str 对象 'hello'
执行 a = 10
a
int 对象 10

第一次赋值,名字 a 贴在 int 对象 10 上;第二次赋值,把标签撕下来,贴到一个新的 str 对象 'hello' 上。名字还是那个名字,对象已经换人了。

可以用 id() 函数验证这件事------它返回对象的身份标识,可以粗略理解为对象在内存中的地址:

python 复制代码
a = 10
print(id(a))    # 例如 2441892725264
a = 'hello'
print(id(a))    # 例如 2441894358960,和上面完全不同

两次 id 完全不同,说明 a 指向的对象换了一个。

想通了"变量是标签不是盒子",很多 Python 的特性就都有了着落。比如这是如何做到"动态类型"的?------类型长在对象上,名字随便换贴,自然动态。这也是为什么 Python 不需要(也没法)在定义时声明类型。这其实也是为什么很多人说"学 Python 要换的不只是语法,而是对变量的心智模型"------盒子思维在 Python 里会处处碰壁,尤其是后面学到函数传参和可变对象的时候。这里先按下不表,以后会详细讲。

2.3 变量的语法

定义变量的语句非常简单:

python 复制代码
a = 10

其中 a 是变量名,=赋值运算符 ,表示把 = 右侧的数据放到 = 左侧的空间中。注意方向是从右往左,不是数学上的相等断言,所以 a = a + 1 这种"数学上不成立"的写法在编程里完全合法。

使用变量无非两种操作------读取和修改:

python 复制代码
a = 10
print(a)     # 读取:10
a = 20       # 修改
print(a)     # 20

# 也可以把一个变量的值赋给另一个变量
a = 10
b = 20
a = b
print(a, b)  # 20 20

在 Python 里修改变量和定义变量用的是同一个 =,看起来没有明显区别------不像 C++ 里"定义时带类型、赋值时不带",一眼就能区分。这也是"变量是标签"模型的自然结果:定义和赋值,本质上都是"往对象上贴标签"这一个动作,本来就没有区分的必要。

2.4 变量命名的规矩

变量名要遵守一定的规则,分硬性和软性两层。硬性规则是语法层面的红线,违反直接报错:

硬性规则 说明 反例
只能由数字、字母、下划线构成 其他符号一律不行 my-name(含 -
数字不能开头 但可以出现在其他位置 1a
不能和关键字重复 ifforclass 这些被语言保留 for = 10
大小写敏感 numNum 是两个变量 混用导致 NameError

想看 Python 全部关键字的话,标准库里有现成的:

python 复制代码
import keyword
print(keyword.kwlist)

软性规则是工程层面的建议,不违反语法,但违反了会被同事嫌弃:

  • 变量名要有描述性,能表达变量的作用,totalCountx 好一万倍
  • 多个单词构成的长名字没关系,含义清晰最重要
  • 多单词时用驼峰命名法,如 totalCountpersonInfo,首个单词之外的单词首字母大写

为什么编程里不建议用数学风格的 xyz?数学上一个证明里的变量撑死四五个,凭符号就能记住;而一个程序动辄几十上百个变量,单个字母根本记不住谁是谁,给维护带来巨大困难。

实际上这里要补充一个和 C++ 习惯直接相关的点:驼峰命名是 C++/Java 圈的主流,但++Python 官方风格指南 PEP 8 给变量的建议是下划线命名法(snake_case),比如 total_count++ 。社区代码几乎清一色下划线风格,大驼峰(PersonInfo)是留给类名用的。从 C++ 过来要主动切换一下命名习惯,不然写出来的代码在 Python 圈子里会显得格格不入。

3. 类型系统:不声明类型 ≠ 没有类型

3.1 先认识四个基础类型

变量里能存的不只是数字,还可以是其他种类的数据。为了区分不同种类的数据,就有了"类型"这个概念。和 C++ / Java 不同,Python 变量的类型不需要显式指定,赋值的时候自然确定------这一点前面已经从"标签模型"的层面解释过原因了。

type() 是个内置函数,可以查看变量(对象)的类型:

python 复制代码
a = 10
print(type(a))     # <class 'int'>
a = 0.5
print(type(a))     # <class 'float'>
a = 'hello'
print(type(a))     # <class 'str'>
a = True
print(type(a))     # <class 'bool'>

四种基础类型整理成一张表,顺便把 C++ 的对应关系放进来:

Python 类型 含义 示例 C++ 中的对应
int 整数 10 int / long long
float 浮点数(小数) 0.5 double
str 字符串 'hello' std::string
bool 布尔 True bool

下面挨个看,每一个都有和 C++ 不一样的地方。

3.2 int:整数居然没有上限

在 C++ 里写 int,一个 32 位整数,最大 2147483647,超界就溢出,而且溢出还是未定义行为;换 long long 撑到 64 位,再大就得 __int128 或者上大数库了。

Python 的 int 呢?表示范围没有上限,只要内存够,想多大就多大:

python 复制代码
print(2 ** 100)
text 复制代码
1267650600228229401496703205376

2 的 100 次方,31 位数,直接算,没有任何问题。

这是如何做到的?CPython(最主流的 Python 解释器)里的整数不是用一个固定宽度的机器字存的,而是对象内部挂了一个"数字数组",每个元素存 30 个二进制位,运算时按需扩展数组。于是 C++ 用 8 个字节加一个 CPU 寄存器就能搞定的事,Python 要走对象分配、逐段计算。

说白了就是:C++ 的 int 是给 CPU 看的,Python 的 int 是给数学看的。代价也明摆着------Python 的整数运算比 C++ 慢一到两个数量级。这是一笔用性能换"永不错误溢出"的交易,对 Python 的目标场景(脚本、胶水、业务逻辑)来说,多数时候是划算的。

3.3 float:只有 float,却是 double

Python 的小数只有 float 一种类型,没有 double。听起来是不是比 C++ 还"落后"?

实际上完全不是。Python 的 float 就是 C++ 的 double------双精度浮点数,8 个字节、64 个二进制位,能精确到小数点后约 15 位。只是 Python 没有提供单精度(C++ 的 float,4 字节)这个选项而已。所以不存在"Python 精度低"这回事,只是选择少。

关于单精度和双精度此处不展开,只要知道:双精度占内存更多、精度更高,现代编程里几乎是默认选择。

3.4 str:单引号双引号一个样

' '" " 引起来的就是字符串,用来表示文本:

python 复制代码
a = 'hello'
print(len(a))        # 5,len() 获取字符串长度

a = 'hello'
b = 'world'
print(a + b)         # helloworld

三个和 C++ 明显不同的点:

其一,Python 里单引号和双引号完全等价,'hello'"hello" 是同一个字符串,只是让你在字符串内容里方便地嵌另一种引号。C++ 里 'a'char"a" 是字符串,完全是两种东西,混用还有隐式转换的坑。

其二,顺着上面这点------++Python 没有"字符"这个类型++ 。单个字符就是长度为 1 的字符串,'a'"a" 没有任何区别。

其三,+ 对字符串做拼接。但++字符串不能和整数/浮点数相加++ ,'hello' + 10 会直接抛 TypeError。Python 在这点上不做隐式转换------又一个和 C++ 不同的哲学:C++ 允许 'a' + 1(char 和 int 混算,结果是 98),Python 认为类型不匹配就该报错,宁可让程序当场停下来,也不悄悄猜你的意图。

字符串是开发中最常用的数据类型,支持的操作非常丰富,这里暂时不展开。

3.5 bool:大写开头,还是 int 的子类

布尔类型只有两个取值:True(真)和 False(假)。所谓布尔,就是初中数学里"命题"那个概念------一个命题要么真要么假,布尔类型就是用来承载这个判定结果的。

对 C++ 出身的人来说,++第一个坑是大小写++ :Python 是 True / False,首字母大写,不是 C++ 的 true / false。手滑写成小写会收获一个 NameError

还有一个冷知识:Python 的 bool 实际上是 int 的子类,True 本质上就是 1False 就是 0。验证一下:

python 复制代码
print(True + True)           # 2
print(isinstance(True, int)) # True
print(10 + True)             # 11

所以整数和布尔值相加是完全合法的操作(后面自测题里就有一道考这个)。有的语言把布尔做成完全独立的类型、禁止和数字混算,Python 则选择继承 int,算是兼容了 C 时代拿 0/1 当真假用的习惯。

3.6 为什么要有类型?

学到这里自然要问:搞这么多类型干嘛?类型系统存在的意义有两个。

**第一,类型决定了数据在内存中占多大空间。**比如 float 占 8 个字节。

这里把单位的账算清楚:计算机用二进制表示数据,每个位只能是 0 或 1;一个二进制位叫一个比特(bit) ,8 个二进制位叫一个字节(Byte) 。所以一个 float 占 8 字节 = 64 个二进制位。我电脑 16GB 内存,换算下来一共是 16 * 1024 * 1024 * 1024 * 8 个二进制位------这就是宿舍楼里房间的总数。

第二,类型约定了对数据能做什么操作。 int / float 能做 + - * /str 只能做 +(而且行为是拼接),做 - * / 会报错,但反过来有 len() 这种字符串专属操作。

说白了,类型系统就是在给数据"归类",相同类型的数据往往有类似的特性和使用规则。归了类,解释器才知道该按哪套规则处理这块数据。

3.7 动态类型:一把双刃剑

前面实验过的这段代码,就是"动态类型"的展示:

python 复制代码
a = 10
print(type(a))    # <class 'int'>
a = 'hello'
print(type(a))    # <class 'str'>

在程序运行过程中,a 的类型从 int 变成了 str,这个特性称为动态类型 。C++ / Java 那种变量定义后类型就固定死的,称为静态类型

动态类型是一把双刃剑:

  • 对中小型程序,能大大节约代码量。比如写一个函数,不用为 intfloat 各写一遍,一段代码天然就能同时支持多种类型
  • 对大型程序,它提高了模块之间的交互成本。别人提供的函数,参数没有类型声明,不读实现根本不知道该传什么------程序猿 A 的代码,程序猿 B 接手要多花好几倍的理解成本

实际上 Python 官方也意识到了这个问题,Python 3.5 之后引入了类型注解(type hints),可以给参数和返回值标注类型,再配合 mypy 这类工具做静态检查,算是动态语言给自己补上的安全带。现阶段知道有这个东西就行,这里不做详细说明。

另外注意区分两件事,很多人会搞混:Python 是"强类型"语言,但同时也是"动态类型"语言。强类型说的是 'hello' + 10 这种类型不匹配的操作不会偷偷帮你转换、而是直接报错;动态说的是变量可以换类型。这是两个独立的维度------Python 是强类型 + 动态,C++ 是强类型 + 静态。

4. 注释:写给人看的代码

4.1 为什么注释这么重要

回到 2.1 的方差代码,把注释去掉:

python 复制代码
avg = (67.5 + 89.0 + 12.9 + 32.2) / 4
total = (67.5 - avg) ** 2 + (89.0 - avg) ** 2 + (12.9 - avg) ** 2 + (32.2 - avg) ** 2
result = total / 3
print(result)

这段代码语法上没有任何问题,但你盯着看,能一眼 get 到它是求方差吗?大概率要顺着公式逆推一遍。加一行注释,就一目了然了:

python 复制代码
# 计算 4 个数字 67.5, 89.0, 12.9, 32.2 的方差
avg = (67.5 + 89.0 + 12.9 + 32.2) / 4

所谓的注释,就是一种特殊的代码------不参与程序执行,纯粹起解释说明的作用,帮程序猿理解代码的执行逻辑。写代码烧脑,读代码同样烧脑,一板一眼的代码之间穿插几句口语化的描述,理解成本会低很多。

这里有句话值得单独强调:代码的第一目标是容易理解,第二目标才是执行正确。代码首先是写给人看的,顺便让机器执行。写注释不光是方便别人,更是方便三个月之后的自己------三个月后回头看没注释的代码,跟看别人的代码没区别。反例可以想想早年医生的手写处方:写的人自己认识,抓药的人靠猜,过俩个月病人拿回来,连医生自己都不认识了。

4.2 注释的两种语法

python 复制代码
# 这是一行注释.
"""
这是文档字符串
这是文档字符串
"""
  • 注释行# 开头,从 # 到行尾都是注释
  • 文档字符串(docstring) :三引号("""''',两者等价)引起来的字符串,可以包含多行内容,一般放在文件 / 函数 / 类的开头

实际上文档字符串和 # 注释有个本质区别:docstring 在运行时是真实存在的字符串对象,会被求值并挂到对象的 __doc__ 属性上,很多工具(help()、文档生成器)都会去读它;# 注释则在解析阶段就被丢弃了。所以严格说 docstring 是"恰好没有副作用的表达式",把它当注释用没问题,但它承担的其实是"文档"职责。

4.3 注释的四条规范

规范 说明
内容准确 注释要和代码一致,代码改了注释也要跟着改;过期注释比没注释更害人
篇幅合理 太精简等于没写,长篇大论又没人看
使用中文 国内公司基本都要求中文注释,外企另说
积极向上 注释里别夹带负能量(比如吐槽领导),这些会跟着代码一起进版本库的

5. 输入输出:和用户打交道

5.1 交互的基本概念

程序需要和用户交互。用户把信息传递给程序的过程,称为输入 ;程序把结果展示给用户的过程,称为输出

最基本的输入输出方式就是控制台:用户在控制台输入一些字符串,程序再往控制台打印一些字符串。PyCharm 运行程序时下方弹出的窗口就是控制台,Windows 自带的 cmd 程序也是。

日常更常见的其实是图形化界面------QQ、浏览器、Steam,用户全程鼠标点按钮,不需要输入任何命令。Python 当然也能开发图形化程序,但 GUI 开发本身是个大话题,此处不做展开。

5.2 输出:print 与 f-string

python 复制代码
print('hello')   # 输出字符串
a = 10
print(a)         # 也能输出其他类型的变量
b = True
print(b)

更多的时候我们需要输出"字符串 + 变量"混合的内容,比如输出 num = 10。Python 3 里最方便的写法是 f-string:

python 复制代码
num = 10
print(f'num = {num}')   # num = 10

f 为前缀的字符串称为 f-string,里面可以用 {} 内嵌变量或表达式,{} 里的内容会被求值后填进字符串。{a + b} 放的就不是变量而是一个加法表达式。Python 还有 % 格式化和 format() 方法等格式化字符串的方式,此处只掌握 f-string 这个最简单的即可,其他的用到再说。

5.3 输入:input 的返回值永远是字符串

python 复制代码
num = input('请输入一个整数: ')
print(f'你输入的整数是 {num}')

input 的参数相当于一个提示信息(也可以没有),返回值就是用户输入的内容。

看起来人畜无害,但这里埋着 Python 入门阶段最经典的坑。++input 的返回值永远是字符串类型++,不管用户输入的是不是数字。看这段代码:

python 复制代码
a = input('请输入第一个整数: ')
b = input('请输入第二个整数: ')
print(f'a + b = {a + b}')

输入 1020,输出是:

text 复制代码
a + b = 1020

1020?这是因为 + 作用在两个字符串上,行为是拼接而不是算术。想让它做算术,得先做类型转换:

python 复制代码
a = input('请输入第一个整数: ')
b = input('请输入第二个整数: ')
a = int(a)          # 把字符串转成 int
b = int(b)
print(f'a + b = {a + b}')    # a + b = 30

类似的,float()bool()str() 可以完成对应的类型转换。

综合练习:输入 4 个小数,求平均值。

python 复制代码
a = input('请输入第一个数字: ')
b = input('请输入第二个数字: ')
c = input('请输入第三个数字: ')
d = input('请输入第四个数字: ')
a = float(a)
b = float(b)
c = float(c)
d = float(d)
avg = (a + b + c + d) / 4
print(f'平均值: {avg}')

这四个数字要输四次。那要是任意多个数字怎么办?这就需要循环了,现在还写不了,后面章节会解决。

6. 运算符:细节里全是魔鬼

6.1 算术运算符

+ - * / % ** // 这一系列进行算术运算的运算符,统称算术运算符。前四个不用多说,后三个每一个都有讲究。

/:除数不能是 0。

python 复制代码
print(10 / 0)
text 复制代码
ZeroDivisionError: division by zero

这里出现了一个新概念------异常 。所谓的异常,就是编程语言中的一种常见机制,表示程序运行过程中出现了"意外情况",导致程序不能继续往下执行。C++ 里整数除零是未定义行为,程序可能直接崩、可能产出垃圾值,全看编译器心情;Python 的态度是明确抛出 ZeroDivisionError,把错误类型和出错位置都告诉你。工程上后者的可诊断性好得多。

%:求余数,不是百分数。

python 复制代码
print(7 % 2)    # 1

7 除以 2,商 3 余 1,小学二年级数学。有同学容易把它当成百分数,注意不是。

**:乘方,还支持小数次方。

python 复制代码
print(4 ** 2)     # 16
print(4 ** 0.5)   # 2.0

4 ** 0.5 就是开平方。C++ 里这套得靠 pow() 函数,Python 直接做成了运算符。前面 2 ** 100 的例子也印证了 int 无上限------乘方运算不会溢出。

//:取整除法(地板除),负数陷阱重灾区。

python 复制代码
print(7 // 2)     # 3
print(-7 // 2)    # -4

7 // 2 是 3,没问题。-7 // 2 呢?如果按 C++ 的思维------C++ 整数除法是向零截断-7 / 2-3------那答案应该是 -3。但 Python 给出 -4

// 是向下取整(地板除),不是截断。-3.5 向下取整是 -4,向零截断才是 -3。这是 Python 和 C++ 在负数除法上最隐蔽的行为差异,两套规则对正数完全一致,只有负数才分道扬镳。

Python 的取舍是一套自洽的数学://% 满足恒等式 a == (a // b) * b + (a % b),并且 ++a % b 的符号和 b 一致++。验证:

python 复制代码
print(-7 % 2)    # 1
# -7 == (-7 // 2) * 2 + (-7 % 2) == (-4) * 2 + 1 == -7 ✓

而 C++ 里 -7 % 2-1。哪个设计更好?各有场景:C++ 的截断贴近硬件行为;Python 的版本在"取模当索引用"的时候有优势------比如环形缓冲 i % len,Python 保证结果永远落在 [0, len) 区间,不会出现负索引。不用纠结谁对谁错,记住两套规则不一样就行。

6.2 关系运算符

< <= > >= == != 这一系列,用来比较操作数之间的关系。关系成立返回 True,不成立返回 False

python 复制代码
a = 10
b = 20
print(a < b)     # True
print(a <= b)    # True
print(a > b)     # False
print(a >= b)    # False
print(a == b)    # False
print(a != b)    # True

关系运算符不只针对数字,还能比较字符串:

python 复制代码
a = 'hello'
b = 'world'
print(a < b)     # True
print(a == b)    # False

字符串比较的规则是字典序 :想象一本英文词典,单词按字母顺序排列,一个单词在词典上越靠前就越小。'hello' < 'world' 成立,因为首个字母 h 排在 w 前面;首字母相同时就比较第二个字母------比如著名的 abandon 长期霸占词典第一页,就是因为它以 ab 开头。

有两点要注意:

  • 字符串判等直接用 == / !=,比较的是内容。这一点对 C 程序员尤其友好------C 里拿 char*== 比的是指针地址,比内容得 strcmp;C++ 的 std::string 重载了 == 才行。Python 的 == 从一开始就是语义比较
  • ++字典序按字符编码比,大写字母排在小写字母前面++ ('Z' 编码 90,'a' 编码 97),所以 'Z' < 'a' 成立。比较用户输入时大小写混着比,容易得到反直觉的结果

对浮点数,不要用 == 判定相等。做个实验:

python 复制代码
print(0.1 + 0.2 == 0.3)    # False
print(0.1 + 0.2)           # 0.30000000000000004

0.1 + 0.2 居然不等于 0.3,还带了个 0.00000000000000004 的小尾巴。为什么?

事实上,浮点数在计算机中的表示不是精确的 。计算机用二进制存数,而 0.1 换算成二进制是 0.0001100110011... 的无限循环小数,存进 64 位的双精度格式时必须舍入截断,误差就此埋下;0.2 同理。两个带误差的数相加,误差就暴露出来了。== 是锱铢必较的,哪怕差这么一点也判不相等。

现在可以回收开头那个伏笔了------为什么 (67.5 + 89.0 + 12.9 + 32.2) / 4 输出的是 50.400000000000006 而不是 50.4?同一类问题:这几个十进制小数在二进制下都存不精确,累加过程中误差冒头了。这不是 Python 的 bug,主流语言全部如此,根源是 IEEE 754 标准的浮点数格式。你在 C++ 里 printf("%.17f", 0.1 + 0.2) 会看到一模一样的尾巴。

(顺便解释一个细节:为什么 print(0.1) 显示的是干净的 0.1,而 print(0.1 + 0.2) 就露馅了?因为 Python 显示浮点数时会选"能往返还原的最短表示"------字符串 0.1 转回浮点数还是那个近似值,所以显示 0.1;而 0.1 + 0.2 的结果没有这么短的往返表示,尾巴就藏不住了。)

那浮点数怎么比较相等?不再严格相等,改判差值是否在允许的误差范围内:

python 复制代码
a = 0.1 + 0.2
b = 0.3
print(-0.000001 < (a - b) < 0.000001)    # True

实际工程实践中误差在所难免,只要保证误差在合理范围内即可------这是浮点数比较的标准姿势,比追求"绝对相等"现实得多。

6.3 逻辑运算符

andornot

运算符 含义 规则
and 并且 两侧均为 True 结果才是 True,一假则假
or 或者 两侧均为 False 结果才是 False,一真则真
not 逻辑取反 True 变 False,False 变 True
python 复制代码
a = 10
b = 20
c = 30
print(a < b and b < c)    # True
print(a < b and b > c)    # False
print(a > b or b > c)     # False
print(a < b or b > c)     # True
print(not a < b)          # False
print(not a > b)          # True

这里的"并且""或者",就是我们日常生活中使用的语义。想象丈母娘提要求:什么叫"有房并且 有车"(俩都得有),什么叫"有房或者有车"(有一样就行),一模一样的逻辑。

连续比较的写法。 a < b and b < c 等价于 a < b < c

python 复制代码
print(a < b < c)    # True

这个设定和大部分编程语言都不一样。C++ 里 a < b < c 会被解析成 (a < b) < c------先算出 a < b 是 0 或 1,再拿这个 0/1 和 c 比较,结果基本不是你想要的,是个著名的坑。Python 直接把连续比较做成了数学直觉,而且 b 只求值一次。前面浮点数比较的例子 -0.000001 < (a - b) < 0.000001,用的就是这个特性。

**短路求值。**和 C++ 的 && / || 类似,Python 的逻辑运算符也有短路规则:

  • and:左侧表达式为 False,整体一定为 False,右侧不再执行
  • or:左侧表达式为 True,整体一定为 True,右侧不再执行

用实验验证:

python 复制代码
print(10 > 20 and 10 / 0 == 1)    # False
print(10 < 20 or 10 / 0 == 1)     # True

如果右侧的 10 / 0 真的执行了,程序必然抛 ZeroDivisionError。但这两行都正常输出了结果,说明右侧压根没被执行。短路求值不只是省一点计算,更是一种保护机制 ------常见的用法是"先检查再访问",比如 s and s[0] == '#':当 s 是空字符串时,右侧的下标访问不会执行,避免了越界。

6.4 赋值运算符

= 表示赋值,前面已经用过很多次了,注意和判等的 == 区分------一个赋值一个比较,写混了逻辑直接错乱。

= 还有两种进阶用法。链式赋值,一行给多个变量赋同一个值:

python 复制代码
a = b = 10

多元赋值,一行给多个变量分别赋值:

python 复制代码
a, b = 10, 20

多元赋值有个非常优雅的应用------交换两个变量。C++ 风格的写法需要临时变量:

python 复制代码
a = 10
b = 20
tmp = a
a = b
b = tmp
# 交换后 a=20, b=10

Python 一行:

python 复制代码
a = 10
b = 20
a, b = b, a
# 交换后 a=20, b=10

a, b = b, a 为什么不需要临时变量就能完成交换?执行顺序是:先把右侧整体求值,b, a 会打包成一个元组 (20, 10),然后再把元组解包、按位置赋给左边的 ab。右侧求值在前、赋值在后,中间不存在"值被覆盖"的窗口期,临时变量自然就省了。

复合赋值运算符+= -= *= /= %=a += 1 等价于 a = a + 1,其他同理:

python 复制代码
a = 10
a = a + 1
print(a)     # 11

b = 10
b += 1
print(b)     # 11

**Python 没有 ++--。**C++ 程序员的手指记忆要强制改掉了,自增自减请用 += 1 / -= 1

为什么 Python 把这么常用的运算符砍了?++ 最大的问题在于前置和后置的语义差异:++i 先自增再返回新值,i++ 先返回旧值再自增。就这么一个差别,养活了无数面试题和线上 bug。Python 的语法设计哲学之一就是++宁可少一种方便的写法,也不要一种容易出错的写法++,干脆整个删掉。这其实也是为什么 Python 代码普遍"长得都差不多"------语言刻意收敛了同一件事的多种表达方式。

(一个冷知识:++i 在 Python 里语法上竟然是"合法"的------它会被解析成 +(+i),正号的正号,什么也不做;而 i++ 是真正的语法错误。所以手滑写了 ++i 不会报错,只会安静地什么都不干,这个坑比直接报错还阴。)

除了上面这些,Python 还有身份运算符isis not)、成员运算符innot in)、位运算符& | ~ ^ << >>)等,此处暂时不介绍,用到的时候再展开。

7. 自测:三道题检验吸收程度

(1)多选 以下关于变量之间加法运算的说法,正确的是:

A. Python 中的字符串之间能够相加

B. Python 中的字符串可以和整数相加

C. Python 中的整数可以和浮点数相加

D. Python 中的整数可以和布尔值相加

(2)单选 以下关于类型的说法,正确的是:

A. Python 中既有字符串类型,也有字符类型

B. Python 中既有 float,也有 double

C. Python 中既有 int,也有 long

D. Python 中的整数表示范围无上限

(3)单选 以下 Python 代码,合法的是:

A. int a = 10

B. a = 10;

C. a = true

D. a = 'aaa' + 10

先自己答,再看解析。

解析:

(1)选 ACD 。A 正确,字符串相加是拼接;B 错误,'aaa' + 10 直接抛 TypeError,Python 不做字符串和数字的隐式转换;C 正确,10 + 0.510.5,int 会提升为 float 参与运算;D 正确------这是最容易漏选的,因为 boolint 的子类,10 + True11

(2)选 D。A 错,Python 没有字符类型,单个字符就是长度为 1 的字符串;B 错,只有 float,但它本身就是双精度;C 错,Python 3 里 int 和 long 已合一,就一个 int,且范围无上限。

(3)选 B 。A 是 C++ 的写法,Python 定义变量不写类型;B 里行尾的分号是合法的 ------Python 允许分号作为语句分隔符,只是不推荐,属于"合法但丑";C 错在 true 小写,Python 的布尔是 True,小写会 NameError;D 是 TypeError,字符串不能和整数相加。

总结

把这一章的差异点串起来看,主线其实就一条:C++ 的语法贴近机器,Python 的语法贴近人的直觉。

  • / 真除法 vs 截断除法------一个贴近数学,一个贴近硬件指令
  • int 无上限 vs 固定宽度------一个给数学看,一个给寄存器看
  • 动态类型 vs 静态类型------一个省代码,一个换编译期安全
  • 没有 ++------砍掉方便,消灭易错
  • 变量是标签 vs 变量是盒子------两种完全不同的心智模型

没有孰优孰劣,只有场景取舍:C++ 用"贴近机器"的设计换来了极致性能,Python 用"贴近直觉"的设计换来了开发效率。理解了这套取舍,再看 Python 后面的很多设计(可变与不可变对象、函数传参、类型注解),很多"为什么"都能提前猜到答案。

目前学的这些还只能写"直线代码"------从上到下执行到底。下一章学逻辑判断(if / else),之后就能写稍微复杂一点的程序了。

相关推荐
小蒜学长13 分钟前
springboot党建云课堂学习与管理系统(代码+数据库+LW)
java·数据库·spring boot·后端·学习
烂蜻蜓18 分钟前
Flask入门教程(十六):中间件与扩展——增强应用功能的两种机制
python·中间件·flask
mldong25 分钟前
六语言引擎实现对比:同构背后的妥协与差异
java·架构
烂蜻蜓2 小时前
Flask入门教程(十二):表单处理——从基础到Flask-WTF完整实践
后端·python·flask
jay神2 小时前
【计算机毕业设计】基于Flask+Vue的电影数据可视化与智能推荐系统
前端·vue.js·python·信息可视化·flask·毕业设计·课程设计
满怀冰雪3 小时前
24-PaddleClas 模型评估、导出与推理部署入门
大数据·人工智能·python·深度学习·paddle
Chester_19998 小时前
CSP202203C.计算资源调度器
开发语言·数据结构·c++·蓝桥杯