面试整理:分享50道硬核Python编程题,面试前过一遍_python写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。 如原列

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Python全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img



既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Python知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024c (备注Python)
img

正文

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。


### 题目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]这个结果,可以按照下面几种方式来修改multiply函数。


方法一:使用生成器,让函数获得i的当前值。



def multiply():
return (lambda x: i * x for i in range(4))

print([m(100) for m in multiply()])


或者



def multiply():
for i in range(4):
yield lambda x: x * i

print([m(100) for m in multiply()])


方法二:使用偏函数,彻底避开闭包。



from functools import partial
from operator import mul

def multiply():
return [partial(mul, i) for i in range(4)]

print([m(100) for m in multiply()])


### 题目011:Python中为什么没有函数重载?


点评:C++、Java、C#等诸多编程语言都支持函数重载,所谓函数重载指的是在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为编译时多态性或者叫前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解Python是动态类型语言,是否知道Python中函数的可变参数、关键字参数这些概念。


首先Python是解释型语言,函数重载现象通常出现在编译型语言中。其次Python是动态类型语言,函数的参数没有类型约束,也就无法根据参数类型来区分重载。再者Python中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。


### 题目012:用Python代码实现Python内置函数max。


点评:这个题目看似简单,但实际上还是比较考察面试者的功底。因为Python内置的max函数既可以传入可迭代对象找出最大,又可以传入两个或多个参数找出最大;最为关键的是还可以通过命名关键字参数key来指定一个用于元素比较的函数,还可以通过default命名关键字参数来指定当可迭代对象为空时返回的默认值。


下面的代码仅供参考:



def my_max(*args, key=None, default=None):
“”"
获取可迭代对象中最大的元素或两个及以上实参中最大的元素
:param args: 一个可迭代对象或多个元素
:param key: 提取用于元素比较的特征值的函数,默认为None
:param default: 如果可迭代对象为空则返回该默认值,如果没有给默认值则引发ValueError异常
:return: 返回可迭代对象或多个元素中的最大元素
“”"
if len(args) == 1 and len(args[0]) == 0:
if default:
return default
else:
raise ValueError(‘max() arg is an empty sequence’)
items = args[0] if len(args) == 1 else args
max_elem, max_value = items[0], items[0]
if key:
max_value = key(max_value)
for item in items:
value = item
if key:
value = key(item)
if value > max_value:
max_elem, max_value = item, value
return max_elem


### 题目013:写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。


点评:送人头的题目,不解释。



def count_letters(items):
result = {}
for item in items:
if isinstance(item, (int, float)):
result[item] = result.get(item, 0) + 1
return result


也可以直接使用Python标准库中collections模块的Counter类来解决这个问题,Counter是dict的子类,它会将传入的序列中的每个元素作为键,元素出现的次数作为值来构造字典。



from collections import Counter

def count_letters(items):
counter = Counter(items)
return {key: value for key, value in counter.items()
if isinstance(key, (int, float))}


### 题目014:使用Python代码实现遍历一个文件夹的操作。


点评:基本也是送人头的题目,只要用过os模块就应该知道怎么做。


Python标准库os模块的walk函数提供了遍历一个文件夹的功能,它返回一个生成器。



import os

g = os.walk(‘/Users/Hao/Downloads/’)
for path, dir_list, file_list in g:
for dir_name in dir_list:
print(os.path.join(path, dir_name))
for file_name in file_list:
print(os.path.join(path, file_name))


说明:os.path模块提供了很多进行路径操作的工具函数,在项目开发中也是经常会用到的。如果题目明确要求不能使用os.walk函数,那么可以使用os.listdir函数来获取指定目录下的文件和文件夹,然后再通过循环遍历用os.isdir函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。


### 题目015:现有2元、3元、5元共三种面额的货币,如果需要找零99元,一共有多少种找零的方式?


点评:还有一个非常类似的题目:“一个小朋友走楼梯,一次可以走1个台阶、2个台阶或3个台阶,问走完10个台阶一共有多少种走法?”,这两个题目的思路是一样,如果用递归函数来写的话非常简单。



from functools import lru_cache

@lru_cache()
def change_money(total):
if total == 0:
return 1
if total < 0:
return 0
return change_money(total - 2) + change_money(total - 3) +
change_money(total - 5)


说明:在上面的代码中,我们用lru\_cache装饰器装饰了递归函数change\_money,如果不做这个优化,上面代码的渐近时间复杂度将会是图片,而如果参数total的值是99,这个运算量是非常巨大的。lru\_cache装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。


