【python】进阶之函数详解(三)

3 函数

3.1类型提示

  • Python 3.6后加入了新功能:类型提示,用来声明一个变量的类型
  • 在FastAPI中,类型提示可以用到Swagger文档中
1
2
3
4
5
6
7
def get_name_with_age(name: str, age: int) -> str:
pass
# 基本类型: int、float、bool、bytes
from typing import List:
def process_items(items: List[str]): # 复杂的类型提示
for item in items:
print(item)

3.2 函数是一等公民

  • 在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。

  • python中一切且对象,所以函数也是对象,函数也是一等公民。

  • 也就是说函数可以作为函数参数,可以作为函数返回值,也可以赋值给变量。

示例1:函数当变量被赋值引用

1
2
3
4
5
def add(x, y):
print(x + y)

func = add # func
func(3, 4) # 等同于 add(x, y); 类似变量的赋值引用

示例2:函数做容器元素的参数

1
2
3
4
5
6
7
def add(x, y):
print(x + y)

funcs = [add]

for f in funcs:
f(1, 1)

示例3:函数做实参

1
2
3
4
5
6
7
8
def func(x):
return x + 1


def add(x, func):
return func(x)

print(add(3, func)) # 打印:4

示例4:函数做返回值

1
2
3
4
5
6
7
8
9
def add(x, y):
return x+y

def foo():
return add


func = foo() # 相当于 func = add
func(3, 4) # x相当于 add(3, 4)

3.3闭包函数

前面说了,函数是一等公民,所以函数可以作为入参和返回值传递

  • 内嵌函数包含对外层函数作用域中变量的引用(非全局作用域的变量,是外层函数内部的变量),那么该内嵌函数就是闭包函数,称为闭包(Closures)
  • 正常函数内部变量在函数调用之后就会被回收,但是闭包函数的出现,破坏了这个规则,让内部变量可以在函数外部使用
1
2
3
4
5
6
7
8
9
10
11
12
def outer():
x = 10
def inner():
return x + 1 # 此时 inner是闭包函数(引用了外层函数作用域的变量)
return inner


x = 10
def outer():
def inner():
return x + 1 # 此时,inner不是闭包函数(没有引用外层函数作用域的变量)
return inner

【如何判断闭包函数】

  • 方法1:是否引用外层函数作用域中的变量
  • 方法2:通过函数的closure属性,查看到闭包函数所包裹的外部变量。不是闭包该值为None
1
2
func.__closure__
f.__closure__[0].cell_contents

【闭包函数的用途】

  • 为函数体传参。一次传参,后续使用无须再传参 。闭包函数的这种特性有时又称为惰性计算。
  • 装饰器(重要)
  • 示例2:为函数体传参
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests


def outer(url):
def inner():
response = requests.get(url)
print(response.text)
return inner


baidu = outer("http://www.baidu.com")
baidu()
baidu()

作用:

  • 1、实现函数工厂:动态地生成和返回具有不同参数的函数
  • 2、保护数据: 将变量封装在闭包中,限制对变量的直接访问,实现数据的封装和隐藏
  • 3、装饰器:用于在函数前后执行额外的代码,如性能分析,日志收集等,用于加强函数

3.4 普通装饰器【有另开一篇】

目的:在不修改原有函数代码的情况下,增强函数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 假设要在运行函数的时候,加上日志打印函数的方法:
def outfun(fun):
def innerfun(*args,**kwargs):
print(f"当前方法的名称是:{fun.__name__}")
re = fun(*args,**kwargs)
print(f"当前方法的名称是:{fun.__name__}")
return re
return innerfun

@outfun
def add(a,b):
return a+b
result = add(1,2) # 运行的是上面定义的被装饰过后的内嵌函数innerfun
print(result)
# 输出结果
# 当前方法的名称是:add
# 当前方法的名称是:add
# 3
# 加了装饰器语法@outfun之后,当我们运行add()方法的时候,实际上运行的是被装饰过后的函数innerfun

3.5 带参数装饰器

知道装饰器的原理之后,带参数的装饰器实现就很简单,就是在外层函数的外面,再嵌套一层函数,且最外层函数的入参就是带参装饰器的入参,接上面

被装饰的函数需要参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 再到外层定义一个最外层函数
def overfun(login):
# 先定义一个装饰器函数
def outfun(fun):
def innerfun(*arg,**kwargs):
if login== "debug":
print(f"函数的名称是:{fun.__name__},等级是debug")
result = fun(*arg.**kwargs)
if login=="info":
print(f"函数的名称是:{fun.__name__},等级是info")
return result
return innerfun
return outfun

@overfun(login="info")
def testadd(a,b):
return a+b
r = testadd(1,2)
print(r)

使用装饰器时可以给装饰器传参数

-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time


def wrapper(flag):
def outer(func):
def inner(*args, **kwargs):
start = time.time()
rets = func(*args, **kwargs)
end = time.time()
if flag == "am":
print(f'{func}运行用时(am):{end - start}s')
elif flag == "pm":
print(f'{func}运行用时(pm):{end - start}s')
return rets

return inner
return outer


@wrapper("am")
def foo(x, y):
time.sleep(2)
return x + y


