05---python面向过程的编程---函数
大纲
⭕在解决一个复杂问题的时候,顺序式编程在可读性和代码复用性方面的体验感很差,可以采用面向过程的编程。
掌握函数的定义和调用
掌握位置参数、关键字参数和参数默认值的使用
掌握参数返回值的定义和使用
一、函数的定义及调用 (一)为什么要用函数 (二)创建和调用函数
整体结构:输入---处理---输出
1、函数的定义
def 函数名(参数):
函数体
返回值
2、三要素:参数、函数体、返回值 3、函数的调用
# 求正方形的面积
def fun(side):area = pow(side, 2)return area area = fun(5)
area # 25
(三)参数 1、形参与实参
形参(缺省参数、不定长参数):函数定义时的参数---变量名
实参(位置参数、关键字参数):函数调用时的参数---变量的值
2、位置参数
严格按照位置顺序,用实参对形参进行赋值(关联)
实参与形参个数必须一一对应,一个不能多,一个不能少
一般用在参数比较少的时候
def fun(x, y, z):print(x, y, z)fun(1, 2, 3) # x = 1; y = 2; z = 3
⭕在/左边必须是位置参数
3、关键字参数
打破位置限制,以形参 = 实参的形式进行值的传递
实参与形参数量上一一对应
多用在参数比较多的场合
def fun(x, y, z):print(x, y, z)fun(y=1, z=2, x=3) # x = 3; y = 1; z = 2
位置参数可以与关键字参数混合使用,位置参数必须放在关键字参数前面
不能为同一个形参重复传值
fun(1, z=2, y=3) # 1 3 2
fun(1, z=2, x=3) # 报错,重复给x赋值
⭕*左边可以是任意类型的参数,右边必须是关键字参数(不然会被打包)
4、默认参数
在定义阶段就给形参赋值
默认参数必须放在非默认参数后面
调用函数时,可以不对该形参传值;也可以按正常的形参进行传值,会覆盖默认值
默认参数赋值等号两侧不需加空格
def stu(name, age, sex="male"):print(name, age, sex)stu("努力君", 24) # 努力君 24 male
stu("幽默君", 25, "female") # 幽默君 25 female
⚠️默认参数应该设置为不可变类型(数字、字符串、元组)
让参数变成可选的
def name(first, last, middle=None):if middle:return first+middle+lastelse:return first+lastprint(name("马", "梅")) # 马梅
print(name("马", "梅", "冬")) # 马冬梅
5、元组型不定长参数*args(元组的打包与解包)
不知道会传过来多少参数,已知数量的形参正常赋值,多余的传给*args
不知道会传过来多少参数,*args在前,后面的参数得用关键字参数,这里可以解释为什么*左边可以是任意类型的参数,右边必须是关键字参数
def fun(x, y, z, *args):print(x, y ,z)print(args)fun(1, 2, 3, 4, 5, 6) # 多余的参数,打包传递给args
'''
1 2 3
(4, 5, 6)
'''
也可以将实参打散,使用*对其进行解包
args = (1, 2, 3, 4)
def fun(a, b, c, d):print(a, b, c ,d)
fun(args) # 报错
fun(*agrs) # 解包,传四个参数 1 2 3 4
6、字典型不定长参数 ** 将关键字参数变为键值对
def fun(x, y, z, **kwargs):print(x, y ,z)print(kwargs)fun(1, 2, 3, a=4, b=5, c=6) # 多余的参数,以字典的形式打包传递给kwargs
'''
1 2 3
{'a': 4, 'b': 5, 'c': 6}
'''
⭕解包操作要求内容最少为四个,得到键值
def myfunc(a,b,c,d):print(a,b,c,d)kwargs = {'a':1,'b':2,'c':3,'d':4}
myfunc(**kwargs) # 1 2 3 4
⭕可变长参数的组合使用
def fun(*args, **kwargs):print(args)print(kwargs)fun(1, 2, 3, a=4, b=5, c=6)
'''
(1, 2, 3)
{'a': 4, 'b': 5, 'c': 6}
'''
(四)函数体与变量作用域范围 1、函数体 2、变量
局部变量:仅在函数体内定义和发挥作用
在函数体中定义的变量,在函数执行完毕,局部变量就已经被释放掉了,不能再使用
全局变量:外部定义的都是全局变量,整个代码中都能使用
在函数外面定义的全局变量在函数中不能进行修改,否则系统会自动创建一个同名的函数变量进行覆盖,不会改变全局变量
如何在函数体里定义并修改全局变量???
变量 (声明该函数内的变量为全局变量)
全局变量是否为可变类型,不可变要申明为
def add(x, y):global zz = x+yreturn z print(add(2, 9))
print(z) # 11,可以正常使用
(五)返回值 1、单个返回值 2、以元组的形式返回多个值
返回值以逗号分开,打包返回,可以解包赋值
def fun(x):return 1, x, x**2, x**3 # 逗号分开,打包返回a, b , c, d = fun(3) # 解包赋值
print("a={},b={},c={},d={}".format(a,b,c,d)) # a=1,b=3,c=9,d=27
3、没有语句,函数返回值为None
def fun():print("我是盾山狂热爱好者")bool1 = fun() # 我是盾山狂热爱好者
print(bool1) # None
⚠️可以有多个 语句,一旦其中一个执行,代表了函数运行的结束
(六)引用与引用传参
在中,值是靠引用来传递来的。 我们可以用id()来判断两个变量是否为同一个值的引用。 我们可以将id值理解为变量在计算机中的内存地址标示
1、不可变数据类型数值的引用
a = 1
b = a
id(a) # 输出:13033816
id(b) # 输出:13033816, 注意两个变量的id值相同 a = 2
id(a) # 输出:13033792, 注意a的id值已经变了
id(b) # 输出:13033816, b的id值依旧
2、可变数据类型列表的引用
a = [1, 2]
b = a
id(a) # 输出:139935018544808
id(b) # 输出:139935018544808, 注意两个变量的id值相同 a.append(3)
print(a) # 输出:[1, 2, 3]
id(a) # 输出:139935018544808, 注意a与b始终指向同一个地址
3、数据类型可变还是不可变的本质
⭕在改变数据具体的值时,其内存的id地址索引是否改变?变的话当前数据为不可变数据类型,不变的话则为可变数据类型
4、引用传参
中函数参数是引用传递(注意不是值传递)。因此,可变类型与不可变类型的变量分别作为函数参数时,产生的影响是不同的。具体来说,对于不可变类型,因变量不能修改,所以函数中的运算不会影响到变量自身;而对于可变类型来说,函数体中的运算有可能会更改传入的参数变量。
a = [1,2,3]
def add(arr):arr += arradd(a)
print(a)
输出的结果为[1,2,3,1,2,3]。即传入是实参是可变数据类型列表,且通过函数改变了原本的值。
a = [1,2,3]
def add(arr):arr = arr + arr # 创建了一个新的局部变量aadd(a)
print(a)
输出的结果为[1,2,3]。即传入是实参是可变数据类型列表,但是没有通过函数改变了原本的值
⭕主要原因是 ‘=’ 和 ‘+=’ 的区别:‘=’ 的使用会直接创建一个新的变量,而 ‘+=’ 则是直接对原变量进行操作
二、关于函数的建议 (一)命名格式
字母小写及下划线组合
有实际意义,见明知意
⚠️保留字(关键字)不能作为函数名
(二)对于函数功能,复杂函数要有注释 (三)函数定义前后各空两行
def f1():# 空出两行
def f2():def f3():
三、函数高阶用法 (一)递归函数 1、递归函数与迭代的对比
让递归正常工作一定要有结束条件
# 用迭代与递归的方式求阶乘
def fun0(n):result = 1for i in range(1,n+1):result *= ireturn result
fun0(5) # 120def fun1(n):if i == 1:return 1else:return n * fun1(n-1)
fun1(5) # 120
# 斐波那契数列
def fun2(n):a = 1b = 1c = 1while n > 2:c = a+ba = bb = cn -= 1return c
fun2(12)def fun3(n):if n == 1 or n == 2:return 1else:return fun3(n-1)+fun3(n-2)
fun3(12)
2、汉诺塔游戏
规则
def hanoi(n, x, y, z):if n == 1:print(x, '-->', z) # 只有一层,直接从x移动到zelse:hanoi(n-1, x, z, y) # 将x上的n-1个金片移动到yprint(x, '-->', z) # 将最底下的金片从x移动到zhanoi(n-1, y, x, z) # 将y上的n-1个金片移动到zn = int(input("请输入汉诺塔层数:"))
hanoi(n,'A','B','C') # 最终A-->C
(二)函数的嵌套
⭕在内部函数想要改变外部函数的值,可以用进行声明
(三)闭包(工厂函数)
def funA():x=880def funB():print(x)funB()
# funB() # 要通过外部函数funA来调用函数funB
funA() # 880
⭕如何不通过funA(),就可以使用funB()?
def funA(): # 嵌套函数的外层作用域会被保存x=880def funB():print(x)return funB # 函数只有在定义与调用的时候才需要加小括号
# funA() # .funB()> 得到的是funB函数的引用
# funA()() # 880,返回的是funB然后加小括号
funny = funA() # 变量funny指向内部函数,并且知道外层函数传递的值
funny() # 880
def power(exp):def exp_of(base):return base**expreturn exp_of # 返回内部函数名,在主程序中通过power函数将其赋值给某一变量,再传值调用exp_of函数squre = power(2) # square变量指向的exp_of函数记住了外层函数power的值exp
cube = power(3)
squre(2) # 4
squre(5) # 25
cube(2) # 8
cube(5) # 125
⭕现在已经知道闭包的原理---嵌套函数的外层作用域具有记忆能力,让数据保存在外层函数的参数或变量中,然后将内层函数作为返回值返回(这样就可以间接的从外部函数调用内部函数),那么再了解一下闭包与关键字的配合使用
def out():x = 0y = 0def inner(x1, y1):nonlocal x, yx += x1y += y1print(f"现在,x = {x}, y = {y}") # 在字符串前面加f,可以直接把值传进字符串内部的变量return innermove = out() # 现在变量move指向inner函数,并且记忆x、y的值
move(1,2)
move(-2,2)
'''
现在,x = 1, y = 2
现在,x = -1, y = 4
'''
小示例:将角色移动保护起来,不希望别的函数调用它
# 角色移动函数
origin = (0, 0) # 定义原点
legal_x = [-100, 100] # 限定坐标轴范围
legal_y = [-100, 100]def create(pos_x=0, pos_y=0):def moving(direction, step): # [1,0]向右、[-1,0]向左,y轴置0;[0,1]向上[0,-1]向下,x轴置0nonlocal pos_x, pos_ynew_x = pos_x+direction[0]*stepnew_y = pos_y+direction[1]*step# 超出范围的部分会反弹回来if new_x < legal_x[0]:pos_x = legal_x[0]-(new_x-legal_x[0])elif new_x > legal_x[1]:pos_x = legal_x[1]-(new_x-legal_x[1])else:pos_x = new_xif new_y < legal_y[0]:pos_y = legal_y[0]-(new_y-legal_y[0])elif pos_y > legal_y[1]:pos_y = legal_y[1]-(new_y-legal_y[1])else:pos_y = new_yreturn pos_x, pos_yreturn movingmove = create()
print("向右移动120步后,位置是:",move([1,0],20))
print("向右下角移动20步,位置是:",move([1,-1],20))
'''
向右移动120步后,位置是: (20, 0)
向右下角移动20步,位置是: (40, -20)
'''
(四)装饰器
将函数作为参数传入另一个函数
函数名加括号表示一个函数的引用
def myfunc():print("myfunc函数")def report(func):print("开始")func()print("结束")report(myfunc)
'''
开始
myfunc函数
结束
'''
时间管理大师
import timedef time_master(fun):print("程序开始")start = time.time()fun()end = time.time()print("程序结束")print(f"一共耗费时间为:{(end-start):.2f}秒。")def func():time.sleep(2)print("Hello FishC")time_master(func)
'''
程序开始
Hello FishC
程序结束
一共耗费时间为:2.00秒。
'''
在上面需要显式的去调用函数,那么如何通过别的方式在我调用func的时候自动调用函数呢?
import timedef time_master(fun):def call_fun():print("程序开始")start = time.time()fun()end = time.time()print("程序结束")print(f"一共耗费时间为:{(end-start):.2f}")return call_fun@time_master # 把func函数塞到装饰器的参数中
def func():time.sleep(2)print("Hello World!")func() # 相当于在没有语法糖的情况下 func = time_master(func) func()
'''
程序开始
Hello World!
程序结束
一共耗费时间为:2.02
'''
⭕同时使用多个装饰器的调用顺序是从下往上依次调用
def add(func):def inner():x = func()return x+1 # 65return innerdef cube(func):def inner():x = func()return x * x * x # 64return innerdef square(func):def inner():x = func()return x * x # 4return inner@add
@cube
@square # 调用最近的
def test():return 2print(test()) # 65
⭕给装饰器也可以传递参数,实际上就是添加一次调用,通过这次调用将参数传递进去
import timedef logger(msg):def time_master(func):def call_fun():print("程序开始")start = time.time()func()end = time.time()print("程序结束")print(f"一共耗费时间为:{(end-start):.2f}")return call_funreturn time_masterdef funA():time.sleep(1)print("正在调用funA……")def funB():time.sleep(1)print("正在调用funB……")funA = logger(msg="A")(funA) # 第一次调用把参数扔进去,第二次调用把函数扔进去
funB = logger(msg="B")(funB)funA()
funB()
# 装饰器的本质:funA = logger(msg = "A")(funA)
# logger(msg = "A")返回的是time_master,需要再加小括号调用call_fun(),返回call_fun
import timedef logger(msg):def time_master(func):def call_fun():print("程序开始")start = time.time()func()end = time.time()print("程序结束")print(f"一共耗费时间为:{(end-start):.2f}")return call_funreturn time_master@logger(msg='A')
def funA():time.sleep(1)print("正在调用funA……")@logger(msg='B')
def funB():time.sleep(1)print("正在调用funB……")funA()
funB()
'''
程序开始
正在调用funA……
程序结束
一共耗费时间为:1.01
程序开始
正在调用funB……
程序结束
一共耗费时间为:1.00
'''
(五)匿名函数---函数的引用(表达式)
⭕匿名函数本身是用关键字创建的小型函数,在定义时省略def函数声明
变量: 函数体
# 求平方
def square(x):return x*x
square(3)square_y = lambda y:y*y # square_y是一个函数的引用
square_y(4)
# (y)>
⚠️表达式虽然是函数的引用,但是可以用在函数不能用的地方,比如将其作为列表参数
y = [lambda x:x*x, 3, 4, 5]
y[0] # 这是一个函数的引用
y[0](y[1]) # 9
mapped = map(lambda x:ord(x),"FishC")
list(mapped) # [70, 105, 115, 104, 67]def fun(x):return ord(x)
list(map(fun,"FishC")) # [70, 105, 115, 104, 67]
最适合使用在参数列表中,尤其是与key = 搭配
1、排序sort()、()
ls = [(93, 88), (79, 100), (86, 71), (85, 85), (76, 94)]
ls.sort() # 对第一个参数进行升序排序
ls
# [(76, 94), (79, 100), (85, 85), (86, 71), (93, 88)]ls.sort(key = lambda x: x[1]) # 改为根据第二个参数进行排序
ls
# [(86, 71), (85, 85), (93, 88), (76, 94), (79, 100)]ls = [(93, 88), (79, 100), (86, 71), (85, 85), (76, 94)]
temp = sorted(ls, key = lambda x: x[0]+x[1], reverse=True)
temp
# [(93, 88), (79, 100), (85, 85), (76, 94), (86, 71)]
2、 最大最小值max()、min()
ls = [(93, 88), (79, 100), (86, 71), (85, 85), (76, 94)]
n = max(ls, key = lambda x: x[1])
n
# (79, 100)n = min(ls, key = lambda x: x[1])
# (86, 71)
⚠️在中,表达式用于处理简单的计算(不用考虑函数名是什么),def定义函数用于处理复杂的工作
(六)()过滤器 1、描述
() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表
该函数接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判断,然后返回 True 或 False,最后将返回 True 的元素放到新列表中。
2、语法
filter(function, # 判断函数iterable) # 迭代对象
3、实例
过滤出1~100中平方根是整数的数
import math
def is_sqr(x):return math.sqrt(x) % 1 == 0newlist = filter(is_sqr, range(1, 101))
print(newlist) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
4、利用()和表达式
求100内3的倍数
p = list(filter(lambda x:True if x % 3 == 0 else False, range(100)))
print(p)
# [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99]
(七)生成器
如何在函数退出后仍保留状态?
1、闭包:定义相对复杂 2、全局变量:过多使用会污染命名空间 3、生成器:既简单又安全的方式
每调用一次提供一个数据,并且会记住当时的状态。列表、元组这些可迭代对象则是容器,容器里存放的是早已准备好的所有数据
def counter(): # 定义一个counter生成器i = 0while i <= 5:yield i i += 1counter() # 得到一个生成器对象for i in counter(): # counter()在每次调用时提供一个数据print(i)c = counter() # 这样c就是counter生成器
print(c)next(c)
next(c)
next(c)
next(c)
next(c)
next(c)
next(c) # 在输出所有数据后会抛出异常StopIteration
生成器表达式:利用推导的形式实现生成器(与列表表达式很像)
(i**2 for i in range(10)) # 这是一个生成器对象
t = (i**2 for i in range(10))
# next(t)
# next(t)
# next(t)
for i in t:print(i, end=" ")
# 0 1 4 9 16 25 36 49 64 81
4、案例:斐波那契数列
# 斐波那契数列
def fib():back1, back2 = 0, 1while True:yield back1back1, back2 = back2, back1+back2f = fib()for i in f:print(i, end=" ")if i > 1000:break
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597
(八)函数文档、类型注释、内省 1、函数文档
通过help()函数可以了解别的函数的功能介绍,也可以自己写函数文档来帮助别人了解自己写的函数
def exchange(dollar,rate=6.32):'''功能:汇率转换,美元--->人民币参数:- dollar 美元数量- rate 汇率,默认值是6.32(2022.11.3)返回值:- 人民币数量 '''return dollar*rate
exchange(20)
help(exchange) # 可以访问自己写的函数文档
2、类型注释
类型注释是函数的创建者在形参列表中添加希望调用者使用的参数类型,同时也给定了返回值的参数类型
⚠️如果说调用者不想按照创建者的意思来,当然也是可以的
def times(s: str, n: int) -> str: # times(s: str = "FishC", n: int = 3) 可以给定默认值return s*ndef times(s: list, n: int = 3) -> list: # 期望是列表类型return s*ndef times(s: list[int], n: int = 3) -> list: # 期望是整数列表类型return s*ndef times(s: dict[str,int], n: int = 3) -> list: # 期望输入是字典类型return list(s.keys())*n # 输出键
推荐使用第三方模块Mypy
3、内省
是通过特殊的属性来实现内省的
# 对于上面的函数文档的访问
print(exchange.__doc__) # 打印函数文档# 对于类型注释
times.__name__ # 查询函数名字
times.__annotations__ # 以字典的形式打印类型注释{'s': str, 'n': int, 'return': str}
(九)高阶函数 1、定义
当一个函数把另一个函数作为变量的时候,那么该函数就是一个高阶函数
2、 模块:高阶函数和装饰器
import functoolsdef add(x,y):return x+y
functools.reduce(add,[1,2,3,4,5]) # 将可迭代对象的数据一个一个传进前面的函数,进行求和functools.reduce(lambda x,y:x*y,range(1,11)) # 3628800
3、偏函数
对指定的函数进行二次包装,将现有的函数部分参数预先进行绑定,从而得到新的函数
这里是对pow的指数参数进行绑定
import functools
square = functools.partial(pow,exp=2)
square(2) # 4
4、@wraps装饰器
用来装饰装饰器
装饰器的副作用:通过内省后发现,在调用时是函数的引用
import time
import functools
def time_master(fun):@functools.wraps(func)def call_fun():print("程序开始")start = time.time()fun()end = time.time()print("程序结束")print(f"一共耗费时间为:{(end-start):.2f}秒。")return call_fundef func():time.sleep(2)print("Hello FishC")myfun = time_master(func)
myfun()# myfun.__name__ call_fun
myfun.__name__ # func,避免了装饰器的副作用
四、LEGB规则
⭕不能对关键字进行赋值,会改变当前使用的规则
L:Local 局部作用域(需要用声明)
E: 嵌套函数的外层作用域(局部作用域会覆盖外层作用域,除非用声明)
G: 全局作用域
B: 内置作用
⭕总结
面向过程——以过程为中心的编程思想,是一种程序化的编程
面向对象——以人中心的编程思想,是一种拟人化的编程,编程逻辑更符合人类的思想
欢迎点赞 收藏 ⭐留言 如有错误敬请指正!