### 题目016:写一个函数,给定矩阵的阶数`n`,输出一个螺旋式数字矩阵。


例如:n = 2,返回:



1 2
4 3


例如:n = 3,返回:



1 2 3
8 9 4
7 6 5


这个题目本身并不复杂,下面的代码仅供参考。



def show_spiral_matrix(n):
matrix = [[0] * n for _ in range(n)]
row, col = 0, 0
num, direction = 1, 0
while num <= n ** 2:
if matrix[row][col] == 0:
matrix[row][col] = num
num += 1
if direction == 0:
if col < n - 1 and matrix[row][col + 1] == 0:
col += 1
else:
direction += 1
elif direction == 1:
if row < n - 1 and matrix[row + 1][col] == 0:
row += 1
else:
direction += 1
elif direction == 2:
if col > 0 and matrix[row][col - 1] == 0:
col -= 1
else:
direction += 1
else:
if row > 0 and matrix[row - 1][col] == 0:
row -= 1
else:
direction += 1
direction %= 4
for x in matrix:
for y in x:
print(y, end=‘\t’)
print()


### 题目017:阅读下面的代码,写出程序的运行结果。



items = [1, 2, 3, 4]
print([i for i in items if i > 2])
print([i for i in items if i % 2])
print([(x, y) for x, y in zip(‘abcd’, (1, 2, 3, 4, 5))])
print({x: f’item{x ** 2}’ for x in (2, 4, 6)})
print(len({x for x in ‘hello world’ if x not in ‘abcdefg’}))


点评:生成式(推导式)属于Python的特色语法之一,几乎是面试必考内容。Python中通过生成式字面量语法,可以创建出列表、集合、字典。



[3, 4]
[1, 3]
[(‘a’, 1), (‘b’, 2), (‘c’, 3), (‘d’, 4)]
{2: ‘item4’, 4: ‘item16’, 6: ‘item36’}
6


### 题目018:说出下面代码的运行结果。



class Parent:
x = 1

class Child1(Parent):
pass

class Child2(Parent):
pass

print(Parent.x, Child1.x, Child2.x)
Child1.x = 2
print(Parent.x, Child1.x, Child2.x)
Parent.x = 3
print(Parent.x, Child1.x, Child2.x)


点评:运行上面的代码首先输出1 1 1,这一点大家应该没有什么疑问。接下来,通过Child1.x = 2给类Child1重新绑定了属性x并赋值为2,所以Child1.x会输出2,而Parent和Child2并不受影响。执行Parent.x = 3会重新给Parent类的x属性赋值为3,由于Child2的x属性继承自Parent,所以Child2.x的值也是3;而之前我们为Child1重新绑定了x属性,那么它的x属性值不会受到Parent.x = 3的影响,还是之前的值2。



1 1 1
1 2 1
3 2 3


### 题目19:说说你用过Python标准库中的哪些模块。


点评:Python标准库中的模块非常多,建议大家根据自己过往的项目经历来介绍你用过的标准库和三方库,因为这些是你最为熟悉的,经得起面试官深挖的。


