当前位置:   article > 正文

【编程之路】Python 编程进阶(精选 23 道题)_python进阶编程题

python进阶编程题

1.不使用中间变量,交换两个变量 a 和 b 的值

a, b = b, a
  • 1

需要注意,a, b = b, a 这种做法其实并不是元组解包,虽然很多人都这样认为。Python 字节码指令中有 ROT_TWO 指令来支持这个操作,类似的还有 ROT_THREE,对于 3 个以上的元素,如 a, b, c, d = b, c, d, a,才会用到创建元组和元组解包。想知道你的代码对应的字节码指令,可以使用 Python 标准库中 dis 模块的 dis 函数来反汇编你的Python代码。

2.Lambda 函数是什么,举例说明的它的应用场景

Lambda 函数通常用在高阶函数中,主要的作用是通过 向函数传入函数让函数返回函数 最终实现代码的解耦合。

Lambda 函数也叫 匿名函数,它是功能简单用一行代码就能实现的小型函数。Python 中的 Lambda 函数只能写一个表达式,这个表达式的执行结果就是函数的返回值,不用写 return 关键字。Lambda 函数因为没有名字,所以也不会跟其他函数发生命名冲突的问题。

Lambda 函数其实最为主要的用途是把一个函数传入另一个高阶函数(如 Python 内置的 filtermap 等)中来为函数做解耦合,增强函数的灵活性和通用性。

下面的例子通过使用 filtermap 函数,实现了 从列表中筛选出奇数并求平方构成新列表 的操作,因为用到了高阶函数,过滤和映射数据的规则都是函数的调用者通过另外一个函数传入的,因此这 filtermap 函数没有跟特定的过滤和映射数据的规则耦合在一起。

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]
  • 1
  • 2
  • 3

用列表的生成式来实现上面的代码会更加简单明了,代码如下所示。

items = [12, 5, 7, 10, 8, 19]
items = [x ** 2 for x in items if x % 2]
print(items)    # [25, 49, 361]
  • 1
  • 2
  • 3

3.说说 Python 中的浅拷贝和深拷贝

回答这个题目的要点不仅仅是能够说出浅拷贝和深拷贝的区别,深拷贝的时候可能遇到的两大问题,还要说出 Python 标准库对浅拷贝和深拷贝的支持,然后可以说说列表、字典如何实现拷贝操作以及如何通过序列化和反序列的方式实现深拷贝,最后还可以提到设计模式中的原型模式以及它在项目中的应用。

在这里插入图片描述

浅拷贝通常只复制对象本身,而深拷贝不仅会复制对象,还会递归的复制对象所关联的对象。

浅拷贝 只是单纯地进行指针的复制,原变量与新变量指向同一片存储空间,从而导致:修改原变量 / 新变量的值的同时,新变量 / 原变量的值也是随之同时改变。

深拷贝 是另起一片存储空间,将原存储空间的内容复制到新的存储空间中,故:修改原变量 / 新变量的值时,新变量 / 原变量的值不会发生改变。

深拷贝可能会遇到两个问题:

  • 一是一个对象如果直接或间接的引用了自身,会导致无休止的递归拷贝;
  • 二是深拷贝可能对原本设计为多个对象共享的数据也进行拷贝。

Python 通过 copy 模块中的 copydeepcopy 函数来实现浅拷贝和深拷贝操作,其中 deepcopy 可以通过 memo 字典来保存已经拷贝过的对象,从而避免刚才所说的自引用递归问题;此外,可以通过 copyreg 模块的 pickle 函数来定制指定类型对象的拷贝行为。

deepcopy 函数的本质其实就是对象的一次序列化和一次返回序列化,面试题中还考过用自定义函数实现对象的深拷贝操作,显然我们可以使用 pickle 模块的 dumpsloads 来做到,代码如下所示。

import pickle
my_deep_copy = lambda obj: pickle.loads(pickle.dumps(obj))
  • 1
  • 2

列表的切片操作 [:] 相当于实现了列表对象的浅拷贝,而字典的 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)    # 浅拷贝
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

4.Python 是如何实现内存管理的?

