, 在第五天的学习笔记中函数这个主题是关于函数的问题, 函数可以让代码得到复用, 函数可以接受参数, 函数可以返回结果, 函数可以进行递归操作, 函数还可以是匿名的。一、函数基础这一部分的内容, 第一点就是去搞清楚一个事情, 究竟为什么要使用函数。函数是一连串的代码指令的汇总, 这个汇总是为了去解决某一个专门的问题, 而且还能被反复地调用, 如果你把那些需要重复执行的操作写成函数的话, 那么就能防止代码出现冗余的情况, 但是有一个前提条件就是, 必须先对函数进行定义之后, 然后再去使用它。二我们来看一下这个函数的定义, 然后再说说它是怎么被调用的。def print_sign():for _ in range(10):print(-, end)print()print(床前明月光)print_sign()这个定义的格式要写成 def 加上函数名, 后面跟上一对小括号, 然后冒号换行缩进具体的函数体内容其中的函数名称必须遵循小写蛇形命名法的原则, 并且要和代码里变量的命名规范保持完全一致函数本身定义好以后并不会马上执行, 只有在其后需要运行的位置直接写出这个函数的名字来调用它时, 函数内部的那些代码才会正式跑起来。三形参与实参通过设置参数, 能够让进行调用的一方去灵活地进行控制。所谓的形参, 也就是那种被称为形式参数的东西, 它是程序员在定义函数的时候所书写写下来的参数而实参呢, 就是那种被称为实际参数的东西, 它是在函数被调用的那个时刻由外部传入进去的参数。要是定义了形参的话, 那就必须要传入个数正好对应的那些实参才行, 除非那些形参本身是被设定了默认值的, 否则程序运行的时候就必然会报错。def print_sign(num):for _ in range(num):print(-, end)print()print_sign(7)四多参数函数的这种定义方式, 它允许参数的数量没有限制。你可以根据你的具体情况的需要来进行相关定义操作。def print_sign(num, sign):for _ in range(num):print(sign, end)print()print_sign(7, A)print_sign(2, hello)二、在讨论参数传递的方式的时候, 大家通常会把位置参数和关键字参数分成了两种不同的情况来进行具体的分析。位置参数必须要严格按照那个传递过来的顺序去一个一个的放进去, 关键字参数则是用参数名等于值这种形式来传进去的, 至于传的先后顺序它根本就没关系, 但是在混合使用的时候, 关键字参数必须在所有的那个位置参数的后面。def print_sign(num, sign):for _ in range(num):print(sign, end)print()print_sign(7, ^)print_sign(num10, sign)print_sign(sign, num10)print_sign(8, sign*)# print_sign(num8, *)二把那个传过来的参数给解包下来。星号会把列表还有元组里面的每个元素, 按照他们各自的位置关系, 一个一个地传给那些形参双星号则是把字典进行解包处理, 把它变成关键字参数, 这时候字典里面的键名, 必须和那些形参的名字完全一样, 而且数量也要相同, 但是顺序其实是不需要管那么详细的不过集合在解包的时候, 它的顺序是没法得到保证的, 所以在实际的工作当中大家通常是不去使用的。def func(a, b, c):print(a, b, c)list1 [2, 1, 3]func(*list1)tuple1 (2, 1, 3)func(*tuple1)dict1 {b: 2, a: 1, c: 3}三关于那个可变长的参数, 还有这个带有默认数值的参数。在使用星号加上参数名的时候, 是可以接收任意多个的位置参数的, 并且这些参数会被打包成一个元组, 要是可变长度的参数后面还有其他参数需要传递的话, 那就必须要采用关键字参数的方式进行传递了, 对于默认值参数而言, 是在定义环节就给对应的形式参数设定了一个默认值的, 所以在进行调用的操作时, 这个部分是可以被省略的。def func1(a, *nums):print(a, nums)func1(1, 2, 3, 4, 5)def func2(a, *nums, b):print(a, nums, b)func2(1, 2, 3, 4, 5, b66)def func3(a1, bTrue):print(a, b)func3()func3(2, False)四一定要让使用者去用位置参数, 或者是非得让他们去用关键字参数不可。在斜线符号的前面那个部分的参数, 它是一定得用位置参数的方式来进行给值的。而那个星号符号后面的部分所对应的那些参数呢, 是必须得使用关键字参数的这种形式去赋值的。至于处在中间的那一段参数, 它们是比较灵活的, 两种方式都是可以进行使用的。def func1(a, b, /, c, d, *, e, f):print(a, b, c, d, e, f)func1(1, 2, 3, 4, e5, f6)func1(1, 2, c3, d4, e5, f6)# func1(a1, b2, c3, d4, e5, f6)三、传参的底层细节包括可变类型与不可变类型, 还有深拷贝与浅拷贝的区别, 第一部分内容是关于传参的特点。Java 的分值传递和引用传递, 之中是没有这种说法的, 因为在 Java 里面所有的数据都是对象, 都一律属于引用传递, 关于在函数体内部修改参数是否会影响到原始变量, 这一点是完全可以取决于该数据类型本身是属于可变类型还是不可变类型的。def func1(a):a 1print(func1函数中a的取值, a)b 10func1(b)print(实参参数b的取值, b)def func2(nums):nums[0] 1print(func2函数中nums的取值, nums)list1 [1, 2, 3]func2(list1)print(list1的元素值为, list1)二直接赋值这个操作行为, 以及与之对应的这种叫做作深浅拷贝的概念。方式说明直接把list2赋值为list1, 也就是用赋值的方法把list1交给list2。它是复制了存在堆内存里面的地址值, 让两个变量都指向同一个对象, 所以一旦修改了一个, 另一个也会跟着变。对于list1这个列表来说, 浅拷贝可以通过使用list1.copy()这个方法来实现, 或者是直接用list1来。外层是在创建新的对象, 内层的那些元素是复用的同一个对象。进行一个深拷贝的操作, 使用 copy 这个函数, 把 list1 这个变量传入进去。从外面到里面, 全都去创建新的对象, 使得它们完全是独立存在的。import copylist1 [1, 2, 3, [4, 5, 6]]list2 list1print(id(list1), id(list2))list1 [1, 2, 3, [4, 5, 6]]list2 list1.copy()print(id(list1), id(list2))print(id(list1[3]), id(list2[3]))list1 [1, 2, 3, [4, 5, 6]]list2 copy.deepcopy(list1)print(id(list1), id(list2))print(id(list1[3]), id(list2[3]))四、返回值 一, 用返回结果给调用者。有些函数在执行的时候, 需要把计算的结果返回给那一个调用这个函数的人去使用, 具体的做法是使用那个叫做 的空格关键字来执行返回操作, 这样返回值就会跑到函数被调用的那个原始位置上去。当用到这个表示结束函数的动作以及返回里面内容的部分之后, 代码走到那里就停了,后面剩下的那些代码语句是肯定不会再去执行的。如果一个函数里没有写明那个用来返回的东西, 那么它就会默认地吐出 None 这个东西来作为结果, 这样的话, 我们在离开函数这个范围外面的地方, 就没法去重新利用之前算出来的那些结果了。def my_add(a, b):result a bprint(a与b之和为, result)return resultget_num my_add(13212421421, 2)if get_num % 2 0:print(偶数)else:print(奇数)二 这一些词语还可以有其他的一些不同的使用情况。你可以借助于三目运算符来简化那个 if 或者 else 的判断逻辑操作, 它通常返回像布尔值、数字这一类东西如果是那种不带任何返回值的 语句呢, 专门是用在需要提前结束掉一个函数运行过程的地方去, 这时候你要留意它和那个 break 关键字之间存在的明显区别。def is_even(num):return 偶数 if num % 2 0 else 奇数print(is_even(10))print(is_even(11))def func():for i in range(10):if i 8:returnprint(i)print(func函数执行完毕)func()三可以返回多个值。在后面返回多个值的情况出现时, 如果你用一个变量接收结果的话, 它会把自己打包成一个元组。要是用数量相等的多个变量来接收的话, 这个操作就是解包操作。如果数量和结果个数不一致的话, 程序就会报错出毛病。def func():return 1, 2, 3, [4, 5, 6]r1 func()print(r1)a1, a2, a3, a4 func()print(a1, a2, a3, a4)五、函数的嵌套调用以及递归这部分内容中, 首先我们要去聊一下函数之间的调用和栈之间的那层关系。自定义的这种函数它们之间是直接可以互相进行调用的, 当函数的执行工作开始的时候它将会进入栈里面。这里的栈是指一种数据结构, 这种数据结构的特点是先进后出, 也就是我们常说的FILO, 先进行的栈入操作后进行的栈出操作。def my_func2():print(my_func2 开始执行)print(my_func2 执行完毕)def my_func1():print(my_func1 开始执行)my_func2()print(my_func1 执行完毕)my_func1()二递归所谓递归, 从字面意思看是递进和回归两种状态的结合, 咱们可以简单理解为一句话就是函数自己在调用自己。要是说得更准确一点, 它的核心逻辑是大问题能够通过方法分解成不少小问题的形式去解决, 然后让同一个函数在运行过程中不断地去调用它自身, 以此来一步步地实现目标。但是这里头有一个必须遵守的铁规矩, 就是程序里一定要设定好结束条件, 这个条件通常也被叫做递归的出口, 因为如果大家都不给设置明确的下线, 那么这个调用过程就会没完没了地持续下去, 最后直接导致系统报错或者崩溃。说到实际的使用场景呢, 这类技术非常普遍, 大家常见的比如计算从一加到n的所有数字之和、算某个数的阶乘、求解斐波那契数列里的数值、用快排和归并排序来处理数据、进行文件目录的遍历操作, 还有经典的汉诺塔游戏代码实现等, 这些都是典型的例子。def get_sum(num):if num 1:return 1return num get_sum(num - 1)print(get_sum(5))print(get_sum(100))def get_factorial(num):if num 1 or num 1:return 1return num * get_factorial(num - 1)print(get_factorial(5))六、 关于匿名函数的一, 就是基础部分。这是一个匿名函数, 是面向函数编程的一种体现, 它不关注是谁来做这些事情, 而仅仅只关注到底要做哪些事情, 通常情况下它是常常作为一个参数被传入到另一个函数里面的去, 这样可以使得代码变得更加简洁。def operation(a, b, func):return func(a, b)print(operation(1, 2, lambda a, b: a b))f lambda a, b: a bprint(f(1, 2))print(type(lambda a, b: a b))二 把数据拿到手来, 然后开始着手进行处理。它的主要功能在于对数据进行一种比较精简且经过优化处理的形式操作, 通常在具体应用中会与诸如 map 这样的工具一并使用, 主要涵盖了对数据进行排序、利用 map 进行映射以及转换操作、执行过滤筛选任务, 以及完成数据的合并归约等多个方面。from functools import reduceperson_list [{name: jack, age: 21, address: 深圳},{name: jeery, age: 11, address: 深圳},{name: tom, age: 35, address: 广州},{name: lily, age: 55, address: 东莞},]print(sorted(person_list, keylambda p: p[age], reverseTrue))print(list(map(lambda p: p[name].upper(), person_list)))print(list(filter(lambda p: p[address] 深圳, person_list)))print(reduce(lambda x, y: x y, [1, 2, 3, 4, 5]))print(reduce(lambda total, p: p[age] total, person_list, 0))七、现在我们来讲一讲函数的文档, 以及它里面的类型注解部分。在定义函数的时候, 我们可以给参数还有返回值加上类型注解。这样写能让代码看起来更清楚一些。这样做主要是为了提高可读性, 它并不是强制去检查类型的对不对的。def dog(name: str, age: (1, 99), species: 狗狗的品种) - tuple:print(name, age, species)return True二函数的文档内容, 还有那边用不上的内置工具。所谓的 多行注释, 它的本质其实是被实现成了字符串的形式, 这种形式因此就可以作为函数文档来使用。而代码里面内置的那个特定的符或者功能区域, 可以通过它来查看类型注解的具体内容, 另外还有一个叫做 help的东西, 它能够用来查看函数文档到底是什么。def dog(name: str, age: (1, 99), species: 狗狗的品种) - tuple:这个函数用于讲解函数的注释:param name: 这是名字参数:param age: 年龄参数:param species: 品种参数:return: 返回值print(name, age, species)return Trueprint(dog.__annotations__)help(dog)八、本日重点小结函数三要素包括定义def 函数名():、调用写函数名、返回值。必须先定义再使用。形参是那个在定义函数的时候所使用的参数, 实参则是在这个函数被调用的时候所传入进来的参数, 对于那种带有默认值的形参来说, 它在调用阶段是可以选择不进行传参这个动作的。这里一共有四种传递参数的方法, 第一种叫做按位置来传参数, 第二种是通过关键字去传参数, 第三种是用带星号的解包形式, 这种形式会把列表或者元组按照它们各自的位置来进行处理, 第四种则是使用双星号进行的解包操作, 这种方式能够把字典转换成用来传递的关键字参数, 不过要注意字典里面的键的名称必须要和函数定义时使用的形参的名字完全相同才行。对于函数签名中斜杠前面的参数, 必须使用位置参数的方式进行传递, 而对于星号之后的参数则必须采用关键字的形式来指定, 同时需要明确的是, 那些处于可变长参数后面的其余参数在调用时也必须通过关键字参数的方法来传递。所谓的用全引用的方式来传递参数, 在函数内部对那个参数的值进行更改时, 是否会对外部的实际变量产生实质性的影响, 这个情况具体怎么发展, 完全取决于被传入的数据是属于可变类型还是属于不可变类型这种分类。直接赋值就是复制地址, 浅拷贝会导致外层是新的而内层是同一个, 深拷贝则是全部都变成了新的。当函数执行结束以后, 如果我们要返回多个值, 系统会自动把这些值打包放进一个元组里面去, 然后在用多个变量进行接收的这个过程当中, 这其实就是一个解包的操做。递归这个东西的意思呢, 就是指一个函数自己在内部调用自己, 但是必须得设置一个递归的出口, 如果没设好的话, 就会进入无限的递归状态从而导致报错。匿名函数经常和map结合起来对数据进行加工操作, 这就是面向函数编程的具体做法。