diff --git a/res/builtin-functions.png b/res/builtin-functions.png index 467fd0c..918e5c8 100644 Binary files a/res/builtin-functions.png and b/res/builtin-functions.png differ diff --git a/res/pycharm-install-plugins.png b/res/pycharm-install-plugins.png index 530fb5a..b2475f8 100644 Binary files a/res/pycharm-install-plugins.png and b/res/pycharm-install-plugins.png differ diff --git a/res/pycharm-workspace.png b/res/pycharm-workspace.png index fac84a4..3f4d236 100644 Binary files a/res/pycharm-workspace.png and b/res/pycharm-workspace.png differ diff --git a/res/qrcode.png b/res/qrcode.png index bd85cde..dbf38c3 100644 Binary files a/res/qrcode.png and b/res/qrcode.png differ diff --git a/res/set.png b/res/set.png index 91f45b2..0a5acad 100644 Binary files a/res/set.png and b/res/set.png differ diff --git a/第008课:函数和模块.md b/第008课:函数和模块.md index 4190caf..8e09923 100644 --- a/第008课:函数和模块.md +++ b/第008课:函数和模块.md @@ -124,6 +124,8 @@ print(add(1, 2, 3)) # 6 print(add(c=50, a=100, b=200)) # 350 ``` +> **注意**:带默认值的参数必须放在不带默认值的参数之后,否则将产生`SyntaxError`错误,错误消息是:`non-default argument follows default argument`,翻译成中文的意思是“没有默认值的参数放在了带默认值的参数后面”。 + #### 可变参数 接下来,我们还可以实现一个对任意多个数求和的`add`函数,因为Python语言中的函数可以通过星号表达式语法来支持可变参数。所谓可变参数指的是在调用函数时,可以向函数传入0个或任意多个参数。将来我们以团队协作的方式开发商业项目时,很有可能要设计函数给其他人使用,但有的时候我们并不知道函数的调用者会向该函数传入多少个参数,这个时候可变参数就可以派上用场。下面的代码演示了用可变参数实现对任意多个数求和的`add`函数。 diff --git a/第020课:函数使用进阶.md b/第020课:函数使用进阶.md new file mode 100644 index 0000000..5b79944 --- /dev/null +++ b/第020课:函数使用进阶.md @@ -0,0 +1,199 @@ +## 第020课:函数使用进阶 + +在之前的课程中,我们讲到过关于函数的知识,我们还讲到过Python中常用的数据类型,这些类型的变量都可以作为函数的参数或返回值;通过前几节课的学习,我们又知道了写在类中的函数通常称之为方法,它代表了类或者对象可以接收的消息。如果我们把这些知识汇总一下,我们的函数就可以做更多的事情。 + +### 关键字参数 + +下面是一个判断传入的三条边长能否构成三角形的函数,在调用函数传入参数时,我们可以指定参数名,也可以不指定参数名,代码如下所示。 + +```Python +def can_form_triangle(a, b, c): + print(f'a = {a}, b = {b}, c = {c}') + return a + b > c and b + c > a and a + c > b + + +# 调用函数传入参数不指定参数名按位置对号入座 +print(can_form_triangle(1, 2, 3)) +# 调用函数通过“参数名=参数值”的形式按顺序传入参数 +print(can_form_triangle(a=1, b=2, c=3)) +# 调用函数通过“参数名=参数值”的形式不按顺序传入参数 +print(can_form_triangle(c=3, a=1, b=2)) +``` + +在没有特殊处理的情况下,函数的参数都是**位置参数**,也就意味着传入参数的时候对号入座即可,如上面代码的第7行所示,传入的参数值`1`、`2`、`3`会依次赋值给参数`a`、`b`、`c`。当然,也可以通过`参数名=参数值`的方式传入函数所需的参数,因为指定了参数名,传入参数的顺序可以进行调整,如上面代码的第9行和第11行所示。 + +调用函数时,如果希望函数的调用者必须以`参数名=参数值`的方式传参,可以用**命名关键字参数**取代位置参数。所谓命名关键字参数,是在函数的参数列表中,写在`*`之后的参数,代码如下所示。 + +```Python +def can_form_triangle(*, a, b, c): + print(f'a = {a}, b = {b}, c = {c}') + return a + b > c and b + c > a and a + c > b + + +# TypeError: can_form_triangle() takes 0 positional arguments but 3 were given +# print(is_valid_for_triangle(3, 4, 5)) +# 传参时必须使用“参数名=参数值”的方式,位置不重要 +print(can_form_triangle(a=3, b=4, c=5)) +print(can_form_triangle(c=5, b=4, a=3)) +``` + +> **注意**:上面的`can_form_triangle`函数,参数列表中的`*`是一个分隔符,`*`前面的参数都是位置参数,而`*`后面的参数就是命名关键字参数。 + +我们之前讲过在函数的参数列表中可以使用**可变参数**`*args`来接收任意数量的参数,但是我们需要看看,`*args`是否能够接收带参数名的参数。 + +```Python +def calc(*args): + result = 0 + for arg in args: + result += arg + return result + + +print(calc(a=1, b=2, c=3)) +``` + +执行上面的代码会引发`TypeError`错误,错误消息为`calc() got an unexpected keyword argument 'a'`,由此可见,`*args`并不能处理带参数名的参数。我们在设计函数时,如果既不知道调用者会传入的参数个数,也不知道调用者会不会指定参数名,那么同时使用可变参数和**关键字参数**。关键字参数会将传入的带参数名的参数组装成一个字典,参数名就是字典中键值对的键,而参数值就是字典中键值对的值,代码如下所示。 + +```Python +def calc(*args, **kwargs): + result = 0 + for arg in args: + result += arg + for value in kwargs.values(): + result += value + return total + + +print(calc()) # 0 +print(calc(1, 2, 3)) # 6 +print(calc(a=1, b=2, c=3)) # 6 +print(calc(1, 2, c=3, d=4)) # 10 +``` + +> **提示**:**不带参数名的参数(位置参数)必须出现在带参数名的参数(关键字参数)之前**,否则将会引发异常。例如,执行`calc(1, 2, c=3, d=4, 5)`将会引发`SyntaxError`错误,错误消息为`positional argument follows keyword argument`,翻译成中文意思是“位置参数出现在关键字参数之后”。 + +### 高阶函数的用法 + +在前面几节课中,我们讲到了面向对象程序设计,在面向对象的世界中,一切皆为对象,所以类和函数也是对象。函数的参数和返回值可以是任意类型的对象,这就意味着**函数本身也可以作为函数的参数或返回值**,这就是所谓的**高阶函数**。 + +如果我们希望上面的`calc`函数不仅仅可以做多个参数求和,还可以做多个参数求乘积甚至更多的二元运算,我们就可以使用高阶函数的方式来改写上面的代码,将加法运算从函数中移除掉,具体的做法如下所示。 + +```Python +def calc(*, init_value, op, *args, **kwargs): + result = init_value + for arg in args: + result = op(result, arg) + for value in kwargs.values(): + result = op(result, value) + return result +``` + +注意,上面的函数增加了两个参数,其中`init_value`代表运算的初始值,`op`代表二元运算函数。经过改造的`calc`函数不仅仅可以实现多个参数的累加求和,也可以实现多个参数的累乘运算,代码如下所示。 + +```Python +def add(x, y): + return x + y + + +def mul(x, y): + return x * y + + +print(calc(init_value=0, op=add, 1, 2, 3, x=4, y=5)) # 15 +print(calc(init_value=1, op=mul, 1, 2, x=3, y=4, z=5)) # 120 +``` + +通过对高阶函数的运用,`calc`函数不再和加法运算耦合,所以灵活性和通用性会变强,这是编程中一种常用的技巧,但是最初学者来说可能会稍微有点难以理解。需要注意的是,将函数作为参数和调用函数是有显著的区别的,**调用函数需要在函数名后面跟上圆括号,而把函数作为参数时只需要函数名即可**。上面的代码也可以不用定义`add`和`mul`函数,因为Python标准库中的`operator`模块提供了代表加法运算的`add`和代表乘法运算的`mul`函数,我们直接使用即可,代码如下所示。 + +```Python +import operator + +print(calc(init_value=0, op=operator.add, 1, 2, 3, x=4, y=5)) # 15 +print(calc(init_value=1, op=operator.mul, 1, 2, x=3, y=4, z=5)) # 120 +``` + +Python内置函数中有不少高阶函数,我们前面提到过的`filter`和`map`函数就是高阶函数,前者可以实现对序列中元素的过滤,后者可以实现对序列中元素的映射,例如我们要去掉一个整数列表中的奇数,并对所有的偶数求平方得到一个新的列表,就可以直接使用这两个函数来做到,具体的做法是如下所示。 + +```Python +def is_even(num): + return num % 2 == 0 + + +def square(num): + return num ** 2 + + +numbers1 = [35, 12, 8, 99, 60, 52] +numbers2 = list(map(square, filter(is_even, numbers1))) +print(numbers2) # [144, 64, 3600, 2704] +``` + +当然,要完成上面代码的功能,也可以使用列表生成式,列表生成式的做法更为简单优雅。 + +```Python +numbers1 = [35, 12, 8, 99, 60, 52] +numbers2 = [num ** 2 for num in numbers1 if num % 2 == 0] +print(numbers2) # [144, 64, 3600, 2704] +``` + +### Lambda函数 + +在使用高阶函数的时候,如果作为参数或者返回值的函数本身非常简单,一行代码就能够完成,那么我们可以使用**Lambda函数**来表示。Python中的Lambda函数是没有的名字函数,所以很多人也把它叫做**匿名函数**,匿名函数只能有一行代码,代码中的表达式产生的运算结果就是这个匿名函数的返回值。上面代码中的`is_even`和`square`函数都只有一行代码,我们可以用Lambda函数来替换掉它们,代码如下所示。 + +```Python +numbers1 = [35, 12, 8, 99, 60, 52] +numbers2 = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers1))) +print(numbers2) # [144, 64, 3600, 2704] +``` + +通过上面的代码可以看出,定义Lambda函数的关键字是`lambda`,后面跟函数的参数,如果有多个参数用逗号进行分隔;冒号后面的部分就是函数的执行体,通常是一个表达式,表达式的运算结果就是Lambda函数的返回值,不需要写`return` 关键字。 + +如果需要使用加减乘除这种简单的二元函数,也可以用Lambda函数来书写,例如调用上面的`calc`函数时,可以通过传入Lambda函数来作为`op`参数的参数值。当然,`op`参数也可以有默认值,例如我们可以用一个代表加法运算的Lambda函数来作为`op`参数的默认值。 + +```Python +def calc(*args, init_value=0, op=lambda x, y: x + y, **kwargs): + result = init_value + for arg in args: + result = op(result, arg) + for value in kwargs.values(): + result = op(result, value) + return result + + +# 调用calc函数,使用init_value和op的默认值 +print(calc(1, 2, 3, x=4, y=5)) # 15 +# 调用calc函数,通过lambda函数给op参数赋值 +print(calc(1, 2, 3, x=4, y=5, init_value=1, op=lambda x, y: x * y)) # 120 +``` + +> **提示**:注意上面的代码中的`calc`函数,它同时使用了可变参数、关键字参数、命名关键字参数,其中命名关键字参数要放在可变参数和关键字参数之间,传参时先传入可变参数,关键字参数和命名关键字参数的先后顺序并不重要。 + +有很多函数在Python中用一行代码就能实现,我们可以用Lambda函数来定义这些函数,调用Lambda函数就跟调用普通函数一样,代码如下所示。 + +```Python +import operator, functools + +# 一行代码定义求阶乘的函数 +fac = lambda num: functools.reduce(operator.mul, range(1, num + 1), 1) +# 一行代码定义判断素数的函数 +is_prime = lambda x: x > 1 and all(map(lambda f: x % f, range(2, int(x ** 0.5) + 1))) + +# 调用Lambda函数 +print(fac(10)) # 3628800 +print(is_prime(9)) # False +``` + +> **提示1**:上面使用的`reduce`函数是Python标准库`functools`模块中的函数,它可以实现对数据的归约操作,通常情况下,**过滤**(filter)、**映射**(map)和**归约**(reduce)是处理数据中非常关键的三个步骤,而Python的标准库也提供了对这三个操作的支持。 +> +> **提示2**:上面使用的`all`函数是Python内置函数,如果传入的序列中所有布尔值都是`True`,`all`函数就返回`True`,否则`all`函数就返回`False`。 + +### 简单的总结 + +Python中的函数可以使用可变参数`*args`和关键字参数`**kwargs`来接收任意数量的参数,而且传入参数时可以带上参数名也可以没有参数名,可变参数会被处理成一个元组,而关键字参数会被处理成一个字典。Python中的函数也是对象,所以函数可以作为函数的参数和返回值,也就是说,在Python中我们可以使用高阶函数。如果我们要定义的函数非常简单,只有一行代码且不需要名字,可以将函数写成Lambda函数(匿名函数)的形式。 + +> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。 +> +> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。 +> +> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。 +