Python 提供了 自动化的内存管理,也就是说内存空间的分配与释放都是由 Python 解释器在运行时自动进行的,自动管理内存功能极大的减轻程序员的工作负担,也能够帮助程序员在一定程度上解决内存泄露的问题。以 CPython 解释器为例,它的内存管理有三个关键点:引用计数、标记清理、分代收集。

  • 引用计数:对于 CPython 解释器来说,Python 中的每一个对象其实就是 PyObject 结构体,它的内部有一个名为 ob_refcnt 的引用计数器成员变量。程序在运行的过程中 ob_refcnt 的值会被更新并借此来反映引用有多少个变量引用到该对象。当对象的引用计数值为 0 时,它的内存就会被释放掉。
  • 标记清理:CPython 使用了 “标记-清理”(Mark and Sweep)算法解决容器类型可能产生的循环引用问题。该算法在垃圾回收时分为两个阶段:标记阶段,遍历所有的对象,如果对象是可达的(被其他对象引用),那么就标记该对象为可达;清除阶段,再次遍历对象,如果发现某个对象没有标记为可达,则就将其回收。
  • 分代回收:在循环引用对象的回收中,整个应用程序会被暂停,为了减少应用程序暂停的时间,Python 通过分代回收(空间换时间)的方法提高垃圾回收效率。分代回收的基本思想是:对象存在的时间越长,是垃圾的可能性就越小,应该尽量不对这样的对象进行垃圾回收

5.说一下你对 Python 中迭代器和生成器的理解

迭代器是实现了迭代器协议的对象。跟其他编程语言不同,Python 中没有用于定义协议或表示约定的关键字,像 interfaceprotocol 这些单词并不在 Python 语言的关键字列表中。Python 语言通过魔法方法来表示约定,也就是我们所说的协议,而 __next____iter__ 这两个魔法方法就代表了 迭代器协议。可以通过 for-in 循环从迭代器对象中取出值,也可以使用 next 函数取出迭代器对象中的下一个值。

生成器是迭代器的语法升级版本,可以用更为简单的代码来实现一个迭代器。生成器本质是一个函数,它记住了上一次返回时在函数体中的位置,对生成器函数的第二次调用,跳转到函数上一次挂起的位置,而且记录了程序执行的上下文,生成器不仅仅记住了它的数据状态,也记住了它执行的位置。

迭代器是一种支持 next() 操作的对象,它包含了一组元素,执行 next() 操作时,返回其中一个元素,当所有元素都返回时,再执行会报异常。

迭代器是一个更抽象的概念,任何对象,如果它的类有 next 方法和 iter 方法返回自己本身。对于 stringlistdicttuple 等这类容器对象,使用 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()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如果用生成器的语法来改写上面的代码,代码会简单优雅很多。

def fib(num):
    a, b = 0, 1
    for _ in range(num):
        a, b = b, a + b
        yield a
  • 1
  • 2
  • 3
  • 4
  • 5

6.正则表达式的 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

7.Python 中为什么没有函数重载?

C++、Java、C# 等诸多编程语言都支持函数重载,所谓函数重载指的是 在同一个作用域中有多个同名函数,它们拥有不同的参数列表(参数个数不同或参数类型不同或二者皆不同),可以相互区分

重载也是一种多态性,因为通常是在编译时通过参数的个数和类型来确定到底调用哪个重载函数,所以也被称为 编译时多态性 或者叫 前绑定。这个问题的潜台词其实是问面试者是否有其他编程语言的经验,是否理解 Python 是 动态类型语言,是否知道 Python 中函数的可变参数、关键字参数这些概念。

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

8.对于 Python 中位置参数、关键字参数、默认参数、可变参数的理解

  • 位置函数:调用函数时根据函数定义的参数位置来传递参数。
  • 关键字参数:用于函数调用,通过 “键-值” 形式加以指定。可以让函数更加清晰、容易使用,同时也清除了参数的顺序需求。
  • 默认参数:用于定义函数,为参数提供默认值,调用函数时可传可不传该默认参数的值(注意:所有位置参数必须出现在默认参数前,包括函数定义和调用)
  • 可变参数:定义函数时,有时候我们不确定调用的时候会传递多少个参数(不传参也可以)。此时,可用包裹(packing)位置参数,或者包裹关键字参数,来进行参数传递,会显得非常方便。

9.函数参数 *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)
  • 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)  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里插入图片描述

10.说说你用过 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

11.__ 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__其他魔术方法

12.什么是鸭子类型(duck typing)?

在 Python 语言中,有很多 bytes-like 对象(如:bytesbytearrayarray.arraymemoryview)、file-like 对象(如:StringIOBytesIOGzipFilesocket)、path-like 对象(如:strbytes)。

其中 file-like 对象都能支持 readwrite 操作,可以像文件一样读写,这就是所谓的 对象有鸭子的行为就可以判定为鸭子的判定方法。再比如 Python 中列表的 extend 方法,它需要的参数并不一定要是列表,只要是可迭代对象就没有问题。

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

13.说一下 Python 中变量的作用域

Python 中有四种作用域,分别是

  • 局部作用域(Local):没有内部函数时,函数体为本地作用域。
  • 嵌套作用域(Embedded):包含内部函数时,函数体为函数嵌套作用域。
  • 全局作用域(Global):一般模块文件顶层声明的变量具有全局作用域,从外部来看,模块的全局变量就是一个模块对象的属性,仅限于单个模块文件中。
  • 内置作用域(Built-in):Python 运行时的环境为内置作用域,它包含了 Python 的各种预定义变量和函数。

