第三章 函数
前面我们学的知识,理论上已经足够我们编写所有的功能了。
但是如果想实现一个大规模的程序,很快我们就会发现问题。
比如在计算美元和人民币的汇率,我们用一个顺序结构来实现
可能会类似下面的代码
dollors = input('>>>') dollors = float(dollors) rmb = dollors/ 6 print("$%s -> ¥%s" %(dollors, rmb))
这段代码很有效,但是如果大型的程序,我们可能不止在一个地方需要用到美元转人民币的功能,
每一次用到,我们现在的做法就是把这段代码重新写一遍。可能你觉得没啥,不过就是复制粘贴一遍吗。
好,现在假设我们的程序中有100个地方使用了这个功能,也就是这段代码被复制了100遍,在我们的代码文件里。
现在我们美元人民币的汇率变了,变成1:8,我们需要开始改之前的100段代码来适应新的需求。
如果这样写代码,搞程序开发,生活还有什么意思?
和搬砖就真的没区别了吧?所以我们需要用到函数!
3.1 函数的定义
def 函数名(参数1,参数2...): '函数功能说明' 函数功能代码块 return 结果
定义一个函数就相当于预先定义了一个功能,这个功能,需要输入一些参数,经过他的处理后,我们可以拿到结果。
这其实是一种叫封装的思想,函数的定义和使用是分开的,
我们把实现一个功能的代码,放在一个黑盒子里,相当于定义一个函数,这个黑盒子,实现一个功能,只要我们传入参数,他就给我们结果。
设计好了之后,我们使用功能时,只需要提供参数就行了,完全不用关心盒子里面是怎么动作的。

使用函数来编写汇率转换的功能
def dollor_to_rmb(dollor, rate=6): ''' 输入美元和汇率,计算对应的人民币 :param dollor: 美元 :param rate: 对应的汇率, 默认为6 :return: ''' dollor = float(dollor) rmb = dollor / rate return rmb dollors = input('>>>') rmb = dollor_to_rmb(dollors) print("$%s -> ¥%s" % (dollors, rmb)) rmb = dollor_to_rmb(dollors, 8) print("$%s -> ¥%s" % (dollors, rmb))
函数的注释是函数代码块中的第一个字符串,没有注释的函数一样可以运行
但是建议大家实现函数时都加上注释,这是一个高素质的优秀的开发人员需要具备的自觉
养成这个习惯后对日后的维护非常有帮助。
想知道一个函数的功能除了去查看代码中的注释,还可以使用内置的help函数来查看
比如想知道我们定义函数的功能注释
print(help(dollor_to_rmb))
Help on function dollor_to_rmb in module __main__: dollor_to_rmb(dollor, rate=6) 输入美元和汇率,计算对应的人民币 :param dollor: 美元 :param rate: 对应的汇率, 默认为6 :return:
3.2 参数和返回值
形式参数:我们定义函数的时候,定义的参数就是形参
实际参数:我们调用函数时传递的参数,就叫实参
函数体:定义函数冒号后面缩进的代码块,就叫函数体
返回值:函数体中的代码,执行到return返回。如果没有return语句,python默认在函数体最后加了个 return None,表示什么都没返回。

