首页IT科技python深拷贝浅拷贝和赋值(Python常见面试题001-005,涉及深浅拷贝、MRO、函数可变参数、作用域、is和==的区别等)

python深拷贝浅拷贝和赋值(Python常见面试题001-005,涉及深浅拷贝、MRO、函数可变参数、作用域、is和==的区别等)

时间2025-05-02 05:11:36分类IT科技浏览6781
导读:Python常见面试题001-005 参考资料...

Python常见面试题001-005

参考资料

https://github.com/taizilongxu/interview_python

https://github.com/hantmac/Python-Interview-Customs-Collection

https://github.com/kenwoodjw/python_interview_question

有些来自上面(但我也做了自己的补充),有些来自网络或书籍

本文不准备写编程题             ,偏重于理论一些               。你要的话去刷leetcode就是了                   。

倒序描述                       ,限于篇幅      ,可能要连载

005. 说说你对浅拷贝               、深拷贝的理解

浅拷贝 shallow copy 深拷贝 deep copy

浅拷贝

第一次见浅拷贝是在

>>> help(list.copy) Help on method_descriptor: copy(self, /) Return a shallow copy of the list.

list列表的copy方法         ,那浅拷贝到底是怎样的行为表现呢?

示例代码

list1 = [1,2,3] list2 = list1.copy() list1.append(4) print(list2) # [1,2,3] list1改变了                       ,list2没变          , 浅拷贝是互相不会影响的

来看这段

list1 = [1,2,3] list2 = list1 list1.append(4) print(list2) # [1,2,3,4] ,赋值是引用      ,同时指向了一个内存区域                      ,现在你改变了内存的信息              ,自然一起改变了

在第一个例子中

list1 = [1,2,3] list2 = list1.copy() print(id(list1)) print(id(list2)) # 显然2个id的值是不一样的   ,所以改变了你                     ,并不会改变我 print(list1 == list2) # True print(list1 is list2) # False 来看看其他的浅拷贝方式

切片

可变序列的切片创建了一个浅拷贝                  ,不可变序列的切片创建了一个引用

list1 = [1,2,3] list2 = list1[:] list1.append(4) print(list2) # [1,2,3] 跟list1不一样 print(list2 is list1) # False tuple1 = (1,2,3) tuple2 = tuple1[:] print(tuple1 is tuple2) # True 浅拷贝,是指重新分配一块内存                 ,创建一个新的对象                      ,里 面的元素是原对象中子对象的引用        。因此   ,如果原对象中的元素不可变             ,那倒无所谓;但如果元 素可变                       ,浅拷贝通常会带来一些副作用

构造器

常见的浅拷贝的方法      ,是使用数据类型本身的构造器

set1 = {1,2,3} set2 = set(set1) print(set2 is set1) # False

copy.copy()

copy.copy则可以用于任意的数据类型的浅拷贝

>>> from copy import copy >>> help(copy) Help on function copy in module copy: copy(x) Shallow copy operation on arbitrary Python objects. See the modules __doc__ string for more info.

示例代码

dict1 = {"name":"wuxianfeng","age":18} from copy import copy dict2 = copy(dict1) print(dict2 is dict1) dict1[height] = 180 print(dict2)

特殊情况

来看下面的这些例子

l1 = [[1, 2], (30, 40)] l2 = list(l1) l1.append(100) l1[0].append(3) print(l2) # 请问此时l2是什么?

按照前面的理解         ,list构造器会产生浅拷贝

print(l2 is l1) # False # 说明这是2个不同的对象

按理说l1的变化不会影响到l2

但事实是影响了

[[1, 2, 3], (30, 40)] # 第四行代码发生作用了

仔细看会发现                       ,line3没有改变          ,line4改变了

再看一个操作

# 承上 l1[1] +=(50,60) print(l2) # 还是[[1, 2, 3], (30, 40)] 但l1肯定是变了的

完整的解释

l1 = [[1, 2], (30, 40)] l2 = list(l1) # 对l1执行浅拷贝      ,赋予l2            。 # 因为浅拷贝里的元素是对原对象元素的引用                      ,因此l2中的元素和l1中的元素指向同一个列表和元组对象 l1.append(100) # 这个操作不会对l2产生任何影响              ,因为l2和l1作为整体是两个不同的对象   ,并不共享内存地址 l1[0].append(3) # l1[0] 是[1, 2] # 因为l1和l2的[1, 2]是同一个对象                     ,不信? # print(id(l1[0])) # print(id(l2[0])) # print(l1[0] is l2[0]) # True # 所以l1[0].append(3) 这步会对l1和l2的[1,2]都追加一个元素3 l1[1] +=(50,60) # 最麻烦的是这个 # l1[1] 是(30,40) ,是个元组 # l2[1] 的确是同一个对象 # l1[1] +=(50,60) 会创建一个新的                  ,注意是新的!元组 # 但l2不会

