2022-11-15 ★ 小结 70-79 函数_变量_拷贝_参数_lambda_eval_递归

发布于:2022-11-16 ⋅ 阅读:(8) ⋅ 点赞:(0) ⋅ 评论:(0)

函数_变量_拷贝_参数_lambda_eval_递归

函数也是对象_内存分析

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()

参数的传递_传递可变对象_内存分析

参数的传递

函数的参数传递本质:从实参到形参的赋值操作
一切都是对象,因此所有赋值操作都是引用的赋值,参数的传递都是引用传递,而不是值传递
分类:

  1. 可变对象进行写操作,直接作用于源对象本身
    1. 列表、字典、集合、自定义对象等
  2. 不可变对象进行写操作,会产生一个新的“对象空间”,并用新的值填充这块空间
    1. 字符串、元组、函数、数字等

传递可变对象的引用

![[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)

传递不可变对象的引用

不可变对象:数字、字符串、元组、布尔值
实际还是传递对象的引用
在“赋值操作”的时候,由于不可变对象无法修改,系统会新建一个对象

操作:

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)

深拷贝:

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)

我的小结:浅拷贝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)

我的小结:传递不可变对象的时候,由于是浅拷贝,如果需要修改不可变对象,其子对象如果是可变对象,则是可以修改的,并且是直接修改源对象,因为子对象在浅拷贝中只是被拷贝了引用(也就是“通道”或“脐带”😳)

参数的类型_位置参数_默认值参数_命名参数

参数的几种类型

位置参数

按位置传递的参数,需要个数和形参匹配,不匹配就报错

默认值参数

必须位于普通的位置参数后
如果传入参数的时候,默认值参数的位置传入了新参数,默认值会被抹去

命名参数

也叫关键字参数

# 测试命名参数  
  
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))

可变参数

可变参数,可变数量的参数,2种情况

  1. 一个星号,将多个参数收集到一个元组对象中
  2. 两个星号,将多个参数收集到一个字典对象中

我的小结:不确定数量的参数,没有名字*para,成元组,有名字的**para,成字典

强制命名参数

在带星号的可变参数后,增加新的参数,必须是强制命名参数(a=3)

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)

递归函数_函数调用内存分析_栈帧的创建

递归函数

递归函数:自己调用自己的函数,在函数体内部直接或间接地自己调用自己
递归函数必须包含两个部分:

  1. 终止条件:递归结束,通常用于返回值,不再调用自己
  2. 递归步骤:第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()

在函数里调用自己的问题:
必须要有一个终止的条件
先进后出,后进先出,第一个打开的栈帧最后一个退出

# 调用自己,有终止条件  
def test01(n):  
    print("test01",n)  
    if n == 0:  
        print("over")  
    else:  
        test01(n-1)  
	print("test01***",n)  
	
test01(4)

我的小结:递归的部分(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)

我的小结:注意递归的时候要有返回值,否则可能下次调用会没有返回值