不使用中间变量,交换两个变量a和b的值。
a, b = b, a
需要注意,a, b = b, a
这种做法其实并不是元组解包,虽然很多人都这样认为。Python 字节码指令中有 ROT_TWO
指令来支持这个操作,类似的还有 ROT_THREE
,对于 3 个以上的元素,如 a, b, c, d = b, c, d, a
,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用 Python 标准库中 dis
模块的 dis
函数来反汇编你的Python代码。
Lambda 函数是什么,举例说明的它的应用场景。
Lambda 函数通常用在高阶函数中,主要的作用是通过 向函数传入函数 或 让函数返回函数 最终实现代码的解耦合。
Lambda 函数也叫 匿名函数,它是功能简单用一行代码就能实现的小型函数。Python 中的 Lambda 函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写 return
关键字。Lambda 函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。
Lambda 函数其实最为主要的用途是把一个函数传入另一个高阶函数(如 Python 内置的 filter
、map
等)中来为函数做解耦合,增强函数的灵活性和通用性。
下面的例子通过使用 filter
和 map
函数,实现了 从列表中筛选出奇数并求平方构成新列表 的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此这 filter
和 map
函数没有跟特定的过滤和映射数据的规则耦合在一起。
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]
说说 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) # 浅拷贝
Python 是如何实现内存管理的?
Python 提供了 自动化的内存管理,也就是说内存空间的分配与释放都是由 Python 解释器在运行时自动进行的,自动管理内存功能极大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以 CPython 解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。
ob_refcnt
的引用计数器成员变量。程序在运行的过程中 ob_refcnt
的值会被更新并借此来反映引用有多少个变量引用到该对象。当对象的引用计数值为 0 时,它的内存就会被释放掉。说一下你对 Python 中迭代器和生成器的理解。
迭代器是实现了迭代器协议的对象。跟其他编程语言不同,Python 中没有用于定义协议或表示约定的关键字,像 interface
、protocol
这些单词并不在 Python 语言的关键字列表中。Python 语言通过魔法方法来表示约定,也就是我们所说的协议,而 __next__
和 __iter__
这两个魔法方法就代表了 迭代器协议。可以通过 for-in
循环从迭代器对象中取出值,也可以使用 next
函数取出迭代器对象中的下一个值。
生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。生成器本质是一个函数,它记住了上一次返回时在函数体中的位置,对生成器函数的第二次调用,跳转到函数上一次挂起的位置,而且记录了程序执行的上下文,生成器不仅仅记住了它的数据状态,也记住了它执行的位置。
迭代器是一种支持 next()
操作的对象,它包含了一组元素,执行 next()
操作时,返回其中一个元素,当所有元素都返回时,再执行会报异常。
迭代器是一个更抽象的概念,任何对象,如果它的类有 next
方法和 iter
方法返回自己本身。对于 string
、list
、dict
、tuple
等这类容器对象,使用 for
循环遍历是很方便的。在后台 for
语句对容器对象调用 iter()
函数,iter()
是 python 的内置函数。iter()
会返回一个定义了 next()
方法的迭代器对象,它在容器中逐个访问容器内元素,next()
也是 python 的内置函数。在没有后续元素时,next()
会抛出一个 StopIteration
异常
生成器(Generator)是创建迭代器的简单而强大的工具。它们写起来就像是正规的函数,只是在需要返回数据的时候使用 yield
语句。每次 next()
被调用时,生成器会返回它脱离的位置(它记忆语句最后一次执行的位置和所有的数据值)。
区别:生成器能做到迭代器能做的所有事,而且因为自动创建了 __iter__()
和 next()
方法,生成器显得特别简洁,而且生成器也是高效的,使用生成器表达式取代列表解析可以同时节省内存。除了创建和保存程序状态的自动方法,当发生器终结时,还会自动抛出 StopIteration
异常。
面试中经常让写生成斐波那契数列的迭代器,大家可以参考下面的代码。
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
正则表达式的 match 方法和 search 方法有什么区别?
正则表达式是字符串处理的重要工具,所以也是面试中经常考察的知识点。
在 Python 中,使用正则表达式有两种方式,一种是直接调用 re
模块中的函数,传入正则表达式和需要处理的字符串;一种是先通过 re
模块的 compile
函数创建正则表达式对象,然后再通过对象调用方法并传入需要处理的字符串。
如果一个正则表达式被频繁的使用,我们推荐用 re.compile
函数创建正则表达式对象,这样会减少频繁编译同一个正则表达式所造成的开销。
match
方法是从字符串的起始位置进行正则表达式匹配,返回 Match对象
或 None
。如果匹配的字符不是在开头处,那么它将会报错。
search
方法会扫描整个字符串来找寻匹配的模式,同样也是返回 Match对象
或 None
。会匹配一整个字符串后得出结果
import re
result1 = re.match('li', 'liadadafbba').group()
result2 = re.match('li', 'addadlidadaf')
print(result1, result2) # li None
result1 = re.search('li', 'liadadafbba').group()
result2 = re.search('li', 'addadlidadaf').group()
print(result1, result2) # li li
Python 中为什么没有函数重载?
C++、Java、C# 等诸多编程语言都支持函数重载,所谓函数重载指的是 在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分。
重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为 编译时多态性 或者叫 前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解 Python 是 动态类型语言,是否知道 Python 中函数的可变参数、关键字参数这些概念。
首先 Python 是解释型语言,函数重载现象通常出现在编译型语言中。其次 Python 是动态类型语言, 函数的参数没有类型约束,也就 无法根据参数类型来区分重载。再者 Python 中函数的参数可以有默认值,可以使用可变参数和关键字参数,因此 即便没有函数重载,也要可以让一个函数根据调用者传入的参数产生不同的行为。
对于 Python 中位置参数、关键字参数、默认参数、可变参数的理解。
函数参数 *arg 和 **kwargs 分别代表什么?
Python 中,函数的参数分为 位置参数、关键字参数、可变参数、命名关键字参数。
*args
代表可变参数,可以接收 0 个或任意多个参数,当不确定调用者会传入多少个位置参数时,就可以使用可变参数,它会将传入的参数打包成一个元组。
**kwargs
代表关键字参数,可以接收用参数名=参数值的方式传入的参数,传入的参数的会打包成一个字典。
定义函数时如果同时使用 *args
和**kwargs
,那么函数可以接收任意参数。
*args
:可变长度的 位置参数
在最后一个形参名前加 *
,溢出的位置参数都会被接收,以元组的形式保存下来赋值给该形参。
def foo(x, y, z=1, *args): # 在最后一个形参名 args 前加 * 号
print(x)
print(y)
print(z)
print(args)
foo(1,2,3,4,5,6,7)
**kwargs
:可变长度的 关键字参数
在最后一个形参名前面加 **
,调用函数时,溢出的关键字参数都会被接收,以字典的形式保存下来赋值给该形参。
def foo(x, **kwargs): # 在最后一个参数 kwargs 前加 **
print(x)
print(kwargs)
# 溢出的关键字实参 y=2,z=3 都被 ** 接收,以字典的形式保存下来,赋值给 kwargs
foo(x=1, y=2, z=3)
说说你用过Python标准库中的哪些模块。
模块 | 解释 |
---|---|
sys | 跟 Python 解释器相关的变量和函数,例如:sys.version、sys.exit() |
os | 和操作系统相关的功能,例如:os.listdir()、os.remove() |
re | 和正则表达式相关的功能,例如:re.compile()、re.search() |
math | 和数学运算相关的功能,例如:math.pi、math.e、math.cos |
logging | 和日志系统相关的类和函数,例如:logging.Logger、logging.Handler |
json / pickle | 实现对象序列化和反序列的模块,例如:json.loads、json.dumps |
hashlib | 封装了多种哈希摘要算法的模块,例如:hashlib.md5、hashlib.sha1 |
urllib | 包含了和 URL 相关的子模块,例如:urllib.request、urllib.parse |
itertools | 提供各种迭代器的模块,例如:itertools.cycle、itertools.product |
functools | 函数相关工具模块,例如:functools.partial、functools.lru_cache |
collections / heapq | 封装了常用数据结构和算法的模块,例如:collections.deque |
threading / multiprocessing | 多线程/多进程相关类和函数的模块,例如:threading.Thread |
concurrent.futures / asyncio | 并发编程/异步编程相关的类和函数的模块,例如:ThreadPoolExecutor |
base64 | 提供 BASE-64 编码相关函数的模块,例如:bas64.encode |
csv | 和读写 CSV 文件相关的模块,例如:csv.reader、csv.writer |
profile / cProfile / pstats | 和代码性能剖析相关的模块,例如:cProfile.run、pstats.Stats |
unittest | 和单元测试相关的模块,例如:unittest.TestCase |
__init__ 和 __new__方法有什么区别?
Python 中 调用构造器创建对象 属于两阶段构造过程:
__new__
方法获得保存对象所需的内存空间__init__
执行对内存空间数据的填充(对象属性的初始化)。__new__
方法的返回值是创建好的 Python 对象(的引用),而 __init__
方法的第一个参数就是这个对象(的引用),所以在 __init__
中可以完成对对象的初始化操作。
__new__
是 类方法,它的第一个参数是 类。__init__
是 对象方法,它的第一个参数是 对象。
说一下你知道的 Python 中的魔术方法。
方法 | 解释 |
---|---|
__new__、__init__、__del__ | 创建和销毁对象相关 |
__add__、__sub__、__mul__、__div__、__floordiv__、__mod__ | 算术运算符相关 |
__eq__、__ne__、__lt__、__gt__、__le__、__ge__ | 关系运算符相关 |
__pos__、__neg__、__invert__ | 一元运算符相关 |
__lshift__、__rshift__、__and__、__or__、__xor__ | 位运算相关 |
__enter__、__exit__ | 上下文管理器协议 |
__iter__、__next__、__reversed__ | 迭代器协议 |
__int__、__long__、__float__、__oct__、__hex__ | 类型/进制转换相关 |
__str__、__repr__、__hash__、__dir__ | 对象表述相关 |
__len__、__getitem__、__setitem__、__contains__、__missing__ | 序列相关 |
__copy__、__deepcopy__ | 对象拷贝相关 |
__call__、__setattr__、__getattr__、__delattr__ | 其他魔术方法 |
什么是鸭子类型(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
方法,它需要的参数并不一定要是列表,只要是可迭代对象就没有问题。
说明:动态语言的鸭子类型使得设计模式的应用被大大简化。
说一下 Python 中变量的作用域。
Python 中有四种作用域,分别是
Local
):没有内部函数时,函数体为本地作用域。Embedded
):包含内部函数时,函数体为函数嵌套作用域。Global
):一般模块文件顶层声明的变量具有全局作用域,从外部来看,模块的全局变量就是一个模块对象的属性,仅限于单个模块文件中。Built-in
):Python 运行时的环境为内置作用域,它包含了 Python 的各种预定义变量和函数。搜索一个标识符时,会按照 LEGB 的顺序进行搜索,如果所有的作用域中都没有找到这个标识符,就会引发 NameError
异常。
说一下你对闭包的理解。
闭包是支持一等函数的编程语言(Python、JavaScript 等)中实现 词法绑定 的一种技术。
当捕捉闭包的时候,它的自由变量(在函数外部定义但在函数内部使用的变量)会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。
简单的说,可以将闭包理解为 能够读取其他函数内部变量 的函数。正在情况下,函数的局部变量在函数调用结束之后就结束了生命周期,但是闭包使得局部变量的生命周期得到了延展。使用闭包的时候需要注意,闭包会使得函数中创建的对象不会被垃圾回收,可能会导致很大的内存开销,所以闭包一定不能滥用。
闭包就是能够读取 外部函数内的变量 的函数。
作用1:闭包是将外层函数内的局部变量和外层函数的外部连接起来的一座桥梁。
作用2:将外层函数的变量持久地保存在内存中。
说一下 Python 中的多线程和多进程的应用场景和优缺点。
线程是操作系统分配 CPU 的基本单位,进程是操作系统分配内存的基本单位。通常我们运行的程序会包含一个或多个进程,而每个进程中又包含一个或多个线程。多线程的优点在于多个线程可以共享进程的内存空间,所以进程间的通信非常容易实现
但是如果使用官方的 CPython 解释器,多线程受制于 GIL(Global Interpreter Lock,全局解释器锁),并不能利用 CPU 的多核特性,这是一个很大的问题。使用多进程可以充分利用 CPU 的多核特性,但是进程间通信相对比较麻烦,需要使用 IPC 机制(管道、套接字等)。
多线程 适合那些会 花费大量时间在 I/O 操作上,但 没有太多并行计算需求,且 不需占用太多内存 的 I/O 密集型应用。
多进程 适合执行计算密集型任务(如:视频编码解码、数据处理、科学计算等)、可以分解为多个并行子任务,并能合并子任务执行结果的任务,以及在内存使用方面没有任何限制且不强依赖于 I/O 操作的任务。
Python 中实现 并发编程 通常有 多线程、多进程、异步编程 三种选择。异步编程实现了协作式并发,通过多个相互协作的子程序的用户态切换,实现对 CPU 的高效利用,这种方式也是非常适合 I/O 密集型应用的。
Python 如何实现多线程?
Python 主要是通过 thread
和 threading
这两个模块来实现多线程支持。
Python 的 thread
模块是比较底层的模块,Python 的 threading
模块是对 thread
做了一些封装,可以更加方便的被使用。但是 Python(CPython)由于 GIL(Global Interpreter Lock,全局解释器锁) 的存在无法使用 threading
充分利用 CPU 资源,如果想充分发挥多核 CPU 的计算能力需要使用 multiprocessing
模块(Windows下使用会有诸多问题)。
Python3.x 中已经摒弃了 Python2.x 中采用函数式 thread
模块中的 start_new_thread()
函数来产生新线程方式。
Python3.x 中通过 threading
模块创建新的线程有两种方法:
threading.Thread(Target=executable Method)
,即传递给 Thread
对象一个可执行方法(或对象)。threading.Thread
定义子类并重写 run()
方法。第二种方法中,唯一必须重写的方法是 run()
。谈谈你对 “猴子补丁”(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
对象替代被测对象所依赖的对象的模块。
解释一下线程池的工作原理。
池化技术 就是一种典型空间换时间的策略,我们使用的数据库连接池、线程池等都是池化技术的应用,Python 标准库 currrent.futures
模块的 ThreadPoolExecutor
就是线程池的实现,如果要弄清楚它的工作原理,可以参考下面的内容。
线程池是一种用于 减少线程本身创建和销毁造成的开销 的技术,属于典型的空间换时间操作。如果应用程序需要频繁的将任务派发到线程中执行,线程池就是必选项,因为 创建和释放线程涉及到大量的系统底层操作,开销较大,如果能够在应用程序工作期间,将创建和释放线程的操作变成 预创建和借还操作,将大大减少底层开销。
线程池在应用程序启动后,立即创建一定数量的线程,放入空闲队列中。这些线程最开始都处于阻塞状态,不会消耗 CPU 资源,但会占用少量的内存空间。当任务到来后,从队列中取出一个空闲线程,把任务派发到这个线程中运行,并将该线程标记为已占用。当线程池中所有的线程都被占用后,可以选择自动创建一定数量的新线程,用于处理更多的任务,也可以选择让任务排队等待直到有空闲的线程可用。在任务执行完毕后,线程并不退出结束,而是继续保持在池中等待下一次的任务。 当系统比较空闲时,大部分线程长时间处于闲置状态时,线程池可以自动销毁一部分线程,回收系统资源。
基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销 分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小。
一般线程池都必须具备下面几个组成部分:
举例说明什么情况下会出现 KeyError、TypeError、ValueError。
举一个简单的例子,变量 a
是一个字典,执行 int(a[‘x’])
这个操作就有可能引发上述三种类型的异常。
x
,会引发 KeyError
;x
对应的值不是 str
、float
、int
、bool
以及 bytes-like
类型,在调用 int
函数构造 int
类型的对象时,会引发 TypeError
;a[x]
是一个字符串或者字节串,而对应的内容又无法处理成 int
时,将引发 ValueError
。为什么要使用 with open 打开文件?
f = open("...","wb")
try:
f.write("hello world")
except:
pass
finally:
f.close()
打开文件在进行读写的时候可能会出现一些异常状况,如果按照常规的 f.open
写法,我们需要 try
,except
,finally
,做异常判断,并且文件最终不管遇到什么情况,都要执行 finally
中的 f.close()
,关闭文件,with
方法帮我们实现了 finally
中 f.close
。
with open() as f:
主要打开一个缓存区,将打开的数据储存在缓存区内,进行修改,增减等处理。
with
语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的清理操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等;with
语句即上下文管理器,在程序中用来表示代码执行过程中所处的前后环境。上下文管理器:含有 __enter__
和 __exit__
方法的对象就是上下文管理器。enter()
:在执行 with
语句之前,首先执行该方法,通常返回一个实例对象,如果 with
语句有 as
目标,则将对象赋值给 as
目标。exit()
:在执行 with
语句结束后,自动调用 __exit__()
方法,用户释放资源,若此方法返回布尔值 True
,程序会忽略异常。这个上下文管理器,新手可能不是太好理解。其实你看它做了什么,with open() as f
,然后你下面的代码里就都可以用 f
来做一些操作。也就是说在这个小代码段内,f
就指代了前面的文件,你用 f
,就是在用这个文件。这里带来的另外一个好处当然就是自动打开和关闭文件, 这个大家就都熟悉了。
如何读取大文件,例如内存只有 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
扩展:外部排序 跟上述的情况非常类似,由于处理的数据不能一次装入内存,只能放在读写较慢的外存储器(通常是硬盘)上。排序-归并算法 就是一种常用的外部排序策略。在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个 临时文件,依此进行,将待排序数据组织为多个有序的临时文件,然后在归并阶段将这些临时文件组合为一个大的有序文件,这个大的有序文件就是排序的结果。
说一下你对 Python 中模块和包的理解。
每个 Python 文件就是一个模块,而保存这些文件的文件夹就是一个包,但是这个作为 Python 包的文件夹必须要有一个名为 __init__.py
的文件,否则无法导入这个包。
通常一个文件夹下还可以有子文件夹,这也就意味着一个包下还可以有子包,子包中的 __init__.py
并不是必须的。
模块和包解决了 Python 中 命名冲突 的问题,不同的包下可以有同名的模块,不同的模块下可以有同名的变量、函数或类。
在 Python 中可以使用 import
或 from … import …
来导入包和模块,在导入的时候还可以使用 as
关键字对包、模块、类、函数、变量等进行别名,从而彻底解决编程中尤其是多人协作团队开发时的命名冲突问题。
说一下你知道的 Python 编码规范。
点评:企业的 Python 编码规范基本上是参照 PEP-8
或谷歌开源项目风格指南来制定的,后者还提到了可以使用 Lint
工具来检查代码的规范程度,面试的时候遇到这类问题,可以先说下这两个参照标准,然后挑重点说一下 Python 编码的注意事项。
self
以表示 对象自身。cls
以表示 该类自身。if a is not b
就比 if not a is b
更容易让人理解。None
或者没有元素,应该用 if not x
这样的写法来检查它。if
分支、for
循环、except
异常捕获等中只有一行代码,也不要将代码和 if
、for
、except
等写在一起,分开写才会让代码更清晰。from math import sqrt
比 import math
更好。import
语句,应该将其分为三部分,从上到下分别是 Python标准模块、第三方模块、自定义模块,每个部分内部应该按照模块名称的字母表顺序来排列。参考资料
【1】【建议收藏】50 道硬核的 Python 面试题!
【2】Python 闭包(Closure)详解
【3】用最简单的语言解释 Python 的闭包是什么?