你可以这样验证

print(l1的元组的id,id(l1[1]),l1的元组的id,id(l2[1])) l1[1] +=(50,60) print(l1的元组的id,id(l1[1]),l1的元组的id,id(l2[1])) # 输出参考 l1的元组的id 2411404337920 l1的元组的id 2411404337920 l1的元组的id 2411404749456 l1的元组的id 2411404337920 # 是的,l1中的元组已经变了                 ,l2的没有改变

至此你应该发现了浅拷贝的一些副作用

深拷贝

深拷贝来自copy模块的deepcopy方法

同样看上面的例子

from copy import deepcopy l1 = [[1, 2], (30, 40)] l2 = deepcopy(l1) l1.append(100) l1[0].append(3) l1[1] +=(50,60) print(l2) # [[1, 2], (30, 40)] # 就是你复制的时候的样子

好像deepcopy很完美?

复制的时候的确不希望互相影响

但deepcopy有它的弊端:如果被拷贝对象中存在指向自身的引 用                      ,那么程序很容易陷入无限循环

官方解释

https://docs.python.org/zh-cn/3.9/library/copy.html

Python 的赋值语句不复制对象   ,而是创建目标和对象的绑定关系

对于自身可变             ,或包含可变项的集合                       ,有时要生成副本用于改变操作      ,而不必改变原始对象                   。本模块提供了通用的浅层复制和深层复制操作

copy.copy(x) 浅层复制 copy.deepcopy(x) 深层复制

浅层与深层复制的区别仅与复合对象(即包含列表或类的实例等其他对象的对象)相关:

浅层复制 构造一个新的复合对象         ,然后(在尽可能的范围内)将原始对象中找到的对象的 引用 插入其中           。 深层复制 构造一个新的复合对象                       ,然后          ,递归地将在原始对象里找到的对象的 副本 插入其中        。

深度复制操作通常存在两个问题, 而浅层复制操作并不存在这些问题:

递归对象 (直接或间接包含对自身引用的复合对象) 可能会导致递归循环                    。 由于深层复制会复制所有内容      ,因此可能会过多复制(例如本应该在副本之间共享的数据)

deepcopy() 函数用以下方式避免了这些问题:

保留在当前复制过程中已复制的对象的 "备忘录" (memo) 字典;以及 允许用户定义的类重载复制操作或复制的组件集合

下面这个案例说明了一点什么

from copy import deepcopy x = [1] x.append(x) print(x) # [1, [...]] y = deepcopy(x) print(y) # [1, [...]] 按理说y会无限循环                      ,堆栈溢出              ,但实际上并没有   ,还是deepcopy做了一些事情规避的

004. 请说出下面代码的返回结果是什么?

参考了 https://www.liujiangblog.com/course/python/44

如有侵权                     ,联系删除

示例代码1

class D: pass class C(D): pass class B(C): def show(self): print("i am B") pass class G: pass class F(G): pass class E(F): def show(self): print("i am E") pass class A(B, E): pass a = A() a.show()

结果

i am B

你可以整理出这样的一个继承关系

从执行结果看先走 是A(B,E)中左侧的B这个分支:可见                  ,在A的定义中,继承参数的书写有先后顺序                 ,写在前面的被优先继承              。

你可以查看A的__mro__属性

print(A.__mro__) # 你也可以这样print(A.mro()) # 是个元组 (<class __main__.A>, <class __main__.B>, <class __main__.C>, <class __main__.D>, <class __main__.E>, <class __main__.F>, <class __main__.G>, <class object>)

如果你改成这样

class A(E,B): pass print(A.__mro__)

顺序自然成了这样

(<class __main__.A>, <class __main__.E>, <class __main__.F>, <class __main__.G>, <class __main__.B>, <class __main__.C>, <class __main__.D>, <class object>)

继承关系下的搜索顺序

所以                      ,把代码改为这样   ,输出你应该毫无疑问了

class D: def show(self): print("i am D") pass class C(D): pass class B(C): pass class G: pass class F(G): pass class E(F): def show(self): print("i am E") pass class A(B, E): pass a = A() a.show() # i am D

那么这样呢?

class H: def show(self): print("i am H") pass class D(H): pass class C(D): pass class B(C): pass class G(H): pass class F(G): pass class E(F): def show(self): print("i am E") pass class A(B, E): pass a = A() a.show()

继承关系是这样的

答案是

i am E

看MRO

