python基础之函数详解

Python基础之函数详解

一、函数的定义

到现在为止,我们已经掌握了Python的基本语法和数据类型等相关基础知识了,以进行一个项目的编写了,这个时候,就会发现,很多代码需要我们进行复制粘贴,这简直就是损害我们小文艺的脸面啊,这个时候,我们就需要向函数求助了。

函数的三大优点:

  1. 代码不冗余
  2. 可阅读性高
  3. 可维护性,可拓展性得到提升。

函数定义会发生什么呢?

  1. 先向内存申请一块空间将函数体的内容存放进去
  2. 将函数体的内容和函数名绑定一起
  3. 程序在执行到定义函数的阶段,只识别语法,不会执行函数体的内容

那么函数的语法是什么呢?

def 函数名(有无参数都可以):
	"""描述性信息"""
    代码块
	return 值。

  1. def:这是定义函数的关键字
  2. 函数名:遵循变量名的定义,最好对函数有一个功能性介绍(动词为主)
  3. 括号:内部可以选填参数
  4. “”“描述性信息”“”:可有可无,但是推荐使用,对函数体的功能进行描述
  5. return:可有可无。给出返回值

二、函数的调用

使用函数要遵循一个原则:这个原则就表明了它的内部的一些现象:大家一定要记牢:先定义后使用。是不是很熟悉,没毛病,这就是变量的使用原则。

函数名实际上跟“变量名”有些相似,都是储存一个内存地址,当我们调用函数的时候,最最基本的方式就是函数名加括号的方式。

def func():
	pass

# 最最基本的调用方式。
func()



def func(a,b):
    pass

# 函数体有参数的方式
func(1,2)


三、函数返回值

这时候就不得不讲一下return的作用了,如果我们把函数当做一个工厂,那么参数就是我们送到工厂的原材料,而工厂生产出来的东西就是我们return的值了。他可以是任意数据类型,当函数运行结束的时候,我们可以对其进行赋值,就可以得到我们的产品了。

# 当函数没有return 值的时候,默认返回值为None
def func():
	pass
v= func() 
print(v)  # None


# 当只有一个return,但是并没有写返回值的时候,默认返回值也是None
def func():
	return
	
v= func() 
print(v)  # None

# 当函数有返回值的时候,对其调用进行赋值会得到返回值。
# 返回值可以是单个,也可以是多个
def num_sum(a,b):
	return a+b

sums = num_sum(1,2)
print(sums)  # 3

return 的作用不止如此,他也是函数终结者。当函数体中运行到return的时候,就会终止函数的运行,不会执行return之下的代码。

当然,我们还可以将函数的返回值当做参数传给另外一个函数,因为返回值的本质就是一个变量值。

四、函数的参数

当我们在定义有参函数的时候,括号内部的参数相当于变量名,而在函数调用的时候,传进的值将内存地址绑定给定义阶段的参数。

在定义阶段,函数括号内的参数称为形参。

在调用阶段,函数括号内的参数称为实参。

参数可以有好几种形式表现,比如单个数据类型、key=value、函数等,但是其核心概念就是传入的是个值。

4.1 位置参数

位置形参函数在定义的时候是按照从左到右依次定义的参数。特点是必须被传值,不能多也不能少。

位置实参:函数在调用的时候是按照从左到右依次定义的参数。特点是按顺序与形参一一对应。

# 特点:必须被传值,多少给参数都不行。

def func(x,y):  # 括号内就是位置形参
    print(x,y)
    
func(1,2,3) # 过多的位置实参被传
func(1,)  # 太少的位置实参
func(1,2)  # 完美

4.2 关键字参数

我们还可以利用key=value的形式进行传参,调用阶段的参数是关键字参数。关键字可以指定形参进行传值,可以不参照顺序。

# 关键字参数:按照key=value的形式传入当做参数。
def func(x,y=2): # 默认参数
    print(x,y)

# func(y=2,x=1)  # 关键字实参
# func(1,2)

默认参数在定义阶段已经被赋值了,所以在调用阶段可以不必为该参数传值,当然,如果要是进行传值的话,会以传的值为准。

实参:位置实参和关键字参数的混合使用

