Python教程学习 (2)

更多控制流工具

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语言不一样的地方:

  • forwhile 循环可以包括 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, actiontype)。该函数可用下列方式调用:

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)

下面的函数定义中,kwdsname 当作键,因此,可能与位置参数 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字符。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值