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]
>
> ```
>
>
##### 题目006:说说Python中的浅拷贝和深拷贝。
>
> **点评**:这个题目本身出现的频率非常高,但是就题论题而言没有什么技术含量。对于这种面试题,在**回答的时候一定要让你的答案能够超出面试官的预期**,这样才能**获得更好的印象分**。所以回答这个题目的要点不仅仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候可能遇到的两大问题,还要说出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) # 浅拷贝
##### 题目007:Python是如何实现内存管理的?
>
> **点评**:当面试官问到这个问题的时候,一个展示自己的机会就摆在面前了。你要先反问面试官:“你说的是官方的CPython解释器吗?”。这个反问可以展示出你了解过Python解释器的不同的实现版本,而且你也知道面试官想问的是CPython。当然,很多面试官对不同的Python解释器底层实现到底有什么差别也没有概念。所以,**千万不要觉得面试官一定比你强**,怀揣着这份自信可以让你更好的完成面试。
>
>
>
Python提供了自动化的内存管理,也就是说内存空间的分配与释放都是由Python解释器在运行时自动进行的,自动管理内存功能极大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以CPython解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
**引用计数**:对于CPython解释器来说,Python中的每一个对象其实就是`PyObject`结构体,它的内部有一个名为`ob_refcnt` 的引用计数器成员变量。程序在运行的过程中`ob_refcnt`的值会被更新并藉此来反映引用有多少个变量引用到该对象。当对象的引用计数值为0时,它的内存就会被释放掉。
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
以下情况会导致引用计数加`1`:
* 对象被创建
* 对象被引用
* 对象作为参数传入到一个函数中
* 对象作为元素存储到一个容器中
以下情况会导致引用计数减`1`:
* 用`del`语句显示删除对象引用
* 对象引用被重新赋值其他对象
* 一个对象离开它所在的作用域
* 持有该对象的容器自身被销毁
* 持有该对象的容器删除该对象
可以通过`sys`模块的`getrefcount`函数来获得对象的引用计数。引用计数的内存管理方式在遇到循环引用的时候就会出现致命伤,因此需要其他的垃圾回收算法对其进行补充。
**标记清理**:CPython使用了“标记-清理”(Mark and Sweep)算法解决容器类型可能产生的循环引用问题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,如果对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。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中的节点就是需要释放内存的节点。
**分代回收**:在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。分代回收的基本思想是:**对象存在的时间越长,是垃圾的可能性就越小,应该尽量不对这样的对象进行垃圾回收**。CPython将对象分为三种世代分别记为`0`、`1`、`2`,每一个新生对象都在第`0`代中,如果该对象在一轮垃圾回收扫描中存活下来,那么它将被移到第`1`代中,存在于第`1`代的对象将较少的被垃圾回收扫描到;如果在对第`1`代进行垃圾回收扫描时,这个对象又存活下来,那么它将被移至第2代中,在那里它被垃圾回收扫描的次数将会更少。分代回收扫描的门限值可以通过`gc`模块的`get_threshold`函数来获得,该函数返回一个三元组,分别表示多少次内存分配操作后会执行`0`代垃圾回收,多少次`0`代垃圾回收后会执行`1`代垃圾回收,多少次`1`代垃圾回收后会执行`2`代垃圾回收。需要说明的是,如果执行一次`2`代垃圾回收,那么比它年轻的代都要执行垃圾回收。如果想修改这几个门限值,可以通过`gc`模块的`set_threshold`函数来做到。
##### 题目008:说一下你对Python中迭代器和生成器的理解。
>
> **点评**:很多人面试者都会写迭代器和生成器,但是却无法准确的解释什么是迭代器和生成器。如果你也有同样的困惑,可以参考下面的回答。
>
>
>
迭代器是实现了迭代器协议的对象。跟其他编程语言不通,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
>
> ```
>
>
##### 题目009:正则表达式的match方法和search方法有什么区别?
>
> **点评**:正则表达式是字符串处理的重要工具,所以也是面试中经常考察的知识点。在Python中,使用正则表达式有两种方式,一种是直接调用`re`模块中的函数,传入正则表达式和需要处理的字符串;一种是先通过`re`模块的`compile`函数创建正则表达式对象,然后再通过对象调用方法并传入需要处理的字符串。**如果一个正则表达式被频繁的使用,我们推荐用`re.compile`函数创建正则表达式对象,这样会减少频繁编译同一个正则表达式所造成的开销**。
>
>
>
`match`方法是从字符串的起始位置进行正则表达式匹配,返回`Match`对象或None。`search`方法会扫描整个字符串来找寻匹配的模式,同样也是返回Match对象或None。
![在这里插入图片描述](https://img-blog.csdnimg.cn/0bd3091a726d4d81a1189236df13d7e7.png)
##### 题目010:下面这段代码的执行结果是什么。
def multiply():
return [lambda x: i * x for i in range(4)]
print([m(100) for m in multiply()])
运行结果:
[300, 300, 300, 300]
上面代码的运行结果很容易被误判为`[0, 100, 200, 300]`。首先需要注意的是`multiply`函数用生成式语法返回了一个列表,列表中保存了4个Lambda函数,这4个Lambda函数会返回传入的参数乘以`i`的结果。需要注意的是这里有闭包(closure)现象,`multiply`函数中的局部变量`i`的生命周期被延展了,由于`i`最终的值是`3`,所以通过`m(100)`调列表中的Lambda函数时会返回`300`,而且4个调用都是如此。
如果想得到`[0, 100, 200, 300]`这个结果,可以按照下面几种方式