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

liftword3个月前 (03-01)技术文章30

引言

在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复制文件的9个方法_python怎么复制文件到另一个地方

Python 中有许多“开盖即食”的模块(比如 os,subprocess 和 shutil)以支持文件 I/O 操作。在这篇文章中,你将会看到一些用 Python 实现文件复制的特殊方法。下面我们开...

Python浅拷贝深拷贝之copy、deepcopy

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

Python 文件操作:从基础到实践_python文件rb

在 Python 编程领域,文件操作是一项极为基础且重要的技能。无论是处理日常的数据存储、读取配置信息,还是进行日志记录,文件操作都扮演着不可或缺的角色。通过对文件的读写以及对文件路径的管理,我们能够...

使用Python从PDF文件中提取数据_python读取pdf内容转word

前言数据是数据科学中任何分析的关键,大多数分析中最常用的数据集类型是存储在逗号分隔值(csv)表中的干净数据。然而,由于可移植文档格式(pdf)文件是最常用的文件格式之一,因此每个数据科学家都应该了解...

学习编程第194天 python编程 copy与deepcopy方法复制字典

今天学习的是刘金玉老师零基础Python教程第90期,主要内容是copy与deepcopy方法复制字典数据。一、字典的数据赋值默认情况下,将某个字典A的值通过通过=号赋值给字典B,如果修改了字典A,那...