混合使用的话,一定要注意一下两点:

  1. 位置实参必须放在关键字实参之前。
  2. 不能为同一个形参重复传参。

形参:位置形参和默认参数的混合使用

形参中的混合使用需要注意:

  1. 位置形参必须在默认参数之前。
  2. 默认参数是在定义阶段就被传入的值的内存地址
  3. 建议默认值不推荐使用可变数据类型

4.3 可变长度参数

4.3.1 可变长度形参

如果我们在调用函数的时候,如果传入的实参过多,就回出现错误,但是我们可以使用(*+形参名)或者(**形参名)来接受过多的实参。

原则上形参名可以是任意名,但是约定俗成通常为(*args)和(**kwargs)。

  1. *args。用来接受位置实参。多出来的实参会被保存成元祖。
  2. **kwargs。用来接受关键字参数。多出来的实参会被保存成字典。

当然,在函数内部使用的时候,是不带星号的。

在定义的时候请务必放到默认参数之后。*args在**kwargs之前。

# *args的使用
def foo(x,y,*args):
    print(x,y,args)

foo(1,2,3,4,5,6)  # 1 2 (3,4,5,6)
 
    
# **kwargs的使用
def foo(x,y,**kwargs):
    print(x,y,kwargs)
    
foo(1,2,a=3,c=4)  # 1 2 {"a":3,"c":4}

4.3.2 星号在实参上的应用

多出来的实参有*args和**kwargs接受,那么如果在调用的时候就给实参加上星号会有什么结果呢?

如果我们给实参前加上星号,本质是将该实参进行循环,然后得出的值当做实参传入形参。当然*args后可以跟多个可循环的数据类型,如果是字典只传入key。**kwargs只能使用字典,代表的是将key=value的格式传入。

# 在字典上的应用。
def func(x,y,z):
    print(x,y,z)

func(*{'x':1,'y':2,'z':3}) # func('x','y','z')
func(**{'x':1,'y':2,'z':3}) # func(x=1,y=2,z=3)

当然,传入的实参数量要和形参数量相同,不然会出现错误。

4.3.3 星号的综合应用

def index(x,y,z):
	print(x,y,z)

def foo(*args,**kwargs): # (1,2)传给args,{"z":3}传给kwargs
	# *args将(1,2)转成1,2。**kwargs将{"z":3}转成z=3,然后传入index函数做参数
    index(*args,**kwargs)  
	
foo(1,2,z=3)

当函数定义的参数为*args,**kwargs时,代表函数可以接受任意符合格式的实参,上述的函数调用foo的本质是调用index函数。

4.4 命名关键字参数

这个参数使用的比较少,在定义形参的阶段,如果某些参数出现在星号之后,那么这些参数就被称为命名关键字参数。

# 命名关键字参数
def func(x,y,*,a,b):  # a,b就是命名关键字参数
    print(x,y,a,b)
    
def foo(x,y,*,b=1,a):  # 命名关键字参数是不用按key=value在单个值之后的顺序
    print(x,y,a)

注意,由于命名关键字参数位于星号之后,所以如果不用关键字实参的话,其值是传不进去的,因此必须的用指名道姓的方法传参数,而且调用的时候还必须被传值。

4.5形参和实参的顺序

形参的顺序:位置形参、默认参数、*args、命名关键字参数、**Kwargs。

实参的顺序:位置实参、*args、关键字参数、**kwargs。

五、函数闭包函数

在了解闭包函数之前,我们是需要一些储备知识的。然后,你不知不觉就一边蒙逼一边明白什么是闭包函数了。

5.1 函数的应用。

# 1、函数可以赋值,因为函数名本身引用着函数体的内存地址。
def foo():
    pass

f = foo

# 2、可以把函数当做参数传递给另外一个函数。
def func():
    print()

def foo(x):
    print(x)
    
foo(func)

# 3、可以当作另一个函数的返回值。
def f1():
    return foo

# 4、可以当作容器类型的一个元素。
def foo():
    pass

l = [foo,1,a]


其实看着有这么多应用,但是大家发现没有,函数名其实就是一个变量名,凡是变量名能做到的它都能做到。这点很重要哦。

