面试之 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开发而来。

简述解释型和编译型编程语言

  • 解释型:就是边解释边执行(Python,php)。
  • 编译型:编译后再执行(c、java、c#)。

Python 的解释器种类以及相关特点

  • CPython:官方版本的解释器。使用C语言开发的,所以叫 CPython。在命令行下运行 python 就是启动 CPython 解释器。CPython 是使用最广的 Python 解释器。
  • IPython:IPython 是基于 CPython 之上的一个交互式解释器,也就是说,IPython 只是在交互方式上有所增强,但是执行 Python 代码的功能和 CPython 是完全一样的。CPython 用 >>> 作为提示符,而IPython用 In [序号]: 作为提示符。
  • PyPy:由 Python 写的解释器,它的执行速度是最快。PyPy 采用 JIT 技术,对 Python 代码进行动态编译(注意不是解释),绝大部分 Python 代码都可以在 PyPy 下运行,但是 PyPy 和 CPython 有一些是不同的,这就导致相同的 Python 代码在两种解释器下执行可能会有不同的结果。
  • Jython:Jython 是运行在 Java 平台上的 Python 解释器,可以直接把 Python 代码编译成 Java 字节码执行。
  • IronPython:IronPython 和 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 对内容进行编码默认 ascii,python3 对内容进行编码的默认为utf-8。

字节码和机器码的区别

  • 机器码(machine code),学名机器语言指令,有时也被称为原生码(Native Code),是电脑的CPU可直接解读的数据。运行速度最快,但也非常晦涩难懂,也比较难编写。

  • 字节码(Bytecode)是一种包含执行程序、由一序列 op 代码/数据对 组成的二进制文件。字节码是一种中间码,它比机器码更抽象,需要直译器转译后才能成为机器码的中间代码。
    字节码是一种中间状态(中间码)的二进制代码(文件)。

Python 基础

Python 数据类型

字符串、列表、元组、字典常用的方法

参考链接:Python 之数据类型

  • 字符串:

    1. 字符串用单引号(‘’)或双引号(“”)括起来。
    2. 字符串不可变。

    find:通过元素找索引,可切片,找不到返回 -1。
    index:通过元素找索引,不可切片,找不到报错。
    split:由字符串分割成列表,默认按空格分割。

    captalize:首字母大写,其他字母小写。
    upper:全大写。
    lower:全小写。
    title:每个单词的首字母大写。
    swapcase:大小写翻转。
    startswith:判断以什么为开头,可以切片,整体概念。
    endswith:判断以什么为结尾,可以切片,整体概念。

    strip:默认去掉两侧空格。
    lstrip,rstrip:去掉左边或者右边的空格。
    center:居中,默认空格。
    count:统计元素的个数,可以切片,若没有返回0。
    expandtabs:将一个tab键变成8个空格,如果tab前面的字符长度不足8个,则补全8个。
    replace(old,new,次数):替换。

    isdigit:字符串由字母或数字组成。
    isalpha:字符串只由字母组成。
    isalnum:字符串只由数字组成。

    for i in str:循环。

  • 字典:

    1. 字典无序(不能索引)。
    2. 字典是键值对,唯一一个映射数据类型。
    3. 字典的键必须是可哈希的不可变类型。在同一个字典中,键(key)必须是唯一的。
    4. 创建空字典使用 { } 或者 dict()
    • 列表是有序的对象集合,字典是无序的对象集合。两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。

    keys:输出所有的键。
    valus:输出所有的值。
    items:输出所有的键值对。
    clear:清空字典。
    del:删除键值对,del 的键如果没有则报错,eg:del dic["name"]
    pop:删除键值对,pop根据key删除键值对,并返回对应的值,如果没有key则返回默认返回值,eg:dic.pop("a",'无key默认返回值')。
    popitem:随机删键值对。
    update:改。
    get:查,没有对应键时不会报错,没有可以返回设定的返回值。

  • 列表:

    1. List 写在方括号之间[],元素用逗号隔开。
    2. 和字符串一样,list 可以被索引和切片。
    3. List 可以使用+操作符进行拼接。
    4. List 中的元素是可以改变的。

    索引,切片,加,乘,检查成员。
    增加:
    append:在后面添加。
    insert:按照索引添加。
    expend:迭代着添加,list.extend(seq):在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)。
    删除:
    pop:删除 list 最后一个值并返回。
    remove:可以按照元素去删除。
    clear:清空列表。
    del:1、可以按照索引去删除;2、切片;3、步长(隔着删)。
    改:1、索引;2、切片:先删除,再迭代着添加。
    list.count(obj):统计某个元素在列表中出现的次数。
    list.index(obj):从列表中找出某个值第一个匹配项的索引位置。
    list.reverse():反向列表中元素。
    list.sort([func]):对原列表进行排序。

  • 元组

    1. 与字符串一样,元组的元素不能修改。
    2. 元组也可以被索引和切片,方法和 list 一样。
    3. 注意构造包含0或1个元素的元组的特殊语法规则。
    4. 元组也可以使用+操作符进行拼接。

    cmp(tuple1, tuple2):比较两个元组元素。
    len(tuple):计算元组元素个数。
    max(tuple):返回元组中元素最大值。
    min(tuple):返回元组中元素最小值。
    tuple(seq):将列表转换为元组。

  • 集合

    1. 集合是一个无序不重复元素的序列。
    2. 可以使用大括号 { } 或者 set() 函数创建集合,创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典的。
    3. 同一集合中,只能存储不可变的数据类型,包括整形、浮点型、字符串、元组,无法存储列表、字典、集合这些可变的数据类型,否则 Python 解释器会抛出 TypeError 错误。