搜索一个标识符时,会按照 LEGB 的顺序进行搜索,如果所有的作用域中都没有找到这个标识符,就会引发 NameError 异常。

14.说一下你对闭包的理解

闭包是支持一等函数的编程语言(Python、JavaScript 等)中实现 词法绑定 的一种技术。

当捕捉闭包的时候,它的自由变量(在函数外部定义但在函数内部使用的变量)会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。

简单的说,可以将闭包理解为 能够读取其他函数内部变量 的函数。正在情况下,函数的局部变量在函数调用结束之后就结束了生命周期,但是闭包使得局部变量的生命周期得到了延展。使用闭包的时候需要注意,闭包会使得函数中创建的对象不会被垃圾回收,可能会导致很大的内存开销,所以闭包一定不能滥用。

闭包就是能够读取 外部函数内的变量 的函数。
作用1:闭包是将外层函数内的局部变量和外层函数的外部连接起来的一座桥梁。
作用2:将外层函数的变量持久地保存在内存中。

15.说一下 Python 中的多线程和多进程的应用场景和优缺点

线程是操作系统分配 CPU 的基本单位,进程是操作系统分配内存的基本单位。通常我们运行的程序会包含一个或多个进程,而每个进程中又包含一个或多个线程。多线程的优点在于多个线程可以共享进程的内存空间,所以进程间的通信非常容易实现

但是如果使用官方的 CPython 解释器,多线程受制于 GIL(Global Interpreter Lock,全局解释器锁),并不能利用 CPU 的多核特性,这是一个很大的问题。使用多进程可以充分利用 CPU 的多核特性,但是进程间通信相对比较麻烦,需要使用 IPC 机制(管道、套接字等)。

多线程 适合那些会 花费大量时间在 I/O 操作上,但 没有太多并行计算需求,且 不需占用太多内存 的 I/O 密集型应用。

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

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

16.Python 如何实现多线程?

Python 主要是通过 threadthreading 这两个模块来实现多线程支持。

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

