01-Python 基础

Python 编码规范

点评:企业的 Python 编码规范基本上是参照PEP-8谷歌开源项目风格指南来制定的,后者还提到了可以使用 Lint 工具来检查代码的规范程度,面试的时候遇到这类问题,可以先说下这两个参照标准,然后挑重点说一下 Python 编码的注意事项。

PE8 规范

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

  2. 不要在一句代码中 import 多个库。

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

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

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

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

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

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

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

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

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

  12. 空格的使用:

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

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

  15. 变量命名规范:

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

Python 基础

Python 数据类型

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

参考链接:Python 之数据类型

  • 字符串:

    1. 字符串用单引号(‘’)或双引号(“”)括起来。
    2. 字符串不可变。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    索引,切片,加
    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. List 写在方括号之间 [],元素用逗号隔开。
    2. 和字符串一样,list 可以被索引和切片。
    3. List 可以使用 + 操作符进行拼接。
    4. List 中的元素是可以改变的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    索引,切片,加,乘,检查成员。
    增加:
    append:在后面添加。 ★★★
    insert:按照索引添加。 ★★★
    expend:迭代着添加。eg:list.extend(seq) 在列表末尾一次性追加另一个序列中的多个值(用新列表扩展原来的列表)。
    删除:
    pop:删除 list 最后一个值并返回。★★★
    pop(index):按照索引删除。 ★★★
    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. 元组也可以使用 + 操作符进行拼接。
    1
    2
    3
    4
    5
    cmp(tuple1, tuple2):比较两个元组元素。
    len(tuple):计算元组元素个数。
    max(tuple):返回元组中元素最大值。
    min(tuple):返回元组中元素最小值。
    tuple(seq):将列表转换为元组。
  • 字典:

    1. 字典无序(不能索引)。
    2. 字典是键值对,唯一一个映射数据类型。
    3. 字典的键必须是可哈希的不可变类型。在同一个字典中,键(key)必须是唯一的。
    4. 创建空字典使用 { } 或者 dict()
    5. 列表是有序的对象集合,字典是无序的对象集合。两者之间的区别在于:字典当中的元素是通过键来存取的,而不是通过偏移存取。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    keys: 输出所有的键。       ★★★
    valus:输出所有的值。 ★★★
    items:输出所有的键值对。 ★★★
    clear:清空字典。
    del:删除键值对,del 的键如果没有则报错,eg:del dic["name"]。
    pop:删除键值对,pop 根据 key 删除键值对,并返回对应的值,如果没有 key 则返回默认返回值,eg:dic.pop("a",'无key默认返回值')。
    popitem:随机删键值对。
    update:改。 ★★★
    get:查,没有对应键时不会报错,没有可以返回设定的返回值。★★★
  • 集合

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

列举布尔值为 False 的常见值 ★★★

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

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

常用字符串格式化哪几种

  1. 占位符%: %d 表示那个位置是整数;%f 表示浮点数;%s 表示字符串。

    1
    2
    print('Hello,%s' % 'Python')  
    print('Hello,%d%s%.2f' % (666, 'Python', 9.99)) # 打印:Hello,666Python10.00
  2. format

    1
    2
    3
    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。
1
2
3
4
5
6
7
8
9
10
11
12
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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 例子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。★★★

    1
    2
    3
    4
    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():拉链函数,用于将可迭代的对象作为参数,将对象中对应的元素打包成一个元组,然后返回由这些元组组成的列表迭代器。
    如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同。

    1
    2
    3
    4
    5
    6
    7
    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()

    1
    2
    3
    4
    5
    6
    7
    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 比较两边的内存地址是否相等。如果内存地址相等,那么这两边其实是指向同一个内存地址。可以说如果内存地址相同,那么值肯定相同,但是如果值相同,内存地址不一定相同。

1
2
3
4
5
6
7
8
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 语句是被用来声明全局变量的。

1
2
3
4
5
6
7
x = 2  
def func():
global x
x = 1
return x
func()
print(x) # 1

isinstance 作用以及应用场景

