目录
题目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。