跟着官网学Python(5)流程控制工具

“流程控制很关键。”

01 面临问题

继续跟着官网学Python,第4章其他流程控制工具。
所谓流程控制,我理解就是控制代码走向,是编程的核心。
比如最早接触的C语音面向过程编程,更加重视代码过程控制。
在之前的斐波那契数列输出示例中看到while循环就是流程控制。
那么和其他程序语言一样,Python也有很多流程控制工具,主要包括if条件语句、for循环语句以及函数等。
下面慢慢了解。

02 怎么办

条件if

和其他语言一样,如果很重要。可惜人生没有if。

>>> 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

if else 常规操作,注意没有大括号{},只需相同缩进。
中间可以有任意个elif。
类似其他语言switch case语句(Python没有)。
elif是else if 缩写,必须要缩写。
还有注意elif的条件,遇到第一个符合条件的if或elif就会返回,后面的不会被处理。

for循环

Python中for循环和其他语言不一样,**可以对任意序列进行遍历,**如字符串或列表,遍历顺序和他们在序列中出现的顺序一致。
必须要用in关键字

>>> # Measure some strings:
... words = ['cat', 'window', 'defenestrate']
>>> for w in words:
...     print(w, len(w))
...
cat 3
window 6
defenestrate 12

range()函数

内置range()函数可以快速产生一个数字序列,可以配合for循环使用。
比如range(5) 会产生长度为5的序列[0,1,2,3,4]
其实等价于range(0,5)从0开始到5结束,不包括最后一个。
还可以指定步长,如range(0,10,2) [0,2,4,6,8]
默认步长为1,默认其起始位置0.
因此range(10,2) 返回空序列,range(10,2,-2)返回[10,8,6,4]
请注意,range()只有遍历时才产生序列,你可以尝试print(range(5)),只有list(range(5))才真正变成列表。
一般情况下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

官网说这种情况使用enumerate()函数比较方便,我想了想,好像没用过。
再想想,没用过的原因是英文差,这个单词不熟!

for i,j in enumerate(a):
    print(i,j)
    
0 Mary
1 had
2 a
3 little
4 lamb

enumerate(iterable, start=0)返回一个枚举对象,包含一个计数器(从start开始,默认为0)和通过迭代序列获得的值。
计算机中数组一般都是从0开始编号,人类更容易从1开始,下面转换就很方便。

