Python教程2 - 流程控制

本文是Python教程的第2篇。我们会在第1和第2节分别介绍语句和函数,第3节介绍代码风格,第4节介绍PyCharm IDE的使用,最后1节复习我们所学的知识。这篇教程还有Jupyter Notebook版本,点击此处下载。这篇教程的内容主要来自4. 其他流程控制工具 — Python 3.7.5 文档

语句

接下来我会介绍一些基本的概念,如果觉得很抽象难以理解,可以先学习后面的部分再回来看。即使不能理解这些概念也不会妨碍编程。

语句是构成命令式程序设计的另一个重要组成部分组成,语句之间也可以像表达式一样互相嵌套形成复杂的结构。可以包含其他语句作为子语句的称为复合语句,如我们接下来会讲的ifwhilefor,反之称为简单语句

与表达式不同,语句不会产生值,语句的所有意义在于产生副作用。副作用有两种情况:

  • 修改状态,比如对变量赋值:如a = 1
  • 输入输出:如print('hello')

在保存运行代码的情况(而非交互模式)下,一个没有副作用的语句,如1 + 1,是没有意义的,可以删除,而且这通常是一个潜在的错误。

接下来我来介绍2种我们接触过的语句:

  • 表达式语句:单独一个表达式可以形成语句,称为表达式语句,如刚才提到的1 + 1print('hello')都是表达式语句,后者有输出的副作用,而前者毫无作用;
  • 赋值语句:在Python中赋值是一个语句,如a = 1,赋值语句的副作用就是修改状态。

传统的结构化编程包含以下3种复合语句结构:

  • 顺序:将多个语句挨个写出来,就是顺序结构,程序会依次执行这些语句,下面将给出示例;
  • 选择:根据条件只执行一部分语句,如1.1中的if语句;
  • 循环:根据条件重复执行一段语句,如1.2和1.3中的whilefor语句。

这里给出一个顺序执行求$\sqrt{2}$的例子,方法是牛顿迭代法($x_{n+1}=\frac{1}{2}(x_n + \frac{S}{x})$数列的极限为$\sqrt{S}$),迭代3次,并和直接计算的结果作比较:

a = 2
a = 0.5 * (a + 2 / a)
a = 0.5 * (a + 2 / a)
a = 0.5 * (a + 2 / a)
print('My sqrt is', a)
print('Real sqrt is', 2 ** 0.5)

好啦说了这么多(没啥用的),让我们开始学习流程控制语句吧。

if语句

if示例

先看if语句的例子:

x = int(input("Please enter an integer: "))
if x < 0:
    print('Negative changed to zero')
elif x == 0:
    print('Zero')
elif x == 1:
    print('Single')
else:
    print('More')

上面的例子中有两个新函数,这边介绍以下:

  • input():接受一个可选的字符串参数,打印这个字符串,然后读入一行新的字符串,返回读入的字符串(不包括换行);
  • int():接受一个字符串,返回字符串代表的整数。

所以上面代码的第一行的含义是打印Please enter an integer:,读入一行输入,转成对应的整数,将整数存入变量x

接下来的if和行末的:之间有一个表达式x < 0,这里涉及了布尔类型比较运算符,我来介绍一下。

布尔类型(bool)

布尔类型是一种只有两个值,代表真假的类型。它有两个字面量:TrueFalse,分别代表真和假,你可以在交互式命令行输入它们试试看:

>>> True
True
>>> False
False

比较运算符(comparison operator)

一共有6种比较运算符,它们都是二元中缀的运算符(接受2个操作数,位于2个操作数中间)。所有的比较运算符都具有相同的优先级且比算术运算符低,返回布尔类型。这6个比较运算符分别是:

  • <:小于;
  • >:大于;
  • <=:小于等于;
  • >=: 大于等于;
  • ==:等于;
  • !=:不等于。

这里关于键入这些符号,我有3点提醒大家。首先别在键盘或者输入法中找$\leq$和$\geq$,小于等于和大于等于就是两个字符拼接出来。然后是等于,一定记住是两个等号,一个等号的是赋值。最后也别找$\neq$,记住是!=就行。

接下来我来讲解一些比较运算符的操作数类型。显而易见,数值类型(整数和浮点数)都是可以参与比较的,来看看一些运算符计算的示例吧:

>>> 2.0 <= 4
True
>>> 3 > 5
False
>>> 1.0 == 1 # 混合数学类型比较
True
>>> 0.1 + 0.1 + 0.1 == 0.3 # 浮点数的精度丢失问题
False

最后两个例子我解释下。首先1.0 == 1并不会因为操作数的类型不同而是False,实际上,右边的操作数1会转换为浮点数,和左边比较得到相等的结果。最后一个例子中,3个0.1的和不等于0.3可能就会令很多人困惑,这是因为不同于整数,浮点数的精度是有限的,0.1实际上约是0.10000000000000001,而0.3实际上约是0.29999999999999999,所以3倍0.1就不等于0.3。那么我们该如何判断两个浮点数是不是相等呢?实际上我们会对两个数作差,取绝对值后如果小于某个很小的数(这个很小的数习惯叫epsilon),我们就认为相等:

>>> epsilon = 1e-8
>>> x = 0.1 + 0.1 + 0.1
>>> y = 0.3
>>> abs(x - y) < epsilon
True

这里我们用到了绝对值函数abs(),它接受一个数,返回它的绝对值。

除了数值可以作为比较运算符的操作数,字符串和列表也能参与比较运算,他们的比较涉及一个概念“字典序”。对于两串序列而言,我们先比较第1个元素看谁小,如果第1个元素相同就再比较第2个元素,如果最后前缀的元素都相同,但长度不一样,我们就认为短的小。对于字符串,比较的是每个字符的编码值,你只需要记住以下规则就行:0到9是依次递增不夹杂别的字符,A到Z也是依次递增不夹杂别的字符,a到z也同样是。我们来看例子:

>>> 'a' < 'z'
True
>>> 'a' < 'a0'
True
>>> [1, 1] < [1, 0]
False
>>> [1, 1] < [1, 1, 0]
True

然后我们讲解一下串连比较运算符。一般而言,我们说运算符会具有结合性,但Python的比较运算符不是这样的。我们先看例子:

>>> -1 < 3 < 2   # -1 < 3 且 3 < 2
False
>>> (-1 < 3) < 2 # True < 2
True
>>> -1 < (3 < 2) # -1 < False
True
>>> 1 < 3 > 2    # 1 < 3 且 3 > 2
True

