Python之鸭子类型:魔术方法&深拷贝和浅拷贝

liftword4个月前 (03-01)技术文章31

引言

在Python中,一切皆对象。虽然都是对象,又可以分为可变对象和不可变对象。我们可以通过赋值操作,实现一个看似拷贝对象的作用,但是,由于“变量的标签模型”,我们知道赋值实现的拷贝,本质上只是对同一个对象的两个指向。Python中还提供了浅拷贝、深拷贝的功能,作用于不同的对象类型,又会又不同的呈现。涉及到自定义类型的拷贝行为,我们可以通过定义相应的魔术方法,从而自定义其拷贝行为。

本文的主要内容有:

1、赋值实现的对象“拷贝”

2、浅拷贝与深拷贝

3、通过魔术方法控制浅拷贝与深拷贝


赋值实现的对象“拷贝”

由于在Python一切皆对象的设计理念下,Python中的变量赋值,不再是简单的盒子模型,而是标签模型。

所以,通过赋值实现的“拷贝”,还是同一个对象,通过id()可以清楚地看到这一点。由于前面已经介绍过,这里就不再展开了。

浅拷贝与深拷贝

除了赋值、重新创建对象之外,Python中提供了copy模块,可以支持更加便捷的对象拷贝的功能。

从文档中,可以看到关于浅拷贝(shadow copy)和深拷贝(deep copy)的解释。

先说结论:

1、对于可变对象,比如列表、字典等,浅拷贝会创建一个新的对象(id不同),新的对象存储的跟老的对象存储的内容完全相同,也就是浅拷贝只会拷贝第一层,容器内部有可变类型的元素,其实是指向源对象的元素对象的。

2、对于可变对象,深拷贝会递归进行可变对象及其元素的拷贝,不断产生新的对象,直到遇到不可变对象为止。

3、对于不可变对象,浅拷贝,不会创建新的对象,也就是第一层都不会拷贝。

4、对于不可变对象,深拷贝,分两种情况,如果不可变对象的内部元素也都是不可变对象,则一层也不会拷贝。如果不可变对象的内部元素是可变对象,则会首先会创建一个新的对象,然后再递归看内部是否还有可变对象。

接下来,我们通过代码简单验证一下浅拷贝与深拷贝的使用:

import copy

ints = [1, 2, 3]
floats = [4.1, 5.1, 6.1]
nums = [ints, floats]
print(nums)
# 赋值,实际为同一个对象
nums_ass = nums
print(nums_ass)
print(id(nums) == id(nums_ass))

# 浅拷贝,但是,只拷贝第一层
nums_shadow = copy.copy(nums)
print(nums_shadow)
# 最外层确实产生了一个新的对象
print(id(nums) == id(nums_shadow))
# 但是内部元素还是同一个对象
print(id(nums[0]) == id(nums_shadow[0]))

# 深拷贝,递归进行拷贝,直到遇到不可变对象
nums_deep = copy.deepcopy(nums)
print(nums_deep)
# 最外层是新对象
print(id(nums) == id(nums_deep))
# 内部元素也是新对象
print(id(nums[0]) == id(nums_deep[0]))
# 修改深拷贝对象的元素
nums_deep[0][0] = 100
print(f"nums: {nums}")
print(f"nums_shadow: {nums_shadow}")
print(f"nums_deep: {nums_deep}")
print('=' * 40)
# 修改浅拷贝对象的元素
nums_shadow[0][1] = 200
print(f"nums: {nums}")
print(f"nums_shadow: {nums_shadow}")
print(f"nums_deep: {nums_deep}")


运行结果:

通过运行结果,可以看到对于可变对象的浅拷贝与深拷贝的处理逻辑。

接下来,再来验证一下,不可变对象的浅拷贝与深拷贝。

首先看一下,不可变对象的内部元素也都是不可变对象的拷贝:

import copy

ints = (1, 2, 3)
floats = (4.1, 5.1, 6.1)
nums = (ints, floats)
print(nums)

# 浅拷贝
nums_shadow = copy.copy(nums)
print(f'nums_shadow:{nums_shadow}')
# 不可变对象的浅拷贝,最外层仍是同一个对象
print(id(nums) == id(nums_shadow))

# 深拷贝,递归进行拷贝,直到遇到不可变对象
nums_deep = copy.deepcopy(nums)
print(f'nums_deep:{nums_deep}')
# 不可变对象的深拷贝,最外层仍是同一个对象
print(id(nums) == id(nums_deep))


运行结果:

从结果可以看出,没有产生任何新的对象。


如果一个不可变对象的内部元素是可变对象呢:

import copy

ints = [1, 2, 3]
floats = [4.1, 5.1, 6.1]
nums = (ints, floats)
print(nums)

