Python的函数与生成器

3、函数与生成器

3.1 函数基础

函数是一种将一组语句打包在一起的机制,使得这些语句可以在程序中多次运行。
它是一种通过名称调用的封装过程。函数还可以计算结果值,并允许我们指定不同的参数作为函数输入。

# 定义一个函数来验证用户名和密码
def validate_user(username, password):
    # 检查用户名是否长度合适
    if len(username) < 3:
        return "用户名太短,至少需要3个字符"
   
    # 检查密码长度
    if len(password) < 8:
        return "密码太短,至少需要8个字符"

    # 如果所有验证都通过
    return "验证通过"

# 调用函数验证用户输入
result1 = validate_user("me", "pass1234")
result2 = validate_user("validuser", "password123")

print(result1)  # 用户名太短,至少需要3个字符
print(result2)  # 验证通过

如果我们不使用函数,则在需要验证用户输入的用户名和密码长度的时候,需要将这一段代码复制多次,函数是替代通过剪切和粘贴编程的方式——与其在代码中拥有多个操作的冗余副本,我们可以将其封装成一个单一的函数。这样做,我们可以大幅减少未来的工作量:如果后续需要更改操作,我们只需在函数中更新一次,而不需要在程序的多个地方进行修改。

username = "user"
password = "passwd123"

# 检查用户名长度
if len(username) < 3:
    print("用户名太短,至少需要3个字符")
else:
    # 检查密码长度
    if len(password) < 8:
        print("密码太短,至少需要8个字符")
    else:
        # 如果所有验证都通过
        print("验证通过")

与函数有关的语句和表达式:

语句/表达式

作用

举例

call

调用

myfunc('spam', 'eggs', meat=ham, *rest)

def

定义函数

def printer(messge):print('Hello ' + message)

return

返回

def adder(a, b=1, *c):return a + b + c[0]

global

全局变量

x = 'old';def changer():global x; x = 'new'

nonlocal

非局部变量

def outer():x = 'old';def changer():nonlocal x; x = 'new'

yield

生成器函数

def squares(x):for i in range(x): yield i ** 2

lambda

匿名函数

funcs = [lambda x: x2, lambda x: x3]

3.2

函数的优点

最大化代码重用和最小化代码冗余:

Python中的函数是将你可能希望在多个地方多次使用的逻辑打包的最简单方式。到目前为止,我们编写的所有代码都是立即执行的。函数允许我们对代码进行分组和泛化,以便后续任意多次使用。因为它们允许我们在一个地方编写操作并在多个地方使用它,所以Python函数是该语言中最基本的分解工具:它们允许我们减少程序中的代码冗余,从而减少维护工作。

简化复杂任务与系统分解的关键工具:

函数还提供了一种工具,用于将系统分解成具有明确角色的部分。例如,从头开始制作比萨饼,你会先从和面开始,然后擀平,添加配料,烘烤等等。如果你在编写一个制作比萨的机器人程序,函数可以帮助你将整体的制作比萨任务分解成块——每个过程中的子任务一个函数。单独实现这些较小的任务比一次性实现整个过程要容易。一般来说,函数是关于程序的——如何做某事,而不是你正在做什么。

 

3.3 函数的基本概念

def是可执行语句:我们使用def来定义函数,在Python还没有执行到def语句时,我们定义的函数还没有被创建。

a = myfunc()   #引用的函数还没有创建,cpython中会报错
print(a)
def myfunc():  #运行def语句之后才创建函数对象
    return "hello"

#在 IPython 中,你可能会发现你可以先调用函数,然后再定义它。这是因为 IPython 的交互式环境允许你在运行时定义和修改代码。
#上面的例子正常工作,因为实际上是先执行了 def 语句定义了函数,然后再调用函数。

def创建一个函数对象并为其赋值一个函数名:当Python执行def语句之后,会创建一个函数对象,并将函数名成为这个函数对象的引用(和其他变量赋值一样)。这个函数对象也可以附加任意用户定义的属性来记录数据。

def myfunc():
    return "hello"

a = myfunc
a.data = 100
print(a.data)

lambda函数创建一个对象并直接将结果返回:函数也可以通过 lambda 表达式创建,lambda通常用于编写小段代码,通常用于需要函数对象的地方,但不想使用完整 def 语句定义一个正式的函数。

multiply = lambda x, y: x * y

result = multiply(5, 3)
print(result)  # 输出: 15

 

