# 4.1 Python程序设计基础

## 阅读入口

* [运算符优先级](/knowledge/cs/programming/operator-precedence/)
* [Python切片](/knowledge/cs/programming/python/sequences/)
* [Python对象可变性](/knowledge/cs/programming/python/mutability/)
* [Python字符串格式化](/knowledge/cs/programming/python/string-formatting/)
* [Python内置函数](/knowledge/cs/programming/python/builtins/)

## 求值顺序

一个表达式里出现多个运算符时，需要一套规则决定先算哪一部分。**优先级**决定不同级别运算符的先后，优先级高的先结合；**结合性**决定同一优先级的运算符如何分组。括号可以强制改变默认顺序，写在括号里的部分总是先算。

下表按优先级从高到低排列，同一行的运算符优先级相同：

| 运算符                                                     | 描述                | 示例或备注                            |
| :------------------------------------------------------ | :---------------- | :------------------------------- |
| `(...)`、`[...]`、`{...}`                                 | 括号、列表、字典与集合的显示    | 优先级最高                            |
| `x[i]`、`x[i:j]`、`x.attr`、`f(...)`                       | 下标、切片、属性、函数调用     |                                  |
| `**`                                                    | 幂运算               | `2**3**2`等于`2**(3**2) = 512`，右结合 |
| `~`、`+x`、`-x`                                           | 按位取反、正负号          | 一元运算符                            |
| `*`、`/`、`//`、`%`                                        | 乘、除、整除、取模         | `//` 向下取整                        |
| `+`、`-`                                                 | 加、减               |                                  |
| `<<`、`>>`                                               | 移位                |                                  |
| `&`                                                     | 按位与               |                                  |
| `^`                                                     | 按位异或              |                                  |
| `\|`                                                    | 按位或               |                                  |
| `in`、`not in`、`is`、`is not`、`<`、`<=`、`>`、`>=`、`!=`、`==` | 比较运算符             | 优先级相同，支持链式比较`a < b < c`          |
| `not`                                                   | 逻辑非               |                                  |
| `and`                                                   | 逻辑与               | 短路：`False and x`不计算`x`           |
| `or`                                                    | 逻辑或               | 短路：`True or x`不计算`x`             |
| `if`–`else`                                             | 条件表达式             | `x if c else y`                  |
| `lambda`                                                | Lambda表达式         |                                  |
| `:=`                                                    | 海象运算符，Python 3.8+ | 优先级最低                            |

这张表只适用于Python。C使用不同的运算符集合与优先级，参见[C运算符优先级](/knowledge/cs/programming/c/operator-precedence/)。

