第三章 函数

前面我们学的知识,理论上已经足够我们编写所有的功能了。

但是如果想实现一个大规模的程序,很快我们就会发现问题。

比如在计算美元和人民币的汇率,我们用一个顺序结构来实现

可能会类似下面的代码

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))
  • 在函数内部调用自己的函数
  • 要有一个终止条件,一般是最基本的情况
  • 递归都可以用循环来实现,递归的效率不如循环
  • 递归优势就是实现的时候算法比较清晰简单

 

posted @ 2020-07-10 14:13  人不知所  阅读(247)  评论(0)    收藏  举报