return返回一个结果对象给调用者(caller:当函数被调用时,调用者会停止执行,直到函数完成其工作并将控制权返回给调用者。计算值的函数通过 return语句将其值发送回调用者;返回的值成为函数调用的结果。如果return不返回如何值,则默认返回None

def myfunc():
    print("hello")

a = myfunc()
print(a)

yield将结果对象发送回调用者,但会记住它停止的位置:被称为生成器的函数也可以使用 yield 语句来发送回一个值并暂停它们的执行状态,以便稍后可以恢复执行,从而按照需求产生一系列结果。

def myfunc():
    for x in range(10):
        yield x

a = myfunc()
print(a)

print(next(a))  #依次生成值

list(a)  #使用list将生成器转换成列表

global 声明了要在模块级别(全局)分配的变量:默认情况下,函数中分配的所有名称都是局部的,仅在函数运行时存在。要分配全局变量,函数需要使用global 语句申明它。通常来说,名称总是在作用域中查找————即存储变量的地方————而赋值会将名称绑定到作用域。

x = 10
def myfunc():
    global x
    x = 1

a = myfunc()
x

nonlocal 声明用于指定要在外部函数中赋值的变量nonlocal 允许内层函数修改外层函数中的变量(而不是全局变量)。

def myfunc_outer():
    x = 10  # 外部函数的变量

    def myfunc_inner():
        nonlocal x  # 声明x是外部函数中的变量
        x += 5  # 修改外部函数中的变量
        print(f"Inner function: x = {x}")

    myfunc_inner()
    print(f"Outer function: x = {x}")
#在内部函数修改会影响外部函数的x值
myfunc_outer()

参数通过赋值(对象引用)传递:在 Python 中,参数通过赋值传递给函数,调用者和函数通过引用共享对象,在函数内改变参数的名称不会改变调用者中的对应名称,但如果在函数内修改传入的可变对象,则会改变调用者共享的对象,并且这些修改可以作为函数的结果。

x = 10
def myfunc(arg):
    return arg + 10

a = myfunc(x)
print(x)
print(a)

参数通过位置传递(除非指定其他传递方式):在函数调用中传递的值默认会根据函数定义中的参数名称从左到右进行匹配。为了灵活性,函数调用也可以通过名称关键字语法按名称传递参数,并且可以使用 *pargs **kargs 的星号参数表示法解包任意多的参数进行传递。函数定义使用相同的两种形式来指定参数默认值,并收集接收到的任意多的参数。

def myfunc(arg1, arg2, arg3):
    return f'这是第一个参数{arg1},这是第二个参数{arg2},这是第三个参数{arg3}。'

print(myfunc(1,2,3))
print(myfunc(3,2,1))

参数、返回值和变量都不需要声明:就像Python中的其他变量一样,函数没有类型约束。事实上,函数的任何内容都不需要提前声明:你可以传入任何类型的参数,返回任何类型的对象等等。因此,一个单一的函数往往可以应用于各种对象类型(多态性)——只要对象具有兼容的接口(方法和表达式),无论它们的具体类型是什么,都可以使用。

def myff():
    1+2

a = myff()
print(a)

函数的定义

def 语句的基本形式:也是以首行+冒号(:),如何缩进代码块的形式组成。在首行中定义函数名,在括号中包含参数,如果需要返回值的话,可以使用return语句,

def name(arg1, arg2,... argN):   #name为函数名,括号中为参数
    statements                   #函数的语句
    return value                 #要返回的值,如果使用yield则为生成器函数

在运行时定义函数:

if test:
    def func():            # 如果满足if测试条件则定义一个函数
        ...
else:
    def func():            # 否则以另一种方式定义函数
        ...
...
func()                     # 调用函数

函数对象赋值

othername = func           # 赋值函数对象
othername()                # 调用函数

3.4 函数定义和调用的简单例子

#函数的定义
def times(x, y):   #定义一个名为times的乘法函数,接收两个参数x,y
    return x * y   #返回x*y的值

#函数的调用
times(2, 4)   #传入两个参数2,4

x = times(3.14, 4)     # 将结果保存为x
x

函数的多态性

由于运算符*Python中具有多态性,即对不同的对象具有不同的函数,*对于数值对象为加法,对于字符串对象为乘法,所有上面例子中的函数也具有多态性,多态性在Python中为普遍的概念。

times("hello",10)

3.5 第二个例子:序列交集

将代码封装为函数,有以下优点:
1、可以后续的程序中多次调用
2、可以传入不同的参数,这个函数适用于想要求交集的序列对象
3、后期需要维护代码的时候,只需要修改函数的定义部分即可
4、如果其他的Python程序想要使用这个函数,只需要import(导入)这个模块即可

定义

def intersect(seq1, seq2):
    res = []                     # 创建一个空列表,用于收集结果
    for x in seq1:               # 遍历seq1
        if x in seq2:            # 如果这个元素存在seq2中
            res.append(x)        # 则将这个元素添加到结果中
    return res

调用

s1 = "SPAM"    #求字符串的交集
s2 = "SCAM"
intersect(s1, s2)

多态性

x = intersect([1, 2, 3], (1, 4))     #求元组的交集
x

第一次调用时,我们传入了两个字符串对象,第二次调用时,我们传入了两个元组对象,我们在定义函数时没有指定参数的类型,所以我们定义的intersect函数可以通过遍历这个序列对象,来计算两个对象的交集,只有这两个对象支持迭代遍历(for循环)。

局部变量

res:在函数定义 def 内部的代码可见,并且只在函数运行时存在的变量名。
seq1 seq2:通过参数传入赋值,所以也是局部变量。
x:在for循环内部赋值,也是局部变量。

 

3.6 作用域

Python 作用域决定了变量在程序中的可见性和生命周期,通过将变量限制在特定的代码块(如函数、类或模块)内,作用域可以防止不同代码片段中变量名的冲突。局部变量只在函数内部有效,外部无法访问,而全局变量在整个程序中可用。通过使用局部作用域,Python 能避免同名变量的互相干扰,确保代码的可维护性和可读性。

在代码中给变量赋值的位置决定了变量的命名空间(namespace)。
我们将代码封装为函数时,函数增加了一个命名空间(namespace)层,我们在函数中赋值的变量,仅能在函数中使用,这减少了潜在的变量名冲突。

x = 99                     #全局变量
def func():
    x = 88                 # 局部变量,仅函数内部可见
x

 

3.6.1 作用域规则:

每一个模块文件(.py文件)是一个全局作用域


全局作用域仅针对单个文件,全局变量仅对于本模块而言,其他模块导入时,必须显式的调用,例如:import moduleA.x


在函数中赋值的变量为local变量,除非进行globalnonlocal声明


在函数中使用一个不是在函数内部赋值的变量,Python 会依次从闭包命名空间(Enclosing Namespace)、全局命名空间(Global Namespace)、内置命名空间(Built-in Namespace)查找该变量。


每次调用函数时,创建一个新的local作用域

 

3.6.2 命名空间的LEGB查找顺序

变量的查找顺序:Local-->Enclosing-->Global-->Built-in

open = 11            #全局命名空间Global Namespace

def f1():
    open = 22        #闭包命名空间Enclosing Namespace
    def f2():
        open = 33    #本地命名空间local Namespace
        print(open) 
    f2()
f1()
#调用变量时会先从local Namespace查找,如果local Namespace不存在(注释掉local Namespace),则从Enclosing Namespace查找,
#如果Enclosing Namespace不存在(注释掉Global Namespace)
#则从Global Namespace查找,如果Global Namespace不存在(注释掉Global Namespace),则从Built-in Namespace查找,此时open为内置函数。

在函数的内部,可以使用全局变量和闭包变量,但是无法修改它们的值,除非进行globalnonlocal声明。

3.6.3 global声明

X = 88                         # 全局变量 X

def func():
    global X
    X = 99                     # Global声明,可以修改全局变量的值

func()
print(X)                       # Prints 99

3.6.4 nonlocal声明

def myfunc():               
    x = 88                     #闭包变量
    def myfunc1():
        nonlocal x             #nonlocal声明,可以修改闭包变量的值
        x = 99
    myfunc1()
    print(x)

a = myfunc()

将代码封装为函数,旨在将代码分解成独立的、可重用的功能块。函数应该使用参数与返回值来和其他代码交互,而尽量避免使用全局变量,以防止产生不可预测的副作用。

3.7 参数传递

3.7.1 参数传递基础

参数是通过自动将对象赋值给局部变量名来传递的。
在函数内部对参数名称进行赋值不会影响调用者。
在函数内部更改可变对象参数可能会影响调用者。

 

不可变对象实际上是通过按值传递的。
可变对象实际上是通过按指针传递的。

def changer(a, b):        # 参数名指向传递给它的对象的内存地址
    a = 2                 # 只改变本地(local)变量的值,不影响全局变量
    b[0] = 'spam'         # 可变对象被直接修改b

X = 1; L = [1, 2]
changer(X, L)             #传入不可变对象和可变对象
X                         #X没有改变

L                         #L发生了改变

避免可变对象被修改

L = [1, 2]
changer(X, L[:])          #传入L的副本或者使用copy()
L

3.7.2 参数传递的形式

位置参数(默认):从左到右匹配参数

def myfunc(arg1, arg2, arg3):
    return f'这是第一个参数{arg1},这是第二个参数{arg2},这是第三个参数{arg3}。'

print(myfunc(1,2,3))
print(myfunc(3,2,1))

关键字参数:通过关键字指定参数值

def f(a, b, c): print(a, b, c)
f(c=3, b=2, a=1)

默认参数:如果没有提供该参数,会使用预先设定的默认值

def f(a, b=2, c=3): print(a, b, c)   #必须传入参数a,如果没有传入参数b、c,则使用函数定义时指定的参数值b=2,c=3。

f(1)

f(1, 4, 5)

收集变长参数(函数定义时):收集任意多的位置参数或关键字参数

使用*符号,可以将任意长度的参数收集为元组,由于这是一个正常的元组对象,它支持索引、切片、for循环等操作。

def f(*args): print(args)
f(1)    #传入1个参数

f(1, 2, 3, 4)    #传入4个参数

相似的,使用**符号,可以将任意长度的参数收集为字典。

def f(**args): print(args)
f()    #空字典

f(a=1, b=2)    #传入两个参数

同时使用***收集任意长度的位置参数和关键字参数:

def f(a, *pargs, **kargs): print(a, pargs, kargs)

f(1, 2, 3, x=1, y=2)    #参数1按位置传递,参数2,3被收集为元组对象参数x=1,y=2被收集为字典对象

不定长参数解包(函数调用时):传递任意多的位置参数或关键字参数

def func(a, b, c, d): print(a, b, c, d)

args = (1, 2 ,3, 4)

func(*args)    #将包含4个元素的元组对象args解包为4个独立的参数

仅关键字参数:参数必须通过关键字传入

仅关键字参数和关键字参数的形式一样(name=value)且出现在*args之后,这样定义的参数必须以关键字的形式传入

def kwonly(a, *b, c):    #c为仅关键字参数
    print(a, b, c)

kwonly(1, 2, c=3)    #1为位置参数,2会被收集为一个元组,c=3通过关键字参数传入

kwonly(a=1, c=3)     #1为位置参数,*b传入一个空元组,c=3通过关键字参数传入

kwonly(1, 2, 3)     #3没有通过关键字传入,报错

我们也可以在参数列表中单独使用一个 * 字符来表示函数不接受可变长度的参数,但仍期望所有跟在 * 后面的参数仅以关键字的形式传递。

def kwonly(a, *, b, c):    #b,c为仅关键字参数
    print(a, b, c)

kwonly(1, c=3, b=2)

kwonly(c=3, b=2, a=1)

kwonly(1, 2, 3)    #2,3没有通过关键字传入,报错

联合使用仅关键字参数和默认参数:

def kwonly(a, *, b='spam', c='ham'):
     print(a, b, c)

kwonly(1)    #传入位置参数1,b,c使用默认参数

kwonly(1, c=3)   #传入位置参数1,b使用默认参数,通过关键字传入参数c=3

kwonly(1, 2)    #此函数有1个位置参数和2个仅关键字参数,但是传入了2个位置参数,报错

3.8 函数高级技巧

3.8.1 lambda函数

Lambda 函数是一种简洁的匿名函数,允许你在需要函数对象的地方快速定义函数逻辑,而无需使用完整的 def 语句。


lambda函数的基本形式如下:


lambda 参数1, 参数2,... 参数N : 使用参数计算表达式

f = lambda x, y, z: x + y + z
f(2, 3, 4)

3.8.2 函数式编程工具

 

map 函数:将函数映射到可迭代对象上

将一个函数映射到可迭代对象的每个元素上,返回每一个元素经映射的函数运算的结果。

在编程中有一下常见的任务:遍历一个序列对象,将序列对象的每个元素进行计算(操作)。

counters = [1, 2, 3, 4]
updated = []                #使用空列表收集结果
for x in counters:          #遍历每一个元素
    updated.append(x + 10)  #将每个元素的值加上10
updated

由于这是编程中很常见的任务,Python通过了一个内置函数map(),将函数映射到序列对象的每个元素上。

counters = [1, 2, 3, 4]
def inc(x): return x + 10    #变量x的值加上10
list(map(inc, counters))     #将函数inc映射到counters的每个元素,使用list将迭代器转化为列表

 

filter函数:在可迭代对象中选择项目

filter 函数是Python中的一个内置函数,它接受一个函数和一个可迭代对象作为参数。该函数对可迭代对象中的每个元素应用提供的函数,只有当提供的函数返回True时,相应的元素才会被包含在返回的迭代器中。简而言之,filter 函数用于过滤出满足特定条件的元素。

list(range(-5, 5)) 

list(filter((lambda x: x > 0), range(-5, 5)))   #保留x > 0的值

 

reduce函数:在可迭代对象中选择项目

reduce函数是一个在Pythonfunctools模块中的函数,其基本作用是对一个序列(比如列表)进行某种累积操作。
reduce函数接受两个参数:一个二元函数(接受两个参数的函数)和一个可迭代的序列。reduce函数会从序列的第一个元素开始,将这个二元函数应用于序列的前两个元素,然后将结果和序列的下一个元素作为二元函数的参数,再次应用,如此循环,直到遍历完整个序列,最后返回一个单一的累积结果。

from functools import reduce
reduce((lambda x, y: x + y), [1, 2, 3, 4])

3.9 生成器

3.9.1 生成器函数与生成器表达式

生成器函数:

使用def语句定义函数,但是不使用return返回值,而且使用yield每次返回一个值。

 

生成器表达式:

形式与列表推理表达式相似,返回一个生成器对象,生成器对象每次按要求产生一个值。

3.9.2 生成器函数returnyield的区别

生成器函数与普通函数不同,它们在产生值时会自动挂起和恢复执行及状态,保留了包括代码位置和整个局部作用域在内的状态,使得局部变量能够在函数恢复时保留信息。生成器的主要代码区别在于使用 yield 语句来产生值,而不是返回值,这使得函数能够在保留状态的情况下逐个产生值,而不是一次性计算并返回。

生成器函数与Python中的迭代协议紧密相关,它们通过定义 next 方法支持迭代协议,使得生成器能够被 for 循环和其他迭代上下文使用,从而按需产生值。

def gensquares(N):
    for i in range(N):
        yield i ** 2

x = gensquares(4)
x    #返回生成器对象

next(x)   #使用next()函数每次产生一个值

使用for循环,遍历生成器对象

x = gensquares(4)
for i in x:
    print(i)

生成器函数的优点:

生成器函数的优点是它可以按需生成数据,从而节省内存空间,特别适用于处理大数据集。

3.9.3 生成器表达式

[x ** 2 for x in range(4)]    #列表推理表达式

(x ** 2 for x in range(4))    #生成器表达式

list(x ** 2 for x in range(4)) #与列表推理表达式等效

G = (x ** 2 for x in range(4))

next(G) 

生成器表达式的优点:

生成器表达式是一种高效、内存友好的方式来处理大型数据集,它通过延迟计算(即只在需要时生成数据)来节省内存和计算资源。此外,生成器表达式的语法简洁,使得代码更易读和维护。因此,生成器表达式在处理大数据或构建复杂的数据流时,是一个非常有用的工具。

3.9.4 推理表达式语法总结

列表推理表达式:

[x * x for x in range(10)] 

生成器表达式:

(x * x for x in range(10))

集合推理表达式:

{x * x for x in range(10)}

字典推理表达式:

{x: x * x for x in range(10)}

3.9.5 变量作用域与推理表达式

正如前面提到的,Python中在推理表达式式中赋值的变量实际上是一个嵌套作用域;这些表达式中引用的变量名遵循通常的 LEGB 规则。推理表达式中创建的临时变量不会影响其他作用域的变量。

X = 99
[X for X in range(5)]
X    #不会影响全局变量X的值

Y = 99
for Y in range(5): pass
Y    #for循环中的变量是全局变量

notebook链接:https://www.kaggle.com/code/jeanshendev/python-functions-and-generators

下载本节的示例代码及文件:Python的函数与生成器.ipynb