跳至主要內容

python面试题(2)

深圳kykj-粉丝面试整理约 4297 字大约 14 分钟

一、 使用python实现一个单例模式对象。

在 Python 中,你可以使用多种方法来实现单例模式,其中最常见的方法是使用装饰器或者元类。下面我会为你展示两种常见的实现方式:使用装饰器和使用元类。

1. 使用装饰器实现单例模式:

def singleton(cls):
    instances = {}

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

    return get_instance

@singleton
class SingletonClass:
    def __init__(self, value):
        self.value = value

# 使用
instance1 = SingletonClass("Instance 1")
instance2 = SingletonClass("Instance 2")

print(instance1.value)  # Output: Instance 1
print(instance2.value)  # Output: Instance 1 (same as instance1)
print(instance1 is instance2)  # Output: True

2. 使用元类实现单例模式:

class SingletonMeta(type):
    instances = {}

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

class SingletonClass(metaclass=SingletonMeta):
    def __init__(self, value):
        self.value = value

# 使用
instance1 = SingletonClass("Instance 1")
instance2 = SingletonClass("Instance 2")

print(instance1.value)  # Output: Instance 1
print(instance2.value)  # Output: Instance 1 (same as instance1)
print(instance1 is instance2)  # Output: True

这两种方法都可以实现单例模式,你可以根据自己的喜好选择其中一种。元类的方法在一些高级场景下可能更灵活,但装饰器的方法更为简洁。

当使用 __new__ 函数来实现单例模式时,你可以在 __new__ 方法中控制实例的创建和返回。这里我将为你展示如何使用 __new__ 函数实现单例模式:

class SingletonClass:
    _instance = None

    def __new__(cls, value):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.value = value
        return cls._instance

# 使用
instance1 = SingletonClass("Instance 1")
instance2 = SingletonClass("Instance 2")

print(instance1.value)  # Output: Instance 1
print(instance2.value)  # Output: Instance 1 (same as instance1)
print(instance1 is instance2)  # Output: True

在这个例子中,__new__ 方法首先检查 _instance 属性,如果该属性为 None,则创建一个新的实例并将其赋值给 _instance。如果 _instance 不为 None,则返回现有的实例。这样确保了只有一个实例被创建和使用。

注意,这种方法不是线程安全的。如果你需要在多线程环境下使用单例模式,你需要考虑线程安全性并可能采取额外的措施来保护 _instance 的访问和修改。

优化
实现并发安全的单例模式需要考虑多线程或多进程环境下的竞争条件。以下是使用线程安全的方式来实现并发安全的单例模式的示例:

import threading

class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

# 使用
def create_instance():
    instance = ThreadSafeSingleton()
    print(instance)

threads = []
for _ in range(10):
    thread = threading.Thread(target=create_instance)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

在上面的示例中,我们使用了一个互斥锁 (_lock) 来确保在创建实例时只有一个线程能够进入临界区。这样可以防止多个线程同时创建实例,从而保证了单例模式的线程安全性。

请注意,这只是一个线程安全的示例。如果你需要在多进程环境中使用单例模式,你可能需要使用 multiprocessing.Lock 来实现进程间的同步。

此外,还可以考虑使用更高级的线程安全工具,例如 threading.RLockthreading.Condition,以便更好地满足你的并发需求。

这里只是给出一些范例,具体使用及优化请根据实际场景适配。

二、 请使用异步的方式(多进程/线程/协程等)循环交替打印A和B各50次。

输出示例如下:

A
B
A
B
A
B
A
B
......
#(总共打印A和B各50次)

协程版
下面是使用异步协程的方式来实现交替打印 A 和 B 各 50 次的示例代码:

import asyncio


async def print_a_b(turn, value, total_count):
    for i in range(total_count):
        async with turn:
            print(f"{i + 1}. {value}")
            await asyncio.sleep(0)  # Allow other tasks to run


