参考资料
https://github.com/taizilongxu/interview_python
https://github.com/hantmac/Python-Interview-Customs-Collection
https://github.com/kenwoodjw/python_interview_question
有些来自上面(但我也做了自己的补充),有些来自网络或书籍
本文不准备写编程题,偏重于理论一些。你要的话去刷leetcode就是了。
倒序描述,限于篇幅,可能要连载
第一次见浅拷贝是在
>>> 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则可以用于任意的数据类型的浅拷贝
>>> from copy import copy
>>> help(copy)
Help on function copy in module copy:
copy(x)
Shallow copy operation on arbitrary Python objects.
See the module's __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有它的弊端:如果被拷贝对象中存在指向自身的引 用,那么程序很容易陷入无限循环
Python 的赋值语句不复制对象,而是创建目标和对象的绑定关系
对于自身可变,或包含可变项的集合,有时要生成副本用于改变操作,而不必改变原始对象。本模块提供了通用的浅层复制和深层复制操作
浅层与深层复制的区别仅与复合对象(即包含列表或类的实例等其他对象的对象)相关:
深度复制操作通常存在两个问题, 而浅层复制操作并不存在这些问题:
deepcopy() 函数用以下方式避免了这些问题:
memo
) 字典;以及下面这个案例说明了一点什么
from copy import deepcopy
x = [1]
x.append(x)
print(x) # [1, [...]]
y = deepcopy(x)
print(y) # [1, [...]]
参考了 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
知识点: 函数参数的类型
示例代码
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
知识点: 作用域
示例代码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
对于代码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相关知识单独考虑弄个博文
==是对象的值的比较,也就是对象保存的数据。
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_函数会去 遍历列表中的元素,比较它们的顺序和值是否相等。