1. 如何为函数定义keyword-only参数
函数的参数传递(3.8版本之前)中,通常是有位置参数、关键字参数这两种参数传递方式。
在位置参数中,又有一种变长位置参数的定义和传递方式;在关键字参数传递中,由有一种任意关键字参数定义和传递方式,以及只能关键字参数传递。
所谓的只能关键字参数(keyword-only
参数),即只能以确切的key=value
的形式进行参数传递的方式,而不能是以位置参数的形式进行参数传递,也不能是以任意的key=value
的形式传递。比如定义了函数参数a
为只能关键字参数,那么在函数调用的时候,必须将a
以明确的a=5
的形式进行传递,而不能以位置参数或者x=5
的方式传递。在Python中,函数调用的时候,关键字参数传递必须与函数定义中的参数名字相同。
而参数的传递方式,是在函数对象定义的时候,通过函数的参数列表指定的。
-
对于位置参数的函数定义方式如下所示:
位置参数
def func(a, b): print(a, b)
上述的函数参数
a
,b
就是位置参数,在调用上述函数的时候,参数解析是按照先后顺序完成的。如下所示:func(1, 2)
那么上述函数调用的时候,传递了两个位置参数:1和2。这两个参数在函数内部,会被分别赋值给a和b,即a=1, b=2。
上述这种方式即为位置参数的定义以及参数传递方式。
-
变长位置参数的定义方式如下所示:
变长位置参数
def func(*a): print(type(a), a)
上述的函数定义的参数
a
就是变长位置参数,可以接收任意多个位置参数。其执行效果如下所示:func(1, 2, 3, 4) # 函数调用 <class 'tuple'> (1, 2, 3, 4) # 输出结果
变长位置参数将函数调用中传递的任意多个参数收集到一个元组tuple中。
-
任意关键字参数的定义方式如下所示:
任意关键字参数
def func_kw(**a): print(type(a), a)
上述的函数定义中,就定义了一个接收任意关键字参数的函数。上述函数的执行效果如下所示:
func_kw(1, 2, 3, 4) # 以变长位置参数的形式调用
打印错误信息如下所示:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp/ipykernel_187720/1802656122.py in <module> ----> 1 func_kw(1, 2, 3, 4) TypeError: func_kw() takes 0 positional arguments but 4 were given
异常信息中提示,该函数接收0个位置参数,但是我们在函数调用的时候给定了4个位置参数。
接下来按照关键字参数传递的方式进行函数调用,具体如下所示:
func_kw(a=1, b=2, c=3, d=4)
上述调用的输出结果如下所示:
<class 'dict'> {'a': 1, 'b': 2, 'c': 3, 'd': 4}
任意关键字参数的方式,关键字参数会被收集到一个字典中,引用字典对象的变量名字即为函数定义的时候指定的参数。
-
只能关键字参数的定义方式,需要借助上述的变长位置参数以及任意关键字参数的函数定义方式才能实现,具体如下所示:
只能关键字参数(keyword-only)
def func_kw_only(*a, b, c, **d): print(type(a), a) print(type(b), b) print(type(c), c) print(type(d), d)
上述定义了一个变长位置参数a,以及两个只能关键字参数b和c,最后还有一个任意关键字参数d。
上述函数的调用效果如下所示:
func_kw_only(1, 2, 3, b=4, c=5, d=6, e=7, f=8)
上述函数调用的时候,给出了3个位置参数,其他参数都是通过关键字参数的
key=value
形式传递的。具体效果如下所示:<class 'tuple'> (1, 2, 3) <class 'int'> 4 <class 'int'> 5 <class 'dict'> {'d': 6, 'e': 7, 'f': 8}
从上述的输出结果中可以看出,三个位置参数被收集到元组a中;而b和c这两个参数并没有被收集到任意关键字参数d中,而是单独存在,因为这两个也是关键字参数。任意关键字参数只会将没有明确归属的关键字参数收集起来,所以此处并不会将b和c收集到字典d中。
接下来按照错误的方式进行函数调用,即省略掉关键字参数b和c,看看是否能以位置参数的形式进行值传递。具体如下所示:
func_kw_only(1, 2, 3, 4, 5, d=6, e=7, f=8)
此时函数的执行结果如下所示:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp/ipykernel_187720/2800654355.py in <module> ----> 1 func_kw_only(1, 2, 3, 4, 5, d=6, e=7, f=8) TypeError: func_kw_only() missing 2 required keyword-only arguments: 'b' and 'c'
上述输出显式,在函数调用语句中缺失了两个只能关键字参数b和c。
上述的只能关键字参数定义方式还可以简化一些,即可以省略掉任意关键字参数的定义部分,只需要保留前面的变长位置参数的定义以及其后面的只能关键字参数定义这两个部分即可。具体如下所示:
简化版的只能关键字参数定义:
def func_kw_only_v1(*a, b, c): print(type(a), a) print(type(b), b) print(type(c), c)
调用上述函数:
func_kw_only_v1(1, 2, 3, b=4, c=5)
调用效果如下所示:
<class 'tuple'> (1, 2, 3) <class 'int'> 4 <class 'int'> 5
如果在函数调用的时候,不明确指定指定同名的关键字参数,则会报错。具体如下所示:
func_kw_only_v1(1, 2, 3, 4, 5, d=6, e=7)
调用效果如下:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp/ipykernel_187720/2425023542.py in <module> ----> 1 func_kw_only_v1(1, 2, 3, 4, 5, d=6, e=7) TypeError: func_kw_only_v1() got an unexpected keyword argument 'd'
上述函数调用的时候,没有明确指定关键字参数b和c,而是取而代之指定了关键字参数d和e。但是Python中的关键字参数,在接收参数传递的时候,必须与函数定义的时候指定的关键字参数名字相同。
否则会提示指定了不期望的关键字参数。
如果在函数调用的时候,不指定关键字参数,调用方式如下:
func_kw_only_v1(1, 2, 3, 4, 5, 6, 7)
此时的调用效果如下所示:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp/ipykernel_187720/51130240.py in <module> ----> 1 func_kw_only_v1(1, 2, 3, 4, 5, 6, 7) TypeError: func_kw_only_v1() missing 2 required keyword-only arguments: 'b' and 'c'
上述输出显式,缺失了两个只能关键字参数b和c没有明确指定关键字参数。
上述的只能关键字参数定义方式还可以再简化一些,即可以省略掉变长位置参数的参数名,只需要保留1个星号即可。此时表示不接收额外的位置参数以及其他关键字参数。具体如下所示:
再次简化版的只能关键字参数定义:
def func_kw_only_v2(*, a, b): print(type(a), a) print(type(b), b)
上述函数的正确调用结果如下所示:
func_kw_only_v2(a=5, b=3)
上述调用的输出结果如下所示:
<class 'int'> 5 <class 'int'> 3
当在只能关键字参数的前面加上位置参数的时候,调用方式如下:
func_kw_only_v2(1, 2, 3, a=1, b=2)
上述调用的结果如下所示:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp/ipykernel_187720/957417816.py in <module> ----> 1 func_kw_only_v2(1, 2, 3, a=1, b=2) TypeError: func_kw_only_v2() takes 0 positional arguments but 3 positional arguments (and 2 keyword-only arguments) were given
上述错误提示中显式,这个函数接收2个只能关键字参数,没有位置参数;但是在函数调用中,给定了3个位置参数,所以提示类型错误(TypeError)异常。
挡在只能关键字参数的后面加上其他关键字参数的时候,调用方式如下所示:
func_kw_only_v2(a=1, b=2, c=3, d=4)
上述函数调用的输出结果如下所示:
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) ~\AppData\Local\Temp/ipykernel_187720/2690661467.py in <module> ----> 1 func_kw_only_v2(a=1, b=2, c=3, d=4) TypeError: func_kw_only_v2() got an unexpected keyword argument 'c'
上述错误提示中显式,这个函数调用的时候,传递了一个不期望的关键字参数c,从而导致了类型错误(TypeError)这个异常。
至此,就示例了只能关键字参数的定义以及函数调用中的参数传递方式。
2. 什么是LEGB
由于Python中的变量名字并不能预先定义数据类型,所以Python是在赋值语句发生的地方创建该变量名称。而赋值语句出现在不同的地方,则代表了该变量名称的影响范围的不同,即该变量名称的可见范围是不一样的。
通常在函数定义语句def func_name
中创建的变量名为局部变量,其可见范围仅在该func_name函数内部可见;与此相对应的,在模块中创建的变量名称为全局变量,其对当前这个程序文件的全局可见。函数内部创建的局部变量并不会与函数外面定义的同名全局变量冲突,在函数内部,会优先使用内部定义的局部变量。
除了上述的全局变量(Global
)的影响范围称为全局作用域,局部变量(Local
)的影响范围称为局部作用域。除此之外,还有在嵌套函数中创建的变量,即闭包中的变量(Enclosing
)引用其外层函数中创建的变量。这种嵌套函数中的变量引用其外层函数中创建的变量,称为闭包作用域。
最后Python也提供了内建变量,即Builtins
变量。当在全局变量中也没有找到合适的变量名的时候,Python就回去搜索内建变量。内建变量的影响范围称为内建作用域。
这几个作用域之间的影响范围如下图所示:
从上图中可以看出,内建作用域的影响范围是最大的;随后是全局作用域,但是其被包含在全局作用域的影响范围之内;再接下来是闭包作用域,其影响该函数内部以及在该函数内部定义的嵌套函数的作用域;最后是嵌套函数内部创建的局部变量,其影响范围仅限于该函数内部,即局部作用域。
在函数中变量的查找顺序则是从内向外开始的,当使用的变量名称在内层找不到的时候,才会逐层向外寻找,直到找到为止。如果找到内建作用域,依然没有找到,则抛出异常。
上述的变量搜索顺序,就被称为LEGB
。
关于上述的
LEGB
变量搜索顺序,参见下面的示例代码:a = 5 c = 9 def func1(): a = 3 b = 7 print('a = {}, b = {} in func1\' local scope'.format(a, b)) def func2(): a = 2 print('a = {} in func2\'s local scope'.format(a)) print('b = {} in enclosing scope'.format(b)) # b的搜索范围即为闭包作用域 func2() print('a = {} in global scope'.format(a)) print('c = {} in global scope'.format(c))
上述代码的执行效果如下所示:
a = 5 in global scope c = 9 in global scope a = 3, b = 7 in func1' local scope a = 2 in func2's local scope b = 7 in enclosing scope
3. 二叉树的性质
树不同于列表、元组、链表、栈以及队列这类线性数据结构,树是分级的数据结构。
树最顶端的节点称为根节点,根节点下面的节点被称为子节点。一个节点上方与其直接相连的节点被称为其双亲节点(或者称为父节点,Parent Node)。比如下面的
关于二叉树的一些性质,如下所示:
-
在
l
层的最大节点数为2**l
个 -
高度为
h
的二叉树包含的最大节点数为2**h -1
个 -
在包含N个节点的二叉树中,最小可能高度或者最小层数是上述得到的结果向上取整数,即
-
如果一个二叉树包含
l
个叶子节点,那么这个二叉树至少包含的层数为同样需要对上述结果向上取整,即 -
当二叉树中的每个节点都有0或者2个子节点的时候,叶子节点的数量总是比带有2个子节点的节点个数多1个
4. 简单计算器的实现
目标如下:
运行后提示让用户输入一个数字
提示输入操作符(+ - * /)
再次提示输入一个数字
打印计算结果
在不退出程序的前提下,可以允许用户继续输入新一组数据计
尽可能改善用户体验(新需求)
代码实现:
这次主要在第一版的基础上,增加了输入的数据和操作符的合法性检查。如果此前的输入是正确的,则只会要求重新输入当次的数字或者操作符,不会覆盖此前正确的输入。直到三次输入全部正确为止,才会计算出最终的结果。同时支持有符号数和小数计算。具体代码如下所示:
代码实现:
#encoding=utf-8 """ 1. 运行后提示让用户输入一个数字 2. 提示输入操作符(+ - * /) 3. 再次提示输入一个数字 4. 打印计算结果 5. 在不退出程序的前提下,可以允许用户继续输入新一组数据计 6. 尽可能改善用户体验(新需求) """ import sys import re def calculator_v1(): comp = re.compile('[+-]?[0-9]{1,}[.]?[0-9]{0,}') while True: num1 = input('Please enter num1: ') res1 = comp.match(num1) if res1 is not None and num1 == res1.group(): if '.' in num1: num1 = float(num1) else: num1 = int(num1) else: print('Please enter a number!') continue while True: oper = input('Please enter operator [ + | - | * | / ]: ') if oper not in ('+', '-', '*', '/'): print('Please enter the right operator, the valid operator is [+, -, *, /]!') continue else: break while True: num2 = input('Please enter num2: ') res2 = comp.match(num2) if res2 is not None and num2 == res2.group(): if '.' in num2: num2 = float(num2) else: num2 = int(num2) break else: print('Please enter a number!') continue if oper == '+': res = num1 + num2 elif oper == '-': res = num1 - num2 elif oper == '*': res = num1 * num2 else: res = num1 / num2 out_str = str(num1) + ' ' + oper + ' ' + str(num2) + ' = {}' print(out_str.format(res)) again = input('Do you wanna calculate again? If Not, Please enter q or n!') # 按下q或者n的时候,则不再进行计算,退出程序执行。否则,按下回车则表示继续进行计算 if again == 'q' or again == 'n': sys.exit(0) calculator_v1()
上述代码的执行效果如下所示:
Please enter num1: 10 Please enter operator [ + | - | * | / ]: + Please enter num2: -5 10 + -5 = 5 Do you wanna calculate again? If Not, Please enter q or n! Please enter num1: +-2 # 输入错误的数字形式 Please enter a number! # 提示重新输入数字 Please enter num1: 5.2. # 再次输入错误的数字形式 Please enter a number! Please enter num1: 5.2 Please enter operator [ + | - | * | / ]: + Please enter num2: 5.2. Please enter a number! Please enter num2: 5.2 5.2 + 5.2 = 10.4 Do you wanna calculate again? If Not, Please enter q or n! Please enter num1: 123 Please enter operator [ + | - | * | / ]: + Please enter num2: 234.567j8 # 输入错误的数字 Please enter a number! Please enter num2: 2345.67890 123 + 2345.6789 = 2468.6789 Do you wanna calculate again? If Not, Please enter q or n! Please enter num1: +0123 Please enter operator [ + | - | * | / ]: * Please enter num2: -02.5 123 * -2.5 = -307.5 Do you wanna calculate again? If Not, Please enter q or n!n Process finished with exit code 0
上述就是进行体验优化之后的第二版计算器的代码实现以及执行过程。