![在这里插入图片描述](https://img-blog.csdnimg.cn/db78e78fde294f6fbb5f32d4770f7cc9.png#pic_center)


### 题目20:`init__`和`__new`方法有什么区别?


Python中调用构造器创建对象属于两阶段构造过程,首先执行\_\_new\_\_方法获得保存对象所需的内存空间,再通过\_\_init\_\_执行对内存空间数据的填充(对象属性的初始化)。\_\_new\_\_方法的返回值是创建好的Python对象(的引用),而\_\_init\_\_方法的第一个参数就是这个对象(的引用),所以在\_\_init\_\_中可以完成对对象的初始化操作。\_\_new\_\_是类方法,它的第一个参数是类,\_\_init\_\_是对象方法,它的第一个参数是对象。


### 题目21:输入年月日,判断这个日期是这一年的第几天。


方法一:不使用标准库中的模块和函数。



def is_leap_year(year):
“”“判断指定的年份是不是闰年,平年返回False,闰年返回True”“”
return year % 4 == 0 and year % 100 != 0 or year % 400 == 0

def which_day(year, month, date):
“”“计算传入的日期是这一年的第几天”“”
# 用嵌套的列表保存平年和闰年每个月的天数
days_of_month = [
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
]
days = days_of_month[is_leap_year(year)][:month - 1]
return sum(days) + date


方法二:使用标准库中的datetime模块。



import datetime

def which_day(year, month, date):
end = datetime.date(year, month, date)
start = datetime.date(year, 1, 1)
return (end - start).days + 1


### 题目22:平常工作中用什么工具进行静态代码分析。


点评:静态代码分析工具可以从代码中提炼出各种静态属性,这使得开发者可以对代码的复杂性、可维护性和可读性有更好的了解,这里所说的静态属性包括:


代码是否符合编码规范,例如:PEP-8。


代码中潜在的问题,包括:语法错误、缩进问题、导入缺失、变量覆盖等。


代码中的坏味道。


代码的复杂度。


代码的逻辑问题。


工作中静态代码分析主要用到的是Pylint和Flake8。Pylint可以检查出代码错误、坏味道、不规范的代码等问题,较新的版本中还提供了代码复杂度统计数据,可以生成检查报告。Flake8封装了Pyflakes(检查代码逻辑错误)、McCabe(检查代码复杂性)和Pycodestyle(检查代码是否符合PEP-8规范)工具,它可以执行这三个工具提供的检查。


### 题目23:说一下你知道的Python中的魔术方法。


点评:魔术方法也称为魔法方法,是Python中的特色语法,也是面试中的高频问题。


![在这里插入图片描述](https://img-blog.csdnimg.cn/f64959b8562e4406970aec73a3a9586c.png#pic_center)


### 题目24:函数参数`arg`和`*kwargs`分别代表什么?


Python中,函数的参数分为位置参数、可变参数、关键字参数、命名关键字参数。*args代表可变参数,可以接收0个或任意多个参数,当不确定调用者会传入多少个位置参数时,就可以使用可变参数,它会将传入的参数打包成一个元组。\*\*kwargs代表关键字参数,可以接收用参数名=参数值的方式传入的参数,传入的参数的会打包成一个字典。定义函数时如果同时使用*args和\*\*kwargs,那么函数可以接收任意参数。


### 题目25:写一个记录函数执行时间的装饰器。


点评:高频面试题,也是最简单的装饰器,面试者必须要掌握的内容。


方法一:用函数实现装饰器。



from functools import wraps
from time import time

def record_time(func):

@wraps(func)
def wrapper(\*args, \*\*kwargs):
    start = time()
    result = func(\*args, \*\*kwargs)
    print(f'{func.\_\_name\_\_}执行时间: {time() - start}秒')
    return result

return wrapper

方法二:用类实现装饰器。类有\_\_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

说明:装饰器可以用来装饰类或函数,为其提供额外的能力,属于设计模式中的代理模式。


扩展:装饰器本身也可以参数化,例如上面的例子中,如果不希望在终端中显示函数的执行时间而是希望由调用者来决定如何输出函数的执行时间,可以通过参数化装饰器的方式来做到,代码如下所示。



from functools import wraps
from time import time

def record_time(output):
“”“可以参数化的装饰器”“”

def decorate(func):

    @wraps(func)
    def wrapper(\*args, \*\*kwargs):
        start = time()
        result = func(\*args, \*\*kwargs)
        output(func.__name__, time() - start)
        return result

    return wrapper

return decorate

### 题目26:什么是鸭子类型(duck typing)?


鸭子类型是动态类型语言判断一个对象是不是某种类型时使用的方法,也叫做鸭子判定法。简单的说,鸭子类型是指判断一只鸟是不是鸭子,我们只关心它游泳像不像鸭子、叫起来像不像鸭子、走路像不像鸭子就足够了。换言之,如果对象的行为跟我们的预期是一致的(能够接受某些消息),我们就认定它是某种类型的对象。


在Python语言中,有很多bytes-like对象(如:bytes、bytearray、array.array、memoryview)、file-like对象(如:StringIO、BytesIO、GzipFile、socket)、path-like对象(如:str、bytes),其中file-like对象都能支持read和write操作,可以像文件一样读写,这就是所谓的对象有鸭子的行为就可以判定为鸭子的判定方法。再比如Python中列表的extend方法,它需要的参数并不一定要是列表,只要是可迭代对象就没有问题。


说明:动态语言的鸭子类型使得设计模式的应用被大大简化。


### 题目27:说一下Python中变量的作用域。


Python中有四种作用域,分别是局部作用域(Local)、嵌套作用域(Embedded)、全局作用域(Global)、内置作用域(Built-in),搜索一个标识符时,会按照LEGB的顺序进行搜索,如果所有的作用域中都没有找到这个标识符,就会引发NameError异常。


### 题目28:说一下你对闭包的理解。


闭包是支持一等函数的编程语言(Python、JavaScript等)中实现词法绑定的一种技术。当捕捉闭包的时候,它的自由变量(在函数外部定义但在函数内部使用的变量)会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。简单的说,可以将闭包理解为能够读取其他函数内部变量的函数。正在情况下,函数的局部变量在函数调用结束之后就结束了生命周期,但是闭包使得局部变量的生命周期得到了延展。使用闭包的时候需要注意,闭包会使得函数中创建的对象不会被垃圾回收,可能会导致很大的内存开销,所以闭包一定不能滥用。


### 题目29:说一下Python中的多线程和多进程的应用场景和优缺点。


线程是操作系统分配CPU的基本单位,进程是操作系统分配内存的基本单位。通常我们运行的程序会包含一个或多个进程,而每个进程中又包含一个或多个线程。多线程的优点在于多个线程可以共享进程的内存空间,所以进程间的通信非常容易实现;但是如果使用官方的CPython解释器,多线程受制于GIL(全局解释器锁),并不能利用CPU的多核特性,这是一个很大的问题。使用多进程可以充分利用CPU的多核特性,但是进程间通信相对比较麻烦,需要使用IPC机制(管道、套接字等)。


多线程适合那些会花费大量时间在I/O操作上,但没有太多并行计算需求且不需占用太多内存的I/O密集型应用。多进程适合执行计算密集型任务(如:视频编码解码、数据处理、科学计算等)、可以分解为多个并行子任务并能合并子任务执行结果的任务以及在内存使用方面没有任何限制且不强依赖于I/O操作的任务。


扩展:Python中实现并发编程通常有多线程、多进程和异步编程三种选择。异步编程实现了协作式并发,通过多个相互协作的子程序的用户态切换,实现对CPU的高效利用,这种方式也是非常适合I/O密集型应用的。


### 题目30:说一下Python 2和Python 3的区别。


点评:这种问题千万不要背所谓的参考答案,说一些自己最熟悉的就足够了。


Python 2中的print和exec都是关键字,在Python 3中变成了函数。


Python 3中没有long类型,整数都是int类型。


Python 2中的不等号<>在Python 3中被废弃,统一使用!=。


Python 2中的xrange函数在Python 3中被range函数取代。


Python 3对Python 2中不安全的input函数做出了改进,废弃了raw\_input函数。


Python 2中的file函数被Python 3中的open函数取代。


Python 2中的/运算对于int类型是整除,在Python 3中要用//来做整除除法。


Python 3中改进了Python 2捕获异常的代码,很明显Python 3的写法更合理。


Python 3生成式中循环变量的作用域得到了更好的控制,不会影响到生成式之外的同名变量。


Python 3中的round函数可以返回int或float类型,Python 2中的round函数返回float类型。


Python 3的str类型是Unicode字符串,Python 2的str类型是字节串,相当于Python 3中的bytes。


Python 3中的比较运算符必须比较同类对象。


Python 3中定义类的都是新式类,Python 2中定义的类有新式类(显式继承自object的类)和旧式类(经典类)之分,新式类和旧式类在MRO问题上有非常显著的区别,新式类可以使用class\_\_`属性获取自身类型,新式类可以使用`\_\_slots魔法。


Python 3对代码缩进的要求更加严格,如果混用空格和制表键会引发TabError。


Python 3中字典的keys、values、items方法都不再返回list对象,而是返回view object,内置的map、filter等函数也不再返回list对象,而是返回迭代器对象。


Python 3标准库中某些模块的名字跟Python 2是有区别的;而在三方库方面,有些三方库只支持Python 2,有些只能支持Python 3。


### 题目31:谈谈你对“猴子补丁”(monkey patching)的理解。


“猴子补丁”是动态类型语言的一个特性,代码运行时在不修改源代码的前提下改变代码中的方法、属性、函数等以达到热补丁(hot patch)的效果。很多系统的安全补丁也是通过猴子补丁的方式来实现的,但实际开发中应该避免对猴子补丁的使用,以免造成代码行为不一致的问题。


在使用gevent库的时候,我们会在代码开头的地方执行gevent.monkey.patch\_all(),这行代码的作用是把标准库中的socket模块给替换掉,这样我们在使用socket的时候,不用修改任何代码就可以实现对代码的协程化,达到提升性能的目的,这就是对猴子补丁的应用。


另外,如果希望用ujson三方库替换掉标准库中的json,也可以使用猴子补丁的方式,代码如下所示。



import json, ujson

json.name = ‘ujson’
json.dumps = ujson.dumps
json.loads = ujson.loads


单元测试中的Mock技术也是对猴子补丁的应用,Python中的unittest.mock模块就是解决单元测试中用Mock对象替代被测对象所依赖的对象的模块。


### 题目32:阅读下面的代码说出运行结果。



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()


点评:这道题考查到了两个知识点:


Python中的MRO(方法解析顺序)。在没有多重继承的情况下,向对象发出一个消息,如果对象没有对应的方法,那么向上(父类)搜索的顺序是非常清晰的。如果向上追溯到object类(所有类的父类)都没有找到对应的方法,那么将会引发AttributeError异常。但是有多重继承尤其是出现菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得确定MRO。Python 3中的类以及Python 2中的新式类使用C3算法来确定MRO,它是一种类似于广度优先搜索的方法;Python 2中的旧式类(经典类)使用深度优先搜索来确定MRO。在搞不清楚MRO的情况下,可以使用类的mro方法或mro属性来获得类的MRO列表。


super()函数的使用。在使用super函数时,可以通过super(类型, 对象)来指定对哪个对象以哪个类为起点向上搜索父类方法。所以上面B类代码中的super(B, self).who()表示以B类为起点,向上搜索self(D类对象)的who方法,所以会找到C类中的who方法,因为D类对象的MRO列表是D --> B --> C --> A --> object。



ACBD


### 题目33:编写一个函数实现对逆波兰表达式求值,不能使用Python的内置函数。


点评:逆波兰表达式也称为“后缀表达式”,相较于平常我们使用的“中缀表达式”,逆波兰表达式不需要括号来确定运算的优先级,例如5 \* (2 + 3)对应的逆波兰表达式是5 2 3 + \*。逆波兰表达式求值需要借助栈结构,扫描表达式遇到运算数就入栈,遇到运算符就出栈两个元素做运算,将运算结果入栈。表达式扫描结束后,栈中只有一个数,这个数就是最终的运算结果,直接出栈即可。



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()


### 题目34:Python中如何实现字符串替换操作?


Python中实现字符串替换大致有两类方法:字符串的replace方法和正则表达式的sub方法。


方法一:使用字符串的replace方法。



message = ‘hello, world!’
print(message.replace(‘o’, ‘O’).replace(‘l’, ‘L’).replace(‘he’, ‘HE’))


方法二:使用正则表达式的sub方法。



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



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装饰器,代码如下所示。



@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。



pip install line_profiler


使用line\_profiler。



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开发者都比较熟悉的内容,这个问题回如果答不上来,整个面试基本也就砸锅了。


random.random()函数可以生成[0.0, 1.0)之间的随机浮点数。


random.uniform(a, b)函数可以生成[a, b]或[b, a]之间的随机浮点数。


random.randint(a, b)函数可以生成[a, b]或[b, a]之间的随机整数。


random.shuffle(x)函数可以实现对序列x的原地随机乱序。


random.choice(seq)函数可以从非空序列中取出一个随机元素。


random.choices(population, weights=None, \*, cum\_weights=None, k=1)函数可以从总体中随机抽取(有放回抽样)出容量为k的样本并返回样本的列表,可以通过参数指定个体的权重,如果没有指定权重,个体被选中的概率均等。


random.sample(population, k)函数可以从总体中随机抽取(无放回抽样)出容量为k的样本并返回样本的列表。


扩展:random模块提供的函数除了生成均匀分布的随机数外,还可以生成其他分布的随机数,例如random.gauss(mu, sigma)函数可以生成高斯分布(正态分布)的随机数;random.paretovariate(alpha)函数会生成帕累托分布的随机数;random.gammavariate(alpha, beta)函数会生成伽马分布的随机数。


### 题目37:解释一下线程池的工作原理。


点评:池化技术就是一种典型空间换时间的策略,我们使用的数据库连接池、线程池等都是池化技术的应用,Python标准库currrent.futures模块的ThreadPoolExecutor就是线程池的实现,如果要弄清楚它的工作原理,可以参考下面的内容。


线程池是一种用于减少线程本身创建和销毁造成的开销的技术,属于典型的空间换时间操作。如果应用程序需要频繁的将任务派发到线程中执行,线程池就是必选项,因为创建和释放线程涉及到大量的系统底层操作,开销较大,如果能够在应用程序工作期间,将创建和释放线程的操作变成预创建和借还操作,将大大减少底层开销。线程池在应用程序启动后,立即创建一定数量的线程,放入空闲队列中。这些线程最开始都处于阻塞状态,不会消耗CPU资源,但会占用少量的内存空间。当任务到来后,从队列中取出一个空闲线程,把任务派发到这个线程中运行,并将该线程标记为已占用。当线程池中所有的线程都被占用后,可以选择自动创建一定数量的新线程,用于处理更多的任务,也可以选择让任务排队等待直到有空闲的线程可用。在任务执行完毕后,线程并不退出结束,而是继续保持在池中等待下一次的任务。当系统比较空闲时,大部分线程长时间处于闲置状态时,线程池可以自动销毁一部分线程,回收系统资源。基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小。


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


线程池管理器:用于创建并管理线程池。


工作线程和线程队列:线程池中实际执行的线程以及保存这些线程的容器。


任务接口:将线程执行的任务抽象出来,形成任务接口,确保线程池与具体的任务无关。


任务队列:线程池中保存等待被执行的任务的容器。


### 题目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:说出下面代码的运行结果。



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将引用到新的列表对象,而不再引用默认的那个列表对象。这个题在面试中经常被问到,通常不建议使用容器类型的默认参数,像PyLint这样的代码检查工具也会对这种代码提出质疑和警告。



[10, ‘a’]
[123]
[10, ‘a’]


### 题目40:如何读取大文件,例如内存只有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


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


### 题目41:说一下你对Python中模块和包的理解。


每个Python文件就是一个模块,而保存这些文件的文件夹就是一个包,但是这个作为Python包的文件夹必须要有一个名为\_\_init\_\_.py的文件,否则无法导入这个包。通常一个文件夹下还可以有子文件夹,这也就意味着一个包下还可以有子包,子包中的\_\_init\_\_.py并不是必须的。模块和包解决了Python中命名冲突的问题,不同的包下可以有同名的模块,不同的模块下可以有同名的变量、函数或类。在Python中可以使用import或from … import …来导入包和模块,在导入的时候还可以使用as关键字对包、模块、类、函数、变量等进行别名,从而彻底解决编程中尤其是多人协作团队开发时的命名冲突问题。


### 题目42:说一下你知道的Python编码规范。


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


空格的使用


使用空格来表示缩进而不要用制表符(Tab)。


和语法相关的每一层缩进都用4个空格来表示。




### 最后

不知道你们用的什么环境,我一般都是用的Python3.6环境和pycharm解释器,没有软件,或者没有资料,没人解答问题,都可以免费领取(包括今天的代码),过几天我还会做个视频教程出来,有需要也可以领取~  

给大家准备的学习资料包括但不限于:  

Python 环境、pycharm编辑器/永久激活/翻译插件  

python 零基础视频教程  

Python 界面开发实战教程  

Python 爬虫实战教程  

Python 数据分析实战教程  

python 游戏开发实战教程  

Python 电子书100本  

Python 学习路线规划

![](https://img-blog.csdnimg.cn/d29631674929476f9c3b30f7ff58dff0.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZlaTM0Nzc5NTc5MA==,size_16,color_FFFFFF,t_70)






**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注python)**
![img](https://img-blog.csdnimg.cn/img_convert/aa1166e9c0bd9f8f6dd4e838407f01cc.png)

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
不同的模块下可以有同名的变量、函数或类。在Python中可以使用import或from … import …来导入包和模块,在导入的时候还可以使用as关键字对包、模块、类、函数、变量等进行别名,从而彻底解决编程中尤其是多人协作团队开发时的命名冲突问题。


### 题目42:说一下你知道的Python编码规范。


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


空格的使用


使用空格来表示缩进而不要用制表符(Tab)。


和语法相关的每一层缩进都用4个空格来表示。




### 最后

不知道你们用的什么环境,我一般都是用的Python3.6环境和pycharm解释器,没有软件,或者没有资料,没人解答问题,都可以免费领取(包括今天的代码),过几天我还会做个视频教程出来,有需要也可以领取~  

给大家准备的学习资料包括但不限于:  

Python 环境、pycharm编辑器/永久激活/翻译插件  

python 零基础视频教程  

Python 界面开发实战教程  

Python 爬虫实战教程  

Python 数据分析实战教程  

python 游戏开发实战教程  

Python 电子书100本  

Python 学习路线规划

![](https://img-blog.csdnimg.cn/d29631674929476f9c3b30f7ff58dff0.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ZlaTM0Nzc5NTc5MA==,size_16,color_FFFFFF,t_70)






**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注python)**
[外链图片转存中...(img-aJJSf0Gw-1713106425751)]

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值