Python的串连比较运算符既不是左结合也不是右结合的,而是很符合数学直觉的一种计算方式。-1 < 3 < 2等价于-1 < 33 < 2。而当你加上括号之后,这个魔法就消失了,如(-1 < 3) < 2会先计算(-1 < 3)得到TrueTrue转换为数值类型成1,而后1 < 2得到True;或者-1 < (3 < 2)会先计算(3 < 2)得到FalseFalse转换为数值类型成0,而后-1 < 0得到True。串联的比较运算符不必是同一种运算符,最后一个例子混用了<>,但注意12之间没有任何比较关系。这里的讨论其实涉及了布尔类型到数值类型的类型转换,这种类型转换使得布尔类型也可以参与数值计算,我们会在1.1.5介绍其他类型到布尔类型的转换,这使得其他类型也能作为if等控制语句的判断条件。

if语句格式

再看例子:

if x < 0:
    print('Negative changed to zero')
elif x == 0:
    print('Zero')
elif x == 1:
    print('Single')
else:
    print('More')

我们先说语法if后面和elif后面都是跟一个布尔类型的表达式,这是条件else后面不需要跟条件。而后,后面跟冒号:和换行,换行后必须跟至少一个子语句,子语句必须使用缩进(也就是用空格或者Tab使代码往右侧便宜),且同一块的自语句缩进相同。一般我们使用4空格缩进。elif部分只能出现在中间,可以有零个或多个,else部分只能出现在最后,可以有零个或一个。

然后我们说说语义,程序首先检查if语句的条件,如果if后面的条件成立就执行if子句的内容,然后退出if语句(不参与之后的条件判断);否则再比较第1个elif(如果有)后面的条件,成立了就执行第1个elif子句的内容,然后退出if语句,然后再比较第2个elif(如果有)的条件,以此类推。如果所有的条件不满足,就会执行else子句。

我们来看一些例子。

示例1:若x是奇数,将x1,省略elifelse部分。

x = 3
if x % 2 == 1:
    x = x + 1
print(x) # 4

示例2:猜猜下面的x是多少?记住一旦进入了某个子句内部执行完毕,就不会再去参与别的条件判断了。

x = 4
if x > 0:
    x = -1
elif x < 0:
    x = 1
print(x) # -1

示例3:if语句可以嵌套,上面的示例其实等价于下面的嵌套if语句。实际上elif就是else if的简称,但由于这种嵌套的写法会使代码缩进得越来越深,所以最终出现了elif

x = 4
if x > 0:
    x = -1
else:
    if x < 0:
        x = 1
print(x) # -1

示例4:如果子句只有一条语句,也可以不换行,像下面那样,但可以看出这种写法的可读性不高,所以不建议这么写:

x = 3
if x % 2 == 1: print('x is odd number')
else: print('x is even number')

真假值

上一个例子中x % 2 == 1其实可以简写成x % 2,像下面那样:

x = 3
if x % 2:
    print('x is odd number')
else:
    print('x is even number')

这就是因为其他类型可以转换为布尔类型,从而作为if语句的判断条件。以下值会被认为是假的,转换成False

  • 0:整型0;
  • 0.0:浮点型0;
  • '':空字符串;
  • []:空列表。

除此之外,我们目前学到的其他类型的值都是真的,会转换为True,举个例子:

if []:
    print('[] is true')
else:
    print('[] is false')
if '':
    print('\'\' is true')
else:
    print('\'\' is false')

while语句

我们先写个例子打印1到10:

i = 1
while i <= 10:
    print(i)
    i = i + 1

while语句中间缩进的部分我们称为循环体,循环体不能省略。while只要条件成立就不会不断执行循环体。如果循环无法终止,我们就称之为死循环

for语句

我们经常需要遍历列表中所有的元素进行操作,如果用while,它会是这样的:

words = ['cat', 'window', 'defenestrate']
# 打印每个单词及其长度
i = 0
end = len(words)
while i < end:
    w = words[i]
    print(w, len(w))
    i = i + 1

这种时候用for循环就方便很多,而且可读性更高:

for w in words:
    print(w, len(w))

for语句紧跟在for后面的是标识符(以后我们可以看到是别的东西),然后是in,再然后是一个可迭代对象。它会将可迭代对象的每个值按顺序赋予forin之间标识符,然后在执行循环体。与while相同,循环体不能省略。我们接触到的可迭代对象只有2个,字符串和列表,之后会介绍第3个,range()

一般情况下,一边循环一边修改可迭代对象是个错误,这是你可以对可迭代对象做个拷贝(用切片语法),再迭代。就像下面的代码那样,如果你去掉拷贝,将words[:]改为words会造成死循环:

words = ['cat', 'window', 'defenestrate']
# 将长度超过6的单词拷到尾部
for w in words[:]:
    if len(w) > 6:
        words.append(w)
print(words)
# ['cat', 'window', 'defenestrate', 'defenestrate']

range()函数

有时你只是想要迭代一个计数器,这时候range()函数就很有用,它会返回一个可迭代对象。

range()可以接受1到3个参数,我们来解释一下这3种情况:

  • range(stop):生成一个从0到stop(包含0不包含stop)的递增序列;
  • range(start, stop):生成一个从startstop(包含start不包含stop)的递增序列;
  • range(start, stop, step):生成一个从start开始的等差序列,其步进(公差)是step,如果step是正数(负数),直到数列大于(小于)stop就终止。

我们看示例:

range表达式 可迭代对象包含的数值
range(5) 0, 1, 2, 3, 4
range(5, 10) 5, 6, 7, 8, 9
range(0, 10, 3) 0, 3, 6, 9
range(-10, -100, -30) -10, -40, -70

让我们看一下示例代码,先是打印1到10:

for i in range(1, 11):
    print(i)

再是遍历单词列表,输出它是第几个单词和单词本身:

a = ['Mary', 'had', 'a', 'little', 'lamb']
for i in range(len(a)):
    print(i, a[i])

当你只是打印range,会出现奇怪的结果:

>>> range(10)
range(0, 10)

你并没有看到一串数。因为实际上只有你去把它作为for语句in后面的可迭代对象时,才会生成这一系列的数,这是为了性能考虑的。有时我们也需要将可迭代对象转换为列表,这时可用list()函数。

>>> list(range(5))
[0, 1, 2, 3, 4]

breakcontinue和循环else子句

先看示例:

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, 'is not a prime number')
            break # 跳出最内层(第2个)for循环
    else: # 当for正常结束而非break时执行
        print(n, 'is a prime number')

上面的程序用最原始的试除法来判断数是不是素数。break语句是用于终止最近的循环的。和if一样,forwhile语句也可以有else子句,它的含义书如果forwhile不是通过break终止的,它就会被执行,否则不执行。此外还有continue语句,它用于跳过剩余的循环体,直接进入下一次循环的,如下:

for num in range(2, 10):
    if num % 2 == 0:
        print("Found an even number", num)
        continue # 跳过后面的语句
    print("Found a number", num)

pass语句

