Python基础第六节—函数

本文深入探讨Python函数,包括函数的分类、定义与调用,重点解析函数的内存底层分析,如变量作用域(全局与局部)、栈帧内存管理。此外,还详细讲解了参数传递的不同方式,如引用传递,并通过实例展示了可变对象与不可变对象的区别。同时,介绍了参数类型,如位置参数、默认值参数、命名参数、可变参数和强制命名参数。最后,涉及lambda表达式、eval()函数以及递归函数的内存分析。
摘要由CSDN通过智能技术生成

Python函数用法和底层分析

Python函数的分类

1.内置函数
2.标准库函数:通过import导入库并使用里面的函数
3.第三方库函数:下载安装第三方库后,导入并使用里面的函数
4.用户自定义函数

函数的定义和调用

1.使用def定义函数。Python执行def时,会创建一个函数对象(存储在堆内存中),并绑定到函数名变量上。
2.参数列表:圆括号内是形式参数列表,形参不需要声明类型,也不需要指定函数返回值类型。即使无形参,也要有()。实参列表必须与形参一一对应。
3.若函数体中包含return语句,则结束函数执行并返回值;若无return语句,则返回None。
4.函数应先定义再调用。对于内置函数,其函数对象会自动创建;对于导入的库函数,通过import导入模块时,会执行模块中的def语句。

文档字符串(函数的注释)

建议在函数体开始的部分附上对函数的解释说明。用三个单引号或三个双引号实现,中间加入多行文字。
打印输出函数的文档字符串:help(函数名.doc)

函数也是对象(内存底层分析)

def test1():
    print('haha')
c=test1
c()

在这里插入图片描述
执行def定义函数后,系统就在堆中创建了相应的函数对象,并通过test1这个变量进行引用,test1的值即为函数对象的地址。test1的值赋给了c,则c的值也为函数对象的地址,因此c也可用于引用该函数。

print(id(test1))
print(id(c))
print(c)
print(test1)

在这里插入图片描述
函数是对象,因此函数既可以作为参数传递,也可作为返回值。

变量的作用域(全局变量与局部变量)

变量作用域

变量起作用的范围。不同作用域内的同名变量不互相影响。

全局变量

1.在函数和类定义之外声明的变量。其作用域为定义的模块,从模块开始至模块结束。
2.全局变量降低了函数的通用性和可读性,因此应尽量避免全局变量的使用
3.全局变量一般作为常量使用。
4.若在函数内需改变全局变量的值,要用global关键字声明。

局部变量

1.在函数体中(含形式参数)声明的变量
2.局部变量的查询和访问速度比全局变量快,应优先使用,尤其在循环的时候。在特别强调效率或循环次数多的情况下,可将全局变量转为局部变量以提高效率。
3.若局部变量与全局变量同名,则在函数内隐藏全局变量,仅使用同名的局部变量。

栈帧(stack frame)内存分析

调用函数时,系统在栈中开辟一块”栈帧“,内有局部变量的引用。函数调用完成后,栈帧便被删除。因此,有方法在函数内部访问全局变量,而函数外不可访问局部变量。

a=3
def test2():
    b=10
    print(b*10)
    a=5
    print(a)
print(a)
test2()

在这里插入图片描述

a=3
def test2():
    b=10
    print(b*10)
    global a
    a=5
    print(a)
print(a)
test2()

在这里插入图片描述

a=3
def test2():
    b=10
    print(b*10)
    a=5
    print(a)
    print(locals())  #查看函数内的局部变量(若要查看全局变量,则使用globals())
test2()
print(a)

在这里插入图片描述

局部变量和全局变量效率测试
import math
import time

def quanju():
    start=time.time()
    for i in range(1000000):
        math.sqrt(30)    #引用全局变量math.sqrt
    end=time.time()
    return end-start

def jubu():
    a=math.sqrt   #将全局变量math.sqrt转为仅在函数内部使用的局部变量a
    start=time.time()
    for i in range(1000000):
        a(30)     #引用局部变量a
    end=time.time()
    return end-start

print(quanju())    
print(jubu())

在这里插入图片描述
由输出结果可知,引用局部变量比引用全局变量快。因此,可采用将全局变量转为局部变量的方法来提升效率。

参数的传递