isinstance(对象,类) 判断这个对象是不是这个类或者这个类的子类的实例化,类似 type()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 判断 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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 条件不成立时的结果 例:
    1
    2
    result = 'gt' if 1>3 else 'lt'
    print(result) # lt
  • 理解:如果条件为真,把 if 前面的值赋值给变量,否则把 else 后面的值赋值给变量。
  • 应用场景:简化 if 语句。

lambda 表达式格式以及应用场景

Lambda 表达式(函数)也叫匿名函数,它是用一行代码就能实现的功能简单的小型函数。Python 中的 Lambda 函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写 return 关键字。Lambda 函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。

应用场景:Lambda 函数最主要的用途是把一个函数传入另一个高阶函数(如 Python 内置的 filtermapsort 等)中来为函数做解耦合,增强函数的灵活性和通用性

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

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

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

1
2
3
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]

扩展:用列表推导式来实现上面的代码会更加简单明了,代码如下所示。

1
2
3
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(命名元组)来替代这种类。

1
2
3
4
5
6
7
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() 方法创建命名元组对象的浅拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
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)

总而言之,命名元组能更好的组织数据结构,让代码更加清晰和可读,在很多场景下是元组、字典和数据类的替代品。在需要创建占用空间更少的不可变类时,命名元组就是很好的选择。

是否使用过 functools 中的函数?其作用是什么?★★★

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 用于修复装饰器
import functools


def deco(func):
@functools.wraps(func) # 加在最内层函数正上方
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper


@deco
def index():
"""哈哈哈哈"""
x = 10
print('from index')

print(index.__name__)
print(index.__doc__)

# 加 @functools.wraps
# index
# 哈哈哈哈

# 不加 @functools.wraps
# wrapper
# None

异常处理写法以及如何主动抛出异常 ★★★

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 异常处理 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 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的 清理 操作以释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。

1
2
with open("a.file", "r") as f:
pass

断言和应用场景

断言:条件成立(布尔值为True)时继续往下执行,否则抛出异常。

一般用于:满足某个条件之后,才能执行,否则抛出异常。

语法:assert 判断条件 如果为 False,报错内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 写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 异常。

参数陷阱

def func(a,b=[]) 这种写法有什什么坑?

函数传参为列表陷阱,列表是可变数据类型,可能会在函数执行过程中修改 list 里面的值。

1
2
3
4
5
6
7
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。

简述 深浅拷贝及其实现方法和应用场景 ★★★★★

在 Python 的赋值语句中,如 a = 1,赋值的其实是元素的内存地址。赋值分为以下几种情况:

  • 赋值的是值,如 a = 1。Python 会创建一个新的对象,并把对象的内存地址返回给变量。
  • 赋值的是其他变量,如 b = a。简单来说就是对于同一个对象,增加一个别名。原理就是将一个对象的地址赋值给一个变量,使得变量指向该内存地址。这里要分两种情况讨论:
    • 如果赋的值是不可变数据类型(如 int、str 等):当修改 b 的值时,不会影响 a 的值。
    • 如果赋的值是可变数据类型(如 dict、tuple 等):当对 b 中子对象的值进行修改时,因为 a 和 b 有着相同的内存地址,a 的值也会被修改,比如当 列表a 拷贝到了列表b,如果修改 列表b 里的元素,会把 原列表a 的元素也修改了,这会产生难以预测的后果,所以需要深浅拷贝。

深浅拷贝

  • 浅拷贝:重新分配一块内存,创建一个新的对象,但里面的元素是原对象中各个子对象的引用。
  • 深拷贝:重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 一层的情况:
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 来做到,代码如下:

1
2
3
import pickle

my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))

列表的切片操作 [:] 相当于实现了列表对象的浅拷贝,而字典的 copy 方法可以实现字典对象的浅拷贝。

对象拷贝其实是更为快捷的创建对象的方式。在 Python 中,通过构造器创建对象属于两阶段构造,首先是分配内存空间,然后是初始化。

在创建对象时,我们也可以基于“原型”对象来创建新对象,通过对原型对象的拷贝(复制内存)就完成了对象的创建和初始化,这种做法更加高效,这也就是设计模式中的原型模式。

