12.Python 函数

目录

    • 1. 普通函数
      • 1.1 定义函数
      • 1.2 调用函数
      • 1.3 认识形参和实参
      • 1.4 位置参数
      • 1.5 关键字参数
      • 1.6 默认参数
      • 1.7 可变参数
      • 1.8 混合参数
      • 1.9 函数的返回值
    • 2. 函数作用域
    • 3. 函数表达式
    • 4. 闭包函数
    • 5. 装饰器函数
    • 6. 生成器函数

函数是Python最基本的程序结构,可以最大限度地实现代码重用,最小化代码冗余,提高程序的模块化,提升程序执行效率。

1. 普通函数

1.1 定义函数

函数结构由首行和代码块两部分组成。使用def语句可以创建一个函数对象,并将其赋值给一个变量。

def <name>([arg1[,arg2,..,argN]):
		<statements>

def hi():
		print('Hi,python')
def no():
		pass

def可以出现在任意语句出现的地方,甚至可以嵌套在其他语句中,如if或while语句中等。

参数放在函数名后面小括号内,多个参数以逗号分隔,函数通过return语句设置返回值。

如果在函数体内第一行使用’''或”””添加注释,则调用函数时自动提示注释信息。

def sum(x,y):
		'''求两个数字的和,参数为数字'''
		return x + y # 返回两个参数之和

1.2 调用函数

在程序中使用小括号调用函数,调用函数由3中形式:

  • 语句调用。hi()
  • 表达式运算。abs(-45)*2
  • 参数传递。print(abs(-45))

函数体内的代码块不会自动执行,只有当调用函数的时候,才会被执行。

函数总是先定义,再调用。在定义阶段,Python 只检测语法,不执行代码。如果在定义阶段发现语法错误,将提示错误,但是不判断逻辑错误,只有在调用函数时,才判断逻辑错误。

1.3 认识形参和实参

形参:在定义函数时,声明的参数变量。作为函数的私有变量,仅能在函数内部可见。

实参:在调用函数时,传入的外部对象。

实参可以分为两种类型:

  • 固定值:实参为不可变对象,如数字、布尔值、字符串、元组。
  • 可变值:实参为可变对象,如列表、字典和集合。

如果是可变实参,则被传递形参后,形参能够影响实参。如果是不可变实参,则被传递形参后,互不影响。

def fn(obj):
		obj += obj

a = 1 
fn(a)
print(a) # 调用函数,传入实际参数变量
b = [1] # 输出不变 1
fn(b) # 调用函数,传入实际参数变量
print(b) # 实际参数变量被修改,输出 [1,1]

1.4 位置参数

Python 默认根据位置映射参数传递。函数的实参与形参是一一映射的关系。

  • 如果没有设置默认参数或可变参数,那么实参与形参的个数必须相同,否则出发异常。
  • 如果没有设置关键字参数或可变参数,那么实参与形参的位置顺序必须对应,否则运行时引发错误。
  • 相同位置的实参与形参的数据类型必须保持一致。

Python 不自动检测函数参数的位置顺序,在自定义函数时,有必要添加参数类型检测的功能,确保实参与形参类型的一致性,避免运行时错误。

def abs(x):
    '''求绝对值,参数必须为数字'''
    if not isinstance(x,(int,float)): # 检测参数类型是否为整数或浮点数
        raise TypeError('参数类型不正确') # 参数类型不对,则抛出异常
    if x >= 0:
        return x
    else:
        return -x
abs('a')  # 参数类型不正确

1.5 关键字参数

Python 也允许键值映射的方式传递参数。在调用函数时,可以直接为形参变量赋值,通过设置关键字实现传值,这样就不用考虑实参与形参的位置关系。

在调用函数时,位置参数在左,关键字参数在右。一旦使用关键字参数后,其后就不能使用位置参数,避免重复为同一个形参赋值。

def comp(x,y):
    if not isinstance(x,(int,float)):
        raise TypeError('参数类型不正确')
    if not isinstance(y,(int,float)):
        raise TypeError('参数类型不正确')
    if x > y:
        return x
    else:
        return y
    
print(comp(x=5,y=9)) # 通过键值映射传参
print(comp(y=5,x=9)) # 通过键值映射传参,不用考虑位置
print(comp(5,y=9)) # x通过位置映射,y通过键值
print(comp(5,x=9)) # x通过键值映射,y就不能通过位置映射了,抛出异常

1.6 默认参数

定义函数,可以为形参设置默认值,默认参数仅赋值一次。当调用函数时,如果没有传入实参,函数将使用默认值;如果传入实参,则使用实参变量的值覆盖掉默认值。

def power(x,n=2):
    '''求次方函数,x为要计算的原值,n为次方数'''
    s = 1 # 临时记录乘积
    while n > 0: # 循环计算原值的乘积
        n = n - 1 # 递减次方的次数
        s = s * x # 累积乘积
    return s # 返回n次方结果

print(power(3)) # n默认值2 输出 9
print(power(3,3)) # n为3 输出27

在定义函数时,如果某个参数的值比较固定,可以考虑为其设置默认值。如果设置了默认值,在形参列表中,位置参数必须位于左侧,默认参数位于右侧,此时形参与实参个数可能不一致。

1.7 可变参数

运算符*和**有多重语义,如下:

  • 算术运算:*代表乘法,**代表乘方。
  • 可变形参:在定义函数时,*args代表任意多个无名的形参,类似一个元组;**kwargs代表任意多个无名的关键字参数,类似一个字典。args和kwargs只是通俗命名约定,可以为任意变量名。这个过程也被称为打包参数。
  • 可变实参:在调用函数时,*args代表将一个序列args转换为同等长度个数的实参,**kwargs代表将一个字典kwargs转换为同等长度个数的实参。这个过程称为解包参数。
  • 序列解包:在赋值运算中,*val代表任意多个元素的列表。如a,*b,c=‘python’,则b等于[‘y’,‘t’,‘h’,‘o’]。

打包参数

当无法确定实参的个数时,可以使用可变形参,进行打包参数。

可变位置参数运算符收集所有位置参数到一个新的元组,并将这个元组赋值给运算符绑定的形参变量。形参变量args表示一个可变位置参数,参数类型为元组,可使用for遍历。

def <name>(*args):
		<statements>

def sum(*nums):
    '''求和函数,参数为可变位置参数'''
    i = 0 # 临时变量
    for n in nums: # 遍历可变参数
        if(isinstance(n,(int,float))): # 如果是整数或浮点数,则进行求和
            i += n # 求和
    return i
print(sum(1,2,3,4)) # 10
print(sum(1,2,3,4.4,'a')) # 10.4

可变关键字参数运算符收集所有的关键字参数到一个新的字典,并将这个字典赋值给运算符绑定的形参变量。形参变量kwargs表示一个可变关键字参数,参数类型为字典,可使用for遍历。

def <name>(**kwargs):
		<statements>

def sum(**nums):
    '''求和函数,参数为可变关键字参数'''
    i = 0 # 临时变量
    temp = [] # 临时列表
    for key,value in nums.items(): # 可变参数字典
        if(isinstance(value,(int,float))): 
            i += value # 把值叠加到临时变量
            temp.append(key) # 把值添加到临时列表
    return (temp,i) 
a = sum(a=1,b=2,c=3,d=4)
print(a) # (['a', 'b', 'c', 'd'], 10)
print(' + '.join(a[0]),'=',a[1]) # a + b + c + d = 10

def dict(**kwargs):
    return kwargs
d = dict(a=1,b=2,c=3,d=4,e=5) 
print(d) # {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}

解包参数

解包可迭代对象:在函数调用中,使用*运算符将元组、列表等可迭代对象解包成按序列排列的位置参数。

解包字典:在函数调用中,使用**运算符将字典解包成按序排列的关键字参数。

def sum1(*nums):
    '''求和函数,参数为可变位置参数'''
    i = 0 # 临时变量
    for n in nums: # 遍历可变参数
        if(isinstance(n,(int,float))): # 如果是整数或浮点数,则进行求和
            i += n # 求和
    return i

a = (2,4,6,8,10) # 元组
print(sum1(*a,2,3,4,1)) # 解包元组对象,40
a = [2,4,6,8,10] # 列表
print(sum1(*a,2,3,4,1)) # 解包列表对象 40
a = {2,4,6,8,10} # 集合
print(sum1(*a,2,3,4,1)) # 解包集合对象 40
a = {2:'a',4:'b',6:'c',8:'d',10:'e'} # 字典
print(sum1(*a,2,3,4,1)) # 解包字典对象,键必须为数字,对键进行求和

def sum2(**nums):
    '''求和函数,参数为可变关键字参数'''
    i = 0 # 临时变量
    temp = [] # 临时列表
    for key,value in nums.items(): # 可变参数字典
        if(isinstance(value,(int,float))): 
            i += value # 把值叠加到临时变量
            temp.append(key) # 把值添加到临时列表
    return (temp,i) 

d = {'a':1,'b':2,'c':3,'d':4} 
a = sum2(**d) # 解包字典
print(' + '.join(a[0]),'=',a[1]) # a + b + c + d = 10

1.8 混合参数

混合使用不同的参数。

在定义函数时,参数位置顺序如下:

  • 默认参数被重置:位置参数>默认参数>可变位置参数>可变关键字参数。
  • 默认参数保持默认:位置参数>可变位置参数>默认参数>可变关键字参数。

在调用函数时,参数位置顺序如下:

  • 位置参数>关键字参数>可变位置参数>可变关键字参数。
  • 位置参数>可变位置参数>关键字参数>可变关键字参数。
  • 可变位置参数>位置参数>关键字参数>可变关键字参数。
  • 位置参数>可变位置参数>可变关键字参数>关键字参数。
  • 可变位置参数>位置参数>可变关键字参数>关键字参数。
def f(*args,**kwargs):
    print('args=',args,end=' ')
    print('kwargs=',kwargs)
print(f(1,2,3,4)) # args= (1, 2, 3, 4) kwargs= {}
print(f(a=1,b=2,c=3)) # args= () kwargs= {'a': 1, 'b': 2, 'c': 3}
print(f(1,2,3,4,a=1,b=2,c=3)) # args= (1, 2, 3, 4) kwargs= {'a': 1, 'b': 2, 'c': 3}
print(f('a',1,None,a=1,b='2',c=3)) # args= ('a', 1, None) kwargs= {'a': 1, 'b': '2', 'c': 3}

1.9 函数的返回值

使用return关键字放回值,一旦执行return命令,函数将停止运行并返回结果。如果没有return语句,执行完函数体内所有代码后,也会返回特殊值(None)。

return [表达式]

def f():
    print(1)
    return
    print(2)
f() # 1

def f(x,y):
    if(x<y):
        return 1
    elif(x>y):
        return -1
    else:
        return 0
print(f(3,4)) # 1

def f():
    return 1,2,3,4
print(f())

函数的返回值可以时任意类型,但是返回值只能是一个值,如果要返回多个值,可以放在列表、元组、字典等容器类对象中再返回。

2. 函数作用域

**作用域(scope)**是指变量在程序中可以被访问的有效范围,也称变量的可见性。是静态的,变量的位置决定了该变量能被访问的范围。

只有函数、类和模块会产生作用域,分为4种类型:

  • 本地作用域(local,L层级):每当函数被调用时都创建一个新的本地作用域,包括def函数和lambda表达式函数。
  • 嵌套作用域(enclosing,E层级):相对于上一层的函数而言,也是本地作用域。
  • 全局作用域(global,G层级):每一个模块都是一个全局作用域。
  • 内置作用域(built-in,B层级):在系统内置模块里定义的变量。
n = 1  # 全局变量
def fn(): # 嵌套作用域
    n = 2 # 本地变量
    print(n)
    def sub(): # 本地作用域
        print(n)
    sub()
fn()
print(n)
# 2
# 2
# 1

作用域优先级:本地作用域>嵌套作用域>全局作用域>内置作用域。

相对于内层作用域的变量而言,外层作用域的变量默认是只读的,不能修改。

使用global关键字,可以在本地作用域中声明全局变量。

n = 1  # 全局变量,初始值 1 
def f(): # 嵌套作用域
    def sub(): # 本地作用域
        global n # 声明全局变量 n
        print(n)
        n = 2 # 修改全局变量n的值为2
    return sub()
f()
print(n)
# 1
# 2

使用nonlocal关键字,可以在本地作用域中声明嵌套作用域变量。

n = 0  # 全局变量,初始值 1 
def f(): # 嵌套作用域
    n = 1 # 嵌套变量,初始值 1
    def sub(): # 本地作用域
        nonlocal n # 声明非本地变量 n
        n = 2 # 修改非本地变量n的值为2
        print(n)
    sub()
    print(n)
f()
# 2
# 2

使用globals()locals()内置函数可以访问全局变量和本地变量:

  • globals():以字典形式返回当前位置的全局变量。
    - locals():以字典形式返回当前位置的全部本地变量。

通过globals()locals()返回的字典可以读、写全局变量和本地变量。

3. 函数表达式

函数表达式是一种简化的函数结构:首行没有函数名和小括号,也称匿名函数;函数主体部分省略了代码块,只包含一个表达式,表达式的运算结果作为函数的返回值,也称函数表达式。

使用lambda运算符,可以代替def命令,以表达式的形式定义函数对象。

fn = lambda [arg1[,arg2,...argN]]:expression
  • [arg1[,arg2,…argN]]:可选参数,定义匿名函数的参数,个数不限,逗号隔开。
  • expression:必选参数,为一个表达式,定义函数主体,并能够访问冒号左侧的参数。
  • fn:表示一个变量,用来接收lambda表达式的返回值,返回值为一个函数对象,通过fn可以调用该函数表达式。

lambda是一个表达式,而不是一条语句,特点有:

  • 与def语法比较,lambda不需要小括号,冒号左侧的值序列表示函数的参数;函数不需要return语句,冒号右侧表达式的运算结果就是返回值。
  • 与def语法比较,lambda能够出现在不允许def出现的地方,如元素必须为简单值列表中,或者函数调用中。
  • 与def语法比较,lambda的结构单一,功能有限,主体上是一个表达式,而不是一个代码块。
  • lambda 会产生一个新的本地作用域,拥有独立的命名空间。
t = lambda:True
t()
# 等价于
def func():return True

sum1 = lambda a,b:a+b # 定义匿名求和函数
print(sum1(10,20)) # 调用函数,输出 30

4. 闭包函数

闭包就是一个在函数调用时产生的、持续存在的上下文活动对象。

典型的闭包时一个嵌套结构的函数。内层函数引用外层函数的变量,同时内层函数又被外界引用,当外层函数被调用后,就形成了闭包,这个外层函数也称闭包函数。

def outer(x): # 外层函数
    def inner(y): # 内层函数
        return x+y # 访问外层函数的参数
    return inner # 通过返回内层函数,实现外部引用
f = outer(5) # 调用外层函数,获取引用内层函数
print(f(6)) # 调用内层函数,原外层函数的参数继续存在,输出 11

闭包的优点:

  • 实现外部访问函数内的变量。函数时独立的作用域,它可以访问外部变量,但外部无法访问内部变量。
  • 避免变量污染。使用global、nonlocal关键字,可以开放函数内部变量,但是容易造成内部变量被外部污染。
  • 使函数变量常驻内存,为可持续保存数据提供便利。

闭包的缺点:

  • 常驻内存会增大内存负担。无节制地滥用闭包,容易造成内存泄露。
  • 破坏函数作用域。闭包函数能够改变外层函数内变量的值。
  • 不确定性强。
def outer(x): # 外层函数,闭包体
    def inner(): # 内层函数
        print(x)
    return inner  # 返回内层函数
func = outer(1) # 调用外层函数
print(func.__closure__) # 访问闭包体,输出 (,)
func() # 调用内层函数,输出1

使用lambda表达式设计内层函数和定义闭包结构。

def demo(n): # 定义外层函数
    return lambda s:s**n  # 使用lambda表达式,并返回结果
a = demo(2) # 调用外层函数,获取引用内层函数
print(a(8)) # 调用内层函数,返回 64

demo = lambda n:lambda s:s**n
a = demo(2) # 调用外层函数,获取引用内层函数
print(a(8)) # 调用内层函数,返回 64

5. 装饰器函数

装饰器是一个以函数作为参数,并返回一个函数的函数。装饰器可以为一个函数在不需要做任何代码变动的前提下增加额外功能,如日志、计时、测试、事务、缓存、权限校验等。

应用装饰器的语法:以@开头,接着是装饰器函数的名称和可选的参数,然后是被装饰器的函数。

@decorator(dec_opt_args)
def func_decorated(func_opt_args):
		pass

其中,decorator表示装饰器函数,dec_opt_args为装饰器可选的参数,func_decorated表示被装饰的函数,func_opt_args表示被装饰的函数的参数。

def logging(func): # 日志处理函数
    print('%s is running'%func.__name__) # 打印当前函数正在执行
    func() # 调用参数函数
def foo(): # 业务函数
    print('I am foo')
logging(foo) # 添加日志处理功能
# foo is running
# I am foo

def logging(func):
    def sub(): # 嵌套函数
        print('%s is running'%func.__name__) 
        func()
    return sub # 返回嵌套函数
@logging # 应用装饰器
def foo(): # 业务函数
    print('I am foo')
foo() # 调用业务函数
# foo is running
# I am foo

6. 生成器函数

能够实现一边迭代一边生成元素的机制,称为生成器(generator)。

生成器是一种迭代器,用于按需生成元素,自动实现了迭代器协议,不需要手动实现。

创建生成器的3种方法:

  • 生成器推导式:把一个列表推导式的[]改成(),就创建了一个生成器。
  • 生成器函数:包含yield关键字的Python函数。
  • 生成器工厂函数:返回生成器的函数,函数体内可以没有yield关键字
l = [x*x for x in range(10)] # 列表推导式
g = (x*x for x in range(10)) # 生成器推导式
print(l)  # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(g) #  at 0x7fe94864f5f0>

访问生成器的2种方法:

  • 使用next方法访问下一个值,包括两种方式:通过调用生成器的 generator.next()魔术方法,直接调用next(generator)函数。
  • 使用for循环迭代生成器对象,实际上就是重复调用next()方法,读取所有元素值。
g = (x*x for x in range(10))
next(g)
g.__next__()
for i in g:
    print(i)

函数包括yield关键字,就不再是普通函数,而是生成器函数。调用生成器函数将创建一个生成器对象。

yield 命令内部实现了迭代器协议,同时支持一个状态机,维护者挂起和继续的状态。在挂起状态时,自动保存状态,即生成器函数的本地变量将持续存在,并完整保存这些状态信息;在继续状态时,确保这些信息再度有效,能够恢复函数在挂起的位置继续运行。

yield能够返回一个生成器,访问生成器能够返回yield关键字后面跟随的表达式的值。

def foo(n=1):
    print('starting...') 
    while True: # 无限循环
        res = yield n # 定义生成器的返回值
        n += 1 # 递加参数变量
        print('yield表达式的值:',res) # 打印yield表达式的值
g = foo() # 调用生成器函数,返回生成器对象
# 第一次执行
print(next(g))
'''
starting...
1
'''
#  第二次执行
print(next(g))
'''
yield表达式的值: None
2
'''

在函数体内,yield与return语句功能类似,都能够返回一个值。不同点:return返回值之后,立即结束函数的执行;而yield不会结束函数的运行,只是挂起函数,并记住当前状态。
生成器函数也允许使用return语句,用来终止继续生成值。

生成器对象拥有一个send()方法,能够向生成器函数内部投射一个值,覆盖yield表达式的值。在默认情况下,yield表达式的值总为None。

send()方法提供了一种调用者与生成器之间进行通信的方式,方便调用者随时干预生成器的运算。

def down(n): # 生成器函数
    while n >= 0: 
        m = yield n  # 定义每次迭代生成的值并返回
        if m:
            n = m
        else:
            n -= 1
d = down(5) # 调用生成器函数
for i in d:
    print(i,end=' ')
    if i == 5: # 当打印完第一个元素后
        d.send(3) # 修改yield表达式的值为3
# 5 2 1 0 
# 如果不设置 if i == 5:d.send(3),则输出5 4 3 2 1 0

生成斐波那契数列

def fib(max): # 生成器函数
    n,a,b = 0,0,1  # 初始化
    while n < max: 
        yield b # 返回变量b的值
        a,b = b,a+b # 多重赋值
        n = n+1 # 递增值
    return 'done' 

g = fib(6) # 创建生成器大小
while True:
    try:
        x = next(g) # 读取下一个元素的值
        print(x,end=' ')
    except StopIteration as e: # 不捕获异常
        print(e.value)
        break

# 输出:1 1 2 3 5 8 done

你可能感兴趣的:(Python,python,开发语言)