Python面试题目

91 阅读12分钟

前言

本文精选了一系列具有代表性的Python面试题目,旨在为有一定Python基础的开发者提供进阶复习。这里没有列出过于简单的问题。如果你对Python日常应用已经驾轻就熟,这篇文章将帮助你巩固和回顾关键概念。对于Python新手,建议先打好基础再挑战这些题目。仔细阅读到最后,还有意外惊喜等着你!

另外,允许我小小吐槽一下:在掘金社区推广文章真是件苦差事。我之前精心准备的《Redis面试题目》一文,花了整整一周时间才获得区区18次展示,确实让人有些失落。希望这次我们的努力能得到更多的认可!

1、介绍一下Python

官网回答

  • Python 是一种易于学习、功能强大的编程语言。它具有高效的高级数据结构和简单但有效的面向对象编程方法。 Python 优雅的语法和动态类型及其解释性使其成为大多数平台上许多领域的脚本编写和快速应用程序开发的理想语言。
  • Python 解释器和广泛的标准库可以从 Python 网站 www.python.org/以源代码或二进制形式免费提供给所有主要平台,并且可以免费分发。同一站点还包含许多免费第三方 Python 模块、程序和工具以及其他文档的分发版和指针。
  • Python 解释器可以使用 C 或 C++(或可从 C 调用的其他语言)实现的新函数和数据类型轻松扩展。 Python 也适合作为可定制应用程序的扩展语言。

一般回答

  • Python是一种通用的、高级的、解释型、动态类型的编程语言。

2、介绍一下Python的全局解释器锁(GIL)

在Python中,全局解释器锁(Global Interpreter Lock,简称GIL)一直是备受争议的话题。GIL是CPython解释器的一个特征,它对多线程程序的并发性能产生了限制。

什么是GIL?

  • GIL是Cpython解释器中的一个机制,用于保证在解释器级别上只有一个线程可以执行Python字节码。也就是说,无论你在程序中创建了多少个线程,同一时刻只有一个线程能够执行Python代码,其他线程会被阻塞。这意味着在多核CPU上,Python的多线程程序并不能充分利用硬件资源。

GIL的原理

  • 为了理解GIL原理,你要知道CPython(即官方的Python解释器)使用的一种叫做引用计数的内存管理技术。每个对象都有一个引用计数,记录当前有多少个引用指向该对象。当引用计数归零时,对象会被销毁。
  • 在CPython中,GIL实际上是一把互斥锁,它的作用是保护解释器内部的数据结构免受并发访问的影响。当一个线程获得了GIL后,其他线程就无法执行Python字节码,只能等待GIL的释放。
  • GIL的存在是为了简化CPython解释器的实现,使其更加易于维护和扩展。但它也成为了CPython解释器的性能瓶颈。

GIL的影响

  • GIL对多线程程序的影响主要体现在两个方面:并发性能和多核利用率

并发性能

  • 由于GIL的存在,Python中的多线程程序并不能真正的实现并行执行。即使你在程序中创建了多个线程,它们在执行Python代码时仍然是串行的。这意味着多线程程序在CPU密集型任务上的性能提升会非常有限,甚至可能比单核程序还要慢。

多核利用率

  • 在多核CPU上,Python的多线程程序并不能充分利用所有的CPU核心。因为GIL的存在,同一时刻只有一个线程能够执行Python代码,其他线程被阻塞。这导致在多核CPU上,Python的多线程程序无法真正实现并行计算。

如何解决GIL的限制

  • 使用多进程:由于GIL只存在于单个解释器进程中,我们可以通过使用多个进程而不是多个线程来实现并行计算。在Python中可以使用multiprocessing模块来创建多个进程,并利用多核CPU的优势。
  • 使用异步编程:Python中有许多基于协程的异步编程库,如asyncio、Trio和curio。这些库使用事件循环和协程来实现非阻塞的异步I/O操作,从而提高程序的并发能力。
  • 使用其他支持并发的语言编写并发程序然后与Python进行交互。

3、解释线程、进程和协程

  • 进程是操作系统能够分配资源并独立运行的一个程序。它包含程序代码、数据、堆栈和寄存器等。
  • 线程是进程中的一个执行单元。它可以与其他线程共享进程的资源,例如内存、文件和地址空间。
  • 协程是用户空间线程,由程序员自己管理。它比线程更轻量级,并且可以在同一个线程上执行