print(foo(3, 4))

分析:先调用wrapper(“am”) 返回outer,再看@outer,即普通的装饰器,相当于 foo = outer(foo)

3.5.1 装饰器的优缺点和应用场景

  • 优点: 不改变函数代码的前提下,对函数进行增强

  • 缺点:无法一眼看清楚代码,有些人看不懂,嵌套比较多

  • 应用场景:

    • 1、日志记录,无需写login方法就可打印日志
    • 2、权限控制,判断入参有没有某个属性,如果没有就无权限
    • 3、性能优化:
    • 4、数据验证:在输入输出的数据验证和转换方面,装饰器可以实现输入验证、类型转换
    • 5、代码计时:记录这段函数代码运行的花费的时间

3.6 可变长参数的高级用法

首先python是常规函数是严格按照定义的形参传的,如果实参数量大于形参就会报错

  • 【可边长位置参数】*args 如过在最后一个形参前面加星号,那么在调用函数的时候,溢出的位置实参都会被接收下来,且会以元组的形式保存下来赋值给该形参。这里用 *args
1
2
3
4
5
6
def fun(*args):
print(*args)
foo(1, 2, 3, 4, 5, 6, 7)
# output:
1, 2, 3
(4, 5, 6, 7)
  • 【可变长关键字参数】如果在最后一个形参前面加两个星号 那么在调用函数的时候,溢出的关键字仍然都会被接收,且会以字典的形式保存下来赋值给该形参,一般用 **kwargs传递
1
2
3
4
5
6
7
def fun(x,**kwargs):
print(x)
print(kwargs)
foo(1, y=2, z=3) # 溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs
#output:
1
{'z':3, 'y':2}
  • 【补充一下 *args的用法】 将可迭代对象的元素打散传入函数
1
2
3
4
def fun(x,y):
print(x+y)
list1=[1,2]
fun(*list1) # 这里会将list1列表内的两个元素打散传进函数里面去
  • 【补充一下 **的用法】 可以将字典每个key-value拆开为关键字传参
1
2
3
4
5
def fun(x,y):
print(x+y)

nums = {"a":1,"b":2}
fun(**nums)

3.7 yield【重点】

  • 普通函数在被调用的时候,会从上往下依次执行函数代码。遇到return语句时立即退出函数且返回返回值,再次调用这个函数的试试,函数会再次从头到尾执行一遍

  • 函数生成器被调用时,也是会从上往下执行代码,遇到yield语句后,再yield位置处挂起并且返回yield后的数据出来。此函数再次被调用的时候,会从挂起位置处(也就是yield处)再往下执行函数

  • 【函数生成器】函数体包含yield关键字,就是函数生成器

  • 生成器对象,指对象内置有_iter____和 __nex__t方法,所以生成器本身就是一个迭代器,可以被for循环遍历

1
2
3
4
5
6
7
8
9
def my_range(start,end,step=1):
while start< end:
yield start
start+=step
g=my_range(1,5)
print(g) # 会显示g是一个迭代器对象<generator object my_range at 0x7f0ed8a31310>
print(next(g)) # 1
print(next(g)) # 2
print(next(g)) # 3
  • 生成器函数的使用yiled语句实现的,python的yield表达式也很强大。yield不仅可以从函数体内往外取值,还可以从外部往函数体内传值,传值使用generator.send(value)
1
2
3
4
5
6
7
8
9
10
11
12
def eater():
print('Ready to eat')
while True:
food = yield
print('get the food: %s, and start to eat' %food)


g = eater()
next(g) # 传值前必须先调用一次生成器,让生成器先挂起来,等待接收yield赋值给food
g.send('包子') # 通过send方法将数据传给yield赋值给food,生成器内部有循环又会再次被挂起
g.send('牛奶') # send()必须要有一个实参
g.send(None) # 如果send的是None,则默认执行next(g)

3.8 匿名函数

  • 匿名函数lambda 就是不命名的函数,一般用于一次性使用的场景,不属于任何类
  • 定义有名字的函数用def 定义没名字的函数用lambda
  • fun = lambda 入参 : 返回结果 lambda arguments: expression
1
2
3
4
5
6
7
8
9
def fun(x,y):
return x+y
fun2=lambda x,y:x+y # 冒号前面写入参,后面写返回结果
# 示例二
def sq(x):
return x*x
map(sq,[y for y in range(10)]) # map组合,前面是方法,后面是传参
# 使用lambda
map(lambda x:x*x , [y for y in range(10)]) # 配合内置函数用,效果极佳,简洁有效

3.9 内置函数之filter (其实是一个类)

  • filter是一个类,不是一个函数,查看源码发现
  • fulter的入参是两个:function_or_None和iterable可迭代对象
  • 如果function是None的话,就报错
1
2
3
4
5
6
7
8
9
def fun(x):
return x%2==0
list1 = [1,2,3,4,5,6,7,8,9,10,12,142,352,4235,56,57,86,12,11]
a = filter(fun,list1)# 这里获得的是filter对象,要转化成对应的列表
print(list(a)) # [2, 4, 6, 8, 10, 12, 142, 352, 56, 86, 12]
# 用lambda 实现 定义匿名方法
a = filter(lambda x:x%2==0,list1)
print(list(a)) # [2, 4, 6, 8, 10, 12, 142, 352, 56, 86, 12]