我们可以注意到ifelifelsewhilefor的子句部分都必须需要语句,如果你什么语句都不想指定的时候,就可以用pass语句,它什么事都不做。如下面的死循环的例子:

while True:
    pass

关键字(keyword)

学了这么多语句,我们可以注意到一些单词在程序的语法和语义上扮演着特殊的角色,如ifelifelsewhileforbreakcontinuepassTrueFalse和我们即将学到defNone,这些单词我们称之为关键字。而另一些词,如lenintprintinputabs,它们只是一些实体的名字,我们称之为标识符。你可以通过赋值覆盖掉这些标识符原来的含义。当然,你可以给新的实体起名字,如变量名和下面会接触到的函数名,这些名字不能是关键字。我在这里列出合法标识符的命名要求(虽然可以,不建议包含中文):

  1. 不是关键字;
  2. 由数字、大小写英文字母和下线符_组成;
  3. 不能以数字开头。

函数

函数(function) 可以说是传统命令式编程的最后一个拼图。我们经常会需要将一些功能模块化。比如上面的判断一个数是否是素数,就可以写成一个函数,其输入是一个整数,输出是一个布尔值。除了模块化之外,函数直接或者间接地调用自己,也很多时候能简化问题,我们称之为递归,这会在2.1.4中介绍。模块化和递归是我认为函数最主要的作用。

定义函数

你可以像下面那样定义一个函数。

def add(a, b):
    """Add two number."""
    return a + b

定义函数的时候,函数并不会被执行,只有在调用时,才会真正执行并根据参数返回一个值。调用(call) 这个函数就和调用别的函数那样:

>>> add(1, 2)
3

def后面跟上一个标识符作为函数名称,而后跟上括号括起,逗号分割的标识符列表,我们称这些标识符为,形式参数,如上面的ab。之后的语句从下一行开始,必须缩进,称为函数体,如果函数体不需要任何操作,可以用pass语句。

最后一行的语句是return语句,return语句的后面可以跟个表达式,这个表达式的值就是函数的返回值,作为函数调用的结果。return语句不必存在在函数的最后一行。return语句也可以省略后面的表达式,这样等价于return None;函数末尾也可以没有return语句,也等价于在函数末尾写了return NoneNone这个值表示什么都没有。如果在交互界面下,一个表达式的值是None,那么什么东西都不会打印出来。看下面例子:

def get(pred):
    if pred:
        return 1
   # 没有return等价于“return None”
>>> get() # 缺参数会报错
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: get() missing 1 required positional argument: 'pred'
>>> get(True)
1
>>> get(False) # 什么都没输出,因为返回了None
>>> None # 同样什么都没输出
>>> print(None)
None

函数体的第一个语句可以是字符串文本,就像这里的"""Add two number.""",我们称之为文档字符串(docstring),通常是用3个双引号括住的跨行字符串。它给出函数的一些帮助信息,但不同于注释,它能被Python识别出来,并可以通过help()显示出来。help()携带一个参数,通常是要查询信息的函数等等,返回它的帮助信息,我们会在之后再讲文档字符串:

>>> help(add)
Help on function add in module __main__:

add(a, b)
    Add two number.

接下来我们来细致讲解函数定义的细节、准则和技巧。

形式参数(parameter)、实际参数(argument)与传参方式

正如上文说过的,我们把函数定义中,例如def add(a, b):中的ab称为形式参数,简称形参。而我们把函数调用中,例如add(1, 2)中的12称为实际参数,简称实参。但实际生活中我们可能会混用这些称呼,统称为参数。

在Python中,函数调用会发生实参向形参的赋值,你可以认为这是一种浅拷贝,因而在函数体内再对形参的赋值不会作用到函数外部,如下:

def reassign(a, b):
    a = 2
    b.append(3)
>>> c = 1
>>> d = []
>>> reassign(c, d)
>>> c # c没有发生更改,因为形参a只是被绑定到了新的值2上
1
>>> d
[3]

像上面这种发生赋值的函数调用方式,我们称之为按值调用(call by value)。Python的所有函数调用均是按值调用

当然这个世界上还有很多的调用方式,如按引用调用,这种方式下,对形参的修改会直接反应给外部。不过这不在我们的研究范围内。

作用域(scope)

每个名字都有它有效的范围,即能获取这个名字的范围,我们称之为作用域。如果没有函数,所有的名字的作用域都是很显而易见的,从它定义开始一直到最后。有了函数之后,事情稍微复杂了一些。

我们一般把定义在函数内的变量称为局部变量(local variable),而定义在函数外的变量称为全局变量(global variable),当然啦,如果你没有忘记,还有一类变量叫做形式参数。目前我们就学了这3类变量。看下面这个例子:

>>> c = 2
>>> def f(a):
...     b = 1
...     print(c)
...
>>> f(1)
2
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>> b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined

这里a是形式参数,b是局部变量,c是全局变量。

全局变量的作用域从它定义的位置一直到最后,而局部变量的作用域只从它定义的位置到函数体的结尾,最后,形式参数从性质上来讲更像局部变量,它的作用域只在函数体内部。

按照上个例子,c是全局变量,所以我们甚至能在函数f中引用它,但是函数f中的ab在外面是访问不到的。

上面我们只是访问了c,如果我们试图修改c,那情形就更加复杂了,我们来看下面的代码:

>>> c = 2
>>> def g():
...     c = 1
...     print('inside:', c)
...
>>> g()
inside: 1
>>> print('outside:', c)
outside: 2

很多人的第一反应是,我们明明对c修改了,为什么没有起效呢?这是因为在函数内部的c = 1只是创建了个局部变量,它与全局变量同名。要理解这一切的行为,我们需要讲两个规则:

  1. 在默认情况下,函数内部的赋值只会创建局部变量;
  2. 当需要使用名字(不包含赋值),且有多个同名变量存在的时候,位于最内层作用域的变量被使用。

对于第2点补充一下,你可以理解为当使用一个名字时,我们会从内到外依次去寻找这个变量。所以当我们print('inside:', c)的时候,它在局部作用域就找到了c,所以打印了1。实际上内部的c屏蔽(shadow) 了外部的c

那有没有什么方法能阻止这个行为呢?答案是有的。只要在赋值发生之前用global语句声明变量在全局作用域,就可以使赋值创建或修改全局变量。如下:

>>> c = 2
>>> def h():
...     global c, d
...     c = 3
...     d = 4
...
>>> h()
>>> c
3
>>> d
4

其实一开始有些人会觉得作用域的设计带来了很多不方便。但其实这个设计是很合理的。因为函数是对一些操作的封装,如果你随随便便修改几个全局变量,就会影响到其他函数的运行,那是有很大安全隐患的。毕竟这么多Python库的作者们才不会互相协调好,哪个名字的全局变量归谁使用。

函数设计准则

