首页 > 编程语言 >Python 对象拷贝的详细教程

Python 对象拷贝的详细教程

时间:2023-08-14 19:31:34浏览次数:61  
标签:教程 tuple Python list 对象 dict print 拷贝 id

在本篇文章中,会先介绍 Python 中对象的基础概念,之后会提到对象的深浅拷贝以及区别。在阅读后,应该掌握如下的内容:

  • 理解变量、引用和对象的关系
  • 理解 Python 对象中 identity,type 和 value 的概念
  • 什么是 mutable 和 immutable 对象?以及它们和 hashable 的关系
  • 深浅拷贝的过程以及区别

1.变量,引用和对象

变量无类型,它的作用仅仅在某个时候引用了特定的对象而已,具体在内存中就是一个指针,仅仅拥有指向对象的空间大小。

变量和对象的关系在于引用,变量引用对象后,也就对应了赋值的过程。

在 python 中一切皆为对象,具体在内存中表示一块内存空间,每一个对象都会具有 identity,type 和 value 这三个内容。

Identity, 一旦对象被创建后,Identity 的值便不会发生改变。在 Cpython 中,其值体现为内存中保存对象的地址。is 操作符,比较对象是否相等就是通过这个值。通过 id() 函数查看它的整数形式。

Type, 和 Identity 一样,在对象创建后,Type 也不会发生变化。它主要定义了一些可能支持的值和操作(如对列表来说,会有求长度的操作)。通过 type() 函数可以得到对象的类型。

Value,用于表示的某些对象的值。当对象在创建后值可以改变称为 mutable,否则的话被称为 immutable.

举个例子,比如在 C 中,int x = 4 在内存中,是先分配了一个 int 类型的内存空间,然后把 4 放进空间内。

而 Python 中,x = 4 正好相反,是为 4 分配了一块的内存空间,然后用 x 指向它。由于变量可以指向各种类型的对象,因此不需要像 C 一样声明变量。这也就是 Python 被称为动态类型的意义。

并且在 Python 中,变量可以删除,但对象是无法删除的。

2.immutable 和 mutable 对象

immutable 对象拥有一个固定的值,包括 numbers, strings, tuples. 一个新的值被保存时,一个新的对象就会被创建。这些对象在作为常量的 hash 值中有着非常重要的作用,如作为字典的 key 时。

mutable 对象可以改变自身的值,但 id() 并不会发生改变。

当一些对象包含对其他对象的一些引用时,我们称这些对象为 containers, 例如 list, tuple, dictionary 这些都是 containers. 这里需要注意的是,一个 immutable containers 可以包含对 mutable 对象的引用(如在 tuple 中包含一个 list)。 但这个对象仍然称为 immutable 对象,因为 Identity 是不变的。

3.hashable 对象

当一个对象在生命周期内(实现了__hash__()方法)hash 值不会发生改变,并可以与其他对象进行比较(实现了__eq__()方法),称之为hashable 对象。

在 Python 内置的 immutable 对象 大多数都是 hashable 对象。immutable containers(tuples, frozenset)在引用的对象都是 hashable 对象时,才是hashable 对象。mutable containers 容器都不是 hashable 对象。用户自定义的类都是 hashable 对象,

4.浅拷贝与深拷贝

在介绍对象的拷贝前,先介绍一下 Python 中的赋值操作,可以让我们更好的了解拷贝的过程。

5.赋值操作

赋值操作的右边是简单表达式:

def normal_operation():

    # immutable objects
    # int
    a = 10
    b = 10
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1777364320 , id of b: 1777364320
    print(a == b)  # True
    print(a is b)  # True
    # str
    str_a = '123'
    str_b = '123'
    print('----- str')
    print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
    # id of a:1615046978224 , id of b: 1615046978224
    print(str_a == str_b)  # True
    print(str_a is str_b)  # True

    # tuple
    tuple_a = (1, 2, 3)
    tuple_b = (1, 2, 3)
    print('----- tuple')
    print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
    # id of a:1615047009696 , id of b: 1615047024856
    print(tuple_a == tuple_b)  # True
    print(tuple_a is tuple_b)  # False

    # mutable
    # set
    set_a = {1, 2, 3}
    set_b = {1, 2, 3}
    print('----- set')
    print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
    # id of a:1615045625000 , id of b: 1615047012872
    print(set_a == set_b)  # True
    print(set_a is set_b)  # False

    # list
    list_a = [1, 2, 3]
    list_b = [1, 2, 3]
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:1615047017800 , id of b: 1615045537352
    print(list_a == list_b)  # True
    print(list_a is list_b)  # False

    # dict
    dict_a = {"name": "xxx", "age": "123"}
    dict_b = {"name": "xxx", "age": "123"}
    print('----- dict')
    print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
    # id of a:1615045521696 , id of b: 1615045522128
    print(dict_a == dict_b)  # True
    print(dict_a is dict_b)  # False

