查看下某个值的在内存中的地址
v1 = 'vincent'
print(id(v1))
v2 = [11,22,33]
v3 = [11,22,33]
print( id(v2) )
print( id(v3) )
我们需要知道的是当函数执行传参时,传递的是内存地址。验证:
def func(data):
print(data, id(data)) # vincent 2398921260720
v1 = 'vincent'
func(v1)
print(id(v1)) # 2398921260720
可以看出传递的是内存地址。
Python参数传递的特性好处是什么?
- 主要是可以节省内存。如果执行函数时,每执行一次都要创建一个数据进行传递,那么有可能会将同一个数据创建很多遍,浪费内存空间。
- 当传递内存地址的时候,可以让函数帮我们对值的修改。例如:
def func(data):
data.append(44)
v = [11,22,33]
func(v)
print(v) # [11, 22, 33, 44]
因为v
和data
指向了同一块内存,所以v
的值发生了变化。
不过需要注意的是,要想实现对值的修改,参数必须是可变类型(list/dict/set),在函数内部只能对内部元素进行修改。
例如:
def func(data):
data.upper() # str是不可变类型,无法进行修改
v = 'vincent'
func(v)
print(v) # vincent
def func(data):
data = [44,55] # 不是对内部元素进行修改,而是重新赋值
v = [11,22,33]
func(v)
print(v) # [11, 22, 33]
浅拷贝
深、浅拷贝一般指的都是可变类型,不可变类型(例如str字符串类型)在进行深、浅拷贝时内部都不会去拷贝,而是修改了内存地址。例如:
import copy
v1 = 'hello world'
v2 = copy.copy(v1) # 浅拷贝
print(id(v1)) # 2291448044912
print(id(v2)) # 2291448044912
所以我们进行深拷贝和浅拷贝时,所处理的数据类型基本上都是set
,list
以及dict
。例如:
import copy
v1 = ['hello', 'world']
v2 = copy.copy(v1)
print(v2) # ['hello', 'world']
print(id(v1)) # 2582399205248
print(id(v2)) # 2582399148800
针对可变类型只拷贝第一层,内部不可变类型和可变类型都不会被拷贝
浅拷贝可以拷贝列表,但是拷贝的是列表的地址,列表的内容并没有修改,可以通过查看内容的id来验证。例如:
import copy
v1 = ['hello', 'world', ['Python', 'Java']]
v2 = copy.copy(v1)
print(v2) # ['hello', 'world', ['Python', 'Java']]
# v1[0]指向的是str, 是不可变类型,所以内容并没有拷贝
print(id(v1[0])) # 2582399205248
print(id(v2[0])) # 2582399148800
# 虽然v1[2]是一个list, 是一个可变类型,所以他们的内存地址是一样的,但是v1[2]中的内容并没有拷贝
print(id(v1[2])) # 2718508839296
print(id(v2[2])) # 2718508839296
说明拷贝出来的内容其实是并没有变化,还是指向的同一个变量。
深拷贝
如果想实现传值而不是传地址,那么可以使用深拷贝。
import copy
v1 = ['hello', 'world']
v2 = copy.copy(v1)
print(v2) # ['hello', 'world']
print(id(v1)) # 2582399205248
print(id(v2)) # 2582399148800
对于深拷贝,对于可变类型无论在哪一层都会被拷贝,不可变类型永远不会被拷贝,指向的永远是同一块内存地址。
深拷贝在操作可变类型的时候,内容如果是str
这种不可变类型,内容不会拷贝。如果是可变类型,那么会进行拷贝:
import copy
v1 = ['hello', 'world', ['Python', 'Java']]
v2 = copy.deepcopy(v1)
print(id(v1[2])) # 2917465125312
print(id(v2[2])) # 2917465016256
import copy
def func(data):
data = [44,55]
v = [11,22,33]
new_v = copy.deepcopy(v) # 重新开辟一块地址空间,不影响v的值
func(new_v)
print(v)
特殊情况:元组是不可变类型。如果是浅拷贝,则永远不会拷贝。如果是深拷贝,当元组中的元素都是不可变类型时,永远不会拷贝;如果元素中有可变类型,就会进行拷贝
import copy
v1 = (11, 22, 33)
v2 = copy.deepcopy(v1)
print(id(v1)) # 1651414994304
print(id(v2)) # 1651414994304
# 当内容中存在可变类型时,进行拷贝
v1 = (11, 22, 33, [44, 55])
v2 = copy.deepcopy(v1)
print(id(v1)) # 2212365472848
print(id(v2)) # 2212365517744
函数的返回值也是内存地址
def func():
data = [11, 22, 33]
print(id(data)) # 2875971195072
return data
v1 = func()
print(v1, id(v1)) # [11,22,33] 2875971195072
上述代码的执行过程:
- 执行func函数
data = [11, 22, 33]
创建一块内存区域,内部存储[11,22,33]
,data变量指向这块内存地址。return data
返回data指向的内存地址- v1接收返回值,所以 v1 和 data 都指向
[11,22,33]
的内存地址(两个变量指向此内存,引用计数器为2) - 由函数执行完毕之后,函数内部的变量都会被释放。(即:删除data变量,内存地址的引用计数器-1)
所以,最终v1指向的函数内部创建的那块内存地址。
如果两个函数进行调用,将返回不一样的内存地址:
def func():
data = [11, 22, 33]
print(id(data))
return data
v1 = func()
print(v1, id(v1)) # [11,22,33] 2187125510336
v2 = func()
print(v2, id(v2)) # [11,22,33] 2187126308352
需要注意的是,如果data是字符串或者整型时,会返回的地址是一样的,涉及到Python的缓存机制,这里不表
参数的默认值
当我们在函数中定义了一个参数默认值之后,在函数定义之后,还未执行函数时,Python解释器会帮助我们为函数创建一块区域,存储参数的默认值。
def func(a1,a2=18):
print(a1,a2)
原理:Python在创建函数(未执行)时,如果发现函数的参数中有默认值,则在函数内部会创建一块区域并维护这个默认值。
func("root")
: 执行函数未传值时,则让a2指向 函数维护的那个值的地址。func("admin",20)
:执行函数传值时,则让a2指向新传入的值的地址。
在特定情况【默认参数的值是可变类型 list/dict/set】 & 【函数内部会修改这个值】下,参数的默认值 有坑 。
# 在函数定义的时候,a2初始化了,后面将不会重新初始化
def func(a1,a2=[1,2]):
a2.append(666)
print(a1,a2)
func(100) # 100 [1, 2, 666]
func(200) # 200 [1, 2, 666, 666]
func(99, [77,88]) # 99 [77, 88, 666] a2重新指向了一个新的内存地址,但不影响刚初始化的那块地址
func(300) # 300 [1, 2, 666, 666, 666] a2继续用函数初始化的那块地址