特性进程线程协程
创建由操作系统创建由进程创建由程序员创建
调度由操作系统调度由操作系统调度由程序员调度
资源独立的资源共享进程的资源共享线程的资源
轻量级较重较轻最轻
切换开销中等
并发性最高

4、Python的生成器和迭代器

生成器是一种特殊的迭代器,它可以动态地生成数据流。相比于一次生成所有数据,生成器每次生成一个值并在需要时暂停,从而实现按需生成数据的效果。这种按生成数据的方式不仅节省内存、还可以提高程序的执行效率。

创建生成器可以使用生成器函数和生成器表达式

  • 生成器函数是一种特殊的函数,使用yield关键词来生成值。当使用yield语句生成值时,函数会自动暂停并记录当前状态,下次调用时继续执行。生成器函数可以使用return语句终止函数,也可以使用yield语句多次生成值。
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# 使用生成器函数创建生成器
fib_gen = fibonacci()

# 生成器可以按需生成值
print(next(fib_gen))  # 输出:0
print(next(fib_gen))  # 输出:1
print(next(fib_gen))  # 输出:1
print(next(fib_gen))  # 输出:2
print(next(fib_gen))  # 输出:3

上述代码中,fibonacci是一个生成器函数,使用yield语句生成斐波那契数列的每个值。通过调用next函数可以依次获取生成器中的值。

  • 生成器表达式是一种使用类似列表推导式的语法来创建生成器。生成器表达式使用圆括号包裹表达式,并在其中使用yield关键字来生成值。
# 使用生成器表达式创建生成器
even_gen = (x for x in range(10) if x % 2 == 0)

# 生成器可以按需生成值
print(next(even_gen))  # 输出:0
print(next(even_gen))  # 输出:2
print(next(even_gen))  # 输出:4
print(next(even_gen))  # 输出:6
print(next(even_gen))  # 输出:8

在上述代码中,**(x for x in range(10) if x % 2 == 0)**是一个生成器表达式,用于生成0到9中的偶数。


迭代器是一种支持迭代协议的对象,可以按照特定的顺序逐个访问数据。在Python中,大多数容器(列表,字符串,字典)都是可迭代的,并且可以使用迭代器来遍历其中的元素。

  • 迭代器协议

    迭代器协议是一种规范,用于定义迭代器对象必须实现的方法。根据迭代器协议,一个迭代器对象必须实现以下两个方法:

    • iter():返回迭代器对象本身
    • next():返回迭代器的下一个值。如果已经没有更多的元素,抛出StopIteration异常
    class MyIterator:
        def __init__(self, data):
            self.data = data
            self.index = 0
        
        def __iter__(self):
            return self
        
        def __next__(self):
            if self.index >= len(self.data):
                raise StopIteration
            value = self.data[self.index]
            self.index += 1
            return value
    
    # 使用自定义迭代器遍历列表
    my_iter = MyIterator([1, 2, 3, 4, 5])
    for item in my_iter:
        print(item)
    

    在上述示例中,MyIterator是一个自定义的迭代器类,实现了迭代器协议中的 iternext 方法。通过在for循环中使用自定义迭代器,我们可以逐个遍历列表中的元素。

  • Python提供一些内置的函数和语法来简化迭代过程

    iter()函数

    **iter()**函数可以将可迭代对象转换为迭代器。当我们需要使用迭代器遍历一个可迭代对象时,可以使用 **iter()**函数进行转换。

    下面是一个使用 **iter()**函数遍历字符串的示例:

    string = 'Hello, World!'
    
    # 使用 iter() 函数将字符串转换为迭代器
    string_iter = iter(string)
    
    # 使用 next() 函数逐个遍历字符
    print(next(string_iter))  # 输出:H
    print(next(string_iter))  # 输出:e
    print(next(string_iter))  # 输出:l
    print(next(string_iter))  # 输出:l
    print(next(string_iter))  # 输出:o
    

    在上述示例中,我们使用 **iter()**函数将字符串转换为迭代器,并使用 **next()**函数逐个遍历其中的字符。

    zip()函数

    zip() 函数可以将多个可迭代对象按照索引位置进行压缩,返回一个元组组成的迭代器。这样我们就可以同时遍历多个可迭代对象。

    下面是一个使用 zip() 函数遍历多个列表的示例:

    names = ['Alice', 'Bob', 'Charlie']
    ages = [25, 30, 35]
    
    # 使用 zip() 函数同时遍历多个列表
    for name, age in zip(names, ages):
        print(f'{name} is {age} years old.')
    

    在上述示例中,我们使用 **zip()**函数将 namesages列表压缩成一个元组组成的迭代器,并使用 for循环同时遍历两个列表。