print(A.__mro__) (<class __main__.A>, <class __main__.B>, <class __main__.C>, <class __main__.D>, <class __main__.E>, <class __main__.F>, <class __main__.G>, <class __main__.H>, <class object>)

所以继承树的搜索顺序是这样的

而所有的继承其实都是这2种图形的变化

比如这样的代码

class D(): pass class G(): def show(self): print("i am G") pass class F(G): pass class C(D): pass class B(C,F): pass class E(F): def show(self): print("i am E") pass class A(B, E): pass a = A() a.show()

你先分析下应该输出啥             ,继承树是怎样的                       ,MRO是如何的?

输出

i am E

继承树

MRO

(<class __main__.A>, <class __main__.B>, <class __main__.C>, <class __main__.D>, <class __main__.E>, <class __main__.F>, <class __main__.G>, <class object>)

关于类的继承

子类在调用某个方法或变量的时候      ,首先在自己内部查找         ,如果没有找到                       ,则开始根据继承机制在父类里查找    。 根据父类定义中的顺序          ,以深度优先的方式逐一查找父类! 子类永远在父类前面      ,如果有多个父类                      ,会根据它们在列表中的顺序被检查              ,如果对下一个类存在两个合法的选择   ,选择第一个父类

MRO:即Method Resolution Order(方法解析顺序)

从Python 2.3开始计算MRO一直是用的C3算法

https://www.python.org/download/releases/2.3/mro/

C3算法的简单解释可以参考码农高天的这个视频:https://www.bilibili.com/video/BV1V5411S7dY

003. 请说出下面的代码返回结果是什么?为何?如何改进?

知识点: 函数参数的类型

示例代码

def f(a, L=[]): L.append(a) return L print(f(1)) print(f(1))

据说是国内某上市互联网公司Python面试真题(略作改动)                     ,而实际在python的官网也有

https://docs.python.org/zh-cn/3.9/tutorial/controlflow.html#default-argument-values

烂大街了

典型的错误答案

[1] [1]

实际的答案

[1] [1, 2]

因为Python函数体在被读入内存的时候                  ,默认参数a指向的空列表对象就会被创建,并放在内存里了                     。因为默认参数a本身也是一个变量                 ,保存了指向对象[]的地址                 。每次调用该函数                      ,往a指向的列表里添加一个A。a没有变   ,始终保存的是指向列表的地址             ,变的是列表内的数据!

修改

def f(a, L=None): if L is None: L = [] L.append(a) return L print(f(1)) print(f(1))

官网的重要警告: 默认值只计算一次                  。默认值为列表                   、字典或类实例等可变对象时                       ,会产生与该规则不同的结果

这样的代码

def f(a, L=[]): L.append(a) return L print(f(1)) print(f(2)) # 被我改成了1      ,具有欺骗性一点 print(f(3)) # 去掉了         ,3个放一起                       ,你都能猜到有点猫腻了          ,2个虽然也....总归好一点

上面的函数会累积后续调用时传递的参数

不想在后续调用之间共享默认值时      ,建议用None这样的不可变对象来存储

def f(a, L=None): if L is None: L = [] L.append(a) return L

002. 请分别说出下面的代码返回结果是什么?为何?

知识点: 作用域

示例代码1

def func(x): print(x) print(y) func(1)

示例代码2

y = 1 def func(x): print(x) print(y) func(1)

示例代码3

y = 1 def func(x): print(x) print(y) y = 2 func(1)

示例代码1的执行结果

NameError: name y is not defined

示例代码2的执行结果

1 1

示例代码3的执行结果

UnboundLocalError: local variable y referenced before assignment

解释3

Python 编译函数的定义体时                      ,它判断 b 是局部变量              ,依据是y=2   ,你对它进行了赋值                    。Python 会尝试从本地环境获取 b    。 后面调用 func(1)时                     ,func的定义体会获取并打印局部变量x 的值                  ,但是尝试获取局部变量 y 的值时,发现 y 没有 绑定值就报错了               。 这不是缺陷                 ,这是设计如此(做测试是不是经常听到这句话) Python 不要求声明变量                      ,但是假定在函数定义体中赋值的变 量   ,那就认为它是局部变量

对于代码3的处理

示例代码3(更改)

y = 1 def func(x): global y print(x) print(y) y = 2 func(1)

这样解释器就会把 y 当成全局变量             ,从而找到第一行的y=1并print出来了

从函数的字节码也能看出来这个过程

代码1

def func(x): print(x) print(y) y = 2 from dis import dis dis(func)

字节码