可以看出函数像个运算符(或者这句话更准确来说是运算符像个函数),它有参数和返回值。函数除了以这种方式(返回值)或修改参数所指的对象产生作用外,还可以产生副作用,即修改全局变量或者输入输出。但是,工程上来讲,我们希望函数拥有尽可能小的副作用。实际上全局变量很多时候是万恶的。一开始编程可能觉得没什么问题,但当你程序变得庞大的时候,减少全局变量、函数无副作用(尤其是不要访问修改全局变量)可以使程序逻辑更清晰。

递归(recursion)

在函数中,你不仅能够调用其他函数,你还能调用函数自己,这就被称为递归。我们以求阶乘为例,给出递归版本的和非递归版本的。递归版如下:

def factorial(n):
    if n <= 0:
        return 1
    return n * factorial(n - 1)

像这样使用:

>>> factorial(5)
120

可以看出写递归版基本是无脑的:先考虑边界条件,如果n小于等于0,那阶乘就是1(0的阶乘是1哦);其他情况$n! =n\times(n-1)!$。

突然你看到了知乎提问如何编程求10000!(一万的阶乘)? - 知乎,你跃跃欲试,却发现结果是:

>>> factorial(10000)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in factorial
  File "<stdin>", line 4, in factorial
  File "<stdin>", line 4, in factorial
  [Previous line repeated 995 more times]
  File "<stdin>", line 2, in factorial
RecursionError: maximum recursion depth exceeded in comparison

没错,递归是有深度限制的,递归太多次就会出错,那我们老老实实写循环吧:

def factorial2(n):
    prod = 1
    for i in range(1, n + 1):
        prod = prod * i
    return prod

这个写法其实也没比递归复杂太多。我们有个累乘变量prod,然后生成范围为[1, n + 1)的整数,挨个乘上去。现在你可以去知乎,把下面的答案贴上去,说这么简单的问题,5行代码就能搞定。

>>> factorial2(10000)
2846...0000

于是乎,如果你真的打开了知乎,你发现高票的Python只有一行(红红火火恍恍惚惚)。其实它少了一行from functools import reduce。如果你想知道这是怎么回事。你可以看完下面的高阶函数,再看看2.6 Lambda表达式,最后查查reduce()的帮助。

当然我才不会告诉你,阶乘函数是自带的:

>>> import math
>>> math.factorial(10000)

高阶函数(hight-order function)

Python中所谓的函数名其实是绑定了函数的变量名,你可以让函数绑定到新的变量名上:

>>> f = print # 现在f就是print的别名
>>> f('hello, world!')
hello, world!

那么函数能不能定义在函数里面,答案是可以的,像下面的函数会带来3层嵌套的作用域(全局作用域、a的局部作用域和b的局部作用域):

def a():
    def b():
        pass
    pass

接下来的问题就很有趣啦,函数可不可以作为实际参数,或者作为返回值,答案也是可以的。让我们先试着写看一个叫map的高阶函数,它是Python自带的,它对一个列表中的所有元素进行某一操作,再将操作的结果收集起来。我们实现一个类似的map2函数,代码如下:

def map2(operation, numbers):
    result = []
    for number in numbers:
        result.append(operation(number))
    return result

它是这么用的:

>>> def double(a): return 2 * a
...
>>> map2(double, [1, 2, 3])
[2, 4, 6]
>>> # 当然Python自带map
>>> list(map(double, [1, 2, 3]))

range()一样,Python自带的map()是需要list()才能转换为列表的。再来看map2()函数,它的第一个参数operation就是一个函数,这个函数接受一个数,返回这个数2倍的值。然后在map2的函数体中,我们直接通过operation(number)调用这个函数。

上面讲的是函数作为参数的情况,接下来我们看看函数作为返回值的情况。让我们来看下面这个很烧脑的add2()函数:

def add2(a):
    def f(b):
        return a + b
    return f

我们来看看这个怎么用:

>>> add2(2)(3)
5
>>> add_to_3 = add2(3)
>>> add_to_5 = add2(5)
>>> add_to_3(5)
8
>>> add_to_5(4)
9

这里add2调用1次实际上就是返回了内部的f,其return语句中的加法左操作数被填上对应的值,而再调用f1次,就会填补上return语句中的加法右操作数,这样就可以得到加法的结果。这里非常有趣的是参数a,每次调用add2都会得到一个全新的a,而后a的值就被函数f捕获。所以add_to_3捕获的a是3,而add_to_5捕获的a是5,它们互不相关。这种捕获了外部函数局部变量的内部函数,我们称之为闭包

参数默认值(default argument value)

可以给函数提供的参数提供默认值,这样当这些参数没有提供的时候,也就是给的实际参数个数比定义时的形式参数少的话,没有给出的参数就由默认参数提供。这些参数有时也会被称为可选参数(optional argument),类似地,那些没有默认值的参数就称为必选参数(required argument)。默认参数通过在参数列表中,给参数后面加上=value来设置,如下面的函数:

def power(base, exponent=2):
    return base ** exponent

习惯上我们不在形参和它的默认参数之间添加空格。你可以省略exponent的实际参数,这样exponent值为2,你也可以为它提供一个你想要的值,如下:

>>> power(2)
4
>>> power(2, 3)
8

那么我们能否给base制定默认值,而不给exponent默认值呢?答案是不行的:

>>> def power(base=2, exponent):
...     return base ** exponent
...
  File "<stdin>", line 1
SyntaxError: non-default argument follows default argument

也就是没有默认值的形参必须出现在有默认值的形参之前。这其实是很合理的,因为如果调用power(3),就不知道是base为3,还是exponent为3。

默认值实在函数定义处计算的,所以下面的代码会打印出5:

i = 5

def f(arg=i):
    print(arg)

i = 6
f()

最后一个关于默认值的考点,也是经常出现在各种面试题里的:默认值只会被执行一次。这条规则对于不可变的默认值是没有影响的,但如果默认值是可变对象(如列表),那就会有影响。比如,下面的函数会存储所有调用传递给它的参数:

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

它会打印出:

[1]
[1, 2]
[1, 2, 3]

如果你不想要这种行为(一般也非常不建议默认值是可变的),你可以这样写:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

关键字参数(keyword argument)

有些时候,记住参数的位置可能会很困难,但记住参数的名字会容易很多,而且使用名字来指定参数可读性更高,因而就有了关键字参数。在函数调用的地方,可以使用parameter_name=value方式指定某一名字参数的值。此外还有2条规则:

  • 所有的关键字参数必须出现在普通参数(positional argument)之后,关键字参数之间的顺序不重要;
  • 不能对同一参数指定两次值。

考虑下面的函数:

def print_values(x, y=2, z=3):
    print('x:', x, 'y:', y, 'z:', z)

你可以通过以下的方式调用这个函数:

>>> print_values(1)        # 普通的调用方式
x: 1 y: 2 z: 3
>>> print_values(x=2)      # 通过关键字给非默认参数指定值
x: 2 y: 2 z: 3
>>> print_values(x=3, y=4) # 通过关键字也能给默认参数指定值
x: 3 y: 4 z: 3
>>> print_values(y=5, x=4) # 关键字参数的顺序不重要
x: 4 y: 5 z: 3
>>> print_values(5, 6, 7)  # 给所有参数都指定值
x: 5 y: 6 z: 7
>>> print_values(6, y=7)   # 可以同时使用普通参数和关键字参数
x: 6 y: 7 z: 3

但以下的调用都是无效的:

>>> print_values()         # 缺少必选参数
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: print_values() missing 1 required positional argument: 'x'
>>> print_values(y=2, 1)   # 普通参数在非关键字参数之后
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument
>>> print_values(1, x=1)   # 多次指定同一参数
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: print_values() got multiple values for argument 'x'
>>> print_values(1, i=2)   # 未知的关键字参数
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: print_values() got an unexpected keyword argument 'i'

任意多的参数

有时候,我们希望函数能带任意多的普通参数和/或关键字参数。这时候就要用到一类特殊的语法。在介绍语法之前,我们先简略地介绍两个新的类型元组(tuple)字典(dict)

元组(tuple)

元组和列表几乎完全相同,也是一种序列容器。元组和列表唯一的差别是元组是不可变的,这意味着你不能对元组的元素和切片赋值,也没有append等操作。

创建元组和列表类似,只是是使用圆括号括起。如果元组只有一个元素,必须在元素的后面添加个逗号,这样做了是为了区分改变优先级的圆括号和元组的圆括号。

>>> ()        # 空元组
()
>>> (1,)      # 单元素元组,注意末尾的逗号
(1,)
>>> (1, 2, 3) # 更多元素的元组
(1, 2, 3)

在没有二义性的情况下,括号是可以省略的,就像下面那样:

>>> 1,
(1,)
>>> 1, 2, 3
(1, 2, 3)
>>> 1, 2 + 2, 3 # 逗号的优先级几乎是最低的
(1, 4, 3)

和列表一样,你可以通过索引和切片获取元组某个或某些元素的值,但由于元组是不可变的,你不能对索引或切片赋值:

>>> a = 0, 1, 2, 3
>>> a[1]
1
>>> a[1:]
(1, 2, 3)
>>> a[0] = 4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

元组和列表一样是可迭代对象。先前我们说到list()函数可以将可迭代对象转换为列表,类似地,tuple()函数可以将可迭代对象转换为元组:

>>> tuple([1, 2, 3])
(1, 2, 3)
>>> tuple(range(1, 4))
(1, 2, 3)

可能你的心中会有疑问,为什么有了列表还要有元组,我想这有两方面考虑:

  • 性能:维护一个定长的序列比不定长的序列更简单,因为定长序列不需要考虑序列的扩展(如append())操作,因而在内存占耗和性能上,定长序列都更优;
  • 不变性:有些时候我们需要不变的值,比如下面提到的字典的键,我们必须确保字典的键不能发生变化。

我相信对于Python,第2个方面的考虑吧是更加重要的。

字典(dict)

先前我们学到列表和元组都是序列容器,接下来我们介绍一种关联容器,字典。字典是一种可变的数据结构,它存储的是一种映射关系,可以根据某一个键(key)获取或修改该键对应的值(value)。字典字面量的格式形如{ key1: value1, key2: value2, ... },其中键必须是不可变类型,比如数字类型、字符串和元素都是不可变类型的元组。下面是一些合法的字典字面量:

>>> {}               # 空字典
{}
>>> {' ': 1}         # 字符串到数字的映射
{' ': 1}
>>> {'a': 1, 2: 'b'} # 键和值可以是不同的类型
{'a': 1, 2: 'b'}
>>> {(1, 2): 3}      # 元组也可以作为键
{(1, 2): 3}
>>> {[1, 2]: 3}      # 列表不能作为键
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

你可以通过字典的索引,获取某个键对应的值。也可以对索引赋值,如果这个键不存在则会创建新的键值对,否则旧的值会被覆盖掉。让我们来看例子:

>>> order = {'a': 3, 'b': 2, 'c': 3}
>>> order['b']                       # 获取字典中键对应的值
2
>>> order['a'] = 1                   # 用新值覆盖字典中键对应的旧值
>>> order
{'a': 1, 'b': 2, 'c': 3}
>>> order['d'] = 4                   # 创建新的键值对
>>> order
{'a': 1, 'b': 2, 'c': 3, 'd': 4}

我们会在以后学到关于元组和列表的更多操作。接下来让我们学习任意多的参数。

任意多的参数(续)

我们会发现,所有提供的实参无外乎两种:

  • 普通参数,又称位置参数(positional argument)非关键字参数(non-keyword argument)
  • 关键字参数(keyword argument)

而且注意,所有的位置参数出现在所有的关键字参数之前。

有时候,我们希望函数能接受任意多的位置参数和关键字参数。我们可以在函数形参列表中添加下面两个参数:

  1. *args用于将多余的位置参数捕获成一个名叫args的元组;
  2. **kwargs用于将多余的关键字参数捕获成一个名叫kwargs的字典。

这里argskwargs是惯用的命名。此外*args必须出现在**kwargs之前,看例子:

def func(a, *args, **kwargs):
    print('a:', a)
    print('args:', args)
    print('kwargs:', kwargs)

然后我们可以像下面那样调用这个函数:

>>> func(1)              # 只提供了一个位置参数,args和kwargs为空
a: 1
args: ()
kwargs: {}
>>> func(2, 3, 4)        # 提供了多余的位置参数存放在了args中
a: 2
args: (3, 4)
kwargs: {}
>>> func(a=1)            # 只提供了一个关键字参数,args和kwargs为空
a: 1
args: ()
kwargs: {}
>>> func(2, 3, b=4, c=5) # 提供了多余的位置参数和关键字参数,分别存放在args和kwargs中
a: 2
args: (3,)
kwargs: {'b': 4, 'c': 5}

**kwargs后面是不允许更其他形参的,但*args**kwargs之间可以跟别的参数,就像下面这样:

def func(a, *args, b, **kwargs):
    print('a:', a)
    print('args:', args)
    print('b:', b)
    print('kwargs:', kwargs)

这时参数b只能通过关键字参数的方式提供给参数,但其实这个语法用得并不多:

>>> func(1, 2, b=3)
a: 1
args: (2,)
b: 3
kwargs: {}

最后总结一下,函数的形参列表由以下几个部分组成:

  1. 不带默认值的参数;
  2. 带默认值的参数;
  3. *args
  4. 必须采用关键字参数赋值的参数;
  5. **kwargs

