更多控制流工具
if语句
if... elif...else,没什么特别说明的,直接看例子
>>> x = int(input("Please enter an integer: "))
Please enter an integer: 42
>>> if x < 0:
... x = 0
... print('Negative changed to zero')
... elif x == 0:
... print('Zero')
... elif x == 1:
... print('Single')
... else:
... print('More')
...
More
for语句
Python 的 for 语句与 C 或 Pascal 中的不同。Python 的 for
语句不迭代算术递增数值(如 Pascal),或是给予用户定义迭代步骤和结束条件的能力(如 C),而是在列表或字符串等任意序列的元素上迭代,按它们在序列中出现的顺序。
说得通俗一点,Python的for语句没有显示的循环下标i,也没有显示的 “i < MAX_NUM"这样的的循环终止条件。而是直接对一个指定序列(类比c中的数组)中的每一个元素进行循环。
>>> # Measure some strings:
>>> words = ['cat', 'window', 'defenestrate']
>>> for w in words:
... print(w, len(w))
...
cat 3
window 6
defenestrate 12
很难正确地在迭代多项集的同时修改多项集的内容。更简单的方法是迭代多项集的副本或者创建新的多项集:
# Delete an item
>>> users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}
>>> for user, status in users.copy().items():
... if status == 'inactive':
... del users[user]
...
>>> users
{'Hans': 'active', '景太郎': 'active'}
# Create a new sequence
>>> for user, status in users.items():
... if status == 'active':
... active_users[user] = status
...
>>> active_users
{'Hans': 'active', '景太郎': 'active'}
range()函数
内置函数 range() 用于生成等差数列。默认从0开始,也可以指定起始值;默认步长为1,也可以指定步长,步长可以为负数。
>>> for i in range(5):
... print(i)
...
0
1
2
3
4
>>> list(range(5, 10))
[5, 6, 7, 8, 9]
>>> list(range(5, 10, 3)) # given step size 3
[5, 8]
>>> list(range(-10, -100, -30)) # given a minus step size
[-10, -40, -70]
如果想用类似c的方式,使用索引下标迭代某个序列,可以组合使用range()和len()
>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
... print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb
range() 返回的对象在很多方面和列表的行为一样,但其实它和列表不一样。该对象只有在被迭代时才一个一个地返回所期望的列表项,并没有真正生成过一个含有全部项的列表,从而节省了空间。
也就是说,range()并不等于一个序列,而是一个序列的抽象定义,range()这种对象称为 称为可迭代对象 iterable,很多函数会使用这种可迭代对象作为参数,例如sum()
>>> range(10)
range(0, 10)
>>> sum(range(4))
6
循环中的break、continue语句及else子句
break的基本用法和c语言一样。
下面是和c语言不一样的地方:
for
或while
循环可以包括else
子句。- 在 for 循环中,
else
子句会在循环成功结束最后一次迭代之后执行。 - 在 while 循环中,它会在循环条件变为假值后执行。
- 无论哪种循环,如果因为 break 而结束,那么
else
子句就 不会 执行。
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(n, 'equals', x, '*', n//x)
... break
... else:
... print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
需要注意的是,else属于for而不是if。上面的例子中,如果不使用break,则两个“print”语句会在检测同一个数n的时候都会打印,这个显然是不符合逻辑的。
continue语句的定义也和c语言一样。
pass语句
pass 语句不执行任何动作。语法上需要一个语句,但程序毋需执行任何动作时,可以使用该语句。
我的理解是,由于Python对于代码块的定义不使用括号了,所以很多时候需要有这么一个pass。例如
>>> while True:
... pass # Busy-wait for keyboard interrupt (Ctrl+C)
...
>>> class MyEmptyClass:
... pass # create an empty class
...
>>> def initlog(*args):
... pass # define an empty function
...
match语句
match/case语句很像c语言的switch/case语句,不同是的:只有第一个匹配的模式会被执行。例如,下面的例子中,如果是c的switch/case语句,则case2/3/4都会被执行到。
>>> match status:
... case 2:
... print(status)
... status = status + 1
... case 3:
... print(status)
... status = status + 1
... case 4:
... print(status)
... case _:
... print('default ', status)
...
2
注意最后一个代码块:“变量名” _
被作为 通配符 并必定会匹配成功。如果没有 case 匹配成功,则不会执行任何分支。
可以使用 '|' 作为逻辑合并多个处理相同的case,和c的‘||’一样。
下面重点介绍一个“解包赋值”的概念。
>>> point=(2, 3)
>>> match point:
... case (0, 0):
... print('Origin')
... case (0, y):
... print(f"Y={y}")
... case (x, 0):
... print(f"X={x}")
... case (x, y):
... print(f"X={x}, Y={y}")
... case _:
... raise ValueError("Not a point")
...
X=2, Y=3
首先,字面值(case后面跟着的值)可以扩展为一个值对;第二,上述程序写法中,实际上支持了“变量”,字面值(0, y)/(x, 0)/(x, y)中都包含了变量。
TBD:后面讲到的对“模式”(我理解这个模式就是指case)以及嵌套的说明,感觉还是有些模糊,先放着,后面理解好了再补充
我们可以为模式添加if作为守卫子句。如果守卫子句的值为假,那么match会继续尝试匹配下一个 case 块。注意是先将值捕获,再对守卫子句求值:
>>> point=(3, 3)
>>> match point:
... case (x, y) if x == y:
... print(f"Y=X at {x}")
... case (x, y):
... print(f"Not on the diagonal")
...
Y=X at 3
if守卫子句其实可以被更为普通的c语言逻辑代替,因此Python引入了if守卫子句的作用更像是减少了缩进层次
>>> point = (4, 5)
>>> match point:
... case (x, y):
... if x == y:
... print(f"Y=X at {x}")
... else:
... print(f"Not on the diagonal")
... case _:
... print(f"Invalid point")
...
Not on the diagonal
TODO:"该语句的一些其它关键特性"很多不理解,暂时放着
模式可以使用具名常量。它们必须作为带点号的名称出现。这点有点像C++的新版本,支持带类型名称的枚举。
>>> from enum import Enum
>>> class Color(Enum):
... RED = 'red'
... GREEN = 'green'
... BLUE = 'blue'
...
>>> color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))
Enter your choice of 'red', 'blue' or 'green': blue
>>> match color:
... case Color.RED:
... print('I see red!')
... case Color.GREEN:
... print('I see green!')
... case Color.BLUE:
... print("I'm feeling the blues :(")
...
I'm feeling the blues :(
定义函数
以斐波那契数列函数fib()为例,说明怎样定义函数
>>> def fib(n): # write Fibonacci series up to n
... """Print a Fibonacci series up to n."""
... a, b = 0, 1
... while a < n:
... print(a, end=',')
... a, b = b, a+b
... print()
...
>>> # Now call the function we just defined:
>>> fib(20)
0,1,1,2,3,5,8,13,
函数内的第一条语句是字符串时,该字符串就是文档字符串,也称为 docstring,详见 文档字符串。
函数在 执行 时使用函数局部变量符号表,所有函数变量赋值都存在局部符号表中;引用变量时,首先,在局部符号表里查找变量,然后,是外层函数局部符号表,再是全局符号表,最后是内置名称符号表。因此,尽管可以引用全局变量和外层函数的变量,但最好不要在函数内直接赋值(除非是 global 语句定义的全局变量,或 nonlocal 语句定义的外层函数变量)。
实参是使用 按值调用 来传递的(其中的 值 始终是对象的 引用 而不是对象的值)
函数定义在当前符号表中把函数名与函数对象关联在一起。解释器把函数名指向的对象作为用户自定义函数。还可以使用其他名称指向同一个函数对象,并访问访该函数。从c语言的角度来理解,函数名就是这个函数的指针,这里的指针就是前面提到的函数对象。例如,基于前面定义的fib()函数,可以书写如下代码:
>>> fib
<function fib at 0x000001858F6D4D60>
>>> f = fib
>>> f(30)
0,1,1,2,3,5,8,13,21,
无返回值的函数,系统默认会返回None(是一个内置名称)。如需查看该值,可以使用 print()。注意:fib本身是函数对象,fib(n)才是函数的返回值
>>> print(fib(0))
None
编写不直接输出斐波那契数列运算结果,而是返回运算结果列表的函数也非常简单:
>>> def fib2(n): # return Fibonacci series up to n
... """Return a list containing the Fibonacci series up to n."""
... result = []
... a, b = 0, 1
... while a < n:
... result.append(a)
... a, b = b, a+b
... return result
...
>>> f40 = fib2(40)
>>> print(f40)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
result.append(a)
调用了列表对象 result
的 方法。这点和C++没有区别。需要注意的是:在本例中它等同于 result = result + [a]
,但效率更高。
函数定义详解
函数定义支持可变数量的参数。这里列出三种可以组合使用的形式。这个功能C++也有,但我感觉Python更加灵活。
默认值参数
和C++中的默认参数差不多。
注意默认值在 定义 作用域里的函数定义中求值,所以:
>>> i = 5
>>> def f(arg=i):
... print(arg)
...
>>> i=8
>>> f()
5
重要警告: 默认值只计算一次。但是当默认值为列表、字典或类实例等可变对象时,会产生与该规则不同的结果。例如,下面的函数会累积后续调用时传递的参数:
>>> def f(a, L=[]):
... L.append(a)
... return L
...
>>> print(f(1))
[1]
>>> print(f(2))
[1, 2]
>>> print(f(3))
[1, 2, 3]
不想在后续调用之间共享默认值时,应以如下方式编写函数:
>>> def f(a, L=None): # None is no longer a variant
... if L is None:
... L = []
... L.append(a)
... return L
...
>>> print(f(1))
[1]
>>> print(f(2))
[2]
>>> print(f(3))
[3]
关键字参数
Python除了按照参数列表的位置,使用默认参数的调用方式外,还可以指定参数列表中的参数关键字进行调用。这点比C++的默认参数更加灵活。关键字形参也叫作命名形参。
>>> def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
... print("-- This parrot wouldn't", action, end=' ')
... print("if you put", voltage, "volts through it.")
... print("-- Lovely plumage, the", type)
... print("-- It's", state, "!")
该函数接受一个必选参数(voltage
)和三个可选参数(state
, action
和 type
)。该函数可用下列方式调用:
parrot(1000) # 1 positional argument
parrot(voltage=1000) # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM') # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000) # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump') # 3 positional arguments
parrot('a thousand', state='pushing up the daisies') # 1 positional, 1 keyword
函数调用时,关键字参数必须跟在位置参数后面。所有传递的关键字参数都必须匹配一个函数接受的参数。关键字参数的顺序并不重要。不能对同一个参数多次赋值。
和C/C++显著不同的是,Python函数的参数列表可以接收元组(*name)和字典(**name)形参;并且元组和字典可以同时使用,此时元组必须放在字典的前面。例如,可以定义下面这样的函数:
>>> def cheeseshop(kind, *arguments, **keywords):
... """# *arguments is an element group"""
... """# **keywords is a dictionary"""
... print("-- Do you have any", kind, "?")
... print("-- I'm sorry, we're all out of", kind)
... for arg in arguments:
... print(arg)
... print("-" * 40)
... for kw in keywords:
... print(kw, ":", keywords[kw])
...
>>> cheeseshop("Limburger",
... "It's very runny, sir.",
... "It's really very, VERY runny, sir.",
... shopkeeper="Michael Palin",
... client="John Cleese",
... sketch="Cheese Shop Sketch")
-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch
可以看到,在函数cheeseshop()的调用中,
- "Limburger"对应了普通的形参kind
- "It's very runny, sir."和"It's really very, VERY runny, sir."则对应了元组形参*arguments
- shopkeeper="Michael Palin",client="John Cleese"和sketch="Cheese Shop Sketch"对应了字典形参**keywords
特殊参数
Python函数在定义形参列表时,可以使用符号'/'和'*',用于划分参数种类:仅位置参数,位置或关键词参数,仅关键字参数。具体来说,规则如下:
- 函数定义中未使用
/
和*
时,参数可以按位置或关键字传递给函数。 - 如果参数列表中使用了/(正斜杠),则仅限位置形参应放在
/
前,/
后可以是 位置或关键字 或 仅限关键字 形参。 - 如果定义了仅限关键字形参,应在参数列表中第一个 仅限关键字 形参前添加
*
仅限位置的含义:当仅限位置时,形参的顺序很重要,且这些形参不能用关键字传递。
仅限关键字的含义:表明必须以关键字参数形式传递该形参。
def standard_arg(arg):
print(arg)
def pos_only_arg(arg, /):
print(arg)
def kwd_only_arg(*, arg):
print(arg)
def combined_example(pos_only, /, standard, *, kwd_only):
print(pos_only, standard, kwd_only)
下面的函数定义中,kwds
把 name
当作键,因此,可能与位置参数 name
产生潜在冲突:
>>> def foo(name, **kwds):
... return 'name' in kwds
...
>>> foo('green', **{'name': 2})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>> foo('green', name=2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
我的理解是:foo('green', **{'name': 2})的调用等价于foo('green', name=2),如果不限定第一个name,则显然会产生对name的多次赋值。而如果对第一个name限定为仅限位置参数,则不会产生这种歧义了
>>> def foo(name, /, **kwds):
... return 'name' in kwds
...
>>> foo(1, **{'name': 2})
True
>>> foo('green', name=2)
True
任意实参列表
C/C++中类似的是可变参数列表,比如常用的printf()函数的参数列表。variadic 参数用于采集传递给函数的所有剩余参数,因此,它们通常在形参列表的末尾。如果其不在形参列表的末尾,则*args
形参后的任何形式参数只能是仅限关键字参数,即只能用作关键字参数,不能用作位置参数。
>>> def concat(*args, sep='/'):
... return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep='.')
'earth.mars.venus'
>>> concat("earth", "mars", "venus", '.')
'earth/mars/venus/.'
注意到上面最后一次调用,想通过位置参数的方式传递 '.'给sep,但实际上被当作了*args中的一个。
解包实参列表
有点像c的取内容,但其实不一样。该操作常用于对元组和字典的解包。
>>> args = [3, 6]
>>> list(range(*args)) # call with arguments unpacked from a list
[3, 4, 5]
Lambda 表达式
C++的新定义中也有这个表达式。Lambda表达式应该看成一个函数,而不是一个值。Lambda 函数可用于任何需要函数对象的地方。在语法上,匿名函数只能是单个表达式
>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[0])
>>> 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')]
文档字符串
前面提到,函数的文档字符串为每个函数定义后紧跟的注释部分。以下是文档字符串内容和格式的约定。
第一行应为对象用途的简短摘要。这一行应以大写字母开头,以句点结尾。
文档字符串为多行时,第二行应为空白行,在视觉上将摘要与其余描述分开。
文档字符串第一行 之后 的第一个非空行决定了整个文档字符串的缩进量。
实测发现,上述描述对语法都没有影响,而只是约定俗成。
函数注解
允许对函数的参数,返回值添加标注。添加的标准会以字典的形式存放在函数的 __annotations__
属性中而对函数的其他部分没有影响。形参标注的定义方式是在形参名后加冒号,返回值标注的定义方式是在参数列表和def结束符(:)之间加符号 ->。
标注的形式是一个表达式。
>>> def f(ham: 'hamberger', eggs: 'fried eggs' = 'eggs') -> 'do breakfast':
... print("Annotations:", f.__annotations__)
... print("Arguments:", ham, eggs)
... return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': 'hamberger', 'eggs': 'fried eggs', 'return': 'do breakfast'}
Arguments: spam eggs
'spam and eggs'
上例中,参数ham冒号后面的字符串'hamberger'、eggs冒号后面的字符串'fried eggs'、函数参数列表和定义结束符(冒号)之间的"-> 'do breakfast'"都是函数注解。这些部分直接去掉后,不影响函数定义本身。
小插曲:编码风格
- 缩进,用 4 个空格,不要用制表符
- 一行不超过 79 个字符。
- 用空行分隔函数和类,及函数内较大的代码块。
- 最好把注释放到单独一行。
- 使用文档字符串。
- 运算符前后、逗号后要用空格,但不要直接在括号内使用:
a = f(1, 2) + g(3, 4)
。 - 类和函数的命名要一致;按惯例,命名类用
UpperCamelCase
,命名函数与方法用lowercase_with_underscores
。命名方法中第一个参数总是用self
- 书写代码使用UTF-8或者ASCII字符。