Introduction
上一期我们说到Python最基本的变量和一些数据类型,它们构成了我们程序设计中最基本的一个个细胞。
那么,如何将单个问题的处理方式串联为只需要修改其中部分内容,就可以得到与新的输入相对应的结果,从而避免手动重复修改变量和代码呢?
❝两个栗子
更通俗的一个栗子,假设我们需要处理一年的气温数据,通过读取对应年份的
NetCDF
文件(.nc)
,我们可以将其中一年的平均气温计算出来。假设我们现在的时间序列使用的是
CMIP6
某个GCM
的SSP2-4.5
路径下的气温,加上它对应的Historical
重建数据。也就是我们现在的时间序列年份为1850-2100
,并且它们的储存形式还不是将很多年组合在一个nc
文件里,而是离谱地每个年份对应一个nc
文件(说的就是你们,EC家族的模式)。如果我们只有
20
年或许我们还可以咬咬牙手动改nc
文件路径,通过运行20
次读取的代码实现提取平均,并把它们组合到一起形成序列。然而,对于这样
151
个项目显然过于费劲,我们需要寻求更高效的解决方案。于是,控制流的作用就体现出来了。假设我们的
nc
文件名仅有年份不一样,我们就可以通过循环年份,来实现批量化提取和处理。更进一步,假设我们现在提取的是降水数据,很多时候月数据就需要涉及到乘上对应月份天数的问题。尤其是二月,还需要判断平年、闰年才能计算出准确的月尺度总降水量。
我们可以在循环中添加判断条件,确定是否是闰年来得到二月对应的天数。
这是两个很常见的控制流应用场景,但是足够一窥控制流在程序设计中的重要地位。
如果前面讲到的各种变量、数据类型是一个个细胞,那么控制流就是遍布全身的神经系统和循环系统。
只有通过使用控制流串联其各个组件,才能成为一个真正的有机整体。
条件语句
如前文中的例子所示,很多时候我们并不能对所有的情况都进行相同的处理。因此需要根据条件对不同情况进行限定,以进行不同的操作。于是,便有了Python中常见的条件语句。
Python中我们可以使用if
来给定一个条件,如果满足条件内容,则会执行后续操作;若不满足条件将会跳过if
语句下的内容。
但是,很多时候我们有很多种不同的情况,需要分别对应不同的处理方式。于是,我们可以在if
语句后,使用多个与它同一逻辑层次的elif
语句来进行条件判断。
此外,如果我们确定除了我们给定的情况之外,其他所有的情况下执行的操作相同,可以使用else
语句来进行处理。
month = 3
if month in [1, 3, 5, 7, 8, 10, 12]:
print("31 days")
elif month in [4, 6, 9, 11]:
print("30 days")
else:
print("28 or 29 days")
31 days
这里我们使用条件语句判断输出了某个月份的天数,通过给定相同天数月份组合的列表,用in
判断月份是否属于该列表,便可以输出对应月份天数。
这里需要注意Python中最基本的一个格式,它是Python语法的一部分,也是Python区别于其他编程语言的地方。
我们前面说到「if
、elif
以及else
是同一逻辑层次」,意味着还有很多与它们不是一个层级的内容,如if
等语句后面包含的附属内容。
C语言等其他程序语言中,我们会使用类似于{}
来将该部分内容包含起来,以区分代码块的包含关系。
而在Python中,这里我们使用:
来表示代码块的开始,并使用不同的缩进关系来表达不同层级的逻辑关系。
上面的代码中,如print("31 days")
位于if month in [1, 3, 5, 7, 8, 10, 12]:
后,意味着前者是属于if
语句的附属内容。
而elif month in [4, 6, 9, 11]:
的缩进位置与if
语句相同,意味着它们属于同一逻辑层次。
在Python程序设计时,尤其需要注意缩进位置,以避免层次混乱导致的bug
。
理解了这一层,我们就可以俄罗斯套娃式的疯狂嵌套条件语句了:
year, month = 2100, 2
if year % 4 == 0 and (year % 100!= 0 or year % 400 == 0):
if month == 2:
print("February has 29 days.")
elif month in [4, 6, 9, 11]:
print("April, June, September, and November have 30 days.")
else:
print("All other months have 31 days.")
else:
if month == 2:
print("February has 28 days.")
elif month in [4, 6, 9, 11]:
print("April, June, September, and November have 30 days.")
else:
print("All other months have 31 days.")
February has 28 days.
这里我们首先判断了年份是否为闰年,再对二月的天数进行了判断。于是,我们可以得到由于2100
年为平年,因此其二月天数为28
天。
循环语句
上面是我们对于某一问题,通过判断它属于哪一种情况,从而进行相应的处理。
但如果我们涉及到多个问题,它们的处理方式大同小异,我们就可以使用循环通过修改循环体中对应部分实现重复的计算。
从而避免重复编写相同功能的代码,提高代码的利用率。
for循环
首先,最常见的一种循环方式便是我们已知需要循环的输入内容是什么,循环次数也就与输入变量的个数一致了。
那么可以使用for
循环来实现上述功能:
temperature = [-10, -5, 0, 5, 10, 15, 22, 23, 14, 11, 6, -1]
for i in range(len(temperature)):
print(f'{i + 1:02d}月的平均气温为: {temperature[i] + 273.15}K')
print('\n')
for i in temperature:
print(f'{temperature.index(i) + 1:02d}月的平均气温为: {i + 273.15}K')
01月的平均气温为: 263.15K
02月的平均气温为: 268.15K
03月的平均气温为: 273.15K
04月的平均气温为: 278.15K
05月的平均气温为: 283.15K
06月的平均气温为: 288.15K
07月的平均气温为: 295.15K
08月的平均气温为: 296.15K
09月的平均气温为: 287.15K
10月的平均气温为: 284.15K
11月的平均气温为: 279.15K
12月的平均气温为: 272.15K
01月的平均气温为: 263.15K
02月的平均气温为: 268.15K
03月的平均气温为: 273.15K
04月的平均气温为: 278.15K
05月的平均气温为: 283.15K
06月的平均气温为: 288.15K
07月的平均气温为: 295.15K
08月的平均气温为: 296.15K
09月的平均气温为: 287.15K
10月的平均气温为: 284.15K
11月的平均气温为: 279.15K
12月的平均气温为: 272.15K
上面我们介绍了两种写法,它们的输出是一致的。其中,前者与很多编程语言相似,都是计算需要循环的变量后,尔后在循环体中将变量索引出来。
而后者,则是Python循环的特色之一。它能直接循环如列表这种可迭代对象,并自动将对象中的元素作为循环变量。
类似的,如字典、元组,甚至我们之前学习的数组,以及接下来即将学习的Pandas中的DataFrame
等,都可以直接循环。
当然,对于既需要返回索引值,又需要返回待循环变量的情况,Python提供了一个内置函数:
precipitation = [10, 20, 30, 40, 50, 100, 250, 300, 50, 40, 30, 20]
for i, pr in enumerate(precipitation):
print(f'{i + 1:02d}月的降水量为: {pr:3d} mm')
01月的降水量为: 10 mm
02月的降水量为: 20 mm
03月的降水量为: 30 mm
04月的降水量为: 40 mm
05月的降水量为: 50 mm
06月的降水量为: 100 mm
07月的降水量为: 250 mm
08月的降水量为: 300 mm
09月的降水量为: 50 mm
10月的降水量为: 40 mm
11月的降水量为: 30 mm
12月的降水量为: 20 mm
此外,对于多个需要同时循环的变量,除了使用range
索引出相同位置的元素外,也可以使用zip
函数同时解析多个序列。
temperature = [-10, -5, 0, 5, 10, 15, 22, 23, 14, 11, 6, -1]
precipitation = [10, 20, 30, 40, 50, 100, 250, 300, 50, 40, 30, 20]
# 传统写法
for i in range(len(temperature)):
print(f'{i + 1:02d}月的平均气温为: {temperature[i]}°C, 降水量为: {precipitation[i]} mm')
print('\n')
# 使用zip函数同时解析多个序列
for tmp, pr in zip(temperature, precipitation):
print(f'{temperature.index(tmp) + 1:02d}月的平均气温为: {tmp}°C, 降水量为: {pr} mm')
01月的平均气温为: -10°C, 降水量为: 10 mm
02月的平均气温为: -5°C, 降水量为: 20 mm
03月的平均气温为: 0°C, 降水量为: 30 mm
04月的平均气温为: 5°C, 降水量为: 40 mm
05月的平均气温为: 10°C, 降水量为: 50 mm
06月的平均气温为: 15°C, 降水量为: 100 mm
07月的平均气温为: 22°C, 降水量为: 250 mm
08月的平均气温为: 23°C, 降水量为: 300 mm
09月的平均气温为: 14°C, 降水量为: 50 mm
10月的平均气温为: 11°C, 降水量为: 40 mm
11月的平均气温为: 6°C, 降水量为: 30 mm
12月的平均气温为: -1°C, 降水量为: 20 mm
01月的平均气温为: -10°C, 降水量为: 10 mm
02月的平均气温为: -5°C, 降水量为: 20 mm
03月的平均气温为: 0°C, 降水量为: 30 mm
04月的平均气温为: 5°C, 降水量为: 40 mm
05月的平均气温为: 10°C, 降水量为: 50 mm
06月的平均气温为: 15°C, 降水量为: 100 mm
07月的平均气温为: 22°C, 降水量为: 250 mm
08月的平均气温为: 23°C, 降水量为: 300 mm
09月的平均气温为: 14°C, 降水量为: 50 mm
10月的平均气温为: 11°C, 降水量为: 40 mm
11月的平均气温为: 6°C, 降水量为: 30 mm
12月的平均气温为: -1°C, 降水量为: 20 mm
while循环
除了上述的循环方式外,有时候我们面临着需要重复迭代某段代码,直到满足某些条件才结束循环的情况(如校正某个参数直到模型误差小于容差)。此时,便需要用到while
循环。
假设我们已知地表气温,并设定气温直减率为0.65°C/km
。当我们想要找到0°C
线的高度,我们可以考虑使用while
循环:
surface_temperature = 25.5 # 地表气温
tolerance = 0.1 # 设定迭代气温的容差
height = 0 # 初始海拔
while tolerance > 0.01:
temperature_height = surface_temperature - height * 0.0065 # 计算对应高度气温
tolerance = abs(temperature_height - 0) # 计算当前海拔气温与0°C间的误差
height += 1 # 海拔升高1米
print(height - 1)
print(25.5 - 3922 * 0.0065)
3922
0.0070000000000014495
这里我们可以看到,当海平面位置为25.5°C
,气温直减率为0.65°C/km
时,0°C
线大约位于海拔3922
米处。这与我们验算结果基本一致,误差仅为0.007°C
,小于容差0.1°C
。
当然,这里是一个很简单的例子,显然不使用while
循环也可以简单实现。这里仅是作为一个引入案例,while
循环的使用具有更广阔的应用场景。
如牛顿迭代法,牛顿二分法,实现各类型数值分析算法迭代;以及我们所熟悉的模型率定参数,while
循环是其中极为重要的一种方式,需要结合实际使用场景灵活运用。
控制流中的关键字
在条件、循环语句中,除了常规的执行程序,还有一些特殊的关键字,可以帮助我们控制程序的执行流程。主要包括:pass
、break
、continue
:
-
pass
该语句不进行任何操作,仅用于占位以保持程序结构的完整性和逻辑性。例如,我们需要与别人合作共同开发代码,归属于另一个人的工作我们可以使用pass
语句预留对应模块位置,并使用注释说明该模块内容。
-
break
break
语句用于终止当前循环,并跳出循环体。例如,我们在循环中发现满足某些条件时,就可以使用break
语句跳出循环,避免死循环。
-
continue
continue
语句用于跳过当前循环的当前迭代,并开始下一轮迭代。例如,我们在循环中发现满足某些条件时,就可以使用continue
语句跳过当前迭代,继续下一轮迭代。
# 仅做占位用的pass语句
if 894656 > 56323:
pass # 该选择支正确,但pass语句将不会执行任何操作
else:
print("894656 is not greater than 56323")
# 使用break语句提前跳出循环
for i in range(10):
if i == 5:
break
print(i, '\n')
# 使用continue语句跳过当前循环的剩余部分
for i in range(5):
if i == 3:
continue
print(i) # 3的输出将跳过
5
0
1
2
4
『案例』控制流嵌套
下面,我们可以尝试使用以上控制流的内容来实现一个简单的嵌套结构:
import numpy as np
# 建立一个气温格网数组
temperature = np.array([[20, 25, 30, 35, 40], [10, 15, 20, 25, 30], [0, 5, 10, 15, 20]])
print(temperature, '\n')
# 循环所有纬度
for i in range(temperature.shape[0]):
# 循环所有经度
for j in range(temperature.shape[1]):
# 温度大于等于20°C,则转换为开尔文
if temperature[i][j] >= 20:
temperature[i][j] += 273.15
# 气温小于20°C且大于等于10°C则赋值为1
elif 10 <= temperature[i][j] < 20:
temperature[i][j] = 1
# 其他情况
else:
# 若为5°C,则跳过该循环
if temperature[i][j] == 5:
continue
# 否则赋值为0
else:
temperature[i][j] = 0
print(temperature)
[[20 25 30 35 40]
[10 15 20 25 30]
[ 0 5 10 15 20]]
[[293 298 303 308 313]
[ 1 1 293 298 303]
[ 0 5 1 1 293]]
后记
以上便是Python中控制流的基本概念与用法,仅剩下异常处理部分我们没有提及。按照个人的经历而言,那是比较后面用到的内容了。我们留到函数式编程或炫技部分再讲。
控制流的强大在于串联起了我们一个个分散、独立的模块,通过数据索引和赋值修改变量使得批处理成为可能,从而赋予我们使用程序语言开发大型数值模拟项目和复杂数据处理流程的能力。
但毫无疑问,一旦涉及到这些多层次的逻辑结构很容易把人绕晕,刚开始的debug
过程也将痛不欲生。但通过反复理解逻辑结构和它们的作用范围等,一旦能够融会贯通地使用这些语句,对于实际问题的批量化与自动化处理将大有裨益。
用好控制流的程序设计才能真正地解放生产力。
那么,我们下期再见!
Manuscript: RitasCake
Proof: Philero; RitasCake
获取更多资讯,欢迎订阅微信公众号:Westerlies
跳转和鲸社区,云端运行本文案例。https://www.heywhale.com/mw/project/66221ce2e584e69fbfef87ba