# 浅拷贝
nums_shadow = copy.copy(nums)
print(f'nums_shadow:{nums_shadow}')
# 不可变对象的浅拷贝,最外层仍是同一个对象
print(id(nums) == id(nums_shadow))

# 深拷贝,递归进行拷贝,直到遇到不可变对象
nums_deep = copy.deepcopy(nums)
print(f'nums_deep:{nums_deep}')
# 产生了新对象
print(id(nums) == id(nums_deep))
print(id(nums[0]) == id(nums_deep[0]))


运行结果:

深拷贝时,虽然从外部看元组是不可变对象,但是内部元素是列表,所以本身还是可变的,所以会穿透,直到所有的元素都是不可变对象了。


通过魔术方法控制浅拷贝与深拷贝

接下来,我们看下浅拷贝和深拷贝应用与自定义类型的默认效果:

import copy


class DaGongRen:
    def __init__(self, name, age, hobbies):
        self.name = name
        self.age = age
        self.hobbies = hobbies


zs = DaGongRen('张三', 18, ['读书', '摸鱼'])
zs_shadow = copy.copy(zs)
zs_deep = copy.deepcopy(zs)
print(id(zs))
print(id(zs_shadow))
print(id(zs_deep))
print('=' * 40)
print(id(zs.hobbies))
print(id(zs_shadow.hobbies))
print(id(zs_deep.hobbies))


运行结果:


从运行结果可以看出,自定义类型的浅拷贝和深拷贝机制,与内置类型是基本一致的。浅拷贝只创建一个新对象,内部全是引用源对象。深拷贝则是递归进行新对象的创建。

如果,我们需要自定义浅拷贝与深拷贝的实现机制,可以通过魔术方法进行:

1、__copy__()方法,用于实现浅拷贝的行为。

2、__deepcopy__()方法,用于实现深拷贝的行为。

我们通过代码简单演示一下:

import copy


class DaGongRen:
    def __init__(self, name, age, hobbies):
        self.name = name
        self.age = age
        self.hobbies = hobbies

    def __copy__(self):
        print('正在进行浅拷贝')
        return self.__class__(self.name, self.age, self.hobbies)

    def __deepcopy__(self, memodict={}):
        print('正在进行浅拷贝')
        return self.__class__(copy.deepcopy(self.name), copy.deepcopy(self.age), copy.deepcopy(self.hobbies))


zs = DaGongRen('张三', 18, ['读书', '摸鱼'])
zs_shadow = copy.copy(zs)
zs_deep = copy.deepcopy(zs)


总结

本文介绍了三种对象“拷贝”的方式,也就是赋值、浅拷贝、深拷贝。对比了浅拷贝和深拷贝的运行机制。最后,简单演示了通过魔术方法__copy__()和__deepcopy__()进行自定义浅拷贝和深拷贝行为。

感谢您的拨冗阅读,希望对您有所帮助。

相关文章

Python 文件复制必备技巧:保留原数据、递归复制目录,效果惊艳

引言:使用 shutil 模块来拷贝文件夹下的所有内容到指定目录。会递归地拷贝整个目录及其所有内容(包括子目录和文件)Python 3.8 或更高版本import shutil import os...

Python之掌握文件删除、复制、重命名及内容查找的技巧

亲爱的读者们,今天我们将为大家详细介绍Python中文件操作的四大技巧:文件删除、文件复制、文件重命名以及文件内容查找。掌握这些技巧,将有助于你在编程过程中更加高效地处理文件。一、文件的删除在Pyth...

Python之如何使用Python脚本批量复制一个文件夹下的所有PNG图片

大家好,今天我要与大家分享一个实用的Python脚本,它能帮助你轻松地将一个文件夹中的所有PNG图片批量复制到另一个文件夹。无论是整理电脑文件,还是进行项目备份,这个小工具都能大大提高你的工作效率。以...

Python3导入自定义模块的3种方式_python导入自定义的py文件

开始第一种,直接 import这里有个大前提,就是你的py执行文件和模块同属于同个目录(父级目录),如下图:main.py 和 pwcong模块同在python目录执行文件为main.pypwcong...

python应用-shutil详解文件复制/移动/删除功能

shutil 模块是 Python 的标准库模块(直接导入,无需下载),提供了像在电脑上操作文件、文件夹的功能,如复制、移动、删除文件和文件夹等。shutil 模块的__all__包含的函数如下:复制...

Python自动文件传输工具教程_python 自动上传

大家好!我是你们的Python老师。今天我要和大家分享一个特别实用的主题 - 如何使用Python创建自动文件传输工具。在日常工作中,我们经常需要在不同文件夹之间复制文件,如果能把这个过程自动化,那将...