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

liftword2个月前 (03-01)技术文章15

引言

在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每日一练之如何copy_python中copy用法

概述今天主要来看看Python中的浅拷贝和深拷贝内容,这里用一个实例来说明~需求:将一个列表的数据复制到另一个列表中。思路:使用列表[:],拿不准可以调用copy模块。实现方法:#!/usr/bin/...

Python复制文件的9种办法 :一文全。

Python复制文件 11 分钟阅读将文件从一个目录复制到另一个目录是一项常见的任务。Python 有许多模块(如 os、subprocess 和 sh-util)来支持文件复制。从这篇文章中,可以获...

文件备份用Python,照着复制粘贴代码就可以了

引言在日常开发和运维工作中,数据安全尤为重要,定期备份重要文件是防范数据丢失的有效手段之一。本文将详细介绍如何使用Python实现一个简单的定时备份脚本,该脚本可以按照设定周期自动将指定文件夹或文件复...

linux下文件和目录的拷贝或移动_linux目录拷贝命令

05.linux下文件和目录的拷贝或移动Treetree命令可以以树状图列出文件目录结构使用 -d 选项只显示目录直接使用tree命令显示当前目录的结构;使用tree [目录名]可以显示指定目录的目录...

Python浅拷贝深拷贝之copy、deepcopy

笔记记录20221205:个人总结:1,两者基本区别不大;2,在涉及到子对象时候,两者才有区别;3,在涉及到子对象,且子对象的操作后内存地址没有发生变化(如下方代码:dic1['one'].appen...