在调用函数时,会执行我们定义函数的函数体中的代码
执行后把函数的返回值送到调用函数的地方。
在执行函数体之前,会有一个隐式的操作,就是把函数的实参,赋值给形参
def dollor_to_rmb(dollor, rate=6): # 隐含操作,实参赋值给形参 #dollor = 30 #rate = 8 dollor = float(dollor) rmb = dollor / rate return rmb rmb = dollor_to_rmb(30, 8)
函数的参数和返回值可以任意多个
一个没有参数和返回值的函数
# 参数 import time def func(): print(time.ctime()) ret = func() print(ret)
如果我们没有在函数体中设置函数的返回值,python会给我们返回一个None
函数的形参可以设置默认值, 设置了默认参数的函数,在调用时可以不传对应的参数,
函数执行时会把默认值赋值给形参
调用函数时,可以给指定形参传值,即通过关键字(形参名)调用
但是注意,关键字参数必须在位置参数的右边
# 默认参数 def func(name, age=18, gender="男"): print("姓名:%s" % name) print("年龄:%s" % age) print("性别:%s" % gender) print("-" * 80) # 报错name没有默认参数,必须传一个 # func() func("王富贵") func("赵富贵", 20) func("宋富贵", 16, '保密') # 关键字调用 func('刘富贵', gender="女") func(age=29, name = "张富贵") # 报错,关键字传参必须在位置传参右边 #func(name="孙富贵", 20, '男')
通常情况我们定义几个形参,调用函数的时候就只能传递几个(默认值的也算吧)
但是我们如果定义函数的时候,不知道函数调用的时候会传递几个参数
我们可以通过参数收集的方式,把调用时传进来的参数,全部装在一个容器里,比如元组
然后在函数体内部去访问这个元组。如果是按关键字调用函数,那我们可以把它搜集到一个字典里
# 参数收集 def add(*arg, **kwarg): print('arg', arg) print('kwarg', kwarg) sum = 0 for i in arg: sum += i return sum print(add(1,2,3,34,5)) print(add(1,2,3, name='王富贵'))
arg (1, 2, 3, 34, 5)
kwarg {}
45
arg (1, 2, 3)
kwarg {'name': '王富贵'}
6
注意搜集的关键在于*号,一个*号表示搜集位置参数,两个**号表示搜集的是关键字参数
*号出现在形参列表中表示搜集,如果**号出现在函数调用时,则表示的是解包。
一个*号是解包元组,两个**号是解包字典
# 参数解包 def func(name, age=18, gender="男"): print("姓名:%s" % name) print("年龄:%s" % age) print("性别:%s" % gender) print("-" * 80) zhangsan_tuple = ('zhangsan', 18, "女") func(*zhangsan_tuple) lisi_dict = { 'name':'lisi', 'gender':'女' } func(**lisi_dict)
函数的返回值,通常情况下只有一个,实际上python也只支持返回一个值
那怎么做到函数返回多个值呢,把返回值放到一个容器里呗,比如元组
在return语句中把返回值用逗号隔开就可以了
接受返回值时,利用序列赋值,就好像拿到了函数的多个返回值
# 返回多个值 def get_max_and_min(number_list): return max(number_list), min(number_list) a, b = get_max_and_min([2,9,3,5,1,6]) print(a, b)
3.3 作用域
想一下,我们的变量,是我们自己定义一个名字,然后把值赋给变量
那变量也可以理解为,名字和值两部分组成。你想到了什么,是不是想到了字典。
Python是怎么管理我们代码中的那么多变量呢,和我们猜的一样,通过一个字典来维护的。
这个字典就叫作用域,也有叫命名空间的。
怎么查看作用域呢,使用 内置函数vars就可以查看当前作用域
name = '王富贵' age = 18 print(vars()) {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>,
'__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'name': '王富贵', 'age': 18}
通过 vars我们可以看出当前作用域里面有什么,我们发现我们自己定义的变量都在其中了,其余的是python中内置的一些必要变量,我们暂时不用理会。
注意,我们不要去修改vars返回的字典, 可能会产生意外的后果。
实际上我们定义一个变量,python就会把变量存在这个作用哉里,想访问一个变量,python就去这个作用域查找。
一个程序不只有一个作用域,实际上每一个函数都有一个作用域,我们把函数内部的作用域叫局部作用域
函数外部的作用域叫全局作用域,全局作用域和局部作用域是不同的,下面的代码可以证明
import pprint name = '王富贵' age = 18 def func(): name = "赵小姐" age = 26 pprint.pprint("函数内部的作用域, %s" % vars()) func() pprint.pprint("函数外部的作用域, %s" % vars())
"函数内部的作用域, {'name': '赵小姐', 'age': 26}" ("函数外部的作用域, {'__name__': '__main__', '__doc__': 'abc123', '__package__': None, " "'__loader__': <_frozen_importlib_external.SourceFileLoader object at " "0x105c6fe50>, '__spec__': None, '__annotations__': {}, '__builtins__': " "<module 'builtins' (built-in)>, '__file__': " "'/Users/mac/PycharmProjects/python_test/test.py', '__cached__': None, " "'pprint': <module 'pprint' from " "'/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/pprint.py'>, " "'name': '王富贵', 'age': 18, 'func': <function func at 0x105cc5560>}")
如果把作用域比作一个盒子,全局作用域和局部作用域的关系大概如下

当我们访问一个变量时,变量会优先在代码的当前作用域中寻找,如果当前作用域中没有找到,再去外面一层的作用域中寻找
外面一层的作用域中没找到,就再往外面找,这个过程中一旦找到了与我们访问变量名字一样的变量,就使用他的值,
如果最后到全局作用域中仍没找到,就报错了,因为我们使用了没有定义的变量。
name = '王富贵' age = 18 def func(): age = 26 # 本地作用域没有,使用全局作用域中的nmae print('name:%s' % name) # 本地作用域中有,优先使用本地作用域 print('age:%s' % age) #报错,所有作用域中都 没有gender #pritn('gender:%s' % gender) func()
如果尝试在局部作用域中修改全局变量,我们会发现修改不成功
想在局部使用域中修改全局变量,必须使用global关键字
name = '王富贵' age = 18 def func1(): age = 26 print("函数内部我以为是把age改成 %s" % age) func1() print("执行过fun1后,age是 %s"% age) def func2(): global age age = 26 print("函数内部我以为是把age改成 %s" % age) func2() print("执行过fun2后,age是 %s"% age)
函数内部我以为是把age改成 26 执行过fun1后,age是 18 函数内部我以为是把age改成 26 执行过fun2后,age是 26
函数的内部也可以定义函数,这叫函数的嵌套,嵌套之后,作用域就分成了多层
使用变量仍然是从当前作用域寻找,再向外一层,直至找到或找不到报错。

如果inner函数作用域想改变outer函数作用域变量的值,就不能使用global,应该使用nonlocal
name = '王富贵' age = 18 def outer(): name = '赵小姐' age = 26 def inner(): nonlocal name name = '美丽的赵小姐' print('改变外层作用域name') global age age = 99 print("改变全局作用域age") inner() print('outer中的name:%s' % name) print('outer中的age:%s' % age) outer() print('全局中的name:%s' %name) print('全局中的age:%s' %age)
改变外层作用域name 改变全局作用域age outer中的name:美丽的赵小姐 outer中的age:26 全局中的name:王富贵 全局中的age:99
3.4 函数就是变量
现在要告诉大家一个真相,函数,其实也是一个变量,我们想一下,我们定义函数的时候给函数设置了一个名字
在调用函数的时候使用的是这个名字,如果我们把函数执行的代码当作一个数据类型,叫代码对象
那函数不就是一个变量吗,函数的名字是变量的名字,函数定义的执行代码就是变量的值。
我们看作用域的时候也可以看出来,函数名字,和他的代码,也组成了一个键值对,放在了作用域中
# 函数就是变量 from pprint import pprint name = '王富贵' age = 18 def func(): print('函数体') pprint('作用域:%s' % vars())
("作用域:{'__name__': '__main__', '__doc__': None, '__package__': None, " "'__loader__': <_frozen_importlib_external.SourceFileLoader object at " "0x108a12850>, '__spec__': None, '__annotations__': {}, '__builtins__': " "<module 'builtins' (built-in)>, '__file__': '/Users/mac/Downloads/gc/tt.py', " "'__cached__': None, 'pprint': <function pprint at 0x108a6eb00>, 'name': " "'王富贵', 'age': 18, 'func': <function func at 0x108a6e560>}")
函数既然是变量,那就满足变量的一切特性
函数可以被赋值给另一个变量
from datetime import datetime def func(): time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%D") print('现在的时间是%s' % time_str) x = func x() func()
函数也可以当成实参被传递给另一个函数
比如内置的函数 sort
help(sort)
Help on built-in function sorted in module builtins: sorted(iterable, /, *, key=None, reverse=False) Return a new list containing all items from the iterable in ascending order. A custom key function can be supplied to customize the sort order, and the reverse flag can be set to request the result in descending order.
他的形参key就需要传递一个函数
比如我们想对于以下的一个列表进行排序
students_list = [('王富贵', 18), ('赵小姐', 26), ('甄烈害', 23), ('贾烈害', 77)] print("不使用key,直接排序", sorted(students_list)) def get_age(item): return item[1] print("传递参数key,进行排序", sorted(students_list, key=get_age)) #sorted 内部相当于做了以下的操作 # def sorted(studnets_list, key): # for item in students_list: # key = get_age # sort_key = key(item) # 用sort_key来进行大小的比较
不使用key,直接排序 [('王富贵', 18), ('甄烈害', 23), ('贾烈害', 77), ('赵小姐', 26)] 传递参数key,进行排序 [('王富贵', 18), ('甄烈害', 23), ('赵小姐', 26), ('贾烈害', 77)]
我们想按年龄排序,就必须告诉排序函数我们每一个元素比较大小时,用什么值,就是我们设置key的作用
我们可能觉得调用内置函数时,临时需要给形参传一个函数对象,去临时定义一个函数,有点不方便。
其实上像这种简单的函数,我们可以使用一种匿名函数的方法,用一条短小精湛的语句来实现
# 匿名函数 students_list = [('王富贵', 18), ('赵小姐', 26), ('甄烈害', 23), ('贾烈害', 77)] print(sorted(students_list, key=lambda item:item[1]))
匿名函数实际上就是生成了一个函数对象,我们可以把这个函数对象赋值给变量,其实也相当于定义了一个函数
add = lambda x, y:x+y print(add(1,2)) print(add('山有木兮', '木有枝'))
还有一点,像变量一样,函数要先定义后使用。
3.5 内存管理
python的垃圾回收机制
程序的内存模型,在操作系统上运行的程序,不会直接控制内存硬件
而是由操作系统提供的一个虚拟的内存空间,让应用程序以为自己独占了内存
一个应用程序的内存模型大概如下

在C语言中,程序员定义一块数据在内存中,需要自己调用 函数在堆中分配一块内存
使用完成后要手动释放。大量的程序员在写程序中很难始终如一做到这一点,所以用C语言写程序
经常会有内存泄漏的现象 ,就是程序员使用了一块内存,等他不用的时候,没有去释放 ,这块内存就永远在内存的角落中被遗忘了
等这种没有释放 的内存越来越多,我们的内存可用空间就越来越少,程序就崩溃了。
像python这种高级语言,不存在内存泄漏,因为我们分配内存空间和释放内存空间都由解释器帮我们做了
我们再不用担心内存不即时释放的问题了
python解释器足够智能,当他检测到我们不用一块内存空间就会自动帮我们释放掉
那这是怎么做到的呢
我们在内存中想要使用一块内存,需要通过变量去访问,如果一块内存没有任何变量与之关联,那它就没有存在的必要了吧
这时python就会给我们释放掉这块内存
实际上是通过引用计数来判断的
# 内存管理 # 在内存中分配了一个字符串叫 王富贵 # 赋值给name 变量,王富贵的引用计数加1 name = "王富贵" # 在内存中 分配了一个字符串叫赵小姐 # 赋值给name变量后,王富贵的引用计数就减1,引用计数为0,释放 # 赵小姐的 引用计数加1 name = "赵小姐" # 把变量name赋值给变量n, 赵小姐的引用计数加1 n = name # 删除变量name, 赵小姐的引用计数减1,赵小姐的引用计数为1 大于0,所以还在 del name # 访问变量n就可以访问到赵小姐 print(n)
3.6 闭包和装饰器
函数的形参实际上也是局部变量里的一个变量
函数内部定义的函数也是局部变量的一个变量
def out(name): age = 18 def inner(): print('姓名:%s' % name) print('年龄:%s' % age) print(vars())
return inner out('王富贵')
{'inner': <function out.<locals>.inner at 0x102882b00>, 'age': 18, 'name': '王富贵'}
我们可以让out函数,把变量inner返回,这样,调用out函数会得到一个打印姓名和年龄的函数对象
我们调用这个函数对象,他使用的name是我们调用out时传进去的
def out(name): age = 18 def inner(): print('姓名:%s' % name) print('年龄:%s' % age) return inner wfg = out('王富贵') zxj = out('赵小姐') wfg() zxj()
姓名:王富贵 年龄:18 姓名:赵小姐 年龄:18
wfg和zxj是两个函数变量,对应的是Inner函数的代码,可是inner函数代码中没有name和age
所以就像外一层的作用域中寻找,找到了name和age
name和age在out函数中,本来调用完out后会被解释器释放的数据,因为被inner函数引用,所以就没有被释放
和inner函数一起形成了一个闭包
我们可能会有疑惑,同样的out返回的是同样的inner变量,为啥执行起来结果不一样
这就是闭包的关键,什么是闭包,闭包就是由一个函数 + 一个自由变量
自由变量,是指不属于任何作用域中的一个变量
name 和 age的作用域在out执行结束之后就被释放了
name和age是自由变量,不属于任何作用域的变量本该被释放,恰好得到了inner函数的赏识
就和Inner函数一起形成闭包,闭包就是带着自由变量的函数
wfg和zxj的自由变量不同,所以是两个闭包,所以同样的Inner对象,得到了不同的结果

下面我们来看装饰器
假设有一个用户登陆的功能函数
def login(): print("登陆成功")
后来我们想在log中记录一下login函数执行的时间
我们可能会去修改一下login函数, 在执行login之前,把时间打印到log中去
def login(name): print("%s登陆成功"% name) login('王富贵')
这样做是有效的,不过,如果类似login这样的函数,在我们的一个项目中,有成百上千个,
我们都想在他们执行的时候加上执行时间的log,我们需要去修改这成百上千的函数吗
且不论这豪无意义的重复性劳动,已经成熟的功能,每一次修改都可能会引入BUG。
所以这是一种挺LOW的方法
所以我们需要一种方法,在不改变所有函数原代码的情况下,给函数增加功能
我们学了嵌套函数,是不是可以通过一种包装的手段来实现呢
# 使用函数包装 def login(name): print("%s登陆成功"% name) def login_pack(func, *args, **kwargs): print("%s - INFO : login run" % datetime.now()) return func(*args, **kwargs) login_pack(login, '王富贵')
使用函数包装,没有改变login函数的原代码,给login增加了日志
但是我们在调用login的时候 要用这种奇怪的方式
login_pack(login, '王富贵')
我们虽然没有改变login代码,但是需要把所有调用 login的地方
由
login('王富贵')
改成
login_pack(login, '王富贵')
所以现在我们需要解决的是不需要修改现有的函数调用
我们需要一种更巧妙的方法
# 使用嵌套函数 def login(name): print("%s登陆成功"% name) def package(func): # 隐含操作,func是个局部变量 # func = login def inner(*args, **kwargs): print("%s - INFO : login run" % datetime.now()) return func(*args, **kwargs) return inner # 使用前把login包装一下,调用login的方法就不会变 # 实际上返回了一个闭包,自由变量就是login函数 # 每次调用时都执行inner,inner内部处理了额外的操作 # 然后再调用自由变量login login = package(login) login('王富贵')
到此为止我们就实现了我们的需求,没有改变函数的原代码
没有改函数的调用方式,就完成了函数功能的扩展
装饰器的功能就是这样。
装饰器的本质就是把函数作为自由变量的一个半包。
下面我们来优化一翻我们的装饰器
可能我们觉得使用函数前要包装一下,还是有点麻烦
python给我们提供了一个语法,用来简化这个包装步骤
def package(func): # 隐含操作,func是个局部变量 # func = login def inner(*args, **kwargs): print("%s - INFO : %s run" % (datetime.now(), func.__name__)) return func(*args, **kwargs) return inner # 相当于login = package(login) @package def login(name): print("%s登陆成功"% name) @package def logout(name): print("%s退出登陆"% name) login('王富贵') logout('王富贵')
通过func.__name__可以拿到每一个函数的名字
通过func.__doc__可以拿到函数的注释字符串
我们注意,包装过的login,已经不是login了,他是inner,
所以我们查看包装过的login的名字,会有些尴尬
def package(func): def inner(*args, **kwargs): print("%s - INFO : %s run" % (datetime.now(), func.__name__)) return func(*args, **kwargs) return inner @package def login(name): print("%s登陆成功"% name) print(login.__name__)
这可能是装饰器的一点小遗憾,或者是小BUG
不过我们可以用python给我们定义好的wraps装饰器来包装一下inner来解决
from functools import wraps def package(func): # 使用内置装饰器wraps包装一下inner # inner的函数属性就会和 login一样了 @wraps(func) def inner(*args, **kwargs): print("%s - INFO : %s run" % (datetime.now(), func.__name__)) return func(*args, **kwargs) return inner @package def login(name): '这是一个登陆功能' print("%s登陆成功"% name) print(login.__name__) print(login.__doc__)
以上就是我们一个完整又标准的装饰器的定义 ,大家把上面的代码记住,以后装饰器就这么用就好了。
我们的日志会打印执行时间,但是我们希望,在包装的时候,自己定义时间的格式
这样我们需要一个可以传参的装饰器
# 带参数的装饰器 from functools import wraps def package_with_para(format_str='%Y-%m-%d %H:%M:%S'): def package(func): # 使用内置装饰器wraps包装一下inner # inner的函数属性就会和 login一样了 @wraps(func) def inner(*args, **kwargs): print("%s - INFO : %s run" % (datetime.now().strftime(format_str), func.__name__)) return func(*args, **kwargs) return inner return package # 相当于 login = package_with_para("%Y年-%m月-%d日 %H点%M分%S秒")(login) @package_with_para("%Y年%m月%d日 %H点%M分%S秒") def login(name): '这是一个登陆功能' print("%s登陆成功"% name) @package_with_para() def logout(name): print("%s退出登陆"% name) login('王富贵') logout("赵小姐")
标准装饰器掌握后,带参数的无非就是多包装一层
3.6 递归
递归就是函数自己调用自己
比如我们数学上学的阶层的定义
n! = 1 * 2* 3* 4 ... * n
我们如果用递归来定义阶层的话
n! = (n-1)! * n,
这样定义 还不够完全 ,我们还需要有一个终止 条件
那就是最基本的情况 1! = 1
用代码来实现如下
def factorial(n): if n == 1: return 1 return n * factorial(n - 1) print(factorial(3))

同样我们之前学习的用循环求1+2+3...+100的求和,同样可以用递归来实现
def summary(n): if n == 1: return 1 return n + summary(n - 1) print(summary(100))
- 在函数内部调用自己的函数
- 要有一个终止条件,一般是最基本的情况
- 递归都可以用循环来实现,递归的效率不如循环
- 递归优势就是实现的时候算法比较清晰简单

浙公网安备 33010602011771号