列举布尔值为 False 的常见值 ★★★

0, '', {}, [], (), set(), False, 不成立的表达式, None 等

数值只有 0 视为 False,其余数值(包括小数、负数、复数)均视为 True。
字符串只有空字符串视为 False,其余(包括空格、制表、换行、回车等空白符,也包括字符串’False’)均视为 True。

常用字符串格式化哪几种
  1. 占位符%: %d 表示那个位置是整数;%f 表示浮点数;%s 表示字符串。
    print('Hello,%s' % 'Python')  
    print('Hello,%d%s%.2f' % (666, 'Python', 9.99)) # 打印:Hello,666Python10.00
    
  2. 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 之前是无序的。其长度大小可变,元素可以任意地删减和改变。

相比于列表和元组,字典的性能更优,特别是对于查找、添加和删除操作,字典都能在常数时间复杂度内完成。

而集合和字典基本相同,唯一的区别,就是集合没有键和值的配对,是一系列无序的、唯一的元素组合。集合可以进行交集、并集、补集等操作。

字典和集合的相同点:

  • 字典和集合的键和值都可以为混合类型。比如,键可以是 int,str 等类型。
  • 均有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]
    
    # 例子2:abs()  返回数字的绝对值  
    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]
    
  • mapfilter总结

    • 相同点:
      参数: 都是一个函数名 + 可迭代对象。
      返回值: 都是返回可迭代对象。
    • 区别:
      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内置的filtermap等)中来为函数做解耦合,增强函数的灵活性和通用性。

语法:函数名 = lambda 参数1,参数2:返回值

  • 参数可以有多个,用逗号隔开。
  • 匿名函数不管逻辑多复杂,只能写一行,且逻辑执行结束后的内容就是返回值。
  • 返回值和正常的函数一样可以是任意数据类型。

下面通过使用filtermap函数,实现了从列表中筛选出奇数并求平方构成新列表的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此 filtermap 函数没有跟特定的过滤和映射数据的规则耦合在一起。

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也是一个非常有用但又经常被忽视的类,还有CounterOrderedDictdefaultdictUserDict等类。

在使用面向对象编程语言的时候,定义类是最常见的一件事情,有的时候,我们会用到只有属性没有方法的类,这种类的对象通常只用于组织数据,并不能接收消息,所以我们把这种类称为数据类或者退化的类,就像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 的区别 ★★★

参考:Python2 和 Python3 的区别

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模块中的copydeepcopy函数来实现浅拷贝和深拷贝操作,其中deepcopy可以通过memo字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题;此外,可以通过copyreg模块的pickle函数来定制指定类型对象的拷贝行为。

deepcopy函数的本质其实就是对象的一次序列化和一次返回序列化,面试题中还考过用自定义函数实现对象的深拷贝操作,我们可以使用pickle模块的dumpsloads来做到,代码如下所示。

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
    - py2:range(1000000) 会立即创建,xrange(1000000)生成器
    - py3:range(10000000)生成器
    
    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 的方式

参考回答:迭代器是实现了迭代器协议的对象。跟其他编程语言不同,Python 没有用于定义协议或表示约定的关键字,像interfaceprotocol这些单词并不在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

  1. 函数中使用 yield,可以把该函数变成生成器。一个函数如果是生成一个数组,就必须把数据存储在内存中,如果使用生成器,则在调用的时候才生成数据,可以节省内存。
  2. 生成器方法调用时,不会立即执行。需要调用 next() 或者使用 for 循环来执行。

yield from

  1. 为了让生成器能简易的在其他生成器中直接调用,就产生了yield from。

反射以及应用场景

反射就是把字符映射到实例的变量或实例的方法,然后该方法可以被调用或修改。

反射的本质(核心):基于字符串的事件驱动,利用字符串的形式去操作对象/模块中成员(方法、属性)。

反射的四个重要方法:

  1. getattr 获取对象属性/对象方法。
  2. hasattr 判断对象是否有对应的属性及方法。
  3. delattr 删除指定的属性。
  4. 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的能力。

