理解该程序输出,Python精进一步

前言

某日,某君抛给我一段python代码:

python 复制代码
seq = [1, 2, -1, 0]
seq[0], seq[seq.index(max(seq))] = seq[seq.index(max(seq))], seq[0]
seq[len(seq) - 1], seq[seq.index(min(seq))] = seq[seq.index(min(seq))], seq[len(seq) - 1]
print(seq)

某君不理解程序的输出,想要我解释一下。

再继续阅读之前,读者不妨思考一下程序的输出。

程序的输出

程序输出了[1, 2, 0, -1]

某君的目的

某君要实现的目的很简单:将一个序列里面最大的元素和第一个元素进行交换,将最小的元素和最后一个元素进行交换(暂不考虑效率)。

某君的困惑

某君发现上面的代码不能交换最大的元素但是可以交换最小的元素,这是某君最大的困惑,因为在某君看来,其认为两行代码的逻辑完全一致,要是能够成功运行,那么两行代码应该都成功或者都失败,绝不可能是一个成功一个失败。当某君问我的时候,我也觉得其说的在理。但是能明显的感觉到正确的写法,应该是先记录最大和最小元素的下标然后在进行交换,即正确代码应该是形如下面的代码:

python 复制代码
seq = [1, 2, -1, 0]
minElement = seq.index(min(seq))
maxElement = seq.index(max(seq))
seq[0], seq[maxElement] = seq[maxElement], seq[0]
seq[len(seq) - 1], seq[minElement] = seq[minElement], seq[len(seq) - 1]
print(seq)

上面的代码能够输出预期结果[2, 1, 0, -1]

踏破铁鞋无觅处

对于上面的代码我们需要搞清楚各个语句的执行顺序,对于等号与逗号表达式,其各自的执行顺序如下:

  • 对于赋值运算符而言:从右到左依次执行,有多个等号的时候,从最右边的等号开始执行。
  • 逗号表达式:从左到右依次执行

我们需要考虑上面的代码做了什么,上面的代码实际上有三步:

  1. 获取赋值运算符右边的值,将结果打包成一个元组;
  2. 将元组的值依次赋值给左边的变量。

问题的关键就在于依次 两字,如果是一次性 赋值给左边的话,那么一开始就会对左边的变量计算出来,以交换最大值为例,如果是一次性 赋值,那么上面会变成:seq[0], seq[1] = seq[1], seq[0],即能够进行正常的交换。而如果是依次那么问题就变了:

  1. 计算出赋值运算符右边并包装成元组,即(seq[1], seq[0])也就是(2, 1)
  2. 依次进行赋值,首先是seq[0] = 2,完成该赋值后seq = [2, 2, -1, 0]
  3. 接下来执行seq[s.index(max(seq))] = 1,由于seq已经发生变化此时s.index(max(seq))会返回第一个等于2的元素下标即0,即执行seq[0] = 1,完成赋值后seq = [1, 2, -1, 0]

可以发现上面的代码实际上确实发生了两次交换,只是最后又给换回去了。

接下来我们分析为什么能够成功交换最小值和最后一个元素:

  1. 首先计算出赋值运算符右边并包装成元组,即(seq[2], seq[3])也就是(-1, 0)
  2. 第一次赋值操作seq[3] = -1,完成赋值后seq = [1, 2, -1, -1]
  3. 第二次赋值操作seq[s.index(min(seq))] = 0,由于s.index()返回的是第一个找到的元素,所以其返回值为2,即执行seq[2] = 0,完成赋值后seq = [1, 2, 0, -1]

如果你成功理解了上面的过程可以试着分析如下的代码:

python 复制代码
# 1
seq = [1, 2, -1, 0]
seq[seq.index(max(seq))], seq[0] = seq[0], seq[seq.index(max(seq))]
print(seq)

# 2
seq = [1, 2, -1, 0]
seq[seq.index(min(seq))], seq[len(seq) - 1] = seq[len(seq) - 1], seq[seq.index(min(seq))]
print(seq)

上面代码的输出:

复制代码
[2, 1, -1, 0]
[1, 2, 0, -1]

良好的编程习惯

发生上面的错误时在变化中引入变化。即在上面的过程中同时引入了两个变化的过程,另外的常见错误时,在遍历一个容器的同时,向容器中增加元素,以C++为例,初学者同样容易发生如下的错误:

cpp 复制代码
vector<int> v{0, 1, 2, 3};
for (int i = 0; i < v.size(); i++) { v.push_back(i); }

上面代码的本意是希望在v后面再追加{0, 1, 2, 3}但是由于每一次push_back都会导致v.size()增加,因此上面的代码会发生死循环,正确的如下:

cpp 复制代码
vector<int> v{0, 1, 2, 3};
int n = (int)v.size();
for (int i = 0; i < n; i++) { v.push_back(i); }

在编写代码的时候不应该在变化中引入变化,这样的代码不仅不容易阅读,而且容易出现bug

相关推荐
wan9yu4 分钟前
为什么你需要给 LLM 的数据"加密"而不是"脱敏"?我写了一个开源工具
python
摇滚侠13 分钟前
你是一名 java 程序员,总结定义数组的方式
java·开发语言·python
这个名有人用不31 分钟前
解决 uv 虚拟环境使用 pip 命令提示command not found的办法
python·pip·uv·claude code
Oueii1 小时前
掌握Python魔法方法(Magic Methods)
jvm·数据库·python
2501_908329851 小时前
使用Python自动收发邮件
jvm·数据库·python
2501_908329852 小时前
NumPy入门:高性能科学计算的基础
jvm·数据库·python
2401_874732532 小时前
Python Web爬虫入门:使用Requests和BeautifulSoup
jvm·数据库·python
平常心cyk3 小时前
Python基础快速复习——集合和字典
开发语言·数据结构·python
阿钱真强道3 小时前
34 Python 离群点检测:什么是离群点?为什么要做异常检测?
python·sklearn·异常检测·异常·离群点检测