解包参数列表

上面的函数接受任意参数实际上是将函数的实参打包(pack) 成元组和字典,但有些时候我们需要相反的操作,即将可迭代对象和字典解包(unpack) 成函数的实参。这两个操作结合就能实现参数的转发。

首先可迭代对象(如列表、元组)可以通过解包成一系列的位置参数,方法是在可迭代对象之前加上*。看示例:

>>> list(range(3, 6))  # 普通的调用方式
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args)) # 函数解包的方式
[3, 4, 5]

当然你可以串连多个可迭代对象:

>>> def func(*args):
...     print(args)
...
>>> func(1, *[2, 3], 4, *(5, 6, 7))
(1, 2, 3, 4, 5, 6, 7)

然后字典也可以解包成关键字参数,方法是在字典前加上**。看示例:

>>> def func(**kwargs):
...     print(kwargs)
...
>>> func(a=1, **{'b': 2, 'c': 3}, d=4, **{'e': 5})
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

当然这里的解包仍需要遵循先前的约定,如不能对一个参数赋两次值,所有的位置参数必须在关键字参数之前。

最后,我们来看看函数参数的转发:

def my_pow(*args, **kwargs):
    return pow(*args, **kwargs)

上面代码的第一行是函数任意多参数的语法,第二行是解包参数的语法。这两者配合使得my_pow函数的用法和pow函数的用法一样了,而pow函数可以接受两个参数baseexp,其结果为base ** exp。让我们来看看如何使用my_pow

>>> my_pow(2, 3)
8
>>> my_pow(3, exp=4)
81

Lambda表达式

使用lambda关键字可以创建一个简短的函数,形式是lambda param1, param2, ... : expression,如求把两个参数的和作为返回值,可以用lambda a, b: a + b这个lambda表达式表示。当然lambda表达式也可以没有参数,如lambda: 1。lambda函数与普通函数的唯一差别是,lambda函数的函数体只能是一个表达式,不能包含语句。

来看看下面的示例:

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

这里我们使用了高阶函数,将lambda函数作为返回值。这里lambda函数还捕获了一个局部变量n