可迭代对象与迭代器的区别

  • 可迭代对象是一种可以被迭代的对象。迭代器是一种可以提供一个元素序列的对象。
  • 可迭代对象只需实现 iter 方法。迭代器还必须实现 next 方法。
  • 可迭代对象可以是任何类型,只要它实现了iter 方法。迭代器通常是特定类型的对象,例如列表迭代器或字符串迭代器。

5、Python装饰器

装饰器是Python中一种强大的工具,它允许我们在不修改函数代码的情况下动态地修改函数的行为。装饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数或修改原来的函数。

装饰器的语法

装饰器使用**@**符号来应用于函数或方法上。例如:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("函数调用之前")
        func(*args, **kwargs)
        print("函数调用之后")
    return wrapper

@my_decorator
def my_function():
    print("my_function被调用了")

my_function()

输出

函数调用之前
my_function被调用了
函数调用之后

在装饰器中添加参数,可以使用闭包的方法

def my_decorator(arg1):
    def wrapper(func):
        def inner_wrapper(*args, **kwargs):
            print("arg1:", arg1)
            func(*args, **kwargs)
        return inner_wrapper
    return wrapper

@my_decorator("hello")
def my_function():
    print("my_function被调用了")

my_function()

输出

arg1: hello
my_function被调用了

6、解释闭包

闭包是指引用了外部作用域变量的函数。闭包是在嵌套函数中定义的函数。简单来解释:闭包就像一个装了东西的盒子,盒子里面装着函数的代码,以及函数所需要的所有数据。即使盒子外面的环境已经改变,盒子里面的东西仍然保持不变。

简单的闭包。例如:

def outer_function(x):
    def inner_function(y):
        return x + y
    return inner_function

closure = outer_function(10)

print(closure(20))

输出:

30

闭包的特征:

  • 闭包可以访问外部作用域的变量,即使外部作用域已经结束。
  • 闭包可以修改外部作用域的变量。

7、Python实现单例模式

单例模式是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。当希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。

Python实现单例模式的几种方式

  • 使用模块

其实,Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。

  • 使用装饰器

代码段

def singleton(cls):
    instances = {}

    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return wrapper

@singleton
class MyClass:
    pass

my_instance1 = MyClass()
my_instance2 = MyClass()

print(my_instance1 is my_instance2)

输出:

True
  • 使用类方法

Python

class MyClass:
    def __init__(self):
        pass

    @classmethod
    def get_instance(cls):
        if not hasattr(cls, "_instance"):
            cls._instance = cls()
        return cls._instance

my_instance1 = MyClass.get_instance()
my_instance2 = MyClass.get_instance()

print(my_instance1 is my_instance2)

输出:

True
  • 基于__new__方法实现

Python

class MyClass:
    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance

my_instance1 = MyClass()
my_instance2 = MyClass()

print(my_instance1 is my_instance2)

输出:

True
  • 基于metaclass方式实现

Python

class SingletonMetaclass(type):
    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            cls._instance = super().__call__(cls, *args, **kwargs)
        return cls._instance

class MyClass(metaclass=SingletonMetaclass):
    pass

my_instance1 = MyClass()
my_instance2 = MyClass()

print(my_instance1 is my_instance2)

输出:

True

彩蛋在这!!!

如果你手中有令人兴奋的Python面试题目,欢迎在评论区分享!我将全力以赴为你提供详尽解答。同时,别忘了期待我的下一篇力作,将聚焦Go面试题目。精彩内容,即将呈现,敬请保持关注!

顺便说一嘴,由于最近有点忙这篇文章可能出的比较晚,不过你放心肯定会有的!