更新了基础篇面试题

pull/2/head
jackfrued 2020-05-04 00:21:43 +08:00
parent c141140d1d
commit e1eab04b04
1 changed files with 276 additions and 1 deletions

View File

@ -748,5 +748,280 @@ Python中有四种作用域分别是局部作用域**L**ocal、嵌套
15. Python 3中字典的`keys`、`values`、`items`方法都不再返回`list`对象,而是返回`view object`,内置的`map`、`filter`等函数也不再返回`list`对象,而是返回迭代器对象。
16. Python 3标准库中某些模块的名字跟Python 2是有区别的而在三方库方面有些三方库只支持Python 2有些只能支持Python 3。
####
####题目31谈谈你对“猴子补丁”monkey patching的理解。
“猴子补丁”是动态类型语言的一个特性代码运行时在不修改源代码的前提下改变代码中的方法、属性、函数等以达到热补丁hot patch的效果。很多系统的安全补丁也是通过猴子补丁的方式来实现的但实际开发中应该避免对猴子补丁的使用以免造成代码行为不一致的问题。
在使用`gevent`库的时候,我们会在代码开头的地方执行`gevent.monkey.patch_all()`,这行代码的作用是把标准库中的`socket`模块给替换掉,这样我们在使用`socket`的时候,不用修改任何代码就可以实现对代码的协程化,达到提升性能的目的,这就是对猴子补丁的应用。
另外,如果希望用`ujson`三方库替换掉标准库中的`json`,也可以使用猴子补丁的方式,代码如下所示。
```Python
import json, ujson
json.__name__ = 'ujson'
json.dumps = ujson.dumps
json.loads = ujson.loads
```
单元测试中的`Mock`技术也是对猴子补丁的应用Python中的`unittest.mock`模块就是解决单元测试中用`Mock`对象替代被测对象所依赖的对象的模块。
#### 题目32阅读下面的代码说出运行结果。
```Python
class A:
def who(self):
print('A', end='')
class B(A):
def who(self):
super(B, self).who()
print('B', end='')
class C(A):
def who(self):
super(C, self).who()
print('C', end='')
class D(B, C):
def who(self):
super(D, self).who()
print('D', end='')
item = D()
item.who()
```
> **点评**:这道题考查到了两个知识点:
>
> 1. Python中的MRO方法解析顺序。在没有多重继承的情况下向对象发出一个消息如果对象没有对应的方法那么向上父类搜索的顺序是非常清晰的。如果向上追溯到`object`类(所有类的父类)都没有找到对应的方法,那么将会引发`AttributeError`异常。但是有多重继承尤其是出现菱形继承钻石继承的时候向上追溯到底应该找到那个方法就得确定MRO。Python 3中的类以及Python 2中的新式类使用[C3算法](<https://www.jianshu.com/p/a08c61abe895>)来确定MRO它是一种类似于广度优先搜索的方法Python 2中的旧式类经典类使用深度优先搜索来确定MRO。在搞不清楚MRO的情况下可以使用类的`mro`方法或`__mro__`属性来获得类的MRO列表。
> 2. `super()`函数的使用。在使用`super`函数时,可以通过`super(类型, 对象)`来指定对哪个对象以哪个类为起点向上搜索父类方法。所以上面`B`类代码中的`super(B, self).who()`表示以B类为起点向上搜索`self`D类对象的`who`方法,所以会找到`C`类中的`who`方法,因为`D`类对象的MRO列表是`D --> B --> C --> A --> object`。
```
ACBD
```
#### 题目33编写一个函数实现对逆波兰表达式求值不能使用Python的内置函数。
> **点评**[逆波兰表达式](<https://baike.baidu.com/item/%E9%80%86%E6%B3%A2%E5%85%B0%E5%BC%8F/128437>)也称为“后缀表达式”,相较于平常我们使用的“中缀表达式”,逆波兰表达式不需要括号来确定运算的优先级,例如`5 * (2 + 3)`对应的逆波兰表达式是`5 2 3 + *`。逆波兰表达式求值需要借助栈结构,扫描表达式遇到运算数就入栈,遇到运算符就出栈两个元素做运算,将运算结果入栈。表达式扫描结束后,栈中只有一个数,这个数就是最终的运算结果,直接出栈即可。
```Python
import operator
class Stack:
"""栈FILO"""
def __init__(self):
self.elems = []
def push(self, elem):
"""入栈"""
self.elems.append(elem)
def pop(self):
"""出栈"""
return self.elems.pop()
@property
def is_empty(self):
"""检查栈是否为空"""
return len(self.elems) == 0
def eval_suffix(expr):
"""逆波兰表达式求值"""
operators = {
'+': operator.add,
'-': operator.sub,
'*': operator.mul,
'/': operator.truediv
}
stack = Stack()
for item in expr.split():
if item.isdigit():
stack.push(float(item))
else:
num2 = stack.pop()
num1 = stack.pop()
stack.push(operators[item](num1, num2))
return stack.pop()
```
#### 题目34Python中如何实现字符串替换操作
Python中实现字符串替换大致有两类方法字符串的`replace`方法和正则表达式的`sub`方法。
方法一:使用字符串的`replace`方法。
```Python
message = 'hello, world!'
print(message.replace('o', 'O').replace('l', 'L').replace('he', 'HE'))
```
方法二:使用正则表达式的`sub`方法。
```Python
import re
message = 'hello, world!'
pattern = re.compile('[aeiou]')
print(pattern.sub('#', message))
```
> **扩展**:还有一个相关的面试题,对保存文件名的列表排序,要求文件名按照字母表和数字大小进行排序,例如对于列表`filenames = ['a12.txt', 'a8.txt', 'b10.txt', 'b2.txt', 'b19.txt', 'a3.txt'] `,排序的结果是`['a3.txt', 'a8.txt', 'a12.txt', 'b2.txt', 'b10.txt', 'b19.txt']`。提示一下,可以通过字符串替换的方式为文件名补位,根据补位后的文件名用`sorted`函数来排序,大家可以思考下这个问题如何解决。
#### 题目35如何剖析Python代码的执行性能
剖析代码性能可以使用Python标准库中的`cProfile`和`pstats`模块,`cProfile`的`run`函数可以执行代码并收集统计信息,创建出`Stats`对象并打印简单的剖析报告。`Stats`是`pstats`模块中的类,它是一个统计对象。当然,也可以使用三方工具`line_profiler`和`memory_profiler`来剖析每一行代码耗费的时间和内存这两个三方工具都会用非常友好的方式输出剖析结构。如果使用PyCharm可以利用“Run”菜单的“Profile”菜单项对代码进行性能分析PyCharm中可以用表格或者调用图Call Graph的方式来显示性能剖析的结果。
下面是使用`cProfile`剖析代码性能的例子。
`example.py`
```Python
import cProfile
def is_prime(num):
for factor in range(2, int(num ** 0.5) + 1):
if num % factor == 0:
return False
return True
class PrimeIter:
def __init__(self, total):
self.counter = 0
self.current = 1
self.total = total
def __iter__(self):
return self
def __next__(self):
if self.counter < self.total:
self.current += 1
while not is_prime(self.current):
self.current += 1
self.counter += 1
return self.current
raise StopIteration()
cProfile.run('list(PrimeIter(10000))')
```
如果使用`line_profiler`三方工具,可以直接剖析`is_prime`函数每行代码的性能,需要给`is_prime`函数添加一个`profiler`装饰器,代码如下所示。
```Python
@profiler
def is_prime(num):
for factor in range(2, int(num ** 0.5) + 1):
if num % factor == 0:
return False
return True
```
安装`line_profiler`。
```Bash
pip install line_profiler
```
使用`line_profiler`。
```Bash
kernprof -lv example.py
```
运行结果如下所示。
```
Line # Hits Time Per Hit % Time Line Contents
==============================================================
1 @profile
2 def is_prime(num):
3 86624 48420.0 0.6 50.5 for factor in range(2, int(num ** 0.5) + 1):
4 85624 44000.0 0.5 45.9 if num % factor == 0:
5 6918 3080.0 0.4 3.2 return False
6 1000 430.0 0.4 0.4 return True
```
#### 题目36如何使用`random`模块生成随机数、实现随机乱序和随机抽样?
> **点评**送人头的题目因为Python标准库中的常用模块应该是Python开发者都比较熟悉的内容这个问题回如果答不上来整个面试基本也就砸锅了。
1. `random.random()`函数可以生成`[0.0, 1.0)`之间的随机浮点数。
2. `random.uniform(a, b)`函数可以生成`[a, b]`或`[b, a]`之间的随机浮点数。
3. `random.randint(a, b)`函数可以生成`[a, b]`或`[b, a]`之间的随机整数。
4. `random.shuffle(x)`函数可以实现对序列`x`的原地随机乱序。
5. `random.choice(seq)`函数可以从非空序列中取出一个随机元素。
6. `random.choices(population, weights=None, *, cum_weights=None, k=1)`函数可以从总体中随机抽取(有放回抽样)出容量为`k`的样本并返回样本的列表,可以通过参数指定个体的权重,如果没有指定权重,个体被选中的概率均等。
7. `random.sample(population, k)`函数可以从总体中随机抽取(无放回抽样)出容量为`k`的样本并返回样本的列表。
> **扩展**`random`模块提供的函数除了生成均匀分布的随机数外,还可以生成其他分布的随机数,例如`random.gauss(mu, sigma)`函数可以生成高斯分布(正态分布)的随机数;`random.paretovariate(alpha)`函数会生成帕累托分布的随机数;`random.gammavariate(alpha, beta)`函数会生成伽马分布的随机数。
#### 题目37解释一下线程池的工作原理。
线程池是一种用于减少线程本身创建和销毁造成的开销的技术属于典型的空间换时间操作。如果应用程序需要频繁的将任务派发到线程中执行线程池就是必选项因为创建和释放线程涉及到大量的系统底层操作开销较大如果能够在应用程序工作期间将创建和释放线程的操作变成预创建和借还操作将大大减少底层开销。线程池在应用程序启动后立即创建一定数量的线程放入空闲队列中。这些线程最开始都处于阻塞状态不会消耗CPU资源但会占用少量的内存空间。当任务到来后从队列中取出一个空闲线程把任务派发到这个线程中运行并将该线程标记为已占用。当线程池中所有的线程都被占用后可以选择自动创建一定数量的新线程用于处理更多的任务也可以选择让任务排队等待直到有空闲的线程可用。在任务执行完毕后线程并不退出结束而是继续保持在池中等待下一次的任务。当系统比较空闲时大部分线程长时间处于闲置状态时线程池可以自动销毁一部分线程回收系统资源。基于这种预创建技术线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上执行次数越多每个任务所分担到的线程本身开销则越小。
一般线程池都必须具备下面几个组成部分:
1. 线程池管理器:用于创建并管理线程池。
2. 工作线程和线程队列:线程池中实际执行的线程以及保存这些线程的容器。
3. 任务接口:将线程执行的任务抽象出来,形成任务接口,确保线程池与具体的任务无关。
4. 任务队列:线程池中保存等待被执行的任务的容器。
#### 题目38举例说明什么情况下会出现`KeyError`、`TypeError`、`ValueError`。
举一个简单的例子,变量`a`是一个字典,执行`int(a['x'])`这个操作就有可能引发上述三种类型的异常。如果字典中没有键`x`,会引发`KeyError`;如果键`x`对应的值不是`str`、`float`、`int`、`bool`以及`bytes-like`类型,在调用`int`函数构造`int`类型的对象时,会引发`TypeError`;如果`a[x]`是一个字符串或者字节串,而对应的内容又无法处理成`int`时,将引发`ValueError`。
#### 题目39说出下面代码的运行结果。
```Python
def extend_list(val, items=[]):
items.append(val)
return items
list1 = extend_list(10)
list2 = extend_list(123, [])
list3 = extend_list('a')
print(list1)
print(list2)
print(list3)
```
> **点评**Python函数在定义的时候默认参数`items`的值就被计算出来了,即`[]`。因为默认参数`items`引用了对象`[]`,每次调用该函数,如果对`items`引用的列表进行了操作,下次调用时,默认参数还是引用之前的那个列表而不是重新赋值为`[]`,所以列表中会有之前添加的元素。如果通过传参的方式为`items`重新赋值,那么`items`将引用到新的列表对象,而不再引用默认的那个列表对象。
```
[10, 'a']
[123]
[10, 'a']
```
#### 题目40如何读取大文件例如内存只有4G如何读取一个大小为8G的文件
很显然4G内存要一次性的加载大小为8G的文件是不现实的遇到这种情况必须要考虑多次读取和分批次处理。在Python中读取文件可以先通过`open`函数获取文件对象,在读取文件时,可以通过`read`方法的`size`参数指定读取的大小,也可以通过`seek`方法的`offset`参数指定读取的位置,这样就可以控制单次读取数据的字节数和总字节数。除此之外,可以使用内置函数`iter`将文件对象处理成迭代器对象,每次只读取少量的数据进行处理,代码大致写法如下所示。
```Python
with open('...', 'rb') as file:
for data in iter(lambda: file.read(2097152), b''):
pass
```
在Linux系统上可以通过`split`命令将大文件切割为小片,然后通过读取切割后的小文件对数据进行处理。例如下面的命令将名为`filename`的大文件切割为大小为512M的多个文件。
```Bash
split -b 512m filename
```
如果愿意, 也可以将名为`filename`的文件切割为10个文件命令如下所示。
```Bash
split -n 10 filename
```
> **扩展**:外部排序跟上述的情况非常类似,由于处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。“**排序-归并算法**”就是一种常用的外部排序策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件,然后在归并阶段将这些临时文件组合为一个大的有序文件,这个大的有序文件就是排序的结果。