3 0 LOAD_GLOBAL 0 (print) # 加载全局名称print 2 LOAD_FAST 0 (x) # 加载本地名称x 4 CALL_FUNCTION 1 6 POP_TOP 4 8 LOAD_GLOBAL 0 (print) 10 LOAD_FAST 1 (y) # 加载本地名称y 12 CALL_FUNCTION 1 14 POP_TOP 5 16 LOAD_CONST 1 (2) 18 STORE_FAST 1 (y) 20 LOAD_CONST 0 (None) 22 RETURN_VALUE

示例代码2

y = 1 def func(x): print(x) print(y) from dis import dis dis(func)

字节码

3 0 LOAD_GLOBAL 0 (print) 2 LOAD_FAST 0 (x) 4 CALL_FUNCTION 1 6 POP_TOP 4 8 LOAD_GLOBAL 0 (print) 10 LOAD_GLOBAL 1 (y) # 看这里的变化                       ,是全局变量了 12 CALL_FUNCTION 1 14 POP_TOP 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 进程已结束      ,退出代码为 0

示例代码3

y = 1 def func(x): print(x) print(y) y = 2 from dis import dis dis(func)

字节码

3 0 LOAD_GLOBAL 0 (print) 2 LOAD_FAST 0 (x) 4 CALL_FUNCTION 1 6 POP_TOP 4 8 LOAD_GLOBAL 0 (print) 10 LOAD_FAST 1 (y) # 又变成了本地 12 CALL_FUNCTION 1 14 POP_TOP 5 16 LOAD_CONST 1 (2) 18 STORE_FAST 1 (y) 20 LOAD_CONST 0 (None) 22 RETURN_VALUE

示例代码3(更改)

y = 1 def func(x): global y print(x) print(y) y = 2 from dis import dis dis(func)

字节码

4 0 LOAD_GLOBAL 0 (print) 2 LOAD_FAST 0 (x) 4 CALL_FUNCTION 1 6 POP_TOP 5 8 LOAD_GLOBAL 0 (print) 10 LOAD_GLOBAL 1 (y) 12 CALL_FUNCTION 1 14 POP_TOP 6 16 LOAD_CONST 1 (2) 18 STORE_GLOBAL 1 (y) 20 LOAD_CONST 0 (None) 22 RETURN_VALUE

看不懂字节码不要紧的         ,当然非要                       ,你可以去参考https://docs.python.org/zh-cn/3/library/dis.html

作用域LEGB相关知识单独考虑弄个博文

001. is和==有什么区别

==是对象的值的比较          ,也就是对象保存的数据                   。

is比较的是对象的标识        。

示例代码1

a = [1,2,3] b = [1,2,3] print(a == b) # True print(a is b) # False

is的背后是id      ,is比较为True                      ,说明2个id的返回是一样的

在 CPython 中              ,id() 返回对象的内存地址   , 但是在其他 Python 解释器中可能是别的值            。关键是                     ,ID 一定是唯一的数值标注                  ,而且在 对象的生命周期中绝不会变                   。

上面的话引自 <流畅的python> 8.2 标识        、相等性            、别名

这些知识涉及对象的引用,相关的面试题如浅拷贝/深拷贝                   、重载运算符(==)等

浅拷贝也考虑单独剥离弄个博文或主题

示例代码2

>>> a = 257 >>> b = 257 >>> a is b False >>> a == b True # 但是这个呢? >>> c = d = 256 >>> c is d True

这是因为出于对性能优化的考虑                 ,Python内部会对-5到256的整型维持一个数组                      ,起到一个缓存 的作用           。这样   ,每次你试图创建一个-5到256范围内的整型数字时             ,Python都会从这个数组中返回相对应的引用                       ,而不是重新开辟一块新的内存空间        。

但是      ,如果整型数字超过了这个范围         ,比如上述例子中的257                       ,Python则会为两个257开辟两块 内存区域          ,因此a和b的ID不一样      ,a is b就会返回False了                    。

比较操作符is的速度效率                      ,通常要优于==              。因为is操作符不能被重载              ,这 样   ,Python就不需要去寻找                     ,程序中是否有其他地方重载了比较操作符                  ,并去调用    。执行比较操作符is,就仅仅是比较两个变量的ID而已                     。

但是==操作符却不同                 ,执行a == b相当于是去执行a.__eq__(b)                      ,而Python大部分的数据类型都 会去重载__eq_这个函数   ,其内部的处理通常会复杂一些                 。比如             ,对于列表                       ,__eq_函数会去 遍历列表中的元素      ,比较它们的顺序和值是否相等。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
关闭网页快捷键(如何使用快捷键关闭 Win11) 网站收录的忽上忽下,如何避免?(从收录原因到解决方案,教你避免网站收录的忽上忽下)