【进阶Python】第十讲:可变对象与拷贝

简介: Python虽然相对于C/C++、Java要简单很多,但是它也有很多容易被忽略的特性。如果不了解这些特性,我们就会按照我们意识中的方式去使用某个用法,但是当实现之后发现却“事与愿违”,输出的结果和我们期望的大相径庭。可变对象就是这样一个比较常用的特性,本文就来详细介绍一下Python中可变对象与拷贝。

前言

学会一门编程语言的基础语法比较简单,但是精通一门编程语言却很难,就如同我在PyHubWeekly介绍的一个GitHub项目WTFPython总结的那样,在Python中有很多看似“合情合理”的用法,但是当实现之后发现却“事与愿违”,输出的结果和我们期望的大相径庭,首先来举一个例子,

a = [1, 2, 3, 4]
b = a
print("[a] before append a value: {}".format(id(a)))
print("[b] befaore append a value: {}".format(id(b)))
a.append(5)
print("[a] after append a value: {}".format(id(a)))
print("[b] after append a value: {}".format(id(b)))
print(b)
# 输出
[a] before append a value: 1948986356936
[b] befaore append a value: 1948986356936
[a] after append a value: 1948986356936
[b] after append a value: 1948986356936
[1, 2, 3, 4, 5]

在上面例子中,我首先定义了一个变量a(list),把它赋值给变量b,分别输出它们的id。然后,往a中添加一个值,随后再次输出a、b的id和b的值。

从上面例子中我们可以提取出下列信息:

  • 被赋值的变量、值发生改变后的变量id没有改变
  • 改变a的值,b的值也随之改变

其中比较重要的就是第二点:改变a的值,b的值也随之改变,这就是可变对象的一个特性。如果我们在使用可变对象时,在其他函数中有操作赋值变量的部分,如果疏忽了很容易会造成错误。就如同上面的例子,“明明没有修改b的值,为什么输出的结果却不是[1, 2, 3, 4]

这就引出了理解可变对象的重要性,下面就来详细介绍一下Python的可变对象与拷贝。

可变对象

可变对象中重点的概念就体现在“可变”这个词,可变对象的可变并不是体现在地址可变,而是体现在对象本身(值)的改变。换句话说,可变对象的特点在于当对象本身改变时它的地址不会改变。

与“可变”相对的就是“不可变”,下面就列举一下Python内置可变对象和不可变对象,

  • 可变对象:list,set,dict
  • 不可变对象:int,float,bool,str,tuple,unicode

就如同前面所说的那样,可变对象在修改它的值之后它的地址不会改变,但是不可变对象地址会被改变。下面来举个例子对比一下,

可变对象

>>> a = [1, 2, 3 ,5]
>>> id(a)
1955042932488
>>> a[3] = 4
>>> a
[1, 2, 3, 4]
>>> id(a)
1955042932488

不可变对象

>>> a = 1
>>> id(a)
1706590320
>>> a = 2
>>> id(a)
1706590352

要想理解可变对象和不可变对象本质的区别,就要提及对象引用,我们可以把变量引用分解为两个部分:对象本身、对象引用(可以理解为指针)。

对于不可变对象,当我们对不可变对象进行赋值或者改变对象本身时,它不会改变对象本身,而会改变对象应用,它会去新的地址创建一个新的对象。

对于可变对象,恰恰相反,它不会改变对象引用,而会改变对象本身,会直接在原来地址上修改对象。

可变对象应用

介绍了Python中可变对象和不可变对象的概念和异同点,也许这只是如同一个概念一样存在脑子里,但是却不知道理解这个到底有什么价值?为什么要去学习这些?

“实践出真知”,在编程语言中这一点同样适用,对于很多概念当我们从书本上看到时它只是一段解释、一段文字,却不知道该怎么去用它。

对于Python可变对象,我来介绍一个在赋值方面的应用。

定义变量、赋值是在开发过程中非常常用的操作,我们习以为常的去定义一个变量,然后把它赋值给其他不同的变量,然后传入不同的函数实现不同的功能,在潜意识中,会把“赋值”理解为“复制粘贴”,会把赋值理解为在原来变量的基础上拷贝一份传递给另外一个变量,但是,当我们输出是却发现并不像我们想象的那样,

def print_dict(param, statement):
    print("Current variable is : {}".format(statement))
    for k, v in param.items():
        print("Key: {}, Value: {}".format(k, v))
def main():
    info = {
        "age": 28,
        "country": "China"
    }
    copy_info = info
    print_dict(info, "info")
    info["age"] = 27
    print_dict(copy_info, "copy_info")
# 输出
Current variable is : info
Key: age, Value: 28
Key: country, Value: China
Current variable is : copy_info
Key: age, Value: 27
Key: country, Value: China

在上述例子中,首先定义了一个变量info,然后把它赋值给copy_info,当我们改变info中的值之后发现copy_info中的值也随之改变了。