5.2 函数的嵌套

函数的嵌套共有两种。

# 1、函数的嵌套调用。即在调用一个函数的时候,在其内部又调用另外一个函数。
def foo():
	pass
def func():
	foo()
	
# 2、函数的嵌套定义。即在定义一个函数的时候,在其内部又定义另外一个函数。

def func():
	def foo():
		print(1)

5.3 闭包函数

首先上面都是闭包函数的铺垫。

明确一个概念:闭包函数= 名称空间与作用域+函数嵌套+函数对象。

  1. 闭:该函数是内嵌函数。这字说明了该函数所处的位置,肯定是嵌套函数中的定义函数。

  2. 包:指该函数包含对外层函数作用域名字的引用。注意,自己本身不含有,只有外部函数有。

是不是看不懂,接下来看个例子、

# 闭函数:名称空间与作用域+函数嵌套
def outer():
	x = 1111
	def inter():
		print(x)
	inter()
x = 22222		
outer()

当我们在外界去调用inter函数的时候,由于名称空间与作用域的查找关系(按照定义的时候的查找顺序)的原因,不管我们在外界定义的是否含有x ,他都会使用outer内定义的变量。

# 闭包函数:名称空间与作用域+函数嵌套+函数对象
def outer():
	x = 1111
	def inter():
		print(x)
	return inter		
f1 = outer()

f1()

注意,是把函数名当做变量来用,return后返回的inter是一个函数的内存地址,如果加上括号就是执行函数功能的意思。所以一定不能加括号。在运行Outer的时候,返回一个内部函数名,然后复制给一个全局变量,然后该全局变量就被绑定了inter函数的内存地址,当该函数名后加括号的话,就相当于执行了inter()。

这样我们就可以在任意地方调用函数的局部名称空间。

当然,你也可以把x当做参数进行传递。但是原理一定要搞清楚,因为这是接下来的重点内容的铺垫。

六、装饰器

当我们的软件已经在线上运行的时候,需要拓展新的功能,那么我们 应该怎么办呢?首先我们要提到开放封闭原则。

  1. 开放原则:对拓展功能是开放的。
  2. 封闭原则:对修改源代码则是封闭的。

也就是说我们既不能更改函数的源代码,也不能更改调用方式,这个时候,我们就要用到装饰器了。

装饰器:为函数增加额外的功能且不更改调用方式的函数。

6.1 无参装饰器

下面我们进行循序渐进的讲解,究竟是怎么用到装饰器的,他又是怎么做到的符合开放封闭原则的。我们以为一个函数增加一个计算运行时间的功能为例来进行推导。

import time

def foo(x):
    print("其实我这里有很多代码,但是你看不到")
    time.sleep(3)
    return x
    
# 1、我们直接在函数内部进行更改。
def foo(x):
    start_time = time.time()
    print("其实我这里有很多代码,但是你看不到")
    time.sleep(3)
    stop_time = time.time()
    process_time = stop_time - start_time
    print(process_time)
    return x


这时候虽然我们增加了功能,但是我们已经违反了开放封闭原则,所以失败。

# 2、利用函数的嵌套定义来直接增加新功能

def outer(x):
    start_time = time.time()
    foo(x)
    stop_time = time.time()
    process_time = stop_time - start_time
    print(process_time)
    return x

我们同样实现了功能,调用方式改变,源代码没变。

# 3、利用闭包函数来修改

def outer(foo):
    def inter(x):
        start_time = time.time()
        res = foo(x)
        stop_time = time.time()
        process_time = stop_time - start_time
        print(process_time)
        return res
    return inter

foo = outer(foo)
foo(x)

上述已经实现了不更改函数源码,不更改调用方式了,但是函数被写死了,如果用来修饰多个函数呢?这些函数又有不同个数的参数呢?

# 终极版装饰器的诞生。
def outer(func):
    def inter(*args,**kwargs):
        start_time = time.time()
        res = func(*args,**kwargs)
        stop_time = time.time()
        process_time = stop_time - start_time
        print(process_time)
        return res
    return inter

foo = outer(foo)

这是利用星号在形参和实参不同的作用来实现的。