在 Cpython 中,id() 反映了对象在内存中的地址。可以看到,对于 immutable 对象中的 number 和 string 来说,CPython 本身对其做了一定的优化,在创建相同的内容时,使其 指向了相同的内存地址,从而被复用。

但是,Python 不会对所有 mutable 对象执行此操作,因为实现此功能需要一定的运行时成本。对于在内存中的对象来说,必须首先在内存中搜索对象(搜索意味着时间)。对于 number 和 string 来说,搜索到它们很容易,所以才对其做了这样的优化。

对于其他类型的对象,虽然创建的内容相同,但都在内存中完全创建了一块新的区域。

6. 赋值操作的右边是 Python 中已存在的变量:

def assignment_operation():
    # immutable objects
    # int
    a = 10
    b = a
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1777364320 , id of b: 1777364320
    print(a == b)  # True
    print(a is b)  # True
    # str
    str_a = '123'
    str_b = str_a
    print('----- str')
    print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
    # id of a:2676110142128 , id of b: 2676110142128
    print(str_a == str_b)  # True
    print(str_a is str_b)  # True

    # tuple
    tuple_a = (1, 2, 3)
    tuple_b = tuple_a
    print('----- tuple')
    print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
    # id of a:2676110191640 , id of b: 2676110191640
    print(tuple_a == tuple_b)  # True
    print(tuple_a is tuple_b)  # True

    # mutable
    # set
    set_a = {1, 2, 3}
    set_b = set_a
    print('----- set')
    print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
    # id of a:2676108788904 , id of b: 2676108788904
    print(set_a == set_b)  # True
    print(set_a is set_b)  # True

    # list
    list_a = [1, 2, 3]
    list_b = list_a
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2676110181704 , id of b: 2676110181704
    print(list_a == list_b)  # True
    print(list_a is list_b)  # True

    # dict
    dict_a = {"name": "xxx", "age": "123"}
    dict_b = dict_a
    print('----- dict')
    print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
    # id of a:2676079063328 , id of b: 2676079063328
    print(dict_a == dict_b)  # True
    print(dict_a is dict_b)  # True

而当赋值操作的右边是已经存在的 Python 对象时,不论是什么类型的对象,都没有在内存中创建新的内容,仅仅是声明了一个新的变量指向之前内存中已经创建的对象,就像提供了一个别名一样。

>>> dict_a = {'1':1}
>>> dict_b = dict_a
>>> print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
id of a:140355639151936 , id of b: 140355639151936

>>> dict_b = {}
>>> print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
id of a:140355639151936 , id of b: 140355639922176

由于 dict_b = dict_a操作,让两个变量同时指向了同一块内存区域。自然 id 相等。 当对 dict_b 重新赋值时,仅让 b 指向了另外一块内存区域,并不会影响 a 的指向,由于两块内存区域不同,自然id 并不想等。

7.改变赋值后的对象

def assignment_operation_change():
    # immutable objects
    # int
    a = 10
    print("id of a:{}".format(id(a)))  
    # id of a:1994633728
    b = a 
    a = a + 10
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1994634048 , id of b: 1994633728
    print(a == b)  # False
    print(a is b)  # False

    # mutable objects
    # list
    list_a = [1, 2, 3]
    list_b = list_a
    list_a.append(4)
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2676110181704 , id of b: 2676110181704
    print(list_a == list_b)  # True
    print(list_a is list_b)  # True

当修改 imutable 对象时,由于其本身不可改变,只能在内存中新申请一块新的空间,用于存储修改后的内容。对应上面 a=20 的操作,这时再判断 a 和 b 时,由于指向了内存的不同位置,所以 a,b不在相等。a 原来指向的内存区域不会被回收,因为现在由 b 指向。可以看到 b 指向的内存地址和 a 之前的指向的内存地址是一致的。

当修改 mutable 对象时,由于都指向相同的内存地址,所以对变量 list_a 修改的操作,也会映射到变量 list_b。

总结一下:

  • 指向 imutable 的不同变量,当其中一个变量被修改时,其他变量不受影响,因为被修改后的变量会指向一个新创建的对象。
  • 指向 mutable 对象的不同变量,当其中一个变量修改这个对象时,会影响到指向这个对象的所有变量。

8.浅拷贝

浅拷贝创建了一个对象,这个对象包含了对被拷贝元素的参考。 所以当使用浅拷贝来复制 conainters 对象时,仅仅拷贝了那些嵌套元素的引用。