3-9.2 内置函数之map

map() 是 Python 的内置函数之一,它用于将一个函数应用于一个或多个可迭代对象(如列表、元组等)的每个元素,并返回一个包含结果的新的迭代器。

map() 函数的工作原理是遍历 iterable 中的每个元素,并将每个元素传递给 function 进行处理。function 将对每个元素执行指定的操作,并返回结果。最后,map() 返回一个新的迭代器,其中包含了经过 function 处理后的每个元素。

1
2
3
4
5
6
# 将列表中的每个元素平方
numbers = [1, 2, 3, 4, 5]
squared_numbers = map(lambda x: x ** 2, numbers)

# 打印结果
print(list(squared_numbers)) # 输出: [1, 4, 9, 16, 25]

3.10 内置函数值enumerate

  • enumerate看源码,入参是一个可迭代对象,和一个默认为0的start起始值,
  • 返回:以元组形式返回enumerate对象,元组内是(下标索引,值)的形式,
  • 可以通过给start传参定义起始位置的索引值
1
2
3
name=['asd','asda','qweq','fgdb']
for index value in enumerate(name,start=0): # 默认下标起始值是0
print(index,value) # (0,'asd'),(1,'asda'),(2,'qweq'),(3,'fgdb'),

3.11 模块的本质

  • 一个python文件就是一个模块,
  • 模块是一堆功能的集合,算是一种代码“封装”的方式
  • 内置模块,第三方模块,自定义模块。其实就是各种文件
1
2
3
4
# python三类模块:
# 内置模块:python解释器自带的模块,直接使用
# 第三方模块:python社区伙伴们开源提供的python模块,需要下载后使用
# 自定义模块:自定义模块指的是我们自己编写的脚本文件,文件名就是模块名,如 get_sum.py,get_sum就是模块名
  • 模块先导入后使用。导入模块的方式:import、from、或两者配合使用-
1
2
3
4
5
6
7
8
9
# 导入模块的方式
- 方式1import module_name
- 方式2from modele_name import model_content1, modele_content2, ...
- 方式3from model_name import *
- 方式4import modele_name as nickname

# 导入位置
- 文件头:文件开头导入的模块属于全局作用域
- 函数内:函数内导入的模块属于局部的作用域
  • 导入模块的本质
1
2
3
4
5
6
7
# 1、产生一个新的名称空间(名字是被导入模块名);
# 2、执行被导入模块源文件代码,将产生的名字存放在新的名称空间中;
# 3、将模块名称空间的名字添加到当前执行文件所的名称空间中

# 补充:
- 导入方式:import ..,在当前执行文件的名称空间中放一个被导入模块的名字,通过这个名字引用模块中的名字。
-导入方式:from .. import..,在当前执行文件的名称空间中直接放一个模块中的名字,不能访问模块的其他名字。
  • 模块的本质可以理解为是一个命名空间(Namespace)
  • 命名空间相互隔离的,这个命名了的空间下面的东西,就只属于这个命名空间
  • import 导入,就是导入了一个命名空间,就是导入了一个py文件

3.12 py文件的两种用途

  • 1、当脚本被执行
  • 2、当模块被导入使用
  • 区别:
    • 脚本文件执行的时候,产品的命名空间会在程序解释后失效回收
    • 模块导入运行的试试,产生的命名空间会在引用计数为零的时候回收释放。
  • 每一个py文件都有一个属性:__name___ 这个不是他的命名空间
  • 当py文件被当做模块导入的时候,__name__ ==”模块名”
  • 所以if __name__=“__main__”时,就意味着该模块是主程序入口

3.13 包的本质

  • 包: 就是一堆模块的集合,那单个模块是单个py文件,那包就是好多文件的集合,就是文件夹,且包含一个.__init__文件
  • 导入包,起始就是导入包内init文件
1
2
3
4
5
6
7
8
# 包是一个包含__init__.py文件的文件夹。package
# 对于普通模块(一个py文件),会发生三件事,其中一件事就是执行模块文件的代码。
# 包是一个文件夹,不能是普通模块那样被执行代码,所以给包提供了一个__init__.py文件,导入包就会执行__init__.py文件,这也是__init__.py文件村的意义。
# python3中,文件夹没有__init__.py也可以,但是在python2中包必须要有该文件。
- tools
__init__.py
calculate.py
- main.py
  • 导入包就是导入包内的__init__.py文件
1
2
3
4
#1 包的导入方式和模块的一样:import 和 from...import..两种
- 无论何种方式,无论任何位置,导入时带点的,点的左边都必须是一个包。
#2 包A和包B下有同名模块也不会冲突,如A.a与B.a来自俩个命名空间
#3 import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件
  • 不管是import包名.模块名 还是from 包名 import模块名,带点的时候,点的前面都是包名

【python】进阶之函数详解(三)
http://example.com/2024/03/18/603python进阶之函数详解(三)/
作者
Wangxiaowang
发布于
2024年3月18日
许可协议