[来源：Python运算符优先级](/knowledge/cs/programming/python/operator-precedence/#definition)

表里只列Python的运算符，本节所有表达式题都按这张表判断。教材的例题常写成数学式子的样子，作答时仍按这里的顺序求值。

## 切片的边界与返回值

### 步长决定方向和间隔

* `step`为正：从`start`向右取，停在`stop`之前，每隔`step`个位置取一个；
* `step`为负：从`start`向左取，停在`stop`之后；
* `step`不能为`0`，否则抛出`ValueError`。

### 越界只影响结果，不报错

切片会把越界的端点截到序列边界内，因此结果可能是原序列的一部分，也可能是空序列，但不会抛异常。直接写单个下标`a[100]` 或`a[-100]` 才是索引访问，越界会抛`IndexError`。

> [!WARNING]
> 切片越界和索引越界是两种不同的行为：`a[1:100]` 正常返回能取到的元素，`a[100]` 直接报`IndexError`。判断结果时先看有没有冒号。

### 切片返回新序列

切片得到的是新的序列对象，不是原序列的别名。对列表切片后修改切片，不会改动原列表；但切片只复制最外层，切片里的元素仍与原列表指向同一批对象。

切片是否返回新容器与其中的元素是否被复制是两个层次。容器可变性和别名行为另见[Python对象可变性](/knowledge/cs/programming/python/mutability/)。

[来源：Python切片](/knowledge/cs/programming/python/sequences/#properties)

切片和索引的行为差别都在这一段里。本节做题时先看写法中有没有冒号，再判断越界的后果。

## 对象可变性

### 常见类型

| 类型                             | 可变性 | 备注                       |
| :----------------------------- | :-- | :----------------------- |
| `int`、`float`、`bool`、`complex` | 不可变 | 数值运算产生新对象                |
| `str`                          | 不可变 | 字符串操作产生新字符串              |
| `tuple`                        | 不可变 | 元组保存的引用不能替换，引用目标的内容仍可能改变 |
| `list`                         | 可变  | 可以原地追加、删除或替换元素           |
| `dict`                         | 可变  | 键必须是可哈希对象                |
| `set`                          | 可变  | 元素必须是可哈希对象               |
| `frozenset`                    | 不可变 | 不可变集合，可以作为字典的键           |

“不可变”描述对象自身能否原地改变，不表示对象引用的所有子对象都不可变。例如元组`t = ([1],)` 中的列表仍可执行`t[0].append(2)`，因为这里修改的是列表；而`t[0] = [2]` 试图替换元组保存的引用，会抛出`TypeError`。

### 别名与复制

赋值`b = a`创建的是别名，不会复制对象。需要独立列表时，可以使用浅复制`b = a[:]` 或`b = list(a)`。浅复制会建立新的外层列表，但列表中的元素引用仍与原列表相同。

```python title="alias.py"
a = [1, 2]
b = a
b.append(3)
print(a)  # [1, 2, 3]
```

### 重新绑定

不可变对象上的“修改”会重新绑定变量：

```python title="rebind.py"
s = "ab"
t = s
t += "c"
print(s)  # ab
print(t)  # abc
```

`t += "c"` 没有改变原来那个字符串，而是创建一个新字符串并让`t`指向它，因此`s`看到的仍是`ab`。

[来源：Python对象可变性](/knowledge/cs/programming/python/mutability/#properties)

判断“修改”是否影响另一个变量时，先确认两个变量是否引用同一个对象，再确认对象是否允许原地改变。

## 内置函数与列表方法

Python的**内置函数**可在通常的执行环境中直接调用，无须先导入模块。例如`len`接收一个支持长度的对象并返回长度；`sorted`、`range`和`zip`分别用于排序、整数序列与按位置配对。

对象方法通过对象访问，例如列表的`append`和`sort`。调用前应确认函数或方法的参数、返回值以及是否修改已有对象；名称是否为内置函数，不能单独决定这些行为。

具体规则见[排序](/knowledge/cs/programming/python/sorting/)、[列表追加](/knowledge/cs/programming/python/list-append/) 、[range](/knowledge/cs/programming/python/range/)和[zip](/knowledge/cs/programming/python/zip/)。

[来源：Python内置函数](/knowledge/cs/programming/python/builtins/#definition)

## 排序与列表追加

`sorted(iterable)` 返回新的排序列表，不会原地修改输入；列表的`sort()` 原地排序并返回`None`。两者都可按需要传入排序键或反向排序选项，但适用对象不同：`sorted`接受可迭代对象，`sort`是列表方法。

因此`a = a.sort()` 会把`None`赋给`a`，而原列表已经被排序。需要保留原列表时可使用`sorted(a)`。

[来源：Python排序](/knowledge/cs/programming/python/sorting/#definition)

`a.append(b)` 将`b`整体作为一个元素添加到列表末尾；`a.extend(b)` 遍历`b`并将其中元素逐个添加。`append`可以接收任意对象，`extend`需要可迭代对象。两者都原地修改列表并返回`None`。

[来源：Python列表的append与extend](/knowledge/cs/programming/python/list-append/#definition)

## 整数序列与配对

`range(start, stop, step)` 表示从`start`开始、按`step`递增或递减的整数序列，**不包含**`stop`。省略`start`时起点为`0`；省略`step`时步长为`1`；步长不能为零。

`range`对象不是列表，它以紧凑的方式表示序列，需要列表时可以调用`list(range(...))`。步长的方向与起终点不匹配时，结果为空。

[来源：Python range](/knowledge/cs/programming/python/range/#definition)

`zip`从每个输入依次取一个元素组成元组，返回迭代器。默认情况下，一个输入耗尽时迭代停止，较长输入的其余元素不会出现在结果中；需要列表时可用`list`消费迭代器。

[来源：Python zip](/knowledge/cs/programming/python/zip/#definition)

`sort`、`append`、`extend`三组最容易混，区别都落在“返回新对象、原地修改还是返回`None`”上。

## 学习安排

本节汇总考试中容易混淆、需要记住的Python细节。四块内容分别回答四类问题：

* 表达式先算哪一部分：优先级、结合方向与短路求值；
* 序列的边界怎么算：切片的缺省值、负索引与越界；
* 输出长什么样：对齐、补零、精度与类型；
* 一次调用做了什么：返回新对象、原地修改，还是返回`None`。

四块之间没有推导依赖，可以按自己的薄弱项调整顺序。建议先读知识点里的表格，再合上表手算几个表达式，最后用题集核对。切片和返回值的题最容易在选择题里失分，留到最后复查。

## 课程约定

* 按Python 3的规则作答；`:=` 是Python 3.8才加入的写法，旧版本不能使用。
* 求表达式结果时按Python的实际求值顺序，不按数学书写习惯。例如 `-3**2`先算幂再取负。
* 题目说“切片越界”时按截断或空序列作答；只有单个下标越界才按抛出`IndexError`处理。
* 本节只讨论Python。C的表达式规则见[4.2 C程序设计基础](/courses/cs/csintro/4.2/)；两种语言的优先级差异见 [运算符优先级](/knowledge/cs/programming/operator-precedence/)。

## 易错点

* 幂运算右结合，`2**3**2`不是 `(2**3)**2`；一元负号的优先级低于 `**`。
* `not a == b`先比较后取非；`and`先于`or`，不要按从左到右的顺序平均理解。
* `a = a.sort()` 会把`None`赋给`a`；原地排序与返回新列表要分清。
* `append`加入的是一个元素，`extend`展开的是可迭代对象。
* 切片得到新列表，`b = a`得到的才是别名。

## 复习要点

做优先级题时先在草稿上补括号，再按括号求值；切片题先判断`step`的正负，再套`start`、`stop`的缺省值。这两步能把大部分“看着会、算错”的题挡下来。