在 Python 中,我们可以通过元类的方式来实现原型模式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

简述 生成器、迭代器、可迭代对象以及应用场景 ★★★★★

  • 迭代器:含有 __iter____next__ 方法的对象。

  • 生成器:生成器是迭代器的一种,是自己写的,调动 next 把函数变成迭代器。生成器有两种:

    • 生成器函数:包含了 yield 关键字的函数就叫做生成器函数。
    • 生成器表达式:生成器表达式和列表推导式差不多,我们只需要包列表推导式的 [] 改为 (),这样就是一个生成器表达式了。
      1. 列表推导式与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存
      2. 生成器表达式是按需计算(或称惰性求值,延迟计算)需要的时候才计算
      3. 列表推导式是立即返回值

    应用场景:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    1range/xrange
    - py2:range(1000000) 会立即创建,xrange(1000000) 生成器
    - py3range(1000000) 生成器

    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 中包含的所有字段。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      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 i in 循环从迭代器对象中取出值,也可以使用 next 函数取出迭代器对象中的下一个值。生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。

生成斐波那契数列的迭代器代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

如果用生成器的语法来改写上面的代码,代码会简单优雅很多。

1
2
3
4
5
def fib(num):
a, b = 0, 1
for _ in range(num):
a, b = b, a + b
yield a

简述 yieldyield 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__ 则说明是闭包函数。

闭包的意义与应用:延迟计算。

使用闭包的时候需要注意,闭包会使得函数中创建的对象不会被垃圾回收,可能会导致很大的内存开销,所以闭包一定不能滥用

1
2
3
4
5
6
7
8
9
10
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 缓存。
  • 无参装饰器在用户登录认证中常见。
  • 插入日志、性能测试、事物处理、缓存、权限验证等,有了装饰器,就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

简单装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

带参装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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__ 魔术方法,该类对象就是可调用对象,可以当做装饰器来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

装饰器的运行顺序

1
2
3
4
5
@wrapper1    # 3 func = wrapper1(func)func = wrapper2(inner2) --> f1 = inner2 --> 打印 in wrapper1 --> func = inner1
@wrapper2 # 2 func = wrapper2(func)func = wrapper2(inner3) --> f2 = inner3 --> 打印 in wrapper2 --> func = inner2
@wrapper3 # 1 func = wrapper3(func) --> f3 = func --> 打印in wrapper3 --> func = inner3
def func(): # 先执行离被装饰函数最近的那个装饰器
print('in func')

结果

1
2
3
4
5
6
7
in inner1
in inner2
in inner3
in func
333
222
111

进程、线程、协程 ★★★★★

进程、线程、协程的区别以及应用场景

  • 进程:进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
  • 线程:线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度。
  • 协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。协程避免了无意义的调度,由此可以提高性能;但同时协程也失去了线程使用多 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 不能做字典的 key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

其他

如何读取大文件,例如内存只有 4G,如何读取一个大小为 8G 的文件?

很显然 4G 内存一次性加载大小为 8G 的文件是不现实的,遇到这种情况必须要考虑多次读取和分批次处理。

在 Python 中读取文件可以先通过 open 函数获取文件对象,在读取文件时,可以通过 read 方法的 size 参数指定读取的大小,也可以通过 seek 方法的 offset 参数指定读取的位置,这样就可以控制单次读取数据的字节数和总字节数。

除此之外,可以使用内置函数 iter 将文件对象处理成迭代器对象,每次只读取少量的数据进行处理,代码大致写法如下所示。

1
2
3
with open('...', 'rb') as file:
for data in iter(lambda: file.read(2097152), b''):
pass

在 Linux 系统上,可以通过 split 命令将大文件切割为小片,然后通过读取切割后的小文件对数据进行处理。例如下面的命令将名为 filename 的大文件切割为大小为 512M 的多个文件。

1
split -b 512m filename

如果愿意,也可以将名为 filename 的文件切割为 10 个文件,命令如下所示。

1
split -n 10 filename

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


01-Python 基础
https://flepeng.github.io/interview-20-开发语言类-21-Python-01-Python-基础/
作者
Lepeng
发布于
2020年8月8日
许可协议