这种高阶函数并非只是烧脑用的,它有实际的用处。如列表提供了sort()方法用于排序,它的key参数可以提供给一个函数。这个函数接受列表的元素,返回真正参与比较运算的值。

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort()
>>> pairs
[(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

上面的代码中,第1次sort()会采用元素本身作为比较的参数,进而对这4个元组采用字典序比较。而第2次sort(),我们给了个lambda函数,选用元组的第2个元素,也就是onetwothreefour,进行比较,这种比较是字符串上的字典序。

文档字符串

关于文档字符串,有一些约定:

  • 文档字符串的第一行是简要概述。不应显式声明函数的名称或类型。这一行应以大写字母开头,以句点结尾;
  • 如果文档字符串要有更多行,则第二行应为空白,后面更段略,从而与开头一行分开。

你可以通过func.__doc__来获取函数的文档字符串:

def my_function():
    """Do nothing, but document it.

    No, really, it doesn't do anything.
    """
    pass
>>> print(my_function.__doc__)
Do nothing, but document it.

    No, really, it doesn't do anything.

>>> help(my_function)
Help on function my_function in module __main__:

my_function()
    Do nothing, but document it.

    No, really, it doesn't do anything.

函数标注

Python本身是动态类型语言,也就是运行的时候才会确定类型。但是写代码的时候,有时能知道类型会让我们更好的知道错误,如range()函数的3个参数我们知道都是int类型,如果我们写出了range('a', 'z'),那就会是一个潜在的bug。而且不仅如此,很多集成开发环境(IDE)的自动补全都会依赖于类型的推断,如果IDE能够得知某个变量和函数的具体类型,它的自动补全也能做得更好,方便我们开发。

通过在函数的参数和默认值(如果有)之间加入: type,可以将某个参数标记为type类型,通过在def语句结束的冒号:之前加入-> type,可以将函数的返回值标记为type类型:

def char_at(text: str, index: int) -> str:
    return text[index]

这里这个函数的text和返回值就是字符串类型,而index是整数类型。注意类型注解只是像注释一样提供了信息,它并不会检查类型是否符合。

通过函数的__annotations__属性可以获得所有的注解:

>>> char_at.__annotations__
{'text': <class 'str'>, 'index': <class 'int'>, 'return': <class 'str'>}

下表给出常见的类型对应的注解,当然下表你不必记住,需要的时候搜索即可:

类型名 类型注解
None的类型(如无返回值) None
布尔类型 bool
整数类型 int
浮点数类型 float
字符串类型 str
元素都为type类型的列表类型 List[type]
元素类型依次为type1、…、typeN的元组类型 Tuple[type1, ..., typeN]
键为key类型,值为value类型的字典类型 Dict[key, value]
参数类为type1、…、typeN,返回类型为retType的函数类型 Callable[[type1, ..., typeN], retType]
可能是type1或…或typeN类型 Union[type1, ..., typeN]

对于最后的ListTupleDictCallableUnion类型需要在最开始加上from typing import *,以后我们会将如何导入包。

最后补一句,类型注解其实在平时开发中使用的并不多,一般只有包的作者才会考虑采用类型注解。所以读者如果对这部分内容很困惑,也不必纠结太多,因为这对日常编程的影响不大。

代码风格

PEP 8是大家都遵循的一种代码风格指南。以下是最重要的内容:

  • 使用4空格缩进,不要使用制表符;
  • 一行不超过79个字符,超过了可以换行;
  • 可以用空行分割函数内的代码块;
  • 使用文档字符串;
  • 在除运算符前后使用空格,括号内部不加,如a = f(1, 2) + g(3, 4)
  • 使用lowercase_with_underscores的命名风格命名变量和函数;
  • 使用UTF-8编码,代码标识符不要采用中文。

你可以使用代码风格检查工具或者IDE(集成开发环境)来帮助你达成好的代码风格。下面我们会介绍PyCharm,它是我认为功能最强大的Python IDE之一。

PyCharm的使用

什么时候会用到IDE

先前我们介绍过JupyterLab。它主要是用于交互式的编程,也就是你输入若干行语句或表达式,它就立刻显示运行的结果。对于简短的编程(比如测试函数的用法、大多数数据分析、调用一些机器学习库等,通常来讲它们的代码量并不会特别大),交互式编程是很有帮助的。但有时候:

  • 我们需要开发一些大项目,这些大项目的代码多到如果放在一个文件里会显得很没有组织,这也算一种模块化,关于涉及多个源文件的编程我们会在以后介绍;
  • 我们觉得JupyterLab的代码补全、代码调试(debug)、代码静态分析(inspect)、风格检查(lint)、代码性能分析(benchmark)等等功能不够完善,我们需要一个强大的IDE;
  • 我们需要将我们的代码提供给别人运行,而那些人可能没有安装JupyterLab。

当遇到上面这些情况时,将代码保存成一个个.py文件,用IDE编辑就非常有优势了。

接下来我们介绍如何安装PyCharm。注意我下面的文章是基于PyCharm 2019.3版本的,之后的版本可能步骤会有变化。

安装PyCharm

你可以在Download PyCharm: Python IDE for Professional Developers by JetBrains页面下载PyCharm,它有两个版本:付费的专业(Professional)版和免费的社区(Community)版。如果你是学生,可以在JetBrains Products for Learning获得免费的专业版账号,它的验证邮件可能发得比较慢,需要耐心等待。当然你也可以选择购买,JetBrains(制作PyCharm等IDE的公司)的产品的价格并不便宜。当然社区版的功能也足够大多数人使用了。

下载完毕之后运行会出现安装向导。在选项界面,我建议勾上“Add “Open Folder as Project””,对于需要的人也可以勾上“Create Desktop Shortcut”。然后会出现选择主题界面,有明亮和黑暗两款可以选,这个就看个人喜好了。最后是插件界面,建议不要勾选任何插件,毕竟在这之后是可以再下载插件的,尤其是Vim插件,给很多初学者带来了极大的困扰。

创建工程

IDE通常将一个Python项目组织成工程。让我们来创建一个工程。

运行PyCharm后,点击“Create New Project”就可以创建一个工程。然后你可以选择一个合适的“Location”,.py文件会被直接存放到那个目录中,如果那个目录不存在会创建。如果你不知道怎么取个名字,就把Untitled改成hello吧。

先别急着创建项目,点开“Project Intepreter”,你有以下两个主要选项:

  • New environment using Virtualenv:这个选项是创建一个虚拟的环境,你安装的所有外部的包都会存放在这个虚拟环境中,这在绝大多数情况下是个很好的选择,但也意味着如果你的多个项目共同用一个很大的外部包,那么它们都会独自安装一份,占用很大的磁盘空间(由于pip会缓存安装包,所以并不会多次下载同一版本的同一个包,网络流量不需要担忧);
  • Existing interpreter下点击省略号 -> System Intepreter -> 选择你安装的Python位置(默认是在AppData目录下):这个是使用全局的Python环境,所有的包都会安装到全局的同一个目录下,如果不同的项目依赖不同版本的同一个包,那就会出现问题,这种方案虽然节省存储空间,但可能会存在各种冲突,不过也算一个不错的选择。

如果你很纠结,就使用Virtualenv吧(第一个选项),然后下一步,就可以创建工程。

编辑和运行

进入主界面后,你应该可以看到一个叫Project的窗口位于左侧。如果你没看到。可以在侧栏找到1:Project这个竖着的按钮点开,或者按Alt+1又或者点击View->Tool Windows->Project。

打开完Project窗口后,它显示了项目的目录结构,应该长这样:

IDEA Project Window
{.text-center}

然后右键项目的根目录(这里是写着hello高亮出来的一行)-> New -> Python File,创建一个文件(不需要输入后缀名),比如下面图中所示的main.py

IDEA New File
{.text-center}

然后我们添加下面的示例代码:

def convert_string_to_uppercase(text2):
    return text2.upper()


if __name__ == '__main__':
    text = input('Please input an English word:')
    result = convert_string_to_uppercase(text)
    print('It\'s uppercase is', result)

这个代码的功能就是输入一行字,输出它的大写。虽然我们把函数参数命名为text2以区别text,但这不是必要的,只是方便调试时大家看清楚是哪个变量。你可能很好奇,if __name__ == '__main__':是做啥用的,__name__是内置的全局变量,而当它的值为__main__表示这个文件是正在被执行的文件,而不是其他文件导入的依赖,如果你没听懂我说什么,那就记得加上这句话就行,以后我们还会解释为什么的。

输入完后就是下面的样子,我装的是另一个IDE,叫IDEA,所以一些细节可能不同:

IDEA Add Code
{.text-center}

注意到if语句的左侧有个绿色的箭头,这就是运行的按钮。点击一下会出现一个菜单,选择Run ‘main’就可以运行起来,这时底部会弹出一个窗口,效果就像下面这样:

IDEA Run Window
{.text-center}

点击左侧第一个绿色圆圈箭头的按钮可以重新运行,点击左侧第二个红色的方块可以终止运行,然后你可以输入一个英文单词,它会变长全部大写。

当你运行过一次之后,右上方的工具栏就会变样子:

IDEA Run Toolbar
{.text-center}

这是因为点击if语句左侧的绿色箭头会创建一个可以运行的配置,方便以后再次运行或者调试。下拉框右侧第一个绿色箭头是运行按钮,第二个臭虫是调试按钮。

调试(Debug)

调试是一种特殊的运行模式,在调试模式下,你可以让程序暂停,查看变量的值、函数调用的经过。

在进入调试模式之前,我们需要添加断点(breakpoint)。断点是会在程序运行达到一定条件时暂停程序的运行。主要用到的断点,是行断点:当程序将要但还执行指定行时,暂停运行。我们来在函数的return语句那行添加一个断点,方法是点击行号右侧灰色的的空白区域,此时一个红色的圆点会显示出来,再次点击圆点可以取消断点。添加断点后是下图的样子:

IDEA Add Breakpoint
{.text-center}

接着我们进入调试模式,方法是点击工具栏上的臭虫图标,就是下图红框圈出来的部分。

IDEA Debug Toolbar
{.text-center}

点击后程序会运行起来,同时下方会弹出调试窗口。调试窗口相比运行窗口,其左侧从上往下,多出了第2个继续(灰掉了)和第3个暂停按钮,同时窗口内部分为两个标签页,分别是“Debugger”(下图红框圈出)和“Console”。“Console”标签页会显示程序的输入输出,包括input()输出的提示。

IDEA Debug Window
{.text-center}

让我们随便输入一句话,比如hello,再按下回车。

IDEA Debug Window2
{.text-center}

这时编辑窗口中有一行高亮了出来,表示的是即将执行的一行。同时调试窗口从“Console”标签页切换到了“Debug”标签页。并且有两个子窗口,左边的“Frames”显示的是函数调用栈,右边的“Variables”显示的是当前作用域下的变量。

我们经常称函数调用的这种关系称为函数调用栈。这就涉及到一个概念,什么是栈。栈(stack) 指一种后添加的元素现出来的数据结构,有点像堆碟子,对应的还有一种是队列(queue),也就是指一种先添加的元素先出来的数据结构,有点像人们排队。具体见下方示意图。

栈的示意图 队列的示意图
Stack
Deque

而函数调用时,会创建一个局部作用域,里面包含了局部变量,这被称为栈帧(stack frame),而函数执行完毕时,这个刚插入的栈帧被丢弃。最后调用的那个函数总是先退出。函数一层层栈帧累积出来的数据结构就是一种栈,因此被称为函数调用栈。

继续回到调试界面,这时函数调用栈上有两个栈帧:

  1. 上面就是convert_string_to_uppercase()函数被调用的栈帧,你可以看到参数text2的值;
  2. 下面是全局作用域形成的栈帧,点击后,你可以看到全局变量text的值。

在“Variables”标签页左侧有个“+”按钮,点击后你可以添加一个表达式,IDE会时刻显示这个表达式的值。选中添加的表达式,点击“-”按钮就可以删除。

当成功暂停程序时,你可以选择怎么继续执行,这时就要关注调试窗口的上方,那里有一排按钮,就像下图显示的那样。

IDEA Debug Window Toolbar
{.text-center}

我们暂且讲解图中红框框出来的4个按钮,从左到右依次讲解它们的用途:

  1. 步过:运行到下一行;
  2. 步入:如果这一行有函数调用,则进入该函数调用;
  3. 步出:运行到函数结束;
  4. 运行到光标:运行到光标所在的行。

我们点击两次步过,这时候就可以发现result变量出现了,且它的值是HELLO。最后我们可以点击右侧的继续运行按钮,执行完程序。

其他

PyCharm会通过灰色波浪线、黄色背景、红色波浪线等方式提示你的潜在错误。这时你只要按Alt + Enter就可以得到错误的解决办法之类的。

光标移到函数或变量上,再按Ctrl + Q就能显示文档信息。按住Ctrl键鼠标悬停到变量或函数上就可以显示减压信息。按住Ctrl键,鼠标点击变量或函数会跳转到定义或显示使用情况。

同样的按住Ctrl + /可以注释或者解除注释。

按住Alt,鼠标左键拖拽可以矩形选定。

复习

概念重提

这次我们介绍了Python语句和函数。

Python有下面的这些语句

  • 简单语句:
    • 表达式语句:如函数调用print('hello')
    • 赋值语句:a = 1
    • break语句:用于跳出循环体,必须在循环体中使用;
    • continue语句:用于略过剩余的循环体,进入下一次循环,必须在循环体中使用;
    • pass语句:复合语句或函数定义时都需要一个子语句,如果想要省略子语句,需要加上pass
    • return语句:结束函数的执行,并将值返回给函数的调用处,必须在函数体中使用;
  • 复合语句:
    • if语句:像下面这样。必须包含if分支,可包含任意个elif分支,else分支可选。依次测试条件,若条件满足则执行分支;

      if condition1:
          statements1
      elif condition2:
          statements2
      else:
          statements3
      
    • while语句:像下面这样。如果条件用真则不断执行循环体;

      while condition:
          statements
      
    • for语句:像下面这样。令element依次被赋值为iterable中的元素,执行循环体。iterable为可迭代对象,如range()、列表、元组等等;

      for element in iterable:
          statements
      
    • whilefor语句还可跟else子句,在函数不被break时执行,不太常用。

接下来我们学习了函数。函数定义的格式如下:

def function(param1, param2, ...):
    """This is docstring."""
    statements

定义函数时,可以给函数参数指定默认值。此外可以通过*args**kwargs可以接收任意多剩余的位置参数和关键字参数。总的而言,函数的形参列表包含以下东西:

  1. 不带默认值的参数;
  2. 带默认值的参数;
  3. *args
  4. 必须采用关键字参数赋值的参数;
  5. **kwargs

在调用函数时,可以通过位置参数或者关键字参数给形参提供实参。也可以解包可迭代对象(如列表、元组等)成为位置参数,或者解包字典成关键字参数。不论如何,函数调用必须满足下面两个条件:

  • 不能对一个参数赋两次值;
  • 所有的位置参数必须在关键字参数之前。

此外我们还学习了lambda表达式,这是定义了一个简短的函数方法,其格式如下:

lambda x, y, ...: expression

这个函数没有名字,lambda表达式产生一个值,就是该函数本身。

最后我们还学习了两个新数据结构,元组字典

其中元组用(elements, ...)表示,单元素元组必须结尾添加逗号(element,)。没有歧义时,括号可以省略。其他操作上元组和列表几乎一样,只是元组是不可变的。

字典是关联容器用{key: value, ...}表示,通过dict[key] = value可以创建一个键值对,通过dict[key]可以根据键获取值。

术语表

术语 英文 含义
语句 statement 一个不产生值的最小执行单元
简单语句 simple statement 不包含其他语句的语句
复合语句 compound statement 包含其他语句作为子语句的语句
副作用 side effect 修改程序状态或进行输入输出
表达式语句 expression statement 有表达式组成的语句
赋值语句 assignment statement 注意赋值是一种语句而非表达式
条件语句 condition statement 指Python中的if语句
循环语句 loop statement 指Python中的while语句和for语句
布尔类型 boolean 拥有两种值:真和假的类型
比较运算符 comparison operator 比较两个值的大小或是否相等
字典序 dictionary order 类似英文字典那样的对序列排序的方法
缩进 indent 在行首插入空白符,Python使用缩进判别语法
循环体 loop body 循环语句被循环执行的那一部分
死循环 dead loop 不终止的循环
可迭代对象 iterable object 可以按顺序遍历的对象
关键字 keyword 程序中被保留做特殊用途的英文单词
标识符 identifier 实体的名字
递归 recursion 函数直接或间接调用自己
调用 call 执行另一个函数,将传输传递给它并获取返回值的过程
形式参数 parameter 函数定义时参数列表中的名字
实际参数 argument 函数调用时传给函数的参数
函数体 function body 函数定义中被执行的那一部分
返回值 return value 函数执行完所得到的值
文档字符串 docstring 位于函数、模块等实体开头部分用于生成文档的字符串
按值调用 call by value 函数调用时,实参拷贝给形参的调用方式
作用域 scope 名字有效的范围
局部变量 local variable 定义在函数中的变量,其作用域到函数的结束
全局变量 global variable 定义在模块中的变量,其作用域到模块的结束
屏蔽 shadow 内层作用域的标识符使外层标识符不可见的现象
高阶函数 high-order function 以函数作为参数或返回值的函数
位置参数 position argument 通过实参的位置确定对应的形参的那些参数
关键字参数 keyword argument 通过给出形参明确定对应的形参的那些参数
元组 tuple 一种不可变的序列容器
字典 dictionary 一种关联容器
打包 pack 将给定的实参合成为元组和字典的过程
解包 unpack 将可迭代对象和字典变成实参的过程
lambda表达式 lambda expression 匿名函数

编辑本页

孙子平
孙子平
静态分析方向研究生
下一页
上一页

相关