自此,装饰器就写好了,遵守开放封闭原则,让用户在不知情的情况下,完成了函数的更新。要想掌握装饰器,必须弄懂闭包函数这些前置只是,如果还是不太懂,就继续看看闭包函数板块和参数的知识。

# 装饰器的模板。
def outer(func):
	def inter(*args,**kwargs):
		res = func(*args,**kwargs)
		return res
	return inter

添加装饰器的时候,可以根据模板然后在inter函数里面添加相关功能。

当然,还有一点就是语法糖的使用,我们可以将装饰器的赋值转成在被修饰函数上一行添加@装饰器名。

# 语法糖:在被修饰函数上一行添加@装饰器名。
@outer
def foo(x):
	print(x)

6.2 有参装饰器

当用户进行身份验证时,有的函数要从info文件里得到信息,有的则是从数据库等等,那么如何让让一个装饰器实现那么多功能呢?我们都能想到,那就再给装饰器一个状态参数呗,要知道,之前为了伪装成被装饰参数,内部函数的参数已经固定,外部参数虽然也可以进行参数赋值,但是由于语法糖的存在,导致我们如果在外部函数增加参数之后,语法糖就不能使用了。

综上,我们还可以再次利用给函数传参的两种方式中的第二种,在函数外在套一层,此时的炸U那个时期已经是三个函数定义了。

# 语法糖的实际效果相当于执行了这一步:与被修饰函数名相同的全局变量 = 装饰器(被装饰函数名)
@outer  # foo = outer(foo)
def foo():
	print("1")
    
   
# 有参装饰器的模板

def auth(参数):
    def outer(func):
        def wrapper(*args,**kwargs):
            res = func(*args,**kwargs)
            return res
        return wrapper
    return outer

@auth("参数")
def foo():
    print(123)

当程序运行到语法糖时,由于函数名加括号,开始执行函数返回outer的内存地址,所以实际上语法糖依然是@outer。不过是多执行了一步函数将参数传到内部函数的名称空间而已。

6.3多个装饰器的运行原理

def outer1(func):
    def wrapper1(*args,**kwargs):
        res = func(*args,**kwargs)
        return res
    return wrapper

def outer2(func):
    def wrapper2(*args,**kwargs):
        res = func(*args,**kwargs)
        return res
    return wrapper2

def outer3(func):
    def wrapper3(*args,**kwargs):
        res = func(*args,**kwargs)
        return res
    return wrapper3


@outer1  # foo = outer1(foo)= wrapper1的内存地址
@outer2  # foo = outer2(foo)= wrapper2的内存地址
@outer3  # foo = outer3(foo) = wrapper3的内存地址
def foo():
    print("123")

加载顺序:outer3>outer2>outer1

执行顺序:outer1==>outer2>outer3

七、递归函数

递归函数的本质就是循环,在运行函数体期间一直在不停地调用自己。在Python解释器中,递归有一个最大限制,超过这个限制就会报错。

# 递归的直接调用
def foo():
	print("一直循环我会报错")
	foo()
	
# 递归的间接调用
def foo1():
	print("我是正常的函数,调用了foo我就不正常了")
	foo()

递归函数在调用自己的时候又两个过程

  1. 回溯:在函数中不停地调用自己的过程
  2. 递推:在函数满足条件退出函数之后,不停地将已打开的函数结束的过程。
# while循环
count = 0

while count <5:
    print(count)
    count += 1

# 改成递归函数之后

def cou(count):
    if count > 4:
        return
    print(count)
    count += 1
    cou(count)

cou(0)

据我观察,递归函数应该是主要应用在需要参数且多次循环的场景之上。

八、函数的类型提示

这是一个可用可不用的一个提示。

def foo(x:str,y:"这里面应该写整数",z:list=[1,2,3]):
	pass
	

形参后面加个冒号然后可以写一些提示性信息,声明一下数据类型等,但是如果你不按照规定传参,函数也不会报错,因为这个规定本来就是自己定的 ,但是你打破了这个自己提示的类型,那你声明干嘛呢,,就像调皮的用户,哈哈哈

posted @ 2020-03-17 15:59  小菜鸟是我  阅读(487)  评论(0编辑  收藏  举报