写一个删除列表中重复元素的函数,要求去重后元素相对位置保持不变。
def dedup(a):
b = []
c = set()
for i in range(len(a)):
if a[i] not in c:
b.append(a[i])
c.add(a[i])
return b
print(dedup([2,6,4,5,2,6,8,7,9,2]))
# [2, 6, 4, 5, 8, 7, 9]
使用 Python 代码实现遍历一个文件夹的操作。
Python 标准库 os
模块的 walk
函数提供了遍历一个文件夹的功能,它返回一个生成器。每次会生成一个元组:(root,dirs,files)
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
函数判断哪些是文件夹,对于文件夹可以通过递归调用进行遍历,这样也可以实现遍历一个文件夹的操作。
假设你使用的是官方的 CPython,说出下面代码的运行结果。
a, b, c, d = 1, 1, 1000, 1000
print(a is b, c is d)
def foo():
e = 1000
f = 1000
print(e is f, e is d)
g = 1
print(g is a)
foo()
运行结果
True False
True False
True
上面代码中 a is b
的结果是 True
,但 c is d
的结果是 False
,这一点的确让人费解。CPython 解释器出于性能优化的考虑,把频繁使用的整数对象用一个叫 small_ints
的对象池缓存起来造成的。small_ints
缓存的整数值被设定为 [-5, 256]
这个区间,也就是说,在任何引用这些整数的地方,都不需要重新创建 int
对象,而是直接引用缓存池中的对象。如果整数不在该范围内,那么即便两个整数的值相同,它们也是不同的对象。
CPython 底层为了进一步提升性能还做了另一个设定,对于同一个代码块中值不在 small_ints
缓存范围内的整数,如果同一个代码块中已经存在一个值与其相同的整数对象,那么就直接引用该对象,否则创建新的 int
对象。需要大家注意的是,这条规则对数值型适用,但对字符串则需要考虑字符串的长度,这一点大家可以自行证明。
下面这段代码的执行结果是什么?
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()])
方法二:使用偏函数,彻底避开闭包。
偏函数是将所要承载的函数作为 partial()
函数的第一个参数,原函数的各个参数依次作为 partial()
函数后续的参数,除非使用关键字参数。
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()])
用 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
写一个函数统计传入的列表中每个数字出现的次数并返回对应的字典。
def count_letters(items):
result = {}
for item in items:
if isinstance(item, (int, float)):
result[item] = result.get(item, 0) + 1
return result
isinstance()
是一个内置函数,用于判断一个对象是否是一个已知的类型,类似 type()
。dict.get(key[, value])
。key
:字典中要查找的键。value
:可选,如果指定键的值不存在时,返回该默认值。也可以直接使用 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))}
现有 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)
print(change_money(99))
在上面的代码中,我们用 lru_cache
装饰器装饰了递归函数 change_money
,如果不做这个优化,上面代码的渐近时间复杂度将会是 3 n 3^n 3n,而如果参数 total
的值是 99
,这个运算量是非常巨大的。lru_cache
装饰器会缓存函数的执行结果,这样就可以减少重复运算所造成的开销,这是空间换时间的策略,也是动态规划的编程思想。lru_cache
缓存在应用进程的内存中,应用被关闭则被清空。
写一个函数,给定矩阵的阶数 n,输出一个螺旋式数字矩阵。
关键点是要设置一个螺旋的方向 direction
,一圈过后进行取模操作。
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()
show_spiral_matrix(6)
阅读下面的代码,写出程序的运行结果。
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
说出下面代码的运行结果。
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
。
输入年月日,判断这个日期是这一年的第几天。
不使用标准库中的模块和函数。
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
写一个记录函数执行时间的装饰器。
方法一:用函数实现装饰器。
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
@record_time
def sum_(b):
a = 0
for i in range(b):
a = a + 1
return a
sum_(10000000)
方法二:用类实现装饰器。类有 __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
注意 output 代表的是一个参数,如果不传参,则不会输出函数运行的时间。
@record_time(print)
def sum_(b):
a = 0
for i in range(b):
a = a + 1
return a
sum_(10000000)
阅读下面的代码说出运行结果。
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
(Method Resolution Order
,方法解析顺序)。在没有多重继承的情况下,向对象发出一个消息,如果对象没有对应的方法,那么向上(父类)搜索的顺序是非常清晰的。如果向上追溯到 object
类(所有类的父类)都没有找到对应的方法,那么将会引发 AttributeError
异常。但是有多重继承尤其是出现 菱形继承(钻石继承)的时候,向上追溯到底应该找到那个方法就得确定 MRO
。Python3 中的类以及Python2 中的新式类使用 C3
算法来确定 MRO
,它是一种类似于 广度优先搜索 的方法;Python2 中的旧式类(经典类)使用 深度优先搜索 来确定 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
。
编写一个函数实现对逆波兰表达式求值,不能使用 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()
Python中如何实现字符串替换操作?
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
函数来排序,大家可以思考下这个问题如何解决。
如何使用 random 模块生成随机数、实现随机乱序和随机抽样?
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)
函数会生成伽马分布的随机数。
说出下面代码的运行结果。
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
这样的代码检查工具也会对这种代码提出质疑和警告。
运行下面的代码是否会报错,如果报错请说明哪里有什么样的错,如果不报错请说出代码的执行结果。
class A:
def __init__(self, value):
self.__value = value
@property
def value(self):
return self.__value
obj = A(1)
obj.__value = 2
print(obj.value)
print(obj.__value)
这道题有两个考察点,一个考察点是对 _
和 __
开头的对象属性访问权限以及 @property
装饰器的了解,另外一个考察的点是对动态语言的理解,不需要过多的解释。
Python 内置了 3 种函数装饰器,分别是 @staticmethod
、@classmethod
和 @property
,其中 staticmethod()
、classmethod()
和 property()
都是 Python 的内置函数。
property
函数字如其名,其在装饰器的主要应用在于,我们要实现
对象.属性
的方式操作操作类属性除了使用 property()
函数,还可以使用 @property
装饰器。通过 @property
装饰器,可以直接通过方法名来访问方法,不需要在方法名后添加一对 ()
小括号。
如果不希望代码运行时动态的给对象添加新属性,可以在定义类时使用 __slots__
魔法。例如,我们可以在上面的 A 中添加一行 __slots__ = (‘__value’, )
,再次运行上面的代码,将会在原来的第 10 行处产生 AttributeError
错误。
Python 中的 new-style class 要求继承 Python 中的一个内建类型, 一般继承 object
,也可以继承 list
或者 dict
等其他的内建类型。在 Python 新式类中,可以定义一个变量 __slots__
,它的作用是阻止在实例化类时为实例分配 dict
,默认情况下每个类都会有一个 dict
,通过 __dict__
访问,这个 dict
维护了这个实例的所有属性。
由于每次实例化一个类都要分配一个新的 dict
,因此存在空间的浪费,因此有了 __slots__
。__slots__
是一个元组,包括了当前能访问到的属性。当定义了 slots
后,slots
中定义的变量变成了类的描述符,相当于 Java,C++ 中的成员变量声明,类的实例只能拥有 slots
中定义的变量,不能再增加新的变量。注意:定义了 slots
后,就不再有 dict
。
对下面给出的字典按值从大到小对键进行排序。
prices = {
'AAPL': 191.88,
'GOOG': 1186.96,
'IBM': 149.24,
'ORCL': 48.44,
'ACN': 166.89,
'FB': 208.09,
'SYMC': 21.29
}
sorted
函数的高阶用法在面试的时候经常出现,key
参数可以传入一个函数名或一个 Lambda
函数,该函数的返回值代表了在排序时比较元素的依据。
sorted(prices, key=lambda x: prices[x], reverse=True)
写一个函数,传入的参数是一个列表(列表中的元素可能也是一个列表),返回该列表最大的嵌套深度。例如:列表 [1, 2, 3] 的嵌套深度为 1,列表 [[1], [2, [3]]] 的嵌套深度为 3。
def list_depth(items):
if isinstance(items, list):
max_depth = 1
for item in items:
max_depth = max(list_depth(item) + 1, max_depth)
return max_depth
return 0
有一个通过网络获取数据的函数(可能会因为网络原因出现异常),写一个装饰器让这个函数在出现指定异常时可以重试指定的次数,并在每次重试之前随机延迟一段时间,最长延迟时间可以通过参数进行控制。
方法一:函数实现
from functools import wraps
from random import random
from time import sleep
def retry(*, retry_times=3, max_wait_secs=5, errors=(Exception, )):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(retry_times):
try:
return func(*args, **kwargs)
except errors:
sleep(random() * max_wait_secs)
return None
return wrapper
return decorate
方法二:类实现
from functools import wraps
from random import random
from time import sleep
class Retry(object):
def __init__(self, *, retry_times=3, max_wait_secs=5, errors=(Exception, )):
self.retry_times = retry_times
self.max_wait_secs = max_wait_secs
self.errors = errors
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(self.retry_times):
try:
return func(*args, **kwargs)
except self.errors:
sleep(random() * self.max_wait_secs)
return None
return wrapper
参考资料
【建议收藏】50 道硬核的 Python 面试题!