def shallow_copy():
    # immutable objects
    # int
    a = 10
    b = copy(a)
    print('----- int')
    print("id of a:{} , id of b: {}".format(id(a), id(b)))
    # id of a:1777364320 , id of b: 1777364320
    print(a == b)  # True
    print(a is b)  # True
    # str
    str_a = '123'
    str_b = copy(str_a)
    print('----- str')
    print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
    # id of a:2676110142128 , id of b: 2676110142128
    print(str_a == str_b)  # True
    print(str_a is str_b)  # True

    # tuple
    tuple_a = (1, 2, 3)
    # Three methods of shallow copy
    # tuple_b = tuple_a[:]
    # tuple_b = tuple(tuple_a)
    tuple_b = copy(tuple_a)
    print(id(tuple_b))
    print('----- tuple')
    print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
    # id of a:2676110191640 , id of b: 2676110191640
    print(tuple_a == tuple_b)  # True
    print(tuple_a is tuple_b)  # True

    # mutable
    # set
    set_a = {1, 2, 3}
    # Two methods of shallow copy
    # set_b = set(set_a)
    set_b = copy(set_a)
    print('----- set')
    print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
    # id of a:2099885540520 , id of b: 2099888490984
    print(set_a == set_b)  # True
    print(set_a is set_b)  # False

    # list
    list_a = [1, 2, 3]
    # Three methods of shallow copy
    # list_b = list_a[:]
    # list_b = list(list_b)
    list_b = copy(list_a)
    print('----- list')
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2099888478280 , id of b: 2099888478472
    print(list_a == list_b)  # True
    print(list_a is list_b)  # False
	# Python小白学习交流群:711312441
    # dict
    dict_a = {"name": "xxx", "age": "123"}
    # Two methods of shallow copy
    # dict_b = dict(dict_a)
    dict_b = copy(dict_a)
    print('----- dict')
    print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
    # id of a:2099855880480 , id of b: 2099886881024
    print(dict_a == dict_b)  # True
    print(dict_a is dict_b)  # False

这里有一点需要注意,对于 string 和 number 来说,正如上面提到的 Cpython 做了相应的优化,让不同的变量指向了相同的内存地址,进而 id 的值是相等的。

但对于元组这个 immutable 元素来说,执行 浅拷贝时,也不会创建一个内存区域,只是返回一个老元组的引用。

对于其他的 mutable 对象,在浅拷贝后都会创建一个新的内存区域,包含了被拷贝元素的引用。

浅拷贝正如它的名字那样,当拷贝嵌套的 mutable 元素时,就会出现问题:

def shallow_copy_change_value():
    # list
    list_a = [1, 2, 3, [4, 5, 6]]
    list_b = copy(list_a)
    list_a[0] = 10
    list_a[3].append(7)
    print('----- list')
    print("ia:{} ,b: {}".format(list_a, list_b))
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # a:[10, 2, 3, [4, 5, 6, 7]] ,b: [1, 2, 3, [4, 5, 6, 7]]
    # id of a:1698595158472 , id of b: 1698595159752
    print(list_a == list_b)  # False
    print(list_a is list_b)  # False

下面是对上面 list 浅拷贝的图解:

执行浅拷贝操作:

72a62de32295d24a41f15aecb2f65eac

在 list_b 执行浅拷贝后,创建一个新的对象,新对象中的 list_a[0] 指向 1.

修改 list_a 操作:

f82e60e92a6c04ab94146965ee4e985e

当执行 list_a[0] = 10 操作时,由于 list_a[0] 本身是 number 类型,会重新创建一块区域,用于保存新的值 10. 而新创建的 list_b[0] 并不会受到影响,还会指向之前的内存区域。

当修改list_a[3] 操作时,由于list_a[3] 在浅拷贝后,新创建的对象中不会 嵌套创建 一个新的 list_a[3] 对象,仅仅是指向了之前的 list_a[3] 对象。所以当修改 list_a[3] 时, list_b[3] 也会收到影响。

9.深拷贝

对于深拷贝操作来说,除了会创建一个新的对象外,会还递归的遍历老对象的中的嵌套元素,并形成新的副本。

def shallow_deepcopy_change_value():
    # list
    list_a = [1, 2, 3, [4, 5, 6]]
    list_b = deepcopy(list_a)
    list_a[0] = 10
    list_a[3].append(7)
    print('----- list')
    print("a:{} ,b: {}".format(list_a, list_b))
    print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
    # id of a:2099888478280 , id of b: 2099888478472
    print(list_a == list_b)  # False
    print(list_a is list_b)  # False

下面是对应图解过程:

执行深拷贝操作:

4a050f660c31f509ecde59eb8b589c38

修改 list_a 操作:

cd944895e4e8cb41278113baa1906af9

这里 list_a 和 list_b 已经是完全的不同的两个对象。

总结