进程与线程的区别:

  1. 地址空间:线程是进程内的一个执行单位,进程内至少有一个线程,他们共享进程的地址空间,而进程有自己独立的地址空间。
  2. 资源拥有:进程是资源分配和拥有的单位,同一个进程内线程共享进程的资源。
  3. 线程是处理器调度的基本单位,但进程不是。
  4. 二者均可并发执行。
  5. 每个独立的线程有一个程序运行的入口。

协程与线程:

  1. 一个线程可以有多个协程,一个进程也可以单独拥有多个协程,这样Python中则能使用多核CPU。
  2. 线程进程都是同步机制,而协程是异步。
  3. 协程能保留上一次调用时的状态。

多线程的优点在于多个线程可以共享进程的内存空间,所以进程间的通信非常容易实现;但是如果使用官方的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资源,但会占用少量的内存空间。
  • 当任务到来后,从队列中取出一个空闲线程,把任务派发到这个线程中运行,并将该线程标记为已占用。
  • 当线程池中所有的线程都被占用后,可以选择自动创建一定数量的新线程,用于处理更多的任务,也可以选择让任务排队等待直到有空闲的线程可用。
  • 在任务执行完毕后,线程并不退出结束,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程长时间处于闲置状态时,线程池可以自动销毁一部分线程,回收系统资源。

基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小。

一般线程池都必须具备下面几个组成部分:

  1. 线程池管理器:用于创建并管理线程池。
  2. 工作线程和线程队列:线程池中实际执行的线程以及保存这些线程的容器。
  3. 任务接口:将线程执行的任务抽象出来,形成任务接口,确保线程池与具体的任务无关。
  4. 任务队列:线程池中保存等待被执行的任务的容器。
进程锁和线程锁的作用
  • 线程锁:主要用来给方法、代码块加锁。
    当某个方法或者代码块使用锁时,那么在同一时刻至多仅有一个线程在执行该段代码。
    当有多个线程访问同一对象的加锁方法/代码块时,同一时间只有一个线程在执行,其余线程必须要等待当前线程执行完之后才能执行该代码段。
    但是,其余线程是可以访问该对象中的非加锁代码块的。

  • 进程锁: 也是为了控制同一操作系统中多个进程访问一个共享资源,只是因为程序的独立性,各个进程是无法控制其他进程对资源的访问的,但是可以使用本地系统的信号量控制(操作系统基本知识)。

  • 分布式锁: 当多个进程不在同一个系统之中时,使用分布式锁控制多个进程对资源的访问。

刁钻类问题

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 规范

  1. 使用4个空格而不是 tab 键进行缩进。

  2. 每行长度不能超过79。

  3. 使用空行来间隔函数和类,以及函数内部的大块代码。

  4. 必要时候,在每一行下写注释。

  5. 使用文档注释,写出函数注释。

  6. 在类中总是使用self来作为默认。

  7. 尽量不要使用魔法方法。

  8. 默认使用UTF-8,甚至ASCII作为编码方式。

  9. 换行可以使用反斜杠,最好使用圆括号。

  10. 不要在一句import中多个库。

  11. 不要将多句语句写在同一行。if/for/while 语句中,即使执行语句只有一句,也必须另起一行。

  12. 空格的使用:

    • 在操作符和逗号之后使用空格,但是不要在括号内部使用。
    • 各种右括号前不要加空格。
    • 函数的左括号前不要加空格。如 Func(1)
    • 序列的左括号前不要加空格。如 list[2]
    • 逗号、冒号、分号前不要加空格。
    • 操作符左右各加一个空格,不要为了对齐增加空格。
    • 函数默认参数使用的赋值符左右省略空格。
  13. 命名类和函数:

    • 使用 PascalCase(大驼峰) 来命名类。eg:StudentInfoUserInfo
    • 使用 snake_case(蛇形命名法) 来命名函数和方法,类属性(方法和变量)。eg: max_limit
  14. 使用大写命名常量。

  15. 变量命名规范:

    • 以字母,数字,下划线任由结合。
    • 不能命名太长,不使用拼音,中文。
    • 不能以数字开头。
    • 不能用 Python 关键字。

高级特性

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_refref_count的一个副本,所以初始值为ref_count的大小。

    执行垃圾回收时,首先遍历链表A中的节点,并且将当前对象所引用的所有对象的gc_ref1,这一步主要作用是解除循环引用对引用计数的影响。再次遍历链表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的;

深入一点去理解set:set本质也是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

扩展:外部排序跟上述的情况非常类似,由于处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。排序-归并算法 就是一种常用的外部排序策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件,然后在归并阶段将这些临时文件组合为一个大的有序文件,这个大的有序文件就是排序的结果。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值