seasons = ['Spring', 'Summer', 'Fall', 'Winter']
list(enumerate(seasons))
[(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]
list(enumerate(seasons, start=1))
[(1, 'Spring'), (2, 'Summer'), (3, 'Fall'), (4, 'Winter')]

后面第5章循环的技巧部分有更多介绍。

循环中的break、continue和else

前两个用过,循环中真没用过else。
前两个和其他语言一样,
break可以跳出for或while循环。
continue 跳过本次循环后面的语句,继续下一次迭代。
没有分号
关于循环中else语句,就是循环执行完毕后执行的语句,如果循环被break跳出,则不会执行。
比如下面找5以内的素数。

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'equals', x, '*', n//x)
...             break
...     else:
...         # loop fell through without finding a factor
...         print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2

以后可以试试。

pass语句

pass语句什么也不做。当语法上需要一个语句时,但你又不知道干什么,可以先写个pass保证运行。
一般有缩进时,比我我定义函数,但是不知道函数怎么实现,先用pass占位。

函数

上面提到函数function,每个语言中都非常重要,也可以称之为方法,差不多吧,也许类class中函数才叫方法,反正我一直混着用。
首先看一个输出任意范围内斐波那契数列的函数:

>>> 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(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

关键字def定义函数,后面跟函数名和带括号的形式参数列表。后面通过缩进形成函数体。
函数第一行可以写字符串文字,帮助用户更好理解函数,如怎么用,输出那些参数,有什么功能,有些工具可以自动生成。
理论上每个函数都应该假设文档字符串,可惜我经常不加,所以一直是业余程序员。
函数的执行会引入一个用于函数局部变量的新符号表(应该是类似内存空间的概念)。函数中所有的变量赋值都将存储在局部符号变量中。
变量的引用会首先在局部符号表中查找,然后是外层函数的局部变量表,全局符号表,最后是内置名称的符号表。
因此全局变量和外层函数变量不能在内部函数中赋值,除非是有global或nonlocal关键字。
说实话,我很少用。
函数被调用时,实际参数(实参)会被引入被调用函数的本地符号表中,
实参是通过按值调用传递的,其中值始终是对象的引用而不是对象的值。
为了准确,还是看下原文。
thus, arguments are passed using call by value (where the value is always an object reference, not the value of the object)
当一个函数调用另外一个函数时,会为该调用创建一个本地符号表。
函数定义会把函数名引入的当前符号表中。
函数名称的值具有解释器将其识别为一个用户定义函数的类型。
有点拗口,看看原文:
The value of the function name has a type that is recognized by the interpreter as a user-defined function.
这个值可以赋值给其他变量,然后该变量也可以作为函数使用。

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

第一个at 10042ed0有点像之前尝试逆向时学到的程序调用栈的内存地址哈。
你可能注意到了,前面函数定义没有return。
和其他语言不一样,在Python中函数没有return也会默认返回None值(Python 内置名称),可以通过print(fib(100))打印看看。

那么将上面函数改造一下,不打印,直接返回n以内的斐波那契数列列表,代码如下:

>>> 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)    # see below
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

第一次正式接触return,以后可能会用到更多。
从函数内部返回值,没有变量就是None,可以返回元组形式(后面再讲)。
很多时候我return不放代码最后,中间如果满足条件我也直接return。
result.append(a)调用了列表对象的append方法,所谓方法就是属于一个对象的函数。后面学到类Class会有更多介绍。

更多函数知识

Python函数非常重要,因此有些细节需要再看看,
虽然上面的基本够用,下面的以前我也没看过,但是正因为如此导致经常遇到问题,这次好好看看。
如何处理可变函数参数
很多时候函数的参数应该根据情况可变,比如之前分享Base64代码时,可以传入自定义字符串,但是一般情况下又不需要,怎么实现?
默认参数最有用
如果你看官网的函数定义,可以发现很多默认参数,如刚才的enumerate(iterable, start=0)start参数就是默认为0。
这种函数调用时可以不用传递默认参数,采用默认值。
官网给的例子非常好,很多时候程序会让我们选择yes/no,看看下面的实现方式:

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        ok = input(prompt)
        if ok in ('y', 'ye', 'yes'):
            return True
        if ok in ('n', 'no', 'nop', 'nope'):
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

函数有3个变量,提示字符串prompt、最大输入次数和最后失败后提示字符,后面两个参数有默认值,调用时可以不传入。因此可以如下方式调用:

  • 只给必需参数 ask_ok("Do you really want to quit?"
  • 给出一个可选参数ask_ok('OK to overwrite the file?', 2)
  • 给出所有参数ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')
    关于默认值有两点非常重要。
    参数默认值是在函数定义时计算的,不是函数调用时计算,比如下面的默认值是5不是6
i = 5
def f(arg=i):
    print(arg)
i = 6
f() #打印5

默认值只会执行一次,当默认值是可变对象(列表、字典以及大多数类实例)特别要注意。

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

print(f(1))
print(f(2))
print(f(3))
#打印结果如下,默认参数L被传递到后面函数中
[1]
[1, 2]
[1, 2, 3]

如果你不想这样,你得换个其他写法。
还记得上面函数调用的3种方式吗?
如果只想输入两个参数,但是第二个参数又想传递给函数的最后一个参数,怎么办?
ask_ok('OK to overwrite the file?', 'Come on, only yes or no!')
这样是不对的,必须考虑顺序(或者说位置参数,传入参数位置和函数定义顺序从左到右匹配。),或者使用关键字参数kwarg=value来调用函数。
还是之前的ask_ok()函数,可以用下面调用方式

ask_ok(prompt='Do you really want to quit?')        # 1 keyword argument
ask_ok(reminder='Come on, only yes or no!',prompt="Do you really want to quit?")             # 2 keyword arguments
ask_ok('Do you really want to quit?', reminder='Come on, only yes or no!')  # 1 positional, 1 keyword

可以发现位置参数和关键字参数可以混用,且如果只用关键字参数顺序不重要。
但是你可能会发生以下错误:

  • 没给参数
  • 关键字参数后面跟位置参数,位置参数必须在前SyntaxError: positional argument follows keyword argument
  • 参数重复,比如位置参数已经给第一个参数赋值,但是关键字参数又来一次;
  • 关键字写错了

一个星号*和两个星号**实现任意参数传递
定义函数时可以在形式参数前加一个或两个星号,分别用于接收多余的位置参数和关键字参数。
一般来说可变参数在行参列表的末尾,因为他们收集传递给函数的所有剩余输入参数。
出现在可变参数后的任何参数都应该是关键字参数,不能是位置参数。
*args 接收除了已有行参列表以外的位置参数的元组的行参。

def concat(*args, sep="/"):
    return sep.join(args)
print(concat("earth", "mars", "venus"))
print(concat("earth", "mars", "venus", sep="."))
def concat1(*args, sep):
    return sep.join(args)
print(concat1("earth", "mars", "venus", sep="."))

各位可以试试打印结果。

当存在一个形式为**name的最后一个行参时,它会接收一个字典,包含除了与已有行参相对应的关键字参数以外的所有关键字参数。

def myinfo(name,nation = '汉',**keywords):
    print("打印个人信息")
    print("姓名:{}".format(name))
    print("民族:{}".format(nation))
    for kw in keywords:
        print("{}:{}".format(kw,keywords[kw]))
    print()

myinfo("建国",sex="男",age="30")
info = {"sex":"女","age":"29"}
myinfo("胜男",**info)

你能猜到打印什么?

打印个人信息
姓名:建国
民族:汉
sex:男
age:30

打印个人信息
姓名:胜男
民族:汉
sex:女
age:29

*args**keywords可以配合使用,但是*args必须在前,以前看过一点点源代码,发现很多**args,说实话以前是不知道有啥用的,今天学完这个教程才知道。

上面是定义函数时用星号,如果调用函数时用星号,会怎么样?
解包参数列表
某些时候要传递的参数的值已经在列表、元组或字典中,如何快速传入函数中?也就是解包。
*运算符可以将参数从列表或元组中解压成位置参数,**运算符可以将参数从字典中解压成关键字参数。
比如内置range()函数需要起始位置start,结束位置stop和步长step3个参数,可以试试下面的结果

args1 = [3, 6]
args2 = [3, 6, 2]
list(range(*args1))
list(range(*args2))

同样之前有道友问的

site = {"name":"CSDN","url":"www.csdn.net","test":1}
print("网站名:{name},地址:{url}".format(**site))

当时找到了关键字参数,但是一开始理解成了函数定义时的可变参数,实际上是参数解包,两个星号将字典site解包为关键字参数,然后传递给format渲染。
Lambda表达式匿名函数
可以用lambda关键字创建一个小的匿名函数。Lambda函数可以在任何需要函数的地方使用,语法上限于单个表达式。
lambda args:opt冒号左边是函数参数,右边是函数操作逻辑。
可以作为其他函数的返回值,返回一个匿名函数,如

>>> def make_incrementor(n):
...     return lambda x: x + n

也可以把函数作为参数传递进去,特别是很多排序逻辑时,如下面按元组第2个元素进行列表排序:

>>> 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')]

文档字符串的一些约定
第一行简要概述,如果存在多行,第二行应该空白。后面几行应该是一个或多个段落,描述函数的调用约定和副作用等信息。
Python解释器不会从Python中删除多行字符串文字的缩进,因此处理文档的工具必须要进行缩进删除。
一般采用第一行后第一个非空行确定整个字符串文档的缩进量。
fun.__doc__可以获取一个函数的文档字符串。
后面你会接触更多类似__name__的双下划线变量,现在我也讲不明白,很好用。
看一个官方的文档字符串:

>>> print(list.__doc__)
list() -> new empty list
list(iterable) -> new list initialized from iterable's items

函数标注
用于用户自定义函数中使用的类型的完全可选元数据信息。
以字典形式存放在__annotations__属性中,不会影响函数的任何其他部分。
我理解的标注就是指定函数参数和返回值的类型。
行参标注:行参名称后加冒号加表达式。
返回值标注:函数定义冒号前加->加表达式。
比如下面函数的位置参数、关键字参数和返回值都有相应标注。

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...     return ham + ' and ' + eggs

如果输入参数类型不对,

>>> f(1)
Annotations: {'ham': <class 'str'>, 'eggs': <class 'str'>, 'return': <class 'str'>}
Arguments: 1 eggs
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in f
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> f([1])
Annotations: {'ham': <class 'str'>, 'eggs': <class 'str'>, 'return': <class 'str'>}
Arguments: [1] eggs
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in f
TypeError: can only concatenate list (not "str") to list

好像整型可以自动转换字符串,但是list就不行了。
以前还真没主要这个用法,以为Python函数都是无类型的,传递什么都可以,果然需要看官网。

编码风格

现在你可以开始写更长的代码了,比如之前的Base64解码,为了其他人能够轻松阅读你的代码,应该开始采用一个好的编程风格,如PEP8,详细的可以以后阅读,有几个关键点总结如下:

  • 使用4个空格缩进,不要使用制表符
  • 换行,一行不要超过79个字符
  • 使用空行分割函数和类,以及函数内较大的代码块
  • 如果可能,注释单独一行
  • 使用文档字符串
  • 在运算符前后和逗号后面使用空格,但是括号内逗号后不用
  • 以一致的规则为类和函数命名,按照惯例UpperCaseCase命名类,lowercase_with_underscores命名函数和方法。
  • 如果想要国际化,请用UTF-8或纯ASCII编码
  • 不要在标识符中使用非ASCII字符

好了,Python流程控制又学完了。

03 为什么

为什么要这么做?

首先任何东西的官方文档都是最全面最权威的教程。
以前只是受限于英语水平,
对官方网站敬而远之,
遇到问题都百度,
很多答案讲的都不到位,
没有说明为什么?
越到后面,收获越大。
比如学会了循环中用else语句,加深了函数关键字参数的理解,知道函数标注的使用,还有好多其他知识。

04 更好的选择

有没有更好的选择

还是那句话,多敲代码,结合案例,偶然看看官方源代码,加深理解。
在VsCode中按住Ctrl然后点击内置函数(右键跳转定义),可以跳转查看源代码,没事可以学习学习。
模块string源代码

Python内置String模块源代码示例

本章提到很多概念,如元组、列表或字典等在第5章有更详细介绍。

关于形参和实参
很多语言都有类似的概念,为了加强理解,特意百度了下。
函数行参在函数定义及函数体中使用,实参是用来填充行参的,有两种调用方式传值调用和引用调用。
所谓传值调用就是形参,局部变量,初始值是实参的值;引用调用,将实参的内存地址传递给形参,实参会被修改。好像C语言的指针操作。
好像还是有点晕晕的,不过应该暂时够用了。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值