文章目录
函数_变量_拷贝_参数
函数也是对象_内存分析
Python 中,一切都是对象
执行def定义函数后,系统就创建了相应的函数对象
现在可以把对象想成是放在堆里的一个东西
调用就可以反复调用
小括号相当于是“调用”的意思
#测试函数也是对象
def test01():
print("sxtsxt")
test01()
c = test01
c()
![[Pasted image 20221115110159.png]]
我的小结:函数是对象,栈里放的是函数的名字,堆里放的是函数本体
变量的作用域_全局变量_局部变量_栈帧内存分析讲解
变量的作用域(全局变量、局部变量)
全局变量:
作用域定义的模块,从定义的位置开始,直到模块结束
全局变量降低了函数的通用性和可读性,应避免
一般用作常量
函数内要改变全局变量,使用global声明一下
局部变量
在函数体中,包含形式参数,声明的变量
引用比全局变量快
局部变量与全局变量同名,则在函数内隐藏全局变量,只使用同名的局部变量
操作:
# 测试全局变量、局部变量
a = 3 # 全局变量
def test01():
b = 4 # 局部变量
print(b*10)
test01()
print(b) # 出了函数体,变量b就用不了了
尝试解释为什么局部变量不能全局用
![[Pasted image 20221115110939.png]]
局部变量在函数被调用的时候,会在栈里建立一个栈帧,函数运行完之后,栈帧stack frame就被删掉了
在函数内修改全局变量
a = 3 # 全局变量
def test01():
b = 4 # 局部变量
print(b*10)
global a
a = 300
print(a)
locals()输出所有局部变量
globals()输出所有全局变量
局部变量和全局变量效率测试
局部变量效率高,尽量多用,尤其在循环多的时候
可以考虑把全局变量变成局部变量提高效率❓怎么实现:下例实现
#测试局部变量和全局变量的效率
import math
import time
def test01():
start = time.time()
for i in range(10000000):
math.sqrt(30)
end = time.time()
print("耗时{0}".format(end-start))
def test02():
start = time.time()
b = math.sqrt
for i in range(10000000):
b(30)
end = time.time()
print("耗时{0}".format(end-start))
test01()
test02()
参数的传递_传递可变对象_内存分析
参数的传递
函数的参数传递本质:从实参到形参的赋值操作
一切都是对象,因此所有赋值操作都是引用的赋值,参数的传递都是引用传递,而不是值传递
分类:
- 对可变对象进行写操作,直接作用于源对象本身
- 列表、字典、集合、自定义对象等
- 对不可变对象进行写操作,会产生一个新的“对象空间”,并用新的值填充这块空间
- 字符串、元组、函数、数字等
传递可变对象的引用
![[Pasted image 20221115113905.png]]
我的小结:传递参数是可变对象的时候(列表、字典、集合、自定义对象),实际上传递的是参数的引用地址,在函数体中不会拷贝被传递的对象,而是可以通过对地址的引用,直接修改被传递的对象,因此调用函数的时候,被传递对象的地址始终不发生改变,因为没有新对象生成。可以把这个“地址引用”想象成一个通道,一个脐带…所以可以直接操作源对象
我的小结:传递可变参数时,函数体中对所传递的参数的改变,会直接改变源对象
# 传递参数
# 可变对象
a = [10,20]
print(id(a))
print(a)
print('*'*10)
def test01(m):
print(id(m))
m.append(390)
print(id(m))
test01(a)
print(a)
传递不可变对象的引用
不可变对象:数字、字符串、元组、布尔值
实际还是传递对象的引用
在“赋值操作”的时候,由于不可变对象无法修改,系统会新建一个对象
![[Pasted image 20221115120652.png]]
操作:
a = 100
def f1(n):
print("n:", id(n)) # a的id
n = n + 200 # a+200
print("n:",id(n)) # a+200的id
print(n) a+200的值
f1(a)
print("a:",id(a)) a的id
浅拷贝和深拷贝
为了更深入地了解参数传递的底层远离,要搞清楚浅拷贝copy和深拷贝deepcopy
两个都是内置函数
浅拷贝:不拷贝子对象内容,只拷贝子对象的引用
深拷贝:脸子对象的内存也全部拷贝一份,对子对象的修改不会影响源对象
浅拷贝
import copy
b = copy.copy(a)
操作:
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("浅拷贝...")
print("a:",a)
print("b:",b)
![[Pasted image 20221115121622.png]]
深拷贝:
a = [10,20,[5,6]]
b = copy.deepcopy(a)
print("a:",a)
print("b:",b)
b.append(30)
b[2].append(7)
print("浅拷贝...")
print("a:",a)
print("b:",b)
![[Pasted image 20221115122047.png]]
我的小结:浅拷贝copy的时候,子对象由于只被拷贝了引用,因此类似于开了一个通道,拷贝后产生的新对象如果修改了子对象,也就是通过了这个通道,就会直接把源对象也改了,也就是浅拷贝里,源对象的子对象与新对象之间没有隔离;深拷贝则相反。
我的小结:联系传递参数来想,传递可变参数的时候,就是类似浅拷贝,传递不可变参数的时候,就是类似深拷贝。留了“脐带”的、可变的对象类型,就可以直接改源对象
传递不可变对象是浅拷贝
传递不可变对象时,如果发生拷贝,是浅拷贝
# 传递不可变对象时,如果发生拷贝,是浅拷贝
a = 10
print("a:",a)
def test01(m):
print("m:",id(m))
m=20
print(m)
print("m:",id(m))
test01(a)
传递不可变对象,如果发生修改,是重新新建对象来完成的
a = (10,20,[5,6])
print("a:",id(a))
def test01(m):
print("m:",id(m))
m[2][0]=888
print(m)
print("m:",id(m))
test01(a)
![[Pasted image 20221115122803.png]]
我的小结:传递不可变对象的时候,由于是浅拷贝,如果需要修改不可变对象,其子对象如果是可变对象,则是可以修改的,并且是直接修改源对象,因为子对象在浅拷贝中只是被拷贝了引用(也就是“通道”或“脐带”😳)
参数的类型_位置参数_默认值参数_命名参数
参数的几种类型
位置参数
按位置传递的参数,需要个数和形参匹配,不匹配就报错
![[Pasted image 20221115123503.png]]
默认值参数
必须位于普通的位置参数后
![[Pasted image 20221115123530.png]]
如果传入参数的时候,默认值参数的位置传入了新参数,默认值会被抹去
命名参数
也叫关键字参数
# 测试命名参数
def f1(a, b, c):
print(a,b,c)
f1(8,9,10) # 位置参数
f1(c=10,a=20,b=30) # 命名参数
def test02(a,b,c=10,d=15) # 默认值参数必须位于其它参数后面
print("{0}-{1}-{2}-{3}".format(a,b,c,d))
![[Pasted image 20221115123629.png]]
可变参数
可变参数,可变数量的参数,2种情况
- 一个星号,将多个参数收集到一个元组对象中
- 两个星号,将多个参数收集到一个字典对象中
- ![[Pasted image 20221115124235.png]]
![[Pasted image 20221115124214.png]]
我的小结:不确定数量的参数,没有名字
*para
,成元组,有名字的**para
,成字典
强制命名参数
在带星号的可变参数后,增加新的参数,必须是枪支明明参数
![[Pasted image 20221115124334.png]]
lambda表达式和匿名函数
用来声明匿名函数
是一种简单的、在同一行中定义函数的方法
实际生成了一个函数对象
只允许包含一个表达式,不能包含复杂语句
该表达式的计算结果就是函数的返回值
语法:
lambda arg1, arg2, arg3...:<表达式>
arg1…:函数的参数
表达式:函数体
运算结果:表达式的运算结果
操作:
f = lambda a,b,c:a+b+c
print(f) # 结果:<function <lambda> at 0x00000166D55E04A0>
print(f(2,3,4))
g = [lambda a:a*2, lambda b:b*3, lambda c:c*4]
print(g[0](6),g[1](3),g[2](9))
f = lambda a,b,c,d:a*b*c*d
def test01(a,b,c,d):
return a*b*c*d
def test02(a,b,c,d):
e = a*b*c*d
return e
❓是不是上门两个def是一样的?
我的小结:圆括号既是元组,也是调用函数的时候的“调用”动作,内部放的就是参数,
h[0](2,3)
的形式可以好好理解一下(调用列表h中的第0个函数,传入2和3两个参数)
我的小结:lambda的形式一个是简洁,另一个不用老想着要不要return,因为会直接返回表达式的计算结果出来
eval()函数用法
功能:将字符串str当成有效的表达式,来求值,并返回计算结果
语法:
eval(source[, globals[, locals]]) → value
参数:
source:一个Python的表达式或函数compile()返回的代码对象
globals:可选,必须是dictionnary
locals:可选,任意映射对象
举例:
s = "print('abcde')"
eval(s)
a = 10
b = 20
c = eval("a+b")
用处:可以从文件或者别的什么地方读取文本,运行
我的小结:eval()可以把识别到的字符串变成代码!使代码变得灵活
传递参数:
dict1 = dict(a=100,b=200)
d = evals("a+b",dict1)
print(d)
递归函数_函数调用内存分析_栈帧的创建
递归函数
![[Pasted image 20221115130243.png]]
递归函数:自己调用自己的函数,在函数体内部直接或间接地自己调用自己
递归函数必须包含两个部分:
- 终止条件:递归结束,通常用于返回值,不再调用自己
- 递归步骤:第n步和第n-1步相关联
# 测试递归函数的基本原理
def test01():
print("test01")
test02()
def test02():
print("test02")
test01()
在函数里调用别的函数的过程:
test01(),调用test01,建立一个test01的栈帧,test01中调用test02,再建立一个test02的栈帧,然后test02的栈帧关掉,最后test01的栈帧关掉
# 调用自己
def test01():
print("test01")
test01()
print("######")
def test02():
print("test02")
test01()
在函数里调用自己的问题:
必须要有一个终止的条件
先进后出,后进先出,第一个打开的栈帧最后一个退出
![[Pasted image 20221115130810.png]]
# 调用自己,有终止条件
def test01(n):
print("test01",n)
if n == 0:
print("over")
else:
test01(n-1)
print("test01***",n)
test01(4)
![[Pasted image 20221115131611.png]]
我的小结:递归的部分(n与n+1发生联系的部分),先被调用的先执行(正常顺序),递归之后的部分,后被调用的先执行
递归函数_阶乘计算案例
递归由于会创建大量的函数对象,过量消耗内存和运算能力,在处理大量数据的时候谨慎使用
# 用递归函数计算阶乘
# 5! = 5*4*3*2*1
def factorial(n):
if n == 1:
return 1
else:
return n*factorial(n-1) # 注意这里要有return,不然之后的调用没有参数传入
result = factorial(5)
print(result)
我的小结:注意递归的时候要有返回值,否则可能下次调用会没有返回值