函数的参数传递,本质即为由实参到形参的赋值操作。
实参和形参都是对象的引用。因此,Python函数中的参数传递并非“值传递”,而是“引用的传递”。
具体操作时分为两类:

传递可变对象的引用

1.对可变对象(字典、列表、集合、自定义的对象等)进行“写操作”,直接作用于原对象本身。在函数体中并不创建新的对象拷贝,而是直接修改所传递的对象。

b=[10,20]
def f(m):
    print('m地址为:',id(m))
    m.append(30)
    return m
print(f(b))
print('b地址为:',id(b))
print(b)

在这里插入图片描述
分析:
第一步,在堆中创建列表对象,b为其引用。
第二步,在堆中创建函数对象,f为其引用。
第三步,调用函数,在栈中创建栈帧,内有m,并将b赋值于m,则m也成为该列表对象的引用。m和b的id一致。
第四步,执行函数,使列表对象发生变化。因此,打印b时,会发现输出的列表也发生了变化。

传递不可变对象的引用

2.对不可变对象(数字、字符串、元组、函数等)进行“写操作”,会产生一个新的“对象空间”,并用新的值填充这块空间。

b=100
def f(m):
    print('m地址为:',id(m))
    m+=200
    print('m地址变为:',id(m))
    return m
print(f(b))
print('b地址为:',id(b))
print(b)

在这里插入图片描述
分析:
第一步,在堆中创建整数对象100,b为其引用。
第二步,在堆中创建函数对象,f为其引用。
第三步,调用函数,在栈中创建栈帧,内有m,并将b赋值于m,则m也成为该列表对象的引用。m和b的id一致。
第四步,执行函数。整数对象本身并不会发生变化,因此系统在堆中创建一个新的整数对象300,m变为新整数对象的引用。于是,m的id发生变化,不再与b一致。而b仍为原整数对象100的引用.

传递包含可变子对象的不可变对象的引用
a=(10,20,[5,6])
print('a:',id(a))
def f1(m):
    print('m:',id(m))
    m[2][0]=10
    print('m:',id(m))

f1(a)
print(a)

在这里插入图片描述
分析:
第一步,在堆中创建元组对象,a为其引用。
第二步,在堆中创建函数对象,f1为其引用。
第三步,调用函数,在栈中创建栈帧,内有m,并将a赋值于m,则m也成为该列表对象的引用。m和b的id一致。
第四步,执行函数。元组对象本身并不会发生变化,但其指向的列表子对象的子对象发生了变化。m仍是该元组对象的引用,因此id并不变化,但元组对象的子对象的子对象值发生了变化。于是,m、a的打印输出结果均发生变化。

浅拷贝、深拷贝

copy(浅拷贝):不拷贝子对象的内容,只拷贝子对象的引用。

import copy
a=[10,20,[5,6]]
b=copy.copy(a)
print('a:',a)
print('b:',b)

b.append(30)
b[2].append(7)
print('a:',a)
print('b:',b)

在这里插入图片描述
过程如下图所示:
b为a的浅拷贝,则b只复制了a所指向列表对象的引用。
在这里插入图片描述
deepcopy(深拷贝):连子对象的内存一起拷贝。对子对象的修改不会影响源对象。

import copy
a=[10,20,[5,6]]
b=copy.copy(a)
print('a:',a)
print('b:',b)

b.append(30)
b[2].append(7)
print('a:',a)
print('b:',b)

在这里插入图片描述
过程如下图:
b为a的深拷贝,则b连a的内存一起复制,此后对b的任何修改都不会对a产生影响。
在这里插入图片描述

参数类型

位置参数

按位置传递的参数称为位置参数。函数调用时,实参默认按位置顺序传递,需要个数和形参匹配。

默认值参数

为某些参数设定默认值。默认值参数放在位置参数之后。
如:def f(a,b,c=10,d=20):
print(a,b,c,d)
则f(2,3): 2 3 10 20
f(2,3,4): 2 3 4 20
f(2,3,4,5): 2 3 4 5

命名参数

按形参的名称传递参数。也称关键字参数。
如:def f(a,b,c,d):
print(a,b,c,d)
则f(b=3,a=2,d=5,c=4): 2 3 4 5

可变参数