async def main():
    total_count = 50
    a_turn = asyncio.Lock()
    b_turn = asyncio.Lock()

    a_task = asyncio.create_task(print_a_b(a_turn, 'A', total_count))
    b_task = asyncio.create_task(print_a_b(b_turn, 'B', total_count))

    await asyncio.gather(a_task, b_task)


asyncio.run(main())

在这个示例中,我们定义了一个异步函数 print_a_b 来循环打印指定的值('A' 或 'B')。我们使用 asyncio.Lock 来确保打印操作的互斥,从而避免同时打印 A 和 B。

通过创建两个异步任务(分别打印 A 和 B),我们使用 asyncio.gather 来等待它们完成。

运行以上代码,你会得到类似于你描述的交替打印 A 和 B 的输出。注意,由于异步性质,可能会有微小的延迟,但总体上会保持交替打印。

多线程版

import threading

class AlternatePrinter:
    def __init__(self):
        self.lock = threading.Lock()
        self.condition = threading.Condition(self.lock)
        self.turn = 'A'

    def print_a(self):
        for _ in range(50):
            with self.condition:
                while self.turn != 'A':
                    self.condition.wait()
                print('A')
                self.turn = 'B'
                self.condition.notify()

    def print_b(self):
        for _ in range(50):
            with self.condition:
                while self.turn != 'B':
                    self.condition.wait()
                print('B')
                self.turn = 'A'
                self.condition.notify()

def main():
    printer = AlternatePrinter()

    thread_a = threading.Thread(target=printer.print_a)
    thread_b = threading.Thread(target=printer.print_b)

    thread_a.start()
    thread_b.start()

    thread_a.join()
    thread_b.join()

if __name__ == "__main__":
    main()

在这个示例中,我们定义了一个 AlternatePrinter 类,其中包含两个方法 print_a 和 print_b,分别用于打印 A 和 B。我们使用条件变量 threading.Condition 来确保线程的交替执行。在每次打印完后,线程会改变 turn 变量的值,然后通过 condition.notify() 唤醒等待的线程。

通过创建两个线程分别执行 print_a 和 print_b,线程将会交替打印 A 和 B。

运行以上代码,你会得到你所期望的交替打印 A 和 B 的输出。

三、你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

 
示例 1:

输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false 
调用 isBadVersion(5) -> true 
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
示例 2:

输入:n = 1, bad = 1
输出:1
 

提示:

● 1 <= bad <= n <= 231 - 1
● isBadVersion函数假设已定义好,直接调用即可

这个问题可以使用二分查找来解决,以减少调用 isBadVersion 函数的次数。由于每个错误的版本之后的版本都是错误的,我们可以将问题转化为在版本列表中寻找第一个满足条件的版本,也就是找到最左边的 True 值。以下是使用二分查找的示例代码:

def first_bad_version(n):
    left, right = 1, n
    
    while left < right:
        mid = left + (right - left) // 2
        if isBadVersion(mid):
            right = mid
        else:
            left = mid + 1
    
    return left

# 示例 isBadVersion 函数
def isBadVersion(version):
    # 假设该函数已经定义好,直接调用即可
    pass

# 使用示例
n = 5  # 假设总共有 5 个版本
bad_version = first_bad_version(n)
print("第一个错误的版本:", bad_version)

在这个示例中,我们使用二分查找来找到第一个错误的版本。我们初始化 leftright 分别为 1 和 n,然后在每一步中计算 mid,并根据 isBadVersion(mid) 的结果来更新 leftright。如果 isBadVersion(mid) 返回 True,说明当前版本是错误的,我们将搜索范围缩小到左半部分;否则,搜索范围缩小到右半部分。

最终,当 leftright 相等时,就找到了第一个错误的版本,返回该版本号即可。

这种方法有效地减少了对 isBadVersion 函数的调用次数,通过二分查找的方式快速定位到第一个错误的版本。

