面试之 Python 基础
爱好
为什么学习 Python
大学学的是计算机专业学长建议我学Python然后自己通过看视屏教程和向有学过 Python 的同学学习了 Python。
Python 入门比较简单它简单易学生态圈比较强大涉及的地方比较多特别是在人工智能和数据分析这方面。Python 目前的发展方向包括
- 爬虫
- 自动化运维
- 全栈
- 大数据、数据分析
- 人工智能
通过什么途径学习 Python
刚开始跟着网上里面跟着视频学基础再后来网上到看技术贴比如廖雪峰的Python教程等后来有时间到GitHub上面找一些小项目学习。
编程基础
谈谈对 Python 的了解和其他语言的区别
优点:
- Python 属于解释型语言当程序运行时是一行一行的解释并运行所以调式代码很方便开发效率高。
- Python 易于学习语法简洁优美功能强大标准库与第三方库都非常强大而且应用领域也非常广用少量的代码构建出很多功能高效的高级数据结构。
- 龟叔给 Python 定位是任其自由发展、优雅、明确、简单。
- Python 完全支持面向对象数据类型是动态类型的。
- Python 是跨平台且开源的。可移植性可扩展性可嵌入性都很强。
缺点
- 运行速度慢Python的运行速度相较与C肯定是慢了。
- Python弱类型强类型是指不允许隐式变量类型转换弱类型则允许隐式类型转换。
与其他语言相比
- 与java相比在很多方面Python 比 Java 简单比如 java 中所有变量必须声明才能使用而 Python 不需要声明用少量的代码构建出很多功能。
- 与php相比python 标准包直接提供了工具并且相对于PHP代码更易于维护。
- 与c相比Python 和 CPython 这门语言是由C开发而来。
简述解释型和编译型编程语言
- 解释型就是边解释边执行Pythonphp。
- 编译型编译后再执行c、java、c#。
Python 的解释器种类以及相关特点
- CPython官方版本的解释器。使用C语言开发的所以叫 CPython。在命令行下运行 python 就是启动 CPython 解释器。CPython 是使用最广的 Python 解释器。
- IPythonIPython 是基于 CPython 之上的一个交互式解释器也就是说IPython 只是在交互方式上有所增强但是执行 Python 代码的功能和 CPython 是完全一样的。CPython 用
>>>
作为提示符而IPython用In [序号]:
作为提示符。 - PyPy由 Python 写的解释器它的执行速度是最快。PyPy 采用 JIT 技术对 Python 代码进行动态编译注意不是解释绝大部分 Python 代码都可以在 PyPy 下运行但是 PyPy 和 CPython 有一些是不同的这就导致相同的 Python 代码在两种解释器下执行可能会有不同的结果。
- JythonJython 是运行在 Java 平台上的 Python 解释器可以直接把 Python 代码编译成 Java 字节码执行。
- IronPythonIronPython 和 Jython 类似只不过 IronPython 是运行在 .Net 平台上的 Python 解释器可以直接把 Python 代码编译成 .Net 的字节码。
小结
- Python 的解释器很多但使用最广泛的还是 CPython。
- 如果要和 Java 或 .Net 平台交互最好的办法不是用 Jython 或 IronPython而是通过网络调用来交互确保各程序之间的独立性。
位和字节的关系
- 位bit又名比特简写为b数据传输是以大多是以 位 为单位进行传输的。一个位就代表一个0或1即一个二进制二进制是构成存储器的最小单位。
- 字节Byte简写为B数据存储是以字节为单位存储的字节是最小一级的信息单位。
1字节 = 8位
。每8个位bit组成一个字节Byte。
b、B、KB、MB、GB 的关系
1B = 8 bit
1kb = 1024 B
1 MB = 1024 KB
1 GB = 1024 MB
ascii、unicode、utf-8、gbk 区别
- ascii最多只能用8位来表示一个字节即
2^8 = 256
所以ASCII码最多只能表示 256 个符号。 - unicode万国码任何一个字符==两个字节。
- utf-8万国码的升级版一个中文字符==三个字节英文是一个字节欧洲的是 2 个字节。
- gbk国内版本一个中文字符==2个字节英文是一个字节。
- gbk 转 utf-8 需通过媒介 unicode。
python2 对内容进行编码默认 asciipython3 对内容进行编码的默认为utf-8。
字节码和机器码的区别
-
机器码(machine code)学名机器语言指令有时也被称为原生码Native Code是电脑的CPU可直接解读的数据。运行速度最快但也非常晦涩难懂也比较难编写。
-
字节码Bytecode是一种包含执行程序、由一序列
op 代码/数据对
组成的二进制文件。字节码是一种中间码它比机器码更抽象需要直译器转译后才能成为机器码的中间代码。
字节码是一种中间状态中间码的二进制代码文件。
Python 基础
Python 数据类型
字符串、列表、元组、字典常用的方法
参考链接Python 之数据类型
-
字符串
- 字符串用单引号(‘’)或双引号(“”)括起来。
- 字符串不可变。
find通过元素找索引可切片找不到返回 -1。
index通过元素找索引不可切片找不到报错。
split由字符串分割成列表默认按空格分割。captalize首字母大写其他字母小写。
upper全大写。
lower全小写。
title每个单词的首字母大写。
swapcase大小写翻转。
startswith判断以什么为开头可以切片整体概念。
endswith判断以什么为结尾可以切片整体概念。strip默认去掉两侧空格。
lstriprstrip去掉左边或者右边的空格。
center居中默认空格。
count统计元素的个数可以切片若没有返回0。
expandtabs将一个tab键变成8个空格如果tab前面的字符长度不足8个则补全8个。
replace(old,new,次数)替换。isdigit字符串由字母或数字组成。
isalpha字符串只由字母组成。
isalnum字符串只由数字组成。for i in str循环。
-
字典
- 字典无序不能索引。
- 字典是键值对唯一一个映射数据类型。
- 字典的键必须是可哈希的不可变类型。在同一个字典中键(key)必须是唯一的。
- 创建空字典使用
{ }
或者dict()
- 列表是有序的对象集合字典是无序的对象集合。两者之间的区别在于字典当中的元素是通过键来存取的而不是通过偏移存取。
keys输出所有的键。
valus输出所有的值。
items输出所有的键值对。
clear清空字典。
del删除键值对del 的键如果没有则报错eg:del dic["name"]
。
pop删除键值对pop根据key删除键值对并返回对应的值如果没有key则返回默认返回值eg:dic.pop("a",'无key默认返回值')。
popitem随机删键值对。
update改。
get查没有对应键时不会报错没有可以返回设定的返回值。 -
列表
- List 写在方括号之间
[]
元素用逗号隔开。 - 和字符串一样list 可以被索引和切片。
- List 可以使用+操作符进行拼接。
- List 中的元素是可以改变的。
索引切片加乘检查成员。
增加
append在后面添加。
insert按照索引添加。
expend迭代着添加list.extend(seq)
在列表末尾一次性追加另一个序列中的多个值用新列表扩展原来的列表。
删除
pop删除 list 最后一个值并返回。
remove可以按照元素去删除。
clear清空列表。
del1、可以按照索引去删除2、切片3、步长隔着删。
改1、索引2、切片先删除再迭代着添加。
list.count(obj)
统计某个元素在列表中出现的次数。
list.index(obj)
从列表中找出某个值第一个匹配项的索引位置。
list.reverse()
反向列表中元素。
list.sort([func])
对原列表进行排序。 - List 写在方括号之间
-
元组
- 与字符串一样元组的元素不能修改。
- 元组也可以被索引和切片方法和 list 一样。
- 注意构造包含0或1个元素的元组的特殊语法规则。
- 元组也可以使用+操作符进行拼接。
cmp(tuple1, tuple2)比较两个元组元素。
len(tuple)计算元组元素个数。
max(tuple)返回元组中元素最大值。
min(tuple)返回元组中元素最小值。
tuple(seq)将列表转换为元组。 -
集合
- 集合是一个无序不重复元素的序列。
- 可以使用大括号
{ }
或者set()
函数创建集合创建一个空集合必须用set()
而不是{ }
因为{ }
是用来创建一个空字典的。 - 同一集合中只能存储不可变的数据类型包括整形、浮点型、字符串、元组无法存储列表、字典、集合这些可变的数据类型否则 Python 解释器会抛出 TypeError 错误。
列举布尔值为 False 的常见值 ★★★
0, '', {}, [], (), set(), False, 不成立的表达式, None 等
数值只有 0 视为 False其余数值包括小数、负数、复数均视为 True。
字符串只有空字符串视为 False其余包括空格、制表、换行、回车等空白符也包括字符串’False’均视为 True。
常用字符串格式化哪几种
- 占位符%:
%d
表示那个位置是整数%f
表示浮点数%s
表示字符串。print('Hello,%s' % 'Python') print('Hello,%d%s%.2f' % (666, 'Python', 9.99)) # 打印Hello,666Python10.00
- format
res='{} {} {}'.format('egon',18,'male') == egon 18 male res='{1} {0} {1}'.format('egon',18,'male') == 18 egon 18 res='{name} {age} {sex}'.format(sex='male',name='egon',age=18)
Python 可变类型和不可变类型 ★★★
- 可变数据类型列表、字典、可变集合。
- 不可变数据类型数字、字符串、元组、不可变集合、布尔。
字典和集合的区别 ★★★
字典是一系列由键key和值value配对组成的元素的集合。
在 Python3.7+字典被确定为有序在 3.6 中字典有序是一个 implementation detail在 3.7 才正式成为语言特性因此 3.6 中无法 100% 确保其有序性而 3.6 之前是无序的。其长度大小可变元素可以任意地删减和改变。
相比于列表和元组字典的性能更优特别是对于查找、添加和删除操作字典都能在常数时间复杂度内完成。
而集合和字典基本相同唯一的区别就是集合没有键和值的配对是一系列无序的、唯一的元素组合。集合可以进行交集、并集、补集等操作。
字典和集合的相同点
- 字典和集合的键和值都可以为混合类型。比如键可以是 intstr 等类型。
- 均有key值且key值不重复。
- 不可放入可变的对象否则无法保证内部值不重复。
- 有初始值后均可重新赋值。
- 想要判断一个元素在不在字典或集合内可以用
value in dict/set
来判断。 - 除了创建和访问字典和集合也同样支持增加、删除、更新等操作。
- 在大量数据中查找或匹配元素时最好将数据存为字典or集合
字典和集合的不同点
- 字典访问可以直接索引键如果不存在就会抛出异常。也可以使用
get(key, default)
函数 如果键不存在可以返回一个默认值。 - 集合并不支持索引操作因为集合本质上是一个哈希表和列表不一样。
- 字典有value每个key对应一个value集合没有 value。
d = {'b': 1, 'a': 2, 'c': 10}
# 将字典按照键排序
d_sorted_by_key = sorted(d.items(), key=lambda x:x[0])
# 将字典按照值排序
d_sorted_by_value = sorted(d.items(), key=lambda x:x[1])
print(d_sorted_by_key) # [('a', 2), ('b', 1), ('c', 10)]
print(d_sorted_by_value) # [('b', 1), ('a', 2), ('c', 10)]
s = {1, 4, 6, 2}
# 对集合进行排序
print(sorted(s)) # [1, 2, 4, 6]
字典和集合的性能对比
字典和集合是进行过性能高度优化的数据结构特别是对于查找、添加和删除操作。
基础语法
列举常见的内置函数
-
abs()
返回数字的绝对值。 -
map()
根据函数对指定序列做映射函数接收两个参数一个是函数一个是可迭代对象map将传入的函数依次作用到序列的每个元素并把结果作为新的list返回。
返回值
Python2 返回列表。
Python3 返回迭代器。# 例子1 def mul(x): return x*x n = [1,2,3,4,5] res = list(map(mul,n)) print(res) # [1, 4, 9, 16, 25] # 例子2abs() 返回数字的绝对值 ret = map(abs,[-1,-5,6,-7]) print(list(ret)) # [1, 5, 6, 7]
-
filter()
接收一个函数 f(函数)和一个 list可迭代对象这个函数 f 的作用是对每个元素进行判断返回 True 或 False根据判断结果自动过滤掉不符合条件的元素返回由符合条件元素组成的新 list。def is_odd(x): return x % 2 == 1 v=list(filter(is_odd, [1, 4, 6, 7, 9, 12, 17])) print(v) # [1, 7, 9, 17]
-
map
与filter
总结- 相同点
参数: 都是一个函数名 + 可迭代对象。
返回值: 都是返回可迭代对象。 - 区别
filter
是做筛选的结果还是原来就在可迭代对象中的项。
map
是对可迭代对象中每一项做操作的结果不一定是原来就在可迭代对象中的项。
- 相同点
-
zip()
拉链函数用于将可迭代的对象作为参数将对象中对应的元素打包成一个元组然后返回由这些元组组成的列表迭代器。
如果各个迭代器的元素个数不一致则返回列表长度与最短的对象相同。print(list(zip([0,1,3],[5,6,7],['a','b']))) # [(0, 5, 'a'), (1, 6, 'b')] a = [1,2,3] b = [4,5,6] c = [4,5,6,7,8] zipped = zip(a,b) # 打包为元组的列表 [(1, 4), (2, 5), (3, 6)] zip(a,c) # 元素个数与最短的列表一致 [(1, 4), (2, 5), (3, 6)] zip(*zipped) # 与 zip 相反可理解为解压返回二维矩阵式 [(1, 2, 3), (4, 5, 6)]
-
reduce()
函数会对参数序列中元素进行累积函数将一个数据集合(链表、元组等)中的所有数据进行下列操作。
注意Python3已经将reduce() 函数从全局名字空间里移除了它现在被放置在 fucntools 模块里如果想要使用它则需要通过引入 functools 模块来调用 reduce() 函数。from functools import reduce def add(x,y): return x + y print(reduce(add,[1,2,3,4,5])) # 15 print(reduce(lambda x, y: x+y, [1,2,3,4,5])) # 15 print(reduce(add,range(1,101))) # 5050
pass
的作用
pass 是空语句是为了保持程序结构的完整性。pass 不做任何事情一般用做占位语句。
*arg
和 **kwarg
作用 ★★★★★
*args
位置参数。可以接收0个或任意多个参数当不确定调用者会传入多少个位置参数时就可以使用可变参数它会将传入的参数打包成一个元组。**kwargs
关键字参数。可以接收用参数名=参数值
的方式传入的参数传入的参数的会打包成一个字典位置参数一定要放在关键字前面。
定义函数时如果同时使用*args
和**kwargs
那么函数可以接收任意参数。
is
和 ==
的区别 ★★★
-
==
比较两边的数值是否相等即内存地址可以不一样内容一样就可以了。默认会调用对象的__eq__()
方法。 -
is
比较两边的内存地址是否相等。如果内存地址相等那么这两边其实是指向同一个内存地址。可以说如果内存地址相同那么值肯定相同但是如果值相同内存地址不一定相同。
a = "lishi"
str1 = "li"
str2 = "shi"
str3 = str1 + str2
print("a == str3",a == str3) # a == str3 True == 只需要内容相等
print("a is str3",a is str3) # a is str3 False is 需要内存地址相等
print("id(a)",id(a)) # id(a) 38565848
print("id(str3)",id(str3)) # id(str3) 39110280
在比较的时候会存在缓存和小地址池的影响详情请参考 Python 之代码块和小数据池
如何在函数中设置一个全局变量
Python 中的 global
语句是被用来声明全局变量的。
x = 2
def func():
global x
x = 1
return x
func()
print(x) # 1
isinstance
作用以及应用场景
isinstance(对象类)
判断这个对象是不是这个类或者这个类的子类的实例化类似 type()。
# 判断 a 属不属于A这个类可以判断到祖宗类
class A:
pass
class B(A):
pass
a = A()
b = B()
print(isinstance(b,A)) # ===True 判断到祖宗类
# 任何与object都是True,内部都继承object
class A:pass
a = A() # 实例化
print(isinstance(a,object)) # True
isinstance()
与 type()
区别
type()
不会认为子类是一种父类类型不考虑继承关系。isinstance()
会认为子类是一种父类类型考虑继承关系。- 如果要判断两个类型是否相同推荐使用 isinstance()。
a = 2
print(isinstance(a,int)) # True
print(isinstance(a,str)) # False
# type() 与 isinstance() 区别
class A:
pass
class B(A):
pass
print("isinstance",isinstance(A(),A)) # isinstance True
print("type",type(A()) == A) # type True
print('isinstance',isinstance(B(),A) ) # isinstance True
print('type',type(B()) == A) # type False
进阶语法
三元运算写法和应用场景 ★★★
- 语法
条件成立时的结果 if 条件 else 条件不成立时的结果
例result = 'gt' if 1>3 else 'lt' print(result) # lt
- 理解如果条件为真把 if 前面的值赋值给变量否则把 else 后面的值赋值给变量。
- 应用场景简化if语句。
lambda 表达式格式以及应用场景
Lambda 表达式函数也叫匿名函数它是用一行代码就能实现的功能简单的小型函数。Python 中的 Lambda 函数只能写一个表达式这个表达式的执行结果就是函数的返回值不用写return
关键字。Lambda函数因为没有名字所以也不会跟其他函数发生命名冲突的问题。
应用场景Lambda 函数最为主要的用途是把一个函数传入另一个高阶函数如Python内置的filter
、map
等中来为函数做解耦合增强函数的灵活性和通用性。
语法函数名 = lambda 参数1,参数2返回值
- 参数可以有多个用逗号隔开。
- 匿名函数不管逻辑多复杂只能写一行且逻辑执行结束后的内容就是返回值。
- 返回值和正常的函数一样可以是任意数据类型。
下面通过使用filter
和map
函数实现了从列表中筛选出奇数并求平方构成新列表的操作因为用到了高阶函数过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的因此 filter
和 map
函数没有跟特定的过滤和映射数据的规则耦合在一起。
items = [12, 5, 7, 10, 8, 19]
items = list(map(lambda x: x ** 2, filter(lambda x: x % 2, items)))
print(items) # [25, 49, 361]
扩展用列表的生成式来实现上面的代码会更加简单明了代码如下所示。
items = [12, 5, 7, 10, 8, 19]
items = [x ** 2 for x in items if x % 2]
print(items) # [25, 49, 361]
说一下namedtuple
的用法和作用。
点评Python 标准库的
collections
提供了很多有用的数据结构这些内容并不是每个开发者都清楚。此外deque
也是一个非常有用但又经常被忽视的类还有Counter
、OrderedDict
、defaultdict
、UserDict
等类。
在使用面向对象编程语言的时候定义类是最常见的一件事情有的时候我们会用到只有属性没有方法的类这种类的对象通常只用于组织数据并不能接收消息所以我们把这种类称为数据类或者退化的类就像C语言中的结构体那样。我们并不建议使用这种退化的类在 Python 中可以用namedtuple
命名元组来替代这种类。
from collections import namedtuple
Card = namedtuple('Card', ('suite', 'face'))
card1 = Card('红桃', 13)
card2 = Card('草花', 5)
print(f'{card1.suite}{card1.face}')
print(f'{card2.suite}{card2.face}')
命名元组与普通元组一样是不可变容器一旦将数据存储在namedtuple
的顶层属性中数据就不能再修改了也就意味着对象上的所有属性都遵循 一次写入多次读取 的原则。和普通元组不同的是命名元组中的数据有访问名称可以通过名称而不是索引来获取保存的数据不仅在操作上更加简单代码的可读性也会更好。
命名元组的本质就是一个类所以它还可以作为父类创建子类。除此之外命名元组内置了一系列的方法例如可以通过_asdict
方法将命名元组处理成字典也可以通过_replace
方法创建命名元组对象的浅拷贝。
class MyCard(Card):
def show(self):
faces = ['', 'A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']
return f'{self.suite}{faces[self.face]}'
print(Card) # <class '__main__.Card'>
card3 = MyCard('方块', 12)
print(card3.show()) # 方块Q
print(dict(card1._asdict())) # {'suite': '红桃', 'face': 13}
print(card2._replace(suite='方块')) # Card(suite='方块', face=5)
总而言之命名元组能更好的组织数据结构让代码更加清晰和可读在很多场景下是元组、字典和数据类的替代品。在需要创建占用空间更少的不可变类时命名元组就是很好的选择。
异常处理写法以及如何主动抛出异常 ★★★
# 异常处理 except
def temp_convert(var):
try:
return int(var)
except ValueError as Argument:
print ("参数没有包含数字%s"%Argument)
# 调用函数
temp_convert("xyz")
# 以10为基数的int()的无效文字:“xyz”
# 主动曝出异常raise
# raise [Exception [, args [, traceback]]]
# Exception 是异常的类型可以自己定义args 是自已提供的异常参数。
class Networkerror(RuntimeError):
def __init__(self, arg):
self.args = arg
try:
raise Networkerror("Bad hostname")
except Networkerror as e:
print(e.args)
with statement 是什么
with 语句适用于对资源进行访问的场合确保不管使用过程中是否发生异常都会执行必要的 清理操作以释放资源比如文件使用后自动关闭、线程中锁的自动获取和释放等。
with open("a.file", ) as f:
pass
断言和应用场景
断言条件成立布尔值为True时继续往下执行否则抛出异常。
一般用于满足某个条件之后才能执行否则抛出异常。
语法assert 判断条件 如果为False,报错内容
# 写API的时候继承GenericAPIView
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
# You'll need to either set these attributes,
# or override `get_queryset()`/`get_serializer_class()`.
# If you are overriding a view method, it is important that you call
# `get_queryset()` instead of accessing the `queryset` property directly,
# as `queryset` will get evaluated only once, and those results are cached
# for all subsequent requests.
queryset = None
serializer_class = None
# If you want to use object lookups other than pk, set 'lookup_field'.
# For more complex lookup requirements override `get_object()`.
lookup_field = 'pk'
lookup_url_kwarg = None
# The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
# The style to use for queryset pagination.
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get_queryset(self):
assert self.queryset is not None, (
"'%s' should either include a `queryset` attribute, "
"or override the `get_queryset()` method."
% self.__class__.__name__
)
queryset = self.queryset
if isinstance(queryset, QuerySet):
# Ensure queryset is re-evaluated on each request.
queryset = queryset.all()
return queryset
Python 特性
Python3 和 Python2 的区别 ★★★
Python 递归的最大层数
Python中默认的递归层数约为998左右(会报错) 和计算机性能有关系。
Python 中变量的作用域
Python中有四种作用域分别是局部作用域Local、嵌套作用域Embedded、全局作用域Global、内置作用域Built-in搜索一个标识符时会按照LEGB的顺序进行搜索如果所有的作用域中都没有找到这个标识符就会引发NameError
异常。
简述 深浅拷贝及其实现方法和应用场景 ★★★★★
在 Python 的赋值语句中如a = 1
赋值的其实是元素的内存地址。赋值分为以下几种情况
- 赋值的是值如
a = 1
。Python 会创建一个新的对象并把对象的内存地址返回给变量。 - 赋值的是其他变量如
b = a
。简单来说就是对于同一个对象增加一个别名。原理就是将一个对象的地址赋值给一个变量使得变量指向该内存地址。这里要分两种情况讨论- 如果赋的值是不可变数据类型如int、str等当修改 b 的值时不会影响 a 的值。
- 如果赋的值是可变数据类型如dict、tuple等当对 b 中子对象的值进行修改时因为 a 和 b 有着相同的内存地址a 的值也会被修改比如当列表a里的元素是个列表时拷贝了一个新列表b再修改新列表b里的列表元素会把原列表a的元素也修改了这会产生难以预测的后果所以需要深浅拷贝。
深浅拷贝
- 浅拷贝重新分配一块内存创建一个新的对象但里面的元素是原对象中各个子对象的引用。
- 深拷贝重新分配一块内存创建一个新的对象并且将原对象中的元素以递归的方式通过创建新的子对象拷贝到新对象中。因此新对象和原对象没有任何关联。
# 一层的情况
import copy
# 浅拷贝
li1 = [1, 2, 3]
li2 = li1.copy()
li1.append(4)
print(li1, li2) # [1, 2, 3, 4] [1, 2, 3]
# 深拷贝
li1 = [1, 2, 3]
li2 = copy.deepcopy(li1)
li1.append(4)
print(li1, li2) # [1, 2, 3, 4] [1, 2, 3]
# 多层的情况
import copy
# 浅拷贝 指向共有的地址
li1 = [1, 2, 3,[4,5],6]
li2 = li1.copy()
li1[3].append(7)
print(li1, li2) # [1, 2, 3, [4, 5, 7], 6] [1, 2, 3, [4, 5, 7], 6]
# 深拷贝 重指向
li1 = [1, 2, 3,[4,5],6]
li2 = copy.deepcopy(li1)
li1[3].append(7)
print(li1, li2) # [1, 2, 3, [4, 5, 7], 6] [1, 2, 3, [4, 5], 6]
注意这个题目出现的频率非常高但是就题而言没有什么技术含量因此在回答的时候一定要让你的答案能够超出面试官的预期。除了答出这个浅拷贝和深拷贝的区别尽量说出深拷贝的时候可能遇到的两大问题还要说出Python标准库对浅拷贝和深拷贝的支持然后可以说说列表、字典如何实现拷贝操作以及如何通过序列化和反序列的方式实现深拷贝最后还可以提到设计模式中的原型模式以及它在项目中的应用。
浅拷贝通常只复制对象本身而深拷贝不仅会复制对象还会递归的复制对象所关联的对象。
深拷贝可能会遇到两个问题
- 一是一个对象如果直接或间接的引用了自身会导致无休止的递归拷贝。
- 二是深拷贝可能对原本设计为多个对象共享的数据也进行拷贝。
Python 通过copy
模块中的copy
和deepcopy
函数来实现浅拷贝和深拷贝操作其中deepcopy
可以通过memo
字典来保存已经拷贝过的对象从而避免刚才所说的自引用递归问题此外可以通过copyreg
模块的pickle
函数来定制指定类型对象的拷贝行为。
deepcopy
函数的本质其实就是对象的一次序列化和一次返回序列化面试题中还考过用自定义函数实现对象的深拷贝操作我们可以使用pickle
模块的dumps
和loads
来做到代码如下所示。
import pickle
my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
列表的切片操作[:]
相当于实现了列表对象的浅拷贝而字典的copy
方法可以实现字典对象的浅拷贝。
对象拷贝其实是更为快捷的创建对象的方式。在Python中通过构造器创建对象属于两阶段构造首先是分配内存空间然后是初始化。在创建对象时我们也可以基于“原型”对象来创建新对象通过对原型对象的拷贝复制内存就完成了对象的创建和初始化这种做法更加高效这也就是设计模式中的原型模式。在Python中我们可以通过元类的方式来实现原型模式代码如下所示。
import copy
class PrototypeMeta(type):
"""实现原型模式的元类"""
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
# 为对象绑定clone方法来实现对象拷贝
cls.clone = lambda self, is_deep=True: \
copy.deepcopy(self) if is_deep else copy.copy(self)
class Person(metaclass=PrototypeMeta):
pass
p1 = Person()
p2 = p1.clone() # 深拷贝
p3 = p1.clone(is_deep=False) # 浅拷贝
https://zhuanlan.zhihu.com/p/338797138
参数陷阱
def func(a,b=[])
这种写法有什什么坑
函数传参为列表陷阱列表是可变数据类型可能会在函数执行过程中修改 list 里面的值。
def func(a,b=[]):
b.append(1)
print(a,b)
func(a=2) # 2 [1]
func(2) # 2 [1, 1]
func(2) # 2 [1, 1, 1]
函数的默认参数是一个list 当第一次执行的时候实例化了一个list第二次执行还是用第一次执行的时候实例化的地址存储所以三次执行的结果就是 [1, 1, 1]
想每次执行只输出[1]
默认参数应该设置为None。
简述 生成器、迭代器、可迭代对象以及应用场景 ★★★★★
-
迭代器含有
__iter__
和__next__
方法的对象。 -
生成器生成器是迭代器的一种是自己写的调动
next
把函数变成迭代器。生成器有两种- 生成器函数包含了yield关键字的函数就叫做生成器函数。
- 生成器表达式生成器表达式和列表推导式差不多我们只需要包列表推导式的
[]
改为()
这样就是一个生成器表达式了。
应用场景
1、range/xrange - py2range(1000000) 会立即创建xrange(1000000)生成器 - py3range10000000生成器 2、redis获取值hscan_iter用到了 conn = Redis(...) def hscan_iter(self, name, match=None, count=None): cursor = '0' while cursor != 0: # 去redis中获取数据12 # cursor下一次取的位置 # data本地获取的12条数数据 cursor, data = self.hscan(name, cursor=cursor,match=match, count=count) for item in data.items(): yield item 3、stark组件 def index(request): data = [ {'k1':1,'name':'alex'}, {'k1':2,'name':'老男孩'}, {'k1':3,'name':'小男孩'}, ] new_data = [] for item in data: item['email'] = "xxx@qq.com" new_data.append(item) return render(request,'xx.html',{'data':new_data})
-
可迭代对象一个类内部实现
__iter__
方法不包含__next__
方法且调用该方法后返回一个迭代器。
应用场景- wtforms中对form对象进行循环时候显示form中包含的所有字段。
class LoginForm(Form): name = simple.StringField( label='用户名', validators=[ validators.DataRequired(message='用户名不能为空.'), validators.Length(min=6, max=18, message='用户名长度必须大于%(min)d且小于%(max)d') ], widget=widgets.TextInput(), render_kw={'class': 'form-control'} ) pwd = simple.PasswordField( label='密码', validators=[ validators.DataRequired(message='密码不能为空.'), validators.Length(min=8, message='用户名长度必须大于%(min)d'), validators.Regexp(regex="^(?=.\*\[a-z\])(?=.\*\[A-Z\])(?=.\*\\d)(?=.\*\[$@$!%\*?&\])\[A-Za-z\\d$@$!%\*?&\]{8,}", message='密码至少8个字符至少1个大写字母1个小写字母1个数字和1个特殊字符') ], widget=widgets.PasswordInput(), render_kw={'class': 'form-control'} ) form = LoginForm() for item in form: print(item)
- 列表、字典、元组。当时用 for 循环时for 会自动调用
__iter__()
方法将列表、字典、元组变为迭代器。 - 判断一个可迭代对象里是否有值可以使用
for i in iter
的方式
- wtforms中对form对象进行循环时候显示form中包含的所有字段。
参考回答迭代器是实现了迭代器协议的对象。跟其他编程语言不同Python 没有用于定义协议或表示约定的关键字像
interface
、protocol
这些单词并不在Python语言的关键字列表中。Python 通过魔法方法来表示约定也就是我们所说的协议而__next__
和__iter__
这两个魔法方法就代表了迭代器协议。可以通过for-in
循环从迭代器对象中取出值也可以使用next
函数取出迭代器对象中的下一个值。生成器是迭代器的语法升级版本可以用更为简单的代码来实现一个迭代器。
生成斐波那契数列的迭代器代码示例
class Fib(object):
def __init__(self, num):
self.num = num
self.a, self.b = 0, 1
self.idx = 0
def __iter__(self):
return self
def __next__(self):
if self.idx < self.num:
self.a, self.b = self.b, self.a + self.b
self.idx += 1
return self.a
raise StopIteration()
如果用生成器的语法来改写上面的代码代码会简单优雅很多。
def fib(num):
a, b = 0, 1
for _ in range(num):
a, b = b, a + b
yield a
简述 yield 和 yield from 关键字
yield
- 函数中使用 yield可以把该函数变成生成器。一个函数如果是生成一个数组就必须把数据存储在内存中如果使用生成器则在调用的时候才生成数据可以节省内存。
- 生成器方法调用时不会立即执行。需要调用
next()
或者使用for
循环来执行。
yield from
- 为了让生成器能简易的在其他生成器中直接调用就产生了yield from。
反射以及应用场景
反射就是把字符映射到实例的变量或实例的方法然后该方法可以被调用或修改。
反射的本质(核心)基于字符串的事件驱动利用字符串的形式去操作对象/模块中成员(方法、属性)。
反射的四个重要方法
getattr
获取对象属性/对象方法。hasattr
判断对象是否有对应的属性及方法。delattr
删除指定的属性。setattr
为对象设置内容。
应用场景Django中的 CBV 就是基于反射实现的。
闭包 ★★★
如果bar函数在foo函数的代码块中定义那么我们称bar是foo的内部函数。在bar的局部作用域中可以直接访问foo局部作用域中定义的m、n变量。简单的说这种内部函数可以使用外部函数变量的行为就叫闭包。
判断闭包函数的方法改方法是否含有 __closure__
如果含有 __closure__
则说明是闭包函数。
闭包的意义与应用延迟计算。
使用闭包的时候需要注意闭包会使得函数中创建的对象不会被垃圾回收可能会导致很大的内存开销所以闭包一定不能滥用。
def foo():
m=3
n=5
def bar():
a=4
return m+n+a
return bar
bar = foo()
bar() # 12
装饰器 ★★★★★
含义装饰器本质就是函数为其他函数添加附加功能能够在不修改原函数代码的基础上在执行前后进行定制操作是闭包函数的一种应用。
作用装饰器可以用来装饰类或函数为其提供额外的能力属于设计模式中的代理模式。
原则不修改被修饰函数的代码不修改被修饰函数的调用方式。
场景
- Flask 大量使用装饰器路由系统
flask before_request
csrf认证。 - django 内置认证。
- django 缓存。
- 无参装饰器在用户登录认证中常见。
- 插入日志、性能测试、事物处理、缓存、权限验证等有了装饰器就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
简单装饰器
import functools
def wrapper(func):
@functools.wraps(func) # 不改变原函数属性
def inner(*args, **kwargs):
# 执行函数前
a = func(*args, **kwargs)
# 执行函数后
return a
return inner
# 1. 执行wapper函数并将被装饰的函数当做参数。 wapper(老index)
# 2. 将第一步的返回值重新赋值给新 index = wapper(老index)
@wrapper #index=wrapper(index)
def index(x):
return x+100
带参装饰器
from functools import wraps
from time import time
def record_time(canshu):
"""可以参数化的装饰器"""
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time()
result = func(*args, **kwargs)
print(canshu)
return result
return wrapper
return decorate
用类实现装饰器。类有__call__
魔术方法该类对象就是可调用对象可以当做装饰器来使用。
from functools import wraps
from time import time
class Record:
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time()
result = func(*args, **kwargs)
print(f'{func.__name__}执行时间: {time() - start}秒')
return result
return wrapper
进程、线程、协程
进程、线程、协程的区别以及应用场景
-
进程进程拥有自己独立的堆和栈既不共享堆亦不共享栈进程由操作系统调度。
-
线程线程拥有自己独立的栈和共享的堆共享堆不共享栈线程亦由操作系统调度。
-
协程协程是一种用户态的轻量级线程即协程是由用户程序自己控制调度的。协程避免了无意义的调度由此可以提高性能但同时协程也失去了线程使用多CPU的能力。
进程与线程的区别
- 地址空间线程是进程内的一个执行单位进程内至少有一个线程他们共享进程的地址空间而进程有自己独立的地址空间。
- 资源拥有进程是资源分配和拥有的单位同一个进程内线程共享进程的资源。
- 线程是处理器调度的基本单位但进程不是。
- 二者均可并发执行。
- 每个独立的线程有一个程序运行的入口。
协程与线程
- 一个线程可以有多个协程一个进程也可以单独拥有多个协程这样Python中则能使用多核CPU。
- 线程进程都是同步机制而协程是异步。
- 协程能保留上一次调用时的状态。
多线程的优点在于多个线程可以共享进程的内存空间所以进程间的通信非常容易实现但是如果使用官方的CPython解释器多线程受制于GIL全局解释器锁并不能利用CPU的多核特性这是一个很大的问题。使用多进程可以充分利用CPU的多核特性但是进程间通信相对比较麻烦需要使用IPC机制管道、套接字等。
多线程适合那些会花费大量时间在I/O操作上但没有太多并行计算需求且不需占用太多内存的I/O密集型应用。多进程适合执行计算密集型任务如视频编码解码、数据处理、科学计算等、可以分解为多个并行子任务并能合并子任务执行结果的任务以及在内存使用方面没有任何限制且不强依赖于I/O操作的任务。
扩展Python中实现并发编程通常有多线程、多进程和异步编程三种选择。异步编程实现了协作式并发通过多个相互协作的子程序的用户态切换实现对CPU的高效利用这种方式也是非常适合I/O密集型应用的。
Python 中如何使用线程池和进程池
https://blog.csdn.net/fenglepeng/article/details/103986048
https://blog.csdn.net/fenglepeng/article/details/103974862
-
进程相关的模块
- multiprocessing.Process
- multiprocessing.Lock
- multiprocessing.Semaphore
- multiprocessing.Event
- multiprocessing.Queue
- multiprocessing.Pool
-
线程相关模块
Python 提供了几个用于多线程编程的模块包括 thread、threading 和 Queue 等。thread和threading模块允许程序员创建和管理线程。thread模块提供了基本的线程和锁的支持threading提供了更高级别、功能更强的线程管理的功能。Queue模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
避免使用thread模块因为更高级别的threading模块更为先进对线程的支持更为完善而且使用thread模块里的属性有可能会与threading出现冲突其次低级别的thread模块的同步原语很少(实际上只有一个)而threading模块则有很多再者thread模块中当主线程结束时所有的线程都会被强制结束掉没有警告也不会有正常的清除工作至少threading模块能确保重要的子线程退出后进程才退出。
thread模块不支持守护线程当主线程退出时所有的子线程不论它们是否还在工作都会被强行退出。而threading模块支持守护线程守护线程一般是一个等待客户请求的服务器如果没有客户提出请求它就在那等着如果设定一个线程为守护线程就表示这个线程是不重要的在进程退出的时候不用等待这个线程退出- threading.Thread
- threading.RLock
- concurrent.futures.ProcessPoolExecutor 进程池提供异步调用
- concurrent.futures.ThreadPoolExecutor 线程池提供异步调用
线程池/进程池是一种用于减少线程/进程本身创建和销毁造成的开销的技术属于典型的空间换时间操作。如果应用程序需要频繁的将任务派发到线程/进程中执行线程/进程池就是必选项因为创建和释放线程/进程涉及到大量的系统底层操作开销较大如果能够在应用程序工作期间将创建和释放线程/进程的操作变成预创建和借还操作将大大减少底层开销。
线程池和进程池类似下面以线程池为例进行介绍
- 线程池在应用程序启动后立即创建一定数量的线程放入空闲队列中。这些线程最开始都处于阻塞状态不会消耗CPU资源但会占用少量的内存空间。
- 当任务到来后从队列中取出一个空闲线程把任务派发到这个线程中运行并将该线程标记为已占用。
- 当线程池中所有的线程都被占用后可以选择自动创建一定数量的新线程用于处理更多的任务也可以选择让任务排队等待直到有空闲的线程可用。
- 在任务执行完毕后线程并不退出结束而是继续保持在池中等待下一次的任务。当系统比较空闲时大部分线程长时间处于闲置状态时线程池可以自动销毁一部分线程回收系统资源。
基于这种预创建技术线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上执行次数越多每个任务所分担到的线程本身开销则越小。
一般线程池都必须具备下面几个组成部分
- 线程池管理器用于创建并管理线程池。
- 工作线程和线程队列线程池中实际执行的线程以及保存这些线程的容器。
- 任务接口将线程执行的任务抽象出来形成任务接口确保线程池与具体的任务无关。
- 任务队列线程池中保存等待被执行的任务的容器。
进程锁和线程锁的作用
-
线程锁主要用来给方法、代码块加锁。
当某个方法或者代码块使用锁时那么在同一时刻至多仅有一个线程在执行该段代码。
当有多个线程访问同一对象的加锁方法/代码块时同一时间只有一个线程在执行其余线程必须要等待当前线程执行完之后才能执行该代码段。
但是其余线程是可以访问该对象中的非加锁代码块的。 -
进程锁: 也是为了控制同一操作系统中多个进程访问一个共享资源只是因为程序的独立性各个进程是无法控制其他进程对资源的访问的但是可以使用本地系统的信号量控制操作系统基本知识。
-
分布式锁: 当多个进程不在同一个系统之中时使用分布式锁控制多个进程对资源的访问。
刁钻类问题
Python 是传值还是传引用 ★★★
Python 不允许程序员选择采用传值还是传引用。Python 参数传递采用的肯定是传对象引用的方式。这种方式相当于传值和传引用的一种综合。
- 如果函数收到的是一个可变对象比如字典或者列表的引用就能修改对象的原始值相当于通过传引用来传递对象。
- 如果函数收到的是一个不可变对象比如数字、字符或者元组的引用就不能直接修改原始对象相当于通过传值来传递对象。
类和对象能不能作为字典的 key ★★★
字典的 key 要求是任意不可变类型是可 hash 的所以一个对象和类能不能作为字典的key就取决于其有没有__hash__
方法。python 自带的所有类型中除了list、dict、set和tuple之外其余的对象都含有 __hash__
方法都能当key。
查看源代码可以看到object对象是定义了__hash__方法的而list、set和dict都把__hash__赋值为None了所以list、dict、set和tuple 不能做字典的key。
class object:
""" The most base type """
def __hash__(self, *args, **kwargs): # real signature unknown
""" Return hash(self). """
pass
class list(object):
__hash__ = None
class set(object):
__hash__ = None
class dict(object):
__hash__ = None
自定义的类和对象能不能做字典的键取决于是否含有 __hash__()
方法。
说一下你知道的 Python 编码规范
点评企业的Python编码规范基本上是参照PEP-8或谷歌开源项目风格指南来制定的后者还提到了可以使用Lint工具来检查代码的规范程度面试的时候遇到这类问题可以先说下这两个参照标准然后挑重点说一下Python编码的注意事项。
PE8 规范
-
使用4个空格而不是 tab 键进行缩进。
-
每行长度不能超过79。
-
使用空行来间隔函数和类以及函数内部的大块代码。
-
必要时候在每一行下写注释。
-
使用文档注释写出函数注释。
-
在类中总是使用self来作为默认。
-
尽量不要使用魔法方法。
-
默认使用UTF-8甚至ASCII作为编码方式。
-
换行可以使用反斜杠最好使用圆括号。
-
不要在一句import中多个库。
-
不要将多句语句写在同一行。
if/for/while
语句中即使执行语句只有一句也必须另起一行。 -
空格的使用
- 在操作符和逗号之后使用空格但是不要在括号内部使用。
- 各种右括号前不要加空格。
- 函数的左括号前不要加空格。如
Func(1)
。 - 序列的左括号前不要加空格。如
list[2]
。 - 逗号、冒号、分号前不要加空格。
- 操作符左右各加一个空格不要为了对齐增加空格。
- 函数默认参数使用的赋值符左右省略空格。
-
命名类和函数
- 使用
PascalCase(大驼峰)
来命名类。egStudentInfo
、UserInfo
。 - 使用
snake_case(蛇形命名法)
来命名函数和方法类属性方法和变量。eg:max_limit
。
- 使用
-
使用大写命名常量。
-
变量命名规范
- 以字母数字下划线任由结合。
- 不能命名太长不使用拼音中文。
- 不能以数字开头。
- 不能用 Python 关键字。
高级特性
Python垃圾回收机制 ★★★★★
点评当面试官问到这个问题的时候一个展示自己的机会就摆在面前了。你要先反问面试官“你说的是官方的CPython解释器吗”。这个反问可以展示出你了解过Python解释器的不同的实现版本而且你也知道面试官想问的是CPython。当然很多面试官对不同的Python解释器底层实现到底有什么差别也没有概念。所以千万不要觉得面试官一定比你强怀揣着这份自信可以让你更好的完成面试。
Python 提供了自动化的内存管理也就是说内存空间的分配与释放都是由 Python 解释器在运行时自动进行的自动管理内存功能极大的减轻程序员的工作负担也能够帮助程序员在一定程度上解决内存泄露的问题。以CPython解释器为例它的内存管理有三个关键点引用计数、标记清理、分代收集。
简单来说Python 垃圾回收机制主要使用 引用计数 来跟踪和回收垃圾。在 引用计数 的基础上通过 标记-清除 解决容器对象可能产生的循环引用问题。通过 分代回收 以空间换时间的方法提高垃圾回收效率。
-
引用计数
在Python的C源码中有一个名为refchain
的环状双向链表Python程序中一旦创建对象都会把这个对象添加到refchain
这个链表中。
在refchain
中的所有对象内部都有一个ob_refcnt
用来保存当前对象的引用计数器顾名思义就是自己被引用的次数。
当一个对象有新的引用时它的ob_refcnt
就会增加当引用它的对象被删除它的ob_refcnt
就会减少。引用计数为0时该对象生命就结束了。
优点:1.简单 2.实时性。
缺点:1.维护引用计数消耗资源 2.存在循环引用的话不能删除。以下情况会导致引用计数加1
- 对象被创建
- 对象被引用
- 对象作为参数传入到一个函数中
- 对象作为元素存储到一个容器中
以下情况会导致引用计数减1
- 用
del
语句显示删除对象引用 - 对象引用被重新赋值其他对象
- 一个对象离开它所在的作用域
- 持有该对象的容器自身被销毁
- 持有该对象的容器删除该对象
可以通过
sys
模块的getrefcount
函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤因此需要其他的垃圾回收算法对其进行补充。 -
标记-清楚机制
基于引用计数器进行垃圾回收非常方便和简单但他还是存在循环引用的问题导致无法正常的回收一些数据。
基本思路是先按需分配等到内存中的对象打到一定阈值之后会触发标记清除机制。
创建特殊链表专门用于保存列表、元组、字典、集合、自定义类等对象之后再去检查这个链表中的对象是否存在循环引用如果存在则让双方的引用计数器均 -1。该算法在垃圾回收时分为两个阶段
- 标记阶段遍历所有的对象如果对象是可达的被其他对象引用那么就标记该对象为可达
- 清除阶段再次遍历对象如果发现某个对象没有标记为可达则就将其回收。
CPython底层维护了两个双端链表一个链表存放着需要被扫描的容器对象姑且称之为链表A另一个链表存放着临时不可达对象姑且称之为链表B。为了实现“标记-清理”算法链表中的每个节点除了有记录当前引用计数的
ref_count
变量外还有一个gc_ref
变量这个gc_ref
是ref_count
的一个副本所以初始值为ref_count
的大小。执行垃圾回收时首先遍历链表A中的节点并且将当前对象所引用的所有对象的
gc_ref
减1
这一步主要作用是解除循环引用对引用计数的影响。再次遍历链表A中的节点如果节点的gc_ref
值为0
那么这个对象就被标记为“暂时不可达”GC_TENTATIVELY_UNREACHABLE
并被移动到链表B中如果节点的gc_ref
不为0
那么这个对象就会被标记为“可达“GC_REACHABLE
对于”可达“对象还要递归的将该节点可以到达的节点标记为”可达“链表B中被标记为”可达“的节点要重新放回到链表A中。在两次遍历之后链表B中的节点就是需要释放内存的节点。 -
分代回收
分代回收的整体思想是对标记清除中的链表进行优化将那些可能存在循引用的对象拆分到3个链表链表称为0/1/2三代每代都可以存储对象和阈值当达到阈值时就会对相应的链表中的每个对象做一次扫描。
扫描会遍历链表中的每个对象如果存在循环引用就将存在循环引用的对象的引用计数器 -1同时Python解释器也会将计数器等于0可回收和不等于0不可回收的一分为二把计数器等于0的所有对象进行回收把计数器不为0的对象放到另外一个双向链表表即分代回收的下一代
0代和1、2代的threshold和count表示的意义不同。- 0代count表示0代链表中对象的数量threshold表示0代链表对象个数阈值超过则执行一次0代扫描检查。
- 1代count表示0代链表扫描的次数threshold表示0代链表扫描的次数阈值超过则执行一次1代扫描检查。
- 2代count表示1代链表扫描的次数threshold表示1代链表扫描的次数阈值超过则执行一2代扫描检查。
分代回收扫描的门限值可以通过gc
模块的get_threshold
函数来获得该函数返回一个三元组分别表示多少次内存分配操作后会执行0
代垃圾回收多少次0
代垃圾回收后会执行1
代垃圾回收多少次1
代垃圾回收后会执行2
代垃圾回收。需要说明的是如果执行一次2
代垃圾回收那么比它年轻的代都要执行垃圾回收。如果想修改这几个门限值可以通过gc
模块的set_threshold
函数来做到。
-
缓存机制
从上文大家可以了解到当对象的引用计数器为0时就会被销毁并释放内存。
而实际上他不是这么的简单粗暴因为反复的创建和销毁会使程序的执行效率变低。Python中引入了缓存机制机制。
例如引用计数器为0时不会真正销毁对象而是将他放到一个名为 free_list 的链表中之后会再创建对象时不会在重新开辟内存而是在free_list中将之前的对象来并重置内部的值来使用。
GIL 全局解释器锁global interpreter lock ★★★★★
GIL全局解释器锁每个线程在执行时候都需要先获取GIL保证同一时刻只有一个线程可以执行代码即同一时刻只有一个线程使用CPU以此来控制同一时间内共享数据只能被一个任务所修改进而保证数据安全。也就是说多线程并不是真正意义上的同时执行。
对于io密集型任务python的多线程起到作用但对于cpu密集型任务python的多线程几乎占不到任何优势还有可能因为争夺资源而变慢。解决办法就是多进程和协程(协程也只是单CPU,但是能减小切换代价提升性能)。
GIL保护的是解释器级的数据保护用户自己的数据则需要自己加锁处理。
list dict 底层原理 ★★★★★
list 索引取值是 o(1), 查找是 o(n)
dict 取值和查找都是 o(1)
List 和 tuple
List 本质是大小可以动态改变的顺序表或者数组增删改查都是通过索引去修改对应位置的数据通过角标配合表头物理地址计算目标元素的位置因为list 里面存的数据可以是各种类型的所以 List 实际存储的是对应对象的指针或者叫内存地址。
tuple 本质上和 List 一样但是不可修改不可扩容只读。
Dict 和 Set
dict本质上也是顺序表或者数组3.6 之前和 3.6 之后实现细节不太一样。
-
在3.6版本之前Python Dict 底层在初始创建的时候采用的是indice和存储合并在一个二维数组当中。Dictionary采用哈希表原理key作为取值对象进行hash(key)操作得到哈希值然后用值进行 % 字典容量得到要插入的位置。
my_dict = {} my_dict['age'] = 26 my_dict['salary'] = 999999 # Dictionary结构 [[-4234469173262486640, '指向salary的指针', '指向999999的指针'], [1545085610920597121, '执行age的指针', '指向26的指针'], [---, ---, ---], [---, ---, ---], [---, ---, ---], [1278649844881305901, '指向name的指针', '指向kingname的指针'], [---, ---, ---], [---, ---, ---]]
取值和存放都是进行hash然后取模直接访问这个二位数组。当你要循环遍历字典的Key的时候Python底层会遍历这个二维数组如果当前行有数据那么就返回Key指针对应的内存里面的值。如果当前行没有数据那么就跳过。所以总是会遍历整个二位数组的每一行。
-
在版本3.6之后字典的底层数据结构发生了变化现在当你初始化一个空的字典以后它在底层是这样的
my_dict = {} my_dict['address'] = 'xxx' my_dict['salary'] = 999999 ## 此时的内存示意图 indices = [1, 0, None, None, None, None, 2, None] entries = [ [-5954193068542476671, '指向name的指针', '执行kingname的指针'], [9043074951938101872, '指向address的指针','指向xxx的指针'], [7324055671294268046, '指向salary的指针', '指向999999的指针'] ]
实际数据存储和索引进行分开存放indices是数据存放在二维数组的位置其他内容保持不变。这样就保证了Dictionary在添加新的键值对的时候是按照顺序进行依次存放的。当去读取dict内容的时候
hash('salary') # 7324055671294268046 hash('salary') % 8 # 6
那么我就去读indices下标为6的这个值。这个值为2然后再去读 entries 里面下标为2的这一行的数据也就是salary对应的数据了。
set 实现列表去重本质上是通过__hash__和__eq__来实现对每个元素的hash散列判断hash值是否一致一致的话判断对象是否具有一模一样的方法和属性如果都一致则去重因此set元素也必须是可hash的
深入一点去理解setset本质也是dict只不过其键值都一样实现去重其实就是这么个过程首先对key进行hash在dict中这一步是为了获取value的索引这里也一样如果索引相同说明要么数据重复了要么key发生了hash碰撞这时候就去比较两个key对应的value是否相同如果也相同确认是数据重复则去重保留最新的那个如果数据不同说明只是在当前hash算法中两个key刚好发生了hash碰撞概率相当低此时不会发生去重
Python2中使用使用开放地址法解决冲突。
开放寻址法open addressing所有的元素都存放在散列表里当产生哈希冲突时通过一个探测函数计算出下一个候选位置如果下一个获选位置还是有冲突那么不断通过探测函数往下找直到找个一个空槽来存放待插入元素。
开放地址的意思是除了哈希函数得出的地址可用当出现冲突的时候其他的地址也一样可用常见的开放地址思想的方法有线性探测再散列二次探测再散列等这些方法都是在第一选择被占用的情况下的解决方法。
补充
字典和集合的工作原理
不同于其他数据结构字典和集合的内部结构都是一张哈希表。
- 对于字典而言这张表存储了哈希值hash、键和值这 3 个元素。
- 对于集合来说区别就是哈希表内没有键和值的配对只有单一的元素了。
插入操作
每次向字典或集合插入一个元素时Python 会首先计算键的哈希值hash(key)再和 mask = PyDicMinSize - 1 做与操作计算这个元素应该插入哈希表的位置 index = hash(key) & mask。
- 如果哈希表中此位置是空的那么这个元素就会被插入其中。
- 如果此位置已被占用Python 便会比较两个元素的哈希值和键是否相等。若两者都相等则表明这个元素已经存在如果值不同则更新值。若两者中有一个不相等这种情况我们通常称为哈希冲突hash collision意思是两个元素的键不相等但是哈希值相等。这种情况下Python 便会继续寻找表中空余的位置直到找到位置为止。值得一提的是通常来说遇到这种情况最简单的方式是线性寻找即从这个位置开始挨个往后寻找空位。
查找操作
和前面的插入操作类似Python 会根据哈希值找到其应该处于的位置然后比较哈希表这个位置中元素的哈希值和键与需要查找的元素是否相等。如果相等则直接返回如果不等则继续查找直到找到空位或者抛出异常为止。
删除操作
对于删除操作Python 会暂时对这个位置的元素赋于一个特殊的值等到重新调整哈希表的大小时再将其删除。
哈希冲突
哈希冲突的发生往往会降低字典和集合操作的速度。因此为了保证其高效性字典和集合内的哈希表通常会保证其至少留有 1/3 的剩余空间。随着元素的不停插入当剩余空间小于 1/3 时Python 会重新获取更大的内存空间扩充哈希表。不过这种情况下表内所有的元素位置都会被重新排放。虽然哈希冲突和哈希表大小的调整都会导致速度减缓但是这种情况发生的次数极少。所以平均情况下这仍能保证插入、查找和删除的时间复杂度为 O(1)。
其他
如何读取大文件例如内存只有4G如何读取一个大小为8G的文件
很显然4G内存要一次性的加载大小为8G的文件是不现实的遇到这种情况必须要考虑多次读取和分批次处理。在Python中读取文件可以先通过open
函数获取文件对象在读取文件时可以通过read
方法的size
参数指定读取的大小也可以通过seek
方法的offset
参数指定读取的位置这样就可以控制单次读取数据的字节数和总字节数。除此之外可以使用内置函数iter
将文件对象处理成迭代器对象每次只读取少量的数据进行处理代码大致写法如下所示。
with open('...', 'rb') as file:
for data in iter(lambda: file.read(2097152), b''):
pass
在Linux系统上可以通过split
命令将大文件切割为小片然后通过读取切割后的小文件对数据进行处理。例如下面的命令将名为filename
的大文件切割为大小为512M的多个文件。
split -b 512m filename
如果愿意 也可以将名为filename
的文件切割为10个文件命令如下所示。
split -n 10 filename
扩展外部排序跟上述的情况非常类似由于处理的数据不能一次装入内存只能放在读写较慢的外存储器通常是硬盘上。排序-归并算法 就是一种常用的外部排序策略。在排序阶段先读入能放在内存中的数据量将其排序输出到一个临时文件依此进行将待排序数据组织为多个有序的临时文件然后在归并阶段将这些临时文件组合为一个大的有序文件这个大的有序文件就是排序的结果。