1.*param(一个星号):将多个参数收集进一个元组对象。
如:def f1(a,b,*c):
print(a,b,c)
则f1(2,3,4,5): 2 3 (4,5)
2.**param(两个星号):将多个参数收集进一个字典对象。
如:def f2(a,b,**c)
print(a,b,c)
则f2(2,3,‘a’=‘bc’,‘d’=‘ef’): 2 3 {‘a’:‘bc’,‘d’:‘ef’}

强制命名参数

若在带星号的可变参数后添加新的参数,则必须是强制命名参数。
如:def f3(*a,b,c):
print(a,b,c)
若f3(2,3,4): 报错
若f3(2,b=3,c=4): (2,) 3 4

lambda表达式和匿名函数

lambda表达式可以用来声明匿名函数。lambda函数是一种简单的、在同一行内定义函数的方法,实际生成一个函数对象,
lambda表达式只能包含一个表达式,不可包含复杂语句,该表达式计算值就是函数返回值。
基本语法: lambda arg1,arg2,arg3…: <表达式>
arg1,arg2,arg3…即为函数参数,表达式相当于函数体。
若f引用lambda表达式,则f即为函数的引用。

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

g=[lambda a:a*2,lambda b:b*3,lambda c:c*4]
print(g[0](2),g[1](3),g[2](4))
print(g)

在这里插入图片描述

eval()函数

功能:将字符串当作有效表达式,求值并返回计算结果
如:s=“print(‘abcd’)”
eval(s) : ‘abcd’
又如:a=‘10+20’
eval(a) : 30
又如:dict1=dict(a=100,b=200)
d=eval(‘a+b’,dict1)
print(d) : 300

递归函数调用内存分析

递归函数指的是在函数体内部直接或间接调用自己的函数。
递归函数必须包含以下两部分:
1.终止条件:表示递归什么时候结束。一般用于返回值,不再调用自己;
2.递归步骤:把第n步的值和第n-1步相关联。

    print('digui:',n)
    if n==0:
        print('over')
    else:
        digui(n-1)
    print('digui:',n)
    
digui(2)

在这里插入图片描述
内存分析:
第一步:调用函数,创建栈帧,内含对象2的引用。
第二步:函数执行至调用自身的语句时,创建第二个栈帧(压在第一个栈帧之上),内含对象1的引用。
第三步:函数执行至函数执行至调用自身的语句时,创建第三个栈帧(压在第二个栈帧之上),内含对象0的引用。
第四步:print(‘over’),函数停止调用自身,而是继续向下执行并输出。执行完毕后,第三个栈帧删除。
第五步:函数继续向下执行并输出,此时栈帧内含对象1的引用。执行完毕后,第二个栈帧删除。
第六步:函数继续向下执行并输出,此时栈帧内含对象2的引用。执行完毕后,第一个栈帧删除。函数执行完成。

注意:递归函数会大量创建函数对象,过量消耗内存和运算能力,处理大量数据时应谨慎使用。

嵌套函数(内部函数)

例:inner()的定义和调用都在outer()内部。

def outer():
    print('outer running')
    def inner():
        print('inner running')
    inner()
    
outer()   

在这里插入图片描述
嵌套函数的功能:
1.封装-数据隐藏(外部无法访问内部函数)。
2.在函数内部避免重复代码。
3.闭包。

def ChineseName(name,family_name):
    print('{0} {1}'.format(family_name,name))
    
def EnglishName(name,family_name):
    print('{0} {1}'.format(name,family_name))

可通过嵌套函数,将以上两个函数写成一个函数。

def printName(is_Chinese,name,family_name):
	def inner_print(a,b):
		print('{0} {1}'.format(a,b))
	if is_Chinese:
		inner_print(family_name,name)
	else:
		inner_print(name,family_name)

printName(True,'hong','xiao')
printName(False,'hong','xiao')

内部函数可访问外部函数中的变量,但不可修改。若需在内部函数内修改,需使用nonlocal关键字对变量进行声明。

LEGB规则

Python在查找名称时,是按照LEGB规则查找的:Local(函数或类的方法内部)——>Enclosed(嵌套函数(一个函数包裹另一个函数,闭包))——>Global(模块中的全局变量)——>Built in(Python为自己保留的特殊名称)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值