17.谈谈你对 “猴子补丁”(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
  • 1
  • 2
  • 3
  • 4
  • 5

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

18.解释一下线程池的工作原理

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

线程池是一种用于 减少线程本身创建和销毁造成的开销 的技术,属于典型的空间换时间操作。如果应用程序需要频繁的将任务派发到线程中执行,线程池就是必选项,因为 创建和释放线程涉及到大量的系统底层操作,开销较大,如果能够在应用程序工作期间,将创建和释放线程的操作变成 预创建和借还操作,将大大减少底层开销。

线程池在应用程序启动后,立即创建一定数量的线程,放入空闲队列中。这些线程最开始都处于阻塞状态,不会消耗 CPU 资源,但会占用少量的内存空间。当任务到来后,从队列中取出一个空闲线程,把任务派发到这个线程中运行,并将该线程标记为已占用。当线程池中所有的线程都被占用后,可以选择自动创建一定数量的新线程,用于处理更多的任务,也可以选择让任务排队等待直到有空闲的线程可用。在任务执行完毕后,线程并不退出结束,而是继续保持在池中等待下一次的任务。 当系统比较空闲时,大部分线程长时间处于闲置状态时,线程池可以自动销毁一部分线程,回收系统资源。

基于这种预创建技术,线程池将线程创建和销毁本身所带来的开销 分摊到了各个具体的任务上,执行次数越多,每个任务所分担到的线程本身开销则越小。

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

  • 线程池管理器:用于创建并管理线程池。
  • 工作线程和线程队列:线程池中实际执行的线程以及保存这些线程的容器。
  • 任务接口:将线程执行的任务抽象出来,形成任务接口,确保线程池与具体的任务无关。
  • 任务队列:线程池中保存等待被执行的任务的容器。

19.举例说明什么情况下会出现 KeyError、TypeError、ValueError

举一个简单的例子,变量 a 是一个字典,执行 int(a[‘x’]) 这个操作就有可能引发上述三种类型的异常。

  • 如果字典中没有键 x,会引发 KeyError
  • 如果键 x 对应的值不是 strfloatintbool 以及 bytes-like 类型,在调用 int 函数构造 int 类型的对象时,会引发 TypeError
  • 如果 a[x] 是一个字符串或者字节串,而对应的内容又无法处理成 int 时,将引发 ValueError

20.为什么要使用 with open 打开文件?

f = open("...","wb")
try:
	f.write("hello world")
except:
	pass
finally:
	f.close()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

打开文件在进行读写的时候可能会出现一些异常状况,如果按照常规的 f.open 写法,我们需要 tryexceptfinally,做异常判断,并且文件最终不管遇到什么情况,都要执行 finally 中的 f.close(),关闭文件,with 方法帮我们实现了 finallyf.close

with open() as f:
  • 1

主要打开一个缓存区,将打开的数据储存在缓存区内,进行修改,增减等处理。

  • with 语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的清理操作,释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等;
  • with 语句即上下文管理器,在程序中用来表示代码执行过程中所处的前后环境。上下文管理器:含有 __enter____exit__ 方法的对象就是上下文管理器。
  • enter():在执行 with 语句之前,首先执行该方法,通常返回一个实例对象,如果 with 语句有 as 目标,则将对象赋值给 as 目标。
  • exit():在执行 with 语句结束后,自动调用 __exit__() 方法,用户释放资源,若此方法返回布尔值 True,程序会忽略异常。
  • 使用环境:文件读写、线程锁的自动释放等。

这个上下文管理器,新手可能不是太好理解。其实你看它做了什么,with open() as f,然后你下面的代码里就都可以用 f 来做一些操作。也就是说在这个小代码段内,f 就指代了前面的文件,你用 f,就是在用这个文件。这里带来的另外一个好处当然就是自动打开和关闭文件, 这个大家就都熟悉了。

21.如何读取大文件,例如内存只有 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
  • 1
  • 2
  • 3

在 Linux 系统上,可以通过 split 命令将大文件切割为小片,然后通过读取切割后的小文件对数据进行处理。例如下面的命令将名为 filename 的大文件切割为大小为 512M 的多个文件。

split -b 512m filename
  • 1

如果愿意, 也可以将名为 filename 的文件切割为 10 个文件,命令如下所示。

split -n 10 filename
  • 1

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

22.说一下你对 Python 中模块和包的理解

每个 Python 文件就是一个模块,而保存这些文件的文件夹就是一个包,但是这个作为 Python 包的文件夹必须要有一个名为 __init__.py 的文件,否则无法导入这个包。

通常一个文件夹下还可以有子文件夹,这也就意味着一个包下还可以有子包,子包中的 __init__.py 并不是必须的。

模块和包解决了 Python 中 命名冲突 的问题,不同的包下可以有同名的模块,不同的模块下可以有同名的变量、函数或类。

在 Python 中可以使用 importfrom … import … 来导入包和模块,在导入的时候还可以使用 as 关键字对包、模块、类、函数、变量等进行别名,从而彻底解决编程中尤其是多人协作团队开发时的命名冲突问题。

23.说一下你知道的 Python 编码规范

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

  • 空格的使用
    • 使用空格来表示缩进而不要用制表符(Tab)。
    • 和语法相关的每一层缩进都用4个空格来表示。
    • 每行的字符数不要超过 79 个字符,如果表达式因太长而占据了多行,除了首行之外的其余各行都应该在正常的缩进宽度上再加上 4 个空格。
    • 函数和类的定义,代码前后都要用两个空行进行分隔。
    • 在同一个类中,各个方法之间应该用一个空行进行分隔。
    • 二元运算符的左右两侧应该保留一个空格,而且只要一个空格就好。
  • 标识符命名
    • 变量、函数和属性应该使用小写字母来拼写,如果有多个单词就使用下划线进行连接。
    • 类中受保护的实例属性,应该以一个下划线开头。
    • 类中私有的实例属性,应该以两个下划线开头。
    • 类和异常的命名,应该每个单词首字母大写。
    • 模块级别的常量,应该采用全大写字母,如果有多个单词就用下划线进行连接。
    • 类的实例方法,应该把第一个参数命名为 self 以表示 对象自身
    • 类的类方法,应该把第一个参数命名为 cls 以表示 该类自身
  • 表达式和语句
    • 采用内联形式的否定词,而不要把否定词放在整个表达式的前面。例如:if a is not b 就比 if not a is b 更容易让人理解。
    • 不要用检查长度的方式来判断字符串、列表等是否为 None 或者没有元素,应该用 if not x 这样的写法来检查它。
    • 就算 if 分支、for 循环、except 异常捕获等中只有一行代码,也不要将代码和 ifforexcept 等写在一起,分开写才会让代码更清晰。
    • import 语句总是放在文件开头的地方。
    • 引入模块的时候,from math import sqrtimport math 更好。
    • 如果有多个 import 语句,应该将其分为三部分,从上到下分别是 Python标准模块第三方模块自定义模块,每个部分内部应该按照模块名称的字母表顺序来排列。

参考资料

【1】【建议收藏】50 道硬核的 Python 面试题!
【2】Python 闭包(Closure)详解
【3】用最简单的语言解释 Python 的闭包是什么?

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/589938
推荐阅读
相关标签
  

闽ICP备14008679号