这是一个简单却又非常常见的例子,在开发过程中会经常遇到这样的场景:对同样的变量进行不同的操作,如果某个调用中对变量进行了“写”操作,那么在不知不觉中这个赋值的变量已经不是之前的变量了,如果疏忽了这一点,必然会造成输出结果错误。

这样我们就要针对这个问题想一下解决方法,好在Python提供了内置模块copy,能够解决可变对象的上述问题,下面就来介绍一下copy这个模块。

copy

前面介绍了可变对象与不可对象的区别:不可变对象在赋值或者修改之后会重新申请地址,而不是在原来地址上修改对象。根据这一点,Python提供了内置copy模块,能够把变量进行拷贝到一个新的地址。

copy模块中包括两种拷贝方式:

  • 浅拷贝
  • 深拷贝

下面分别来介绍一下这两种拷贝方式。

浅拷贝

对于浅拷贝要根据要拷贝的对象而区分对待,如果它拷贝的是一个不可边对象,那么和赋值操作的作用是相同的,下面着重的介绍一下它在拷贝不可边对象时的特点。

拷贝之所以称为“浅”是因为它只拷贝父对象,不会拷贝对象的内部的子对象。换句话说,如果外层是一个可变对象,内部在包含一个可变对象时,使用浅拷贝它只会拷贝最外层对象,举个例子,

>>> from copy import copy
>>> a = [1, 2, 3, [4, 5, 6]]
>>> b = copy(a)
>>> id(a)
1955044381576
>>> a[0] = 0
>>> a
[0, 2, 3, [4, 5, 6]]
>>> b
[1, 2, 3, [4, 5, 6]]
>>> a[3][1] = 7
>>> a
[0, 2, 3, [4, 7, 6]]
>>> b
[1, 2, 3, [4, 7, 6]]

在上面示例中,定义了一个变量a,它外层是一个list(可变对象),内层又包含了一个可变对象(list),当我们使用浅拷贝(copy.copy)后,改变外层对象的值,赋值变量不会再跟随着改变,但是我们改变内部子对象时,赋值的变量就会跟随着改变。

深拷贝

深拷贝和浅拷贝相对,它不仅拷贝父对象,也拷贝子对象,同样使用上面的例子来验证一下深拷贝的作用。

>>> from copy import deepcopy
>>> a = [1, 2, 3, [4, 5, 6]]
>>> b = deepcopy(a)
>>> a[3][1] = 7
>>> a
[1, 2, 3, [4, 7, 6]]
>>> b
[1, 2, 3, [4, 5, 6]]

现在看一下,即便是修改子对象,赋值变量的值也不会跟随我们的改变。

结语

之所以在这里介绍Python可变对象,就是因为并不是所有的Python内置对象都像我们潜意识认知的那样,“赋值后就如同拷贝了一份数据,原始变量与赋值变量之间不会互相影响”。但是,事实上Python可变对象并不像我们想象的那样,原始变量与赋值变量之间会有互相作用,如果疏忽这一点会发生一些我们无法理解的错误,这类语言特性造成错误在调试过程中也很难定位,当我们了解这些容易被忽略的特性之后则不然,就能够有意识的避免Python特性带来的异常。

相关文章
|
3月前
|
Python
python对象模型
这篇文章介绍了Python中的对象模型,包括各种内置对象类型如数字、字符串、列表、字典等,以及如何使用`type()`函数来查看变量的数据类型。
|
3月前
|
Python
探索Python中的魔法方法:打造你自己的自定义对象
【8月更文挑战第29天】在Python的世界里,魔法方法如同神秘的咒语,它们赋予了对象超常的能力。本文将带你一探究竟,学习如何通过魔法方法来定制你的对象行为,让你的代码更具魔力。
43 5
|
1月前
|
存储 缓存 Java
深度解密 Python 虚拟机的执行环境:栈帧对象
深度解密 Python 虚拟机的执行环境:栈帧对象
60 13
|
1月前
|
索引 Python
Python 对象的行为是怎么区分的?
Python 对象的行为是怎么区分的?
24 3
|
1月前
|
存储 缓存 算法
详解 PyTypeObject,Python 类型对象的载体
详解 PyTypeObject,Python 类型对象的载体
31 3
|
1月前
|
Python
深入解析 Python 中的对象创建与初始化:__new__ 与 __init__ 方法
深入解析 Python 中的对象创建与初始化:__new__ 与 __init__ 方法
19 1
|
1月前
|
缓存 Java 程序员
一个 Python 对象会在何时被销毁?
一个 Python 对象会在何时被销毁?
36 2
|
1月前
|
API Python 容器
再探泛型 API,感受 Python 对象的设计哲学
再探泛型 API,感受 Python 对象的设计哲学
20 2
|
1月前
|
API Python
当调用一个 Python 对象时,背后都经历了哪些过程?
当调用一个 Python 对象时,背后都经历了哪些过程?
22 2
|
1月前
|
存储 API C语言
当创建一个 Python 对象时,背后都经历了哪些过程?
当创建一个 Python 对象时,背后都经历了哪些过程?
19 2