四、请简述下python中的__new__和__init__的区别。

在Python中,__new____init__ 是两个特殊的方法,用于创建和初始化对象。它们在类的实例化过程中起着不同的作用。

  1. __new__ 方法:

    • __new__ 是一个类级别的方法,负责创建并返回实例对象。它在对象创建之前调用,并且通常是静态方法,接受一个类作为第一个参数,然后传递其他参数来创建实例。
    • 该方法的主要作用是控制实例对象的创建过程,可以在创建实例之前做一些额外的处理,或者返回一个已有的实例(单例模式)。
    • 如果重写了 __new__ 方法,通常需要确保返回一个有效的实例对象,否则会导致后续的初始化(__init__)等步骤无法进行。
  2. __init__ 方法:

    • __init__ 是一个实例级别的方法,负责初始化实例的属性。它在对象创建之后调用,接受实例对象作为第一个参数,然后可以传递其他参数来初始化实例的属性。
    • 该方法的主要作用是设置对象的属性,进行一些必要的初始化操作。
    • __init__ 不负责创建实例对象,而是在实例已经创建之后,对其进行属性的初始化。

总结:

  • __new__ 用于创建实例对象,它是类级别的方法,控制对象的创建过程。
  • __init__ 用于初始化实例对象的属性,它是实例级别的方法,对已经创建的对象进行属性的设置。

通常情况下,当你想要自定义对象的创建过程时,你会重写 __new__ 方法;而当你需要设置对象的属性或执行其他初始化操作时,你会重写 __init__ 方法。

五、请描述下Python的多线程管理。

在 Python 中,多线程管理是通过 threading 模块来实现的。Python 中的多线程是基于操作系统的线程实现的,但由于 Python 全局解释器锁(GIL)的存在,多线程并不能真正实现并行运行。GIL 导致在同一时刻只有一个线程可以执行 Python 字节码,从而限制了多线程在 CPU 密集型任务中的性能表现。

以下是 Python 多线程管理的一些关键概念和函数:

  1. 线程创建: 使用 threading.Thread 类可以创建一个新的线程。通过传入一个函数作为参数,可以指定线程要执行的操作。

  2. 线程启动: 调用线程对象的 start() 方法会启动线程,并开始执行指定的函数。

  3. 线程同步: 在多线程环境中,由于共享资源的存在,可能会出现竞争条件。为了确保线程安全,可以使用锁(threading.Lock)等同步原语来协调线程之间的操作。

  4. 锁机制: 锁(threading.Lock)用于防止多个线程同时访问共享资源。在一个线程获得锁之后,其他线程必须等待锁被释放后才能获取锁。

  5. 线程间通信: 多个线程之间可能需要相互通信和协调,可以使用 threading.Eventthreading.Conditionthreading.Semaphore 等进行线程间通信。

  6. 线程状态: 线程可以处于不同的状态,如就绪、运行、阻塞等。可以使用线程对象的 is_alive() 方法来检查线程是否还在运行。

  7. 守护线程: 可以将线程设置为守护线程(thread.daemon = True),这样在主线程退出时,守护线程会随之结束。

  8. 线程池: concurrent.futures 模块提供了线程池和进程池的高级接口,用于并行地执行函数。

需要注意的是,由于 GIL 的存在,Python 中的多线程主要适用于 I/O 密集型任务,如网络请求、文件操作等。对于 CPU 密集型任务,多线程并不能充分利用多核 CPU,此时可以考虑使用多进程来实现并行运算。

总的来说,Python 的多线程管理提供了一些基本的线程操作和同步机制,但由于 GIL 的限制,多线程在并行计算方面可能不如其他语言或多进程方式。

六、请列出Python中可变数据类型和不可变数据类型,CPython是如何实现的?

在 Python 中,数据类型分为可变和不可变两种。可变数据类型是指对象的值可以被修改,而不可变数据类型是指对象的值一旦创建就无法更改。以下是 Python 中常见的可变和不可变数据类型:

可变数据类型(Mutable):

  1. 列表(List)
  2. 字典(Dictionary)
  3. 集合(Set)

不可变数据类型(Immutable):

  1. 数字(int、float、complex)
  2. 字符串(str)
  3. 元组(tuple)
  4. 冻结集合(frozenset)

CPython 是 Python 的标准实现,它在内存中的数据存储方式与数据类型的可变性密切相关。下面简要描述了 CPython 如何实现可变和不可变数据类型:

可变数据类型(如列表、字典、集合)的实现:

  1. 在内存中创建一个对象,对象包含了实际的数据和相关的控制信息,如对象类型、大小等。
  2. 对象中的数据可以被修改,例如对列表进行添加、删除、修改等操作。这些操作可能会引起对象在内存中的位置发生变化,即重新分配内存空间。
  3. 当对象被修改时,CPython 会确保相关的引用仍然指向这个对象,从而保证所有引用该对象的变量都能看到修改后的值。

不可变数据类型(如数字、字符串、元组)的实现:

  1. 在内存中创建一个对象,对象包含实际的数据和控制信息。
  2. 一旦对象被创建,它的值就无法更改。如果对一个不可变对象进行修改操作,实际上会创建一个新的对象,并将修改后的值赋予新的对象。
  3. 不可变对象的值在内存中是固定的,这使得可以对多个变量共享同一个对象,从而节省内存。

需要注意的是,不可变对象之所以被设计成不可变的,一方面是为了避免意外修改数据,另一方面是为了在实现中可以进行一些优化,例如缓存常见的不可变对象,以减少内存占用。

总之,CPython 的数据类型实现方式是基于对象和引用的,根据对象的可变性来决定如何存储和操作数据。这种实现方式使得 Python 的变量和数据类型的使用变得灵活且易于理解。

七、请简述下python 协程的实现原理?协程一般适用于什么场景?

Python 协程的实现原理基于生成器(Generator)和 yield 关键字,以及使用特定的库(如 asyncio)来实现异步编程。协程通过避免阻塞并允许在单个线程内切换执行,实现了高效的异步操作。

实现协程的关键点如下:

  1. 生成器(Generator): 生成器是一种特殊的函数,它使用 yield 关键字来暂停函数的执行并生成一个值,然后在后续调用中恢复执行。生成器在每次调用时会执行到 yield 语句,并将值返回给调用者。

  2. yield 关键字: yield 用于将控制权返回给调用者,并且保留函数的状态。调用者可以使用 .send() 方法向生成器发送值,这个值会被 yield 表达式接收。

  3. 异步库(如 asyncio): 在 Python 中,为了实现高效的协程,通常使用异步库,如 asyncioasyncio 提供了协程和事件循环,允许在单个线程内执行多个协程,根据需要进行切换。

协程适用于以下场景:

  1. 异步编程: 协程在异步编程中起到关键作用。它可以处理大量并发任务,如网络请求、数据库查询等,而不会阻塞整个程序的执行。

  2. 事件驱动编程: 协程适用于事件驱动的编程模型。例如,在用户交互、触发事件、消息传递等情况下,协程可以很好地处理异步任务。

  3. I/O 密集型任务: 当任务主要涉及 I/O 操作时,如读写文件、网络通信等,协程能够在 I/O 操作时切换到其他任务,从而提高程序的效率。

  4. 并发任务管理: 协程可以方便地管理大量并发任务,而不需要关注线程或进程的开销和同步问题。

总之,协程适用于需要高效处理异步操作和并发任务的场景。通过避免阻塞,协程可以在单线程内实现多个任务的协同执行,提高了程序的并发性能和响应性。

八、请简述Python的垃圾回收机制。

Python语法入门之垃圾回收机制open in new window
一文读懂Python垃圾回收机制open in new window