在这篇文章中,主要介绍了 Python 中对象,以及对象的拷贝过程,主要有下面几个重要的内容:

  • Python 中变量没有类型,仅仅可看做一个指针,通过引用指向对象。变量可以删除,但对象不行。
  • Python 对象被创建后,会拥有 identity,type 和 value 三个属性。
  • immutable 和 mutable,主要在于 value 在其生命周期内是否能发生变化。
  • 修改 mutable 对象时,所有指向它的变量都会受到影响。修改 immutable 对象时,指向它的其他变量没有影响。
  • immutable 的大多数对象都是 hashable,但要考虑 immutable containers 的特殊情况。
  • 浅拷贝会创建一个新的内存区域(对象),但其内部是对原对象内部引用的拷贝,在使用 mutable 对象时,存在一定的风险。
  • 深拷贝不但会创建一个新的内存区域(对象),还会递归的创建原对象的所有嵌套对象,但也带来了一些效率的问题。

标签:教程,tuple,Python,list,对象,dict,print,拷贝,id
From: https://blog.51cto.com/u_14246112/7080385

相关文章

  • Python 优雅的使用 paramiko 进行交互式输入输出
    目的:需要ssh链接到Linux主机,执行telnet命令,抓回显匹配制定内容。ssh.exec_command(cmd,bufsize,timeout)#exec_command参数使用只需要执行一次的命令,因为执行完该命令以后,shell会自动回到ssh初始连接的shell状态下ssh.invoke_shell()#在SSHserver端创建一个交互式的shell,且......
  • python中function使用class调用和使用对象调用的区别
    问题在python中,class中函数的定义中有一个特殊的self指针,如果一个函数有一个self参数,通常意味着这是一个非静态函数,也就是调用的时候第一个参数是对象指针,只是这个指针是调用这个函数时由python来自动填充。tsecer@harry:catcls_mth.pyclasstsecer():defharry(self):......
  • Python中threading模块 lock、Rlock的使用
    一、概述在使用多线程的应用下,如何保证线程安全,以及线程之间的同步,或者访问共享变量等问题是十分棘手的问题,也是使用多线程下面临的问题,如果处理不好,会带来较严重的后果,使用python多线程中提供Lock、Rlock、Semaphore、Event、Condition用来保证线程之间的同步,后者保证访问......
  • Python中os._exit(), sys.exit(), exit() 的区别
    在Python3.x中,os._exit(),sys.exit(),和exit()是三个不同的终止程序执行的函数,它们之间有一些区别:os._exit():这个函数是os模块中的一个函数,它用于直接终止程序的执行,并且不会触发任何清理活动或关闭程序中的资源。它不会抛出任何异常或执行任何finally子句。使用os._exit(......
  • 各个时区对应的时差表 及 python中时区获取方式(支持夏令时)
    1Africa/Abidjan+00:002Africa/Accra+00:003Africa/Addis_Ababa+03:004Africa/Algiers+01:005Africa/Asmara+03:006Africa/Bamako+00:007Africa/Bangui+01:008Africa/Banjul+00:009Africa/Bissau+00:0010Africa......
  • Python 优雅的使用 subprocess.Popen 获取实时输出,以及中止 subprocess
    #-*-coding:utf-8-*-importshleximportosimportsignalimporttimeimportthreadingfromsubprocessimportPopen,PIPEdefrun_command(command):process=Popen(shlex.split(command),stdout=PIPE)st=time.time()whileTrue:ou......
  • VMware安装Ubantu教程
    VMware使用教程下载地址https://www.vmware.com/products/workstation-pro/workstation-pro-evaluation.html建议FQ访问镜像源下载地址下载点我在这里边找到合适的源,点击下载。以下内容以Ubantu20.04为例(ubuntu-20.04.6-desktop-amd64.iso),下载地址VMware安装与正......
  • Vuejs装饰器风格开发教程(前言、模板项目、类属性、类方法)
    教程前言AOP切面编程是面向对象开发中的一种经典的编程范式,旨在将横切关注点与核心业务逻辑分离来提高代码的模块性和可维护性。如:日志记录、事务管理等就属于横切关注点。在为H5提供Android原生支持时,曾将插件模块改造为AOP模式,实现插件的自动注册。Java领域的SpringBoo......
  • Python合并两个字典代码
    合并两个字典Python3.5之后,合并字典变得容易起来。我们可以通过**符号解压字典,并将多个字典传入{}中,实现合并。defMerge(dict1,dict2):res={**dict1,**dict2}returnres#两个字典dict1={"name":"Joy","age":25}dict2={"name":"Joy",......
  • Python代码链式比较
    链式比较python有链式比较的机制,在一行里支持多种运算符比较。相当于拆分多个逻辑表达式,再进行逻辑与操作。a=5print(2<a<8)print(1==a<3)输出:TrueFalse......