Python进阶知识
Python中生成器的相关知识
我们创建列表的时候,受到内存限制,容量肯定是有限的,而且不可能全部给他一次枚举出来。Python常用的列表生成式有一个致命的缺点就是定义即生成,非常的浪费空间和效率。
如果列表元素可以按照某种算法推算出来,那我们可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。
要创建一个generator,最简单的方法是改造列表生成式:
1 | a = [x * x for x in range(10)] |
还有一个方法是生成器函数,通过def定义,然后使用yield来支持迭代器协议,比迭代器写起来更简单。
1 | def spam(): |
进行函数调用的时候,返回一个生成器对象。在使用next()调用的时候,遇到yield就返回,记录此时的函数调用位置,下次调用next()时,从断点处开始。
我们完全可以像使用迭代器一样使用 generator ,当然除了定义。定义一个迭代器,需要分别实现 iter() 方法和 next() 方法,但 generator 只需要一个小小的yield。
generator还有 send() 和 close() 方法,都是只能在next()调用之后,生成器处于挂起状态时才能使用的。
python是支持协程的,也就是微线程,就是通过generator来实现的。配合generator我们可以自定义函数的调用层次关系从而自己来调度线程。
3.Python中装饰器的相关知识
装饰器允许通过将现有函数传递给装饰器,从而向现有函数添加一些额外的功能,该装饰器将执行现有函数的功能和添加的额外功能。
装饰器本质上还是一个函数,它可以让已有的函数不做任何改动的情况下增加功能。
接下来我们使用一些例子来具体说明装饰器的作用:
如果我们不使用装饰器,我们通常会这样来实现在函数执行前插入日志:
1 | def foo(): |
虽然这样写是满足了需求,但是改动了原有的代码,如果有其他的函数也需要插入日志的话,就需要改写所有的函数,这样不能复用代码。
我们可以进行如下改写:
1 | import logging |
这样写的确可以复用插入的日志,缺点就是显式的封装原来的函数,我们希望能隐式的做这件事。
我们可以用装饰器来写:
1 | import logging |
其中,use_log函数就是装饰器,它把我们真正想要执行的函数bar()封装在里面,返回一个封装了加入代码的新函数,看起来就像是bar()被装饰了一样。
但是这样写还是不够隐式,我们可以通过@语法糖来起到bar = use_log(bar)的作用。
1 | import logging |
这样子看起来就非常简洁,而且代码很容易复用。可以看成是一种智能的高级封装。
Python中$*args$和$**kwargs$的区别?
∗args和$**kwargs$主要用于函数定义。我们可以将不定数量的参数传递给一个函数。
这里的不定的意思是:预先并不知道函数使用者会传递多少个参数, 所以在这个场景下使用这两个关键字。
∗args
∗args是用来发送一个非键值对的可变数量的参数列表给一个函数。
我们直接看一个例子:
1 | def test_var_args(f_arg, *argv): |
∗∗kwargs
∗∗kwargs允许我们将不定长度的键值对, 作为参数传递给一个函数。如果我们想要在一个函数里处理带名字的参数, 我们可以使用$**kwargs$。
我们同样举一个例子:
1 | def greet_me(**kwargs): |
Python中Numpy的broadcasting机制?
Python的Numpy库是一个非常实用的数学计算库,其broadcasting机制给我们的矩阵运算带来了极大地方便。
我们先看下面的一个例子:
1 | >>> import numpy as np |
上面的代码其实就是把数组$a$和数组$b$中同样位置的每对元素相加。这里$a$和$b$是相同长度的数组。
如果两个数组的长度不一致,这时候broadcasting就可以发挥作用了。
比如下面的代码:
1 | >>> d = a + 5 |
broadcasting会把$5$扩展成$[5,5,5]$,然后上面的代码就变成了对两个同样长度的数组相加。示意图如下(broadcasting不会分配额外的内存来存取被复制的数据,这里只是方面描述):
我们接下来看看多维数组的情况:
1 | >>> e |
在这里一维数组被扩展成了二维数组,和$e$的尺寸相同。示意图如下所示:
我们再来看一个需要对两个数组都做broadcasting的例子:
1 | >>> b = np.arange(3).reshape((3,1)) |
在这里$a$和$b$都被扩展成相同的尺寸的二维数组。示意图如下所示:
总结broadcasting的一些规则:
- 如果两个数组维数不相等,维数较低的数组的shape进行填充,直到和高维数组的维数匹配。
- 如果两个数组维数相同,但某些维度的长度不同,那么长度为1的维度会被扩展,和另一数组的同维度的长度匹配。
- 如果两个数组维数相同,但有任一维度的长度不同且不为1,则报错。
1 | >>> a = np.arange(3) |
接下来我们看看报错的例子:
1 | >>> a = np.arange(3) |
python中@staticmethod和@classmethod使用注意事项
@staticmethod
- 静态方法:staticmethod将一个普通函数嵌入到类中,使其成为类的静态方法。静态方法不需要一个类实例即可被调用,同时它也不需要访问类实例的状态。
- 参数:静态方法可以接受任何参数,但通常不使用self或cls作为第一个参数。
- 访问:由于静态方法不依赖于类实例的状态,因此它们不能修改类或实例的状态。
- 用途:当函数与类相关,但其操作不依赖于类状态时,适合使用静态方法。
@classmethod
- 类方法:classmethod将一个方法绑定到类而非类的实例。类方法通常用于操作类级别的属性。
- 参数:类方法至少有一个参数,通常命名为cls,它指向类本身。
- 访问:类方法可以修改类的状态,但不能修改实例的状态。
- 用途:当方法需要访问或修改类属性,或者需要通过类来创建实例时,适合使用类方法。
使用场景
- 当方法不需要访问任何属性时,使用staticmethod。
- 当方法操作的是类属性而不是实例属性时,使用classmethod。
代码示例
1 | class MyClass: |
问题
在使用falsk-restful这个框架进行模型部署调用时,发现模型推理时间很快,但是完整的一次请求过程非常耗时。在debug的过程中发现,每次请求调用api接口时,模型的推理类都会被实例化,推理类在构造的时候,会在初始化中加载模型,加载模型的过程是耗时较长的。
fixbug
1 | classs Infer(object): |
通过@classmethod方法初始化模型的加载,相当于创建了一个全局变量,在后续的请求调用中,不会一直重复加载。
类属性是类级别的共享状态:
- 类属性属于类对象本身,而不是类的任何特定实例。这意味着,一旦一个类属性被创建(比如
Infer.model
),它就存在于Infer
这个类上,并且被所有Infer
类的实例(以及类本身)共享。 - 它就像一个附属于这个类的“全局”变量(作用域限定在类内)。
Python中有哪些常用的设计模式?
Python作为一种多范式编程语言,支持多种设计模式。以下是AIGC、传统深度学习、自动驾驶领域中Python常用的设计模式:
创建型模式
单例模式(Singleton Pattern)
确保一个类只有一个实例,并提供一个全局访问点。
通俗例子:想象一个系统中有一个打印机管理器(Printer Manager),这个管理器负责管理打印任务。为了确保所有打印任务都能被统一管理,系统中只能有一个打印机管理器实例。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class PrinterManager:
_instance = None
#声明了一个类属性 _instance:这个属性属于 PrinterManager 类本身,将被所有(潜在的)实例共享。
#将这个类属性初始化为 None:这表示在任何实例被创建之前,这个用来存储唯一实例的“容器”是空的。这个初始状态是后续 __new__ 方法中实现单例逻辑的关键#检查点。
#使用 _ 前缀:这是一个内部实现细节。
def __new__(cls, *args, **kwargs):
# 定义 Python 的特殊方法 `__new__`。
# `__new__` 是在 `__init__` 之前被调用的,它负责 *创建* 并 *返回* 类的实例。
# 而 `__init__` 负责 *初始化* 已经创建好的实例。
# 控制实例的创建过程是实现单例的关键,所以我们重写 `__new__`。
# `cls` 参数代表当前的类(也就是 PrinterManager)。
# `*args` 和 `**kwargs` 用来接收创建实例时可能传入的任何位置参数和关键字参数(虽然在这个简单例子中没用到)。
if not cls._instance:
cls._instance = super(PrinterManager, cls).__new__(cls, *args, **kwargs)
return cls._instance
pm1 = PrinterManager()
pm2 = PrinterManager()
print(pm1 is pm2) # 输出: True
工厂方法模式(Factory Method Pattern)
定义一个创建对象的接口,但让子类决定实例化哪一个类。
通俗例子:想象一家新能源汽车工厂,它根据订单生产不同类型的汽车(如轿车、卡车、SUV)。每种汽车都是一个类,通过工厂方法决定创建哪种类型的汽车。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Car:
def drive(self):
pass
class Sedan(Car):
def drive(self):
return "Driving a sedan"
class Truck(Car):
def drive(self):
return "Driving a truck"
class CarFactory:
def create_car(self, car_type):
if car_type == "sedan":
return Sedan()
elif car_type == "truck":
return Truck()
factory = CarFactory()
car = factory.create_car("sedan")
print(car.drive()) # 输出: Driving a sedan
抽象工厂模式(Abstract Factory Pattern)
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
通俗例子:想象一个家具商店,它可以生产不同风格(现代风格、维多利亚风格)的家具。每种风格都有其特定的椅子和桌子,抽象工厂提供了创建这些家具的接口。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class Chair:
def sit(self):
pass
class ModernChair(Chair):
def sit(self):
return "Sitting on a modern chair"
class VictorianChair(Chair):
def sit(self):
return "Sitting on a victorian chair"
class FurnitureFactory:
def create_chair(self):
pass
class ModernFurnitureFactory(FurnitureFactory):
def create_chair(self):
return ModernChair()
class VictorianFurnitureFactory(FurnitureFactory):
def create_chair(self):
return VictorianChair()
factory = ModernFurnitureFactory()
chair = factory.create_chair()
print(chair.sit()) # 输出: Sitting on a modern chair
结构型模式
适配器模式(Adapter Pattern)
将一个类的接口转换为客户希望的另一个接口,适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
通俗例子:想象你有一个老式的播放器,它只能播放CD,但你现在有一个现代的音乐库在你的手机上。你可以使用一个适配器,把手机的音乐格式转换成播放器能够播放的格式。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class OldPlayer:
def play_cd(self):
return "Playing music from CD"
class NewPlayer:
def play_music(self):
return "Playing music from phone"
class Adapter:
def __init__(self, new_player):
self.new_player = new_player
def play_cd(self):
return self.new_player.play_music()
old_player = OldPlayer()
print(old_player.play_cd()) # 输出: Playing music from CD
new_player = NewPlayer()
adapter = Adapter(new_player)
print(adapter.play_cd()) # 输出: Playing music from phone
装饰器模式(Decorator Pattern)
动态地给对象添加一些职责。
通俗例子:想象我们在咖啡店点了一杯咖啡。你可以选择在咖啡上加牛奶、糖或者巧克力。这些添加物是装饰,装饰器模式允许我们动态地添加这些装饰。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Coffee:
def cost(self):
return 5
class MilkDecorator:
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost() + 1
coffee = Coffee()
print(coffee.cost()) # 输出: 5
milk_coffee = MilkDecorator(coffee)
print(milk_coffee.cost()) # 输出: 6
代理模式(Proxy Pattern)
为其他对象提供一种代理以控制对这个对象的访问。
通俗例子:想象我们有一个银行账户。我们可以通过代理(如银行职员或ATM)来访问我们的账户,而不需要直接处理银行系统的复杂操作。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class BankAccount:
def withdraw(self, amount):
return f"Withdrew {amount} dollars"
class ATMProxy:
def __init__(self, bank_account):
self.bank_account = bank_account
def withdraw(self, amount):
return self.bank_account.withdraw(amount)
account = BankAccount()
atm = ATMProxy(account)
print(atm.withdraw(100)) # 输出: Withdrew 100 dollars
行为型模式
观察者模式(Observer Pattern)
定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
通俗例子:想象我们订阅了一份杂志。每当有新一期杂志出版,杂志社就会通知我们。我们是观察者,杂志社是被观察者。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Publisher:
def __init__(self):
self.subscribers = []
def subscribe(self, subscriber):
self.subscribers.append(subscriber)
def notify(self):
for subscriber in self.subscribers:
subscriber.update()
class ConcreteSubscriber(Subscriber):
def update(self):
print("New magazine issue is out!")
publisher = Publisher()
subscriber = ConcreteSubscriber()
publisher.subscribe(subscriber)
publisher.notify() # 输出: New magazine issue is out!
策略模式(Strategy Pattern)
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
通俗例子:想象我们要去旅行,可以选择不同的交通方式(如开车、坐火车、坐飞机)。每种交通方式都是一个策略,策略模式允许我们在运行时选择不同的策略。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class TravelStrategy:
def travel(self):
pass
class CarStrategy(TravelStrategy):
def travel(self):
return "Traveling by car"
class TrainStrategy(TravelStrategy):
def travel(self):
return "Traveling by train"
class TravelContext:
def __init__(self, strategy):
self.strategy = strategy
def travel(self):
return self.strategy.travel()
context = TravelContext(CarStrategy())
print(context.travel()) # 输出: Traveling by car
context.strategy = TrainStrategy()
print(context.travel()) # 输出: Traveling by train
Python中的lambda表达式?
Lambda 表达式,也称为匿名函数,是 Python 中的一个特性,允许创建小型的、一次性使用的函数,而无需使用 def
关键字。
语法
Lambda 函数的基本语法为:
1 | lambda 参数: 表达式 |
主要特征
- Lambda 函数可以有任意数量的参数,但只能有一个表达式。
- 通常用于简短、简单的操作。
- 当 lambda 函数被调用时,表达式会被计算并返回结果。
示例
基本用法
1 | f = lambda x: x * 2 |
多个参数
1 | g = lambda x, y: x + y |
在高阶函数中的应用
Lambda 函数经常与 map()
、filter()
和 sort()
等函数一起使用。
1 | # 按单词中唯一字母的数量对单词列表进行排序 |
lambda word: ...
定义了一个匿名函数: 这部分代码创建了一个临时的、没有名字的函数。word
是这个匿名函数的参数名: 就像你定义一个普通函数def my_function(some_parameter): ...
一样,word
在这里扮演了some_parameter
的角色。它是一个占位符,用来接收将来调用这个lambda
函数时传入的值。sorted
函数负责调用lambda
并传入参数:当
sorted
函数处理words
列表时,它会:- 依次取出
words
列表中的每一个元素(每一个单词字符串)。 - 将取出的那个元素作为参数,传递给
key
指定的函数(也就是我们定义的lambda
函数)。 - 在
lambda
函数执行时,它的参数word
就被赋予了当前正在处理的那个单词的值。
- 依次取出
优点
- 简洁:Lambda 函数允许内联函数定义,使代码更加紧凑。
- 可读性:对于简单操作,lambda 可以通过消除单独的函数定义来提高代码可读性。
- 函数式编程:Lambda 函数在函数式编程范式中很有用,特别是在使用高阶函数时。
局限性
- 单一表达式:Lambda 函数限于单一表达式,这限制了它们的复杂性。
- 可读性:对于更复杂的操作,传统的函数定义可能更合适且更易读。
Lambda 函数为 Python 中创建小型匿名函数提供了强大的工具,特别适用于函数式编程和处理高阶函数的场景。
介绍一下Python中的引用计数原理,如何消除一个变量上的所有引用计数?
Python中的引用计数是垃圾回收机制的一部分,用来跟踪对象的引用数量。
引用计数原理
- 创建对象:当创建一个对象时,其引用计数初始化为1。
- 增加引用:每当有一个新的引用指向该对象时(例如,将对象赋值给一个变量或将其添加到一个数据结构中),对象的引用计数增加。
- 减少引用:每当一个引用不再指向该对象时(例如,变量被重新赋值或被删除),对象的引用计数减少。
- 删除对象:当对象的引用计数降到0时,表示没有任何引用指向该对象,Python的垃圾回收器就会销毁该对象并释放其占用的内存。
实现引用计数的例子
1 | # 创建对象 |
获取对象的引用计数
可以使用sys
模块中的getrefcount
函数来获取对象的引用计数:
1 | import sys |
如何消除一个变量上的所有引用计数
为了确保一个对象上的所有引用都被清除,可以执行以下步骤:
- 删除所有变量引用:使用
del
语句删除所有引用该对象的变量。 - 清除容器引用:如果对象存在于容器(如列表、字典、集合)中,则需要从这些容器中移除对象。
- 关闭循环引用:如果对象存在循环引用(即对象相互引用),需要手动断开这些引用,或使用Python的垃圾回收器来处理。
1 | import gc |
使用上述方法,可以确保对象的引用计数降为0,并且对象被销毁和内存被释放。
循环引用问题
循环引用会导致引用计数无法正常工作,这时需要依靠Python的垃圾回收器来检测和处理循环引用。
1 | import gc |
在上述代码中,node1
和node2
相互引用,形成了一个循环引用。即使删除了node1
和node2
,它们也不会被立即销毁,因为引用计数不为0。这时,需要调用gc.collect()
来强制垃圾回收器处理这些循环引用。
有哪些提高python运行效率的方法?
一. 优化代码结构
1. 使用高效的数据结构和算法
- 选择合适的数据结构:根据需求选择最佳的数据结构。例如,使用
set
或dict
进行元素查找,比使用list
更快。 - 优化算法:使用更高效的算法降低时间复杂度。例如,避免在循环中进行昂贵的操作,使用快速排序算法等。
2. 减少不必要的计算
- 缓存结果:使用
functools.lru_cache
或自行实现缓存,避免重复计算相同的结果。 - 懒加载:延迟加载数据或资源,减少启动时的开销。
3. 优化循环
列表解析:使用列表解析或生成器表达式替代传统循环,代码更简洁,执行速度更快。
1
2
3
4
5
6
7# 传统循环
result = []
for i in range(1000):
result.append(i * 2)
# 列表解析
result = [i * 2 for i in range(1000)]避免过深的嵌套:简化嵌套循环,减少循环次数。
4. 使用生成器
节省内存:生成器按需生成数据,适用于处理大型数据集。
1
2
3def generate_numbers(n):
for i in range(n):
yield i
二、利用高性能的库和工具
1. NumPy和Pandas
- NumPy:用于高效的数值计算,底层由C语言实现,支持向量化操作。
- Pandas:提供高性能的数据结构和数据分析工具。
2. 使用Cython
Cython:将Python代码编译为C语言扩展,显著提高计算密集型任务的性能。
1
2
3# 使用Cython编写的示例函数
cpdef int add(int a, int b):
return a + b
3. JIT编译器
- PyPy:一个支持JIT编译的Python解释器,能自动优化代码执行。
- Numba:为NumPy提供JIT编译,加速数值计算。
4. 多线程和多进程
- 多线程:适用于I/O密集型任务,但受限于全局解释器锁(GIL),对CPU密集型任务效果不佳。
- 多进程:使用
multiprocessing
模块,适用于CPU密集型任务,能充分利用多核CPU。
5. 异步编程
asyncio:用于编写异步I/O操作,适合处理高并发任务。
1
2
3
4
5import asyncio
async def fetch_data():
# 异步I/O操作
pass
三、性能分析和监控
1. 使用性能分析工具
cProfile:标准库中的性能分析器,帮助找出程序的性能瓶颈。
1
python -m cProfile -o output.prof WeThinkIn_script.py
line_profiler:逐行分析代码性能,需要额外安装。
2. 内存分析
- memory_profiler:监控内存使用情况,优化内存占用。
四、优化代码实践
1. 避免全局变量
- 使用局部变量:局部变量访问速度更快,能提高函数执行效率。
2. 减少属性访问
- 缓存属性值:将频繁访问的属性值缓存到局部变量,减少属性查找时间。
3. 字符串连接
使用
join
方法:连接多个字符串时,''.join(list_of_strings)
比使用+
号效率更高。1
2
3
4
5
6
7# 效率较低
result = ''
for s in list_of_strings:
result += s
# 效率较高
result = ''.join(list_of_strings)
4. 合理使用异常
- 避免过度使用异常处理:异常处理会带来额外的开销,应在必要时使用。
五、核心思想总结
我们在这里做一个总结,想要提高Python运行效率需要综合考虑代码优化、工具使用等多个方面。以下是关键步骤:
- 性能分析:首先使用工具找出性能瓶颈,避免盲目优化。
- 代码改进:通过优化算法、数据结构和代码实践,提高代码效率。
- 利用高性能库:使用如NumPy、Cython等库,加速计算密集型任务。
- 并行和异步:根据任务类型,选择多线程、多进程或异步编程。
通过以上方法,我们可以在保持代码可读性的同时,大幅提高Python程序的运行效率。
线程池与进程池的区别是什么?
1. 线程池
线程池是为了管理和复用线程的一种机制。它维护一个线程集合,减少了频繁创建和销毁线程的开销。多个任务可以被提交给线程池,线程池中的线程会从任务队列中取出任务进行执行。
- 适用场景:适合 I/O 密集型任务。由于 I/O 操作通常会阻塞线程,但线程池中的其他线程可以继续处理任务,从而提高并发效率。
- GIL 限制:由于线程仍然受 GIL 影响,线程池不适合处理 CPU 密集型任务。
- 工作机制:线程池中的线程共享相同的内存空间,能快速进行任务调度和上下文切换。
2. 进程池
进程池类似于线程池,但它管理的是一组进程,而非线程。每个进程都有独立的内存空间,不共享全局状态。因此,进程池适用于 CPU 密集型任务,可以充分利用多核 CPU 的优势。
- 适用场景:适合 CPU 密集型任务。多进程池不受 GIL 的限制,因此可以在多核 CPU 上并行处理多个任务。
- 资源隔离:进程之间不共享内存,每个进程有独立的内存空间,这使得进程之间的通信更加复杂,通常需要通过队列、管道等进行数据交换。
- 开销:由于进程的创建和销毁成本较高,进程池能够有效减少频繁创建进程的开销。
线程池与进程池的具体区别
特性 | 线程池 | 进程池 |
---|---|---|
适用任务类型 | I/O 密集型任务 | CPU 密集型任务 |
是否受 GIL 影响 | 受 GIL 限制,无法并行执行 Python 代码 | 不受 GIL 限制,能够并行处理任务 |
资源共享 | 线程间共享内存空间 | 进程间不共享内存,资源隔离 |
上下文切换开销 | 切换开销较小 | 切换开销较大 |
创建销毁开销 | 创建和销毁线程开销较小 | 创建和销毁进程开销较大 |
适用场景 | 网络请求、文件操作等 I/O 密集任务 | 数学计算、数据处理等 CPU 密集任务 |
通信方式 | 共享内存空间,通信简单 | 需要使用管道、队列等机制进行通信 |
如何选择线程池还是进程池?
- I/O 密集型任务:例如网络爬虫、读取大量文件等任务,由于这些任务大多数时间都在等待外部资源,因此可以选择 线程池 来提高任务的并发性。线程的创建和销毁开销较小,并且能够在任务阻塞时快速切换到其他任务继续执行。
- CPU 密集型任务:例如大规模的数学运算或图像处理等任务,需要大量的 CPU 资源来执行。这种情况下,进程池 是更好的选择,因为它能够通过多进程并行处理数据,充分利用多核 CPU 的优势。
小结
- 线程池 适合 I/O 密集型任务,由于线程共享内存并且切换开销较小,能够提高处理效率,但在 CPU 密集型任务中会受到 GIL 的限制。
- 进程池 更适合 CPU 密集型任务,不受 GIL 限制,可以并行执行 Python 代码,但进程之间的隔离使得通信成本较高。
根据任务的类型合理选择线程池或进程池,可以有效提高程序的执行效率。在实践中,通过分析任务的特性并结合具体的需求进行选择是非常重要的。
Q: ProcessPoolExecutor
这是属于线程还是进程
A: ProcessPoolExecutor
属于 进程。它使用多进程的方式来并发执行任务。
具体来说,ProcessPoolExecutor
会创建一个进程池,池中的每个任务都会运行在独立的进程中。这些进程拥有各自独立的内存空间,不共享全局状态,因此 ProcessPoolExecutor
不受 Python 的全局解释器锁(GIL)限制,适合处理 CPU 密集型任务,例如复杂的数学计算、数据分析或大规模的图像处理。
相对于 ThreadPoolExecutor
,ProcessPoolExecutor
的开销较大,因为进程的创建和上下文切换比线程要更耗资源。但是,由于进程是独立的,可以并行执行 Python 代码,因此在多核 CPU 环境下,ProcessPoolExecutor
能够充分利用系统的多核资源。
总结:
- ProcessPoolExecutor:管理多个进程,适合 CPU 密集型任务,不受 GIL 限制。
- ThreadPoolExecutor:管理多个线程,适合 I/O 密集型任务,受 GIL 限制。
Python中什么情况下会产生内存泄漏?
内存泄漏(Memory Leak)概念
内存泄漏是指程序在运行过程中申请内存却未能正确释放,从而导致内存占用不断增加,最终可能耗尽系统内存。虽然 Python 有自动垃圾回收机制(通过引用计数和垃圾收集器),内存泄漏问题在 Python 中不常见,但某些特定场景下依然可能会发生。
Python中的内存管理机制
Python 使用引用计数和垃圾收集器相结合的方式来管理内存:
- 引用计数:每当有一个变量引用某个对象时,该对象的引用计数就会加1;当一个变量不再引用该对象时,引用计数就会减1。如果某个对象的引用计数变为0,则该对象会被释放。
- 垃圾回收器:用于处理循环引用(引用计数无法解决的情况),Python 内置的
gc
模块会定期检测内存中的对象,释放那些不再使用的对象。
虽然 Python 具备上述机制,但仍有可能在某些情况下导致内存泄漏,特别是在复杂应用程序中。下面是 Python 中几种常见的可能导致内存泄漏的场景:
1. 循环引用(Cyclic References)
当两个或多个对象互相引用对方时,虽然它们都已经没有被外部引用,但由于引用计数无法降为0,垃圾回收机制无法自动释放它们,从而导致内存泄漏。
1 | class A: |
解决方法:
- 使用
gc.collect()
来手动触发垃圾回收器,强制收集这些循环引用的对象。 - 尽量避免对象之间的相互引用,或者使用
weakref
模块创建弱引用来打破引用链条。
2. 全局变量或静态对象
如果某些对象被保存为全局变量或静态对象,它们的生命周期可能会持续到程序结束,导致它们的内存一直占用不释放。
1 | global_list = [] |
解决方法:
- 尽量减少不必要的全局变量,确保及时清空或删除全局变量中的不必要数据。
- 当某个全局对象不再需要时,可以通过
del
或者重置为None
来释放它们。
3. 未关闭的文件或网络连接
当打开文件或网络连接时,如果没有显式关闭这些资源,它们会一直占用内存。特别是在循环中不断打开资源但未关闭时,可能会造成内存泄漏。
1 | def open_files(): |
解决方法:
- 使用
with
语句确保文件和连接资源会自动关闭,避免内存泄漏。
1 | def open_files(): |
4. 缓存机制和对象持久化
有时候程序会使用缓存来存储频繁使用的数据,但是如果缓存的清理机制不够完善,数据会不断增长,占用大量内存。
1 | cache = {} |
解决方法:
- 使用限制大小的缓存策略,例如使用
functools.lru_cache
,它可以自动清理过时的数据。 - 定期清理缓存中的旧数据,或者使用缓存淘汰机制(如 LRU,LFU 算法)。
1 | from functools import lru_cache |
5. 长生命周期的对象持有者
某些对象可能被长生命周期的对象(如服务对象、后台线程、事件循环等)持有,这会导致这些对象不会被垃圾回收,从而造成内存泄漏。
解决方法:
- 确保在不再需要某些对象时,显式删除或清理它们的引用。
6. 闭包或匿名函数持有变量
在某些情况下,闭包或匿名函数持有对外部变量的引用,导致这些变量不会被释放,进而导致内存泄漏。
1 | def create_closure(): |
解决方法:
- 确保闭包或匿名函数没有持有不必要的对象引用,或者确保这些引用能被及时释放。
7. 自定义容器类或集合类型
如果自定义了 Python 中的容器类型(如 list
, dict
等),且没有遵循垃圾回收机制的规则,这些容器可能会导致对象无法被正确回收。
解决方法:
- 确保自定义的数据结构遵循 Python 的内存管理机制,正确地管理其包含的对象。
8. 弱引用不当使用
Python 的 weakref
模块允许创建对对象的弱引用,即当对象的引用计数为0时,可以立即释放对象。但不当使用 weakref
可能会导致对对象的引用失效,进而导致内存泄漏。
1 | import weakref |
解决方法:
- 在使用
weakref
时,确保弱引用是必要的,并在对象不再需要时显式删除。
如何检测和解决内存泄漏?
1. 使用 gc
模块
Python 的 gc
模块可以帮助开发者跟踪和检测循环引用问题。通过调用 gc.collect()
可以强制进行垃圾回收,清理循环引用。
1 | import gc |
2. 使用内存分析工具
有一些工具可以帮助监控和分析 Python 程序的内存使用情况:
objgraph
:可以显示 Python 对象之间的引用关系,帮助分析内存泄漏。tracemalloc
:Python 标准库中的内存跟踪工具,可以用于监控内存分配情况。memory_profiler
:提供了对内存使用情况的详细分析,帮助发现内存泄漏。
1 | import tracemalloc |
3. 优化代码
- 避免全局变量和长生命周期对象的不必要引用。
- 使用上下文管理器(如
with
语句)自动管理资源。 - 定期清理缓存和长生命周期的数据。
- 使用工具分析代码并优化内存管理。
介绍一下Python中的封装(Encapsulation)思想
封装 (Encapsulation) 在 Python 中的概念
封装是面向对象编程(OOP)的四大基本原则之一,其他三个是继承(Inheritance)、多态(Polymorphism)和抽象(Abstraction)。封装的核心思想是将对象的数据(属性)和行为(方法)打包在一起,并限制外界对它们的直接访问。通过封装,开发人员可以控制哪些数据可以从外部访问,哪些只能在类的内部使用。
Python 虽然不像一些其他面向对象的编程语言(如 Java、C++)那样严格地限制数据的访问,但它依然支持通过命名约定和访问控制来实现封装的概念。
封装的主要思想
封装主要涉及以下几个方面:
- 隐藏内部实现:对象的内部状态对外界不可见,外界只能通过公开的接口(即方法)访问或修改对象的状态。
- 保护对象的完整性:通过封装,类的设计者可以控制外部如何访问或修改内部数据,避免外部对内部数据进行非法的操作,确保对象的一致性和完整性。
- 提供安全的访问接口:通过定义类的公有方法(public methods),外部可以在不直接操作内部数据的情况下,安全地对对象进行操作。
Python 中的封装机制
在 Python 中,封装的实现主要依赖命名约定和访问控制,Python 没有像某些编程语言那样提供明确的访问权限控制符(如 Java 的 public
、private
、protected
),但它有一些约定俗成的规则来实现封装。
1. 公有成员 (Public Members)
在 Python 中,默认情况下,类的所有属性和方法都是公有的(public)。这意味着外部可以直接访问或修改这些属性和方法。例如:
1 | class MyClass: |
在这个例子中,name
属性和 greet()
方法都是公有的,外部可以直接访问它们。
2. 私有成员 (Private Members)
在 Python 中,使用双下划线 (__
) 开头的属性或方法被认为是私有的,不能被类外部直接访问。这是通过名称重整(name mangling)实现的,Python 会在属性名前加上类名来避免外部访问它们。
1 | class MyClass: |
在这个例子中,__name
属性和 __private_method()
方法是私有的,外部无法直接访问它们。如果尝试访问,会报 AttributeError
错误。但是,可以通过类内部的公有方法来访问私有成员。
注意:虽然双下划线的属性和方法是“私有”的,但实际上 Python 只是对它们的名称进行了重整。你可以通过
_ClassName__attribute
的方式来访问它们,Python 并没有完全禁止访问。这种设计更多的是一种“约定”而不是强制的隐藏。
1 | # 通过名称重整访问私有属性 |
3. 受保护成员 (Protected Members)
在 Python 中,使用单下划线 (_
) 开头的属性或方法被认为是受保护的,这是一个弱封装的约定。受保护的成员不建议在类外部直接访问,但并没有强制限制,可以通过子类继承和扩展时访问。
1 | class MyClass: |
受保护的成员可以在类外部访问,但一般在设计时,约定不应该直接访问这些成员,通常用于类内部或子类中。
4. 公有方法与私有属性的结合使用
一个常见的封装模式是将类的属性设置为私有,然后通过公有的方法(通常称为getter和setter方法)来控制外界如何访问或修改这些属性。这种方法允许对属性的访问进行更精细的控制,避免不当的操作。
1 | class MyClass: |
通过这种设计,程序员可以确保只有经过验证的数据才能修改属性。比如在 set_name
方法中,我们检查输入是否为字符串,如果不是,则抛出异常。这种方式有效地保护了类的内部状态。
5. 属性装饰器 (@property) 的使用
Python 提供了 @property
装饰器来简化 getter 和 setter 方法的定义,允许我们像访问普通属性一样调用方法。这是一种更 Pythonic 的封装方式。
1 | class MyClass: |
命名明确关联性
@property
创建了一个“属性对象”: 当你使用@property
装饰def name(self): ...
这个方法时,Python 不仅仅是标记了这个方法。它实际上创建了一个特殊的属性描述符对象(property object),并将这个对象绑定到类属性name
上。这个属性对象内部封装了你的 getter 方法。- Setter 需要知道它属于哪个属性: 一个类可能有很多个属性,比如
name
,age
,email
等,每个属性都可能需要自己的 getter 和 setter。如果只有一个通用的@setter
装饰器,Python 怎么知道这个 setter 方法是用来设置name
属性的,还是age
属性的呢? @name.setter
的含义:- 这里的
name
指的就是第一步中由@property
创建并绑定到name
这个名字上的那个属性对象。 .setter
是那个属性对象上的一个方法(更准确地说,它返回一个可以用作装饰器的东西)。- 所以,
@name.setter
的完整意思是:“请将下面这个方法注册为名为name
的那个属性的 setter 方法。”
- 这里的
优势:
@property
允许你将方法包装成属性的形式,从而使类的使用更加直观,同时保持了封装性。
@property
:将方法转化为属性,用于读取。@name.setter
:为属性定义赋值逻辑,用于写入。
封装的优势
- 提高代码的安全性:
- 封装隐藏了类的内部细节,防止外部对内部属性进行非法操作,减少了数据不一致或无效数据的风险。
- 提高代码的灵活性:
- 通过封装,可以灵活地修改类的内部实现,而无需修改类的外部使用代码。这种设计允许类的实现细节发生变化而不影响其接口,具有较高的扩展性。
- 更好的代码维护性:
- 封装使得代码更加模块化,每个类或模块只暴露必要的接口,减少了耦合性,增强了代码的可维护性。
- 控制属性访问:
- 通过 getter 和 setter 方法,可以控制对属性的访问和修改操作,确保类的内部状态始终有效。
封装与其他 OOP 概念的关系
- 封装与继承:封装可以结合继承一起使用,通过子类继承父类的公有方法和受保护的属性,封装性依然得以保持。
- 封装与多态:封装和多态相辅相成,封装允许将实现隐藏,而多态允许对象在运行时决定具体调用的实现,使得代码的扩展性更强。
介绍一下Python中的继承(Inheritance)思想
继承是面向对象编程(OOP)的一个核心概念,它允许一个类(称为子类或派生类)从另一个类(称为父类或基类)继承属性和方法。子类可以继承父类的特性,并且可以在此基础上添加自己的新特性,从而实现代码的重用和扩展。Python 作为一门支持面向对象编程的语言,提供了强大的继承机制。
Python中继承的优势:
- 代码重用:子类可以直接使用父类已经定义的方法和属性,避免了重复编写相同的代码片段。
- 可扩展性:子类可以在不修改父类的情况下,添加新的属性和方法,从而使得代码更具可扩展性。这样可以在不影响父类的基础上,为程序添加新的功能。
一、继承的基本概念
1. 父类(基类)
- 定义:被继承的类,提供基本的属性和方法。
- 作用:作为子类的模板,子类可以继承父类的属性和方法。
2. 子类(派生类)
- 定义:从父类继承而来的类,可以新增或重写父类的方法和属性。
- 作用:在继承父类的基础上进行扩展或修改,实现特定的功能。
3. 继承的目的
- 代码重用:避免重复编写相同的代码,提高开发效率。
- 可扩展性:通过继承,子类可以扩展父类的功能。
- 多态性:同一个方法在不同的类中可能有不同的实现,增强程序的灵活性。
二、Python 中的继承实现
1. 基本语法
在 Python 中,继承通过在类定义时指定父类来实现。
1 | class 子类名(父类名): |
2. 示例
父类:
1 | class Animal: |
子类:
1 | class Dog(Animal): |
使用子类:
1 | dog = Dog("Buddy") |
三、继承的类型
1. 单继承
定义:一个子类只继承一个父类。
示例:
1
2
3
4
5class Parent:
pass
class Child(Parent):
pass
2. 多重继承
定义:一个子类继承多个父类。
语法:
1
2class 子类名(父类1, 父类2, ...):
pass示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Flyable:
def fly(self):
return "I can fly!"
class Swimmable:
def swim(self):
return "I can swim!"
class Duck(Flyable, Swimmable):
pass
duck = Duck()
print(duck.fly()) # 输出: I can fly!
print(duck.swim()) # 输出: I can swim!
3. 多层继承
定义:子类继承父类,父类再继承其父类,形成继承链。
示例:
1
2
3
4
5
6
7
8class GrandParent:
pass
class Parent(GrandParent):
pass
class Child(Parent):
pass
四、方法重写(Override)
- 定义:子类重新定义父类的同名方法,以实现不同的功能。
- 作用:让子类能够根据需要修改或扩展父类的方法行为。
示例:
1 | class Vehicle: |
五、调用父类的方法
使用
super()
函数:在子类中调用父类的方法或初始化父类。语法:
1
2
3class 子类名(父类名):
def 方法名(self, 参数):
super().方法名(参数)
示例:
1 | class Person: |
六、继承中的特殊方法
1. __init__
构造函数
继承特性:子类的
__init__
方法会覆盖父类的__init__
方法。注意:如果子类定义了
__init__
方法,需要显式调用父类的__init__
方法来初始化父类的属性。注:为了确保父类的初始化逻辑被执行,当子类定义了自己的
__init__
方法时,通常都需要在子类的__init__
中使用super().__init__(...)
来显式调用父类的__init__
。这是一个非常重要的面向对象编程实践。
示例:
1 | class Parent: |
2. __str__
和 __repr__
方法
- 作用:定义对象的字符串表示形式。
- 继承特性:子类可以重写这些方法,提供自定义的字符串表示。
示例:
1 | class Animal: |
七、继承的注意事项
1. 访问权限
Python 中不存在像 Java 或 C++ 那样的访问修饰符(public、private、protected)。
以双下划线
__
开头的属性或方法被视为私有成员,不能在子类中直接访问。示例:
1
2
3
4
5
6
7
8
9
10class Parent:
def __init__(self):
self.__private_var = 42
class Child(Parent):
def get_private_var(self):
return self.__private_var # 这将引发 AttributeError
child = Child()
print(child.get_private_var())
2. 方法解析顺序(MRO)
- 在多重继承中,Python 使用**方法解析顺序(Method Resolution Order, MRO)**来确定属性和方法的查找顺序。
- 可以使用
类名.mro()
查看 MRO 列表。
示例:
1 | class A: |
介绍一下Python中的多态(Polymorphism)思想
多态(Polymorphism) 是面向对象编程(OOP)的核心概念之一,指的是同一操作作用于不同对象时,能够产生不同的解释和行为。简单来说,多态允许我们在不考虑对象具体类型的情况下,对不同类型的对象执行相同的操作。在 Python 中,多态性通过动态类型和灵活的对象模型得以实现。
一、什么是多态?
1. 定义
- 多态性(Polymorphism):源自希腊语,意为“多种形式”。在编程中,它指的是同一操作在不同对象上具有不同的行为。
2. 多态的类型
- 编译时多态(静态多态):通过方法重载和运算符重载实现(Python 中不支持方法重载,但支持运算符重载)。
- 运行时多态(动态多态):通过继承和方法重写实现(Python 中主要通过这种方式实现多态)。
二、Python 中的多态实现
1. 动态类型和鸭子类型
- 动态类型:Python 是动态类型语言,变量的类型在运行时确定。这使得多态性更自然。
- 鸭子类型(Duck Typing):只要对象具有所需的方法或属性,就可以使用,无需关心对象的具体类型。
示例:
1 | class Dog: |
输出:
1 | Woof! |
- 解释:
animal_speak
函数可以接受任何具有speak
方法的对象,而不关心其具体类型。这就是鸭子类型的体现。
2. 继承和方法重写
- 继承:子类继承父类的方法和属性。
- 方法重写(Override):子类可以重写父类的方法,实现不同的行为。
示例:
1 | class Animal: |
输出:
1 | Woof! |
- 解释:
Animal
类定义了一个抽象方法speak
,子类Dog
和Cat
分别实现了自己的版本。animal_speak
函数调用时,根据传入对象的类型执行对应的方法。
3. 运算符重载
- 运算符重载:在类中定义特殊方法,实现对内置运算符的重载。
示例:
1 | class Vector: |
输出:
1 | Vector(7, 10) |
- 解释:通过定义
__add__
方法,实现了Vector
对象的加法运算。这是 Python 的另一种多态形式。
三、鸭子类型详解
1. 概念
- 鸭子类型:如果一只鸟走起来像鸭子、游泳像鸭子、叫声像鸭子,那么这只鸟可以被称为鸭子。
- 在 Python 中:只要对象具有所需的方法或属性,就可以将其视为某种类型。
示例:
1 | class Bird: |
- 解释:
lift_off
函数可以接受任何具有fly
方法的对象。Fish
对象由于没有fly
方法,调用时会抛出AttributeError
。
四、多态性的优点
1. 提高代码的灵活性
- 可以编写与特定类型无关的代码,处理不同类型的对象。
2. 增强代码的可扩展性
- 添加新类型的对象时,无需修改现有代码,只需确保新对象实现了所需的方法。
3. 代码重用
- 通过多态,可以编写通用的函数或方法,避免重复代码。
五、抽象基类(Abstract Base Class)
- 概念:抽象基类定义了接口规范,子类必须实现特定的方法。
- 作用:确保子类实现必要的方法,提供一致的接口。
示例:
1 | from abc import ABC, abstractmethod |
输出:
1 | Area: 12 |
- 解释:
Shape
是一个抽象基类,定义了area
方法。Rectangle
和Circle
实现了该方法。通过多态,可以统一处理不同形状的面积计算。
六、Python 中不支持方法重载
- 说明:在 Python 中,方法重载(相同方法名,不同参数)并不被支持。后定义的方法会覆盖先前的方法。
- 替代方案:使用默认参数或可变参数。
示例:
1 | class MathOperations: |
- 解释:通过使用默认参数,实现类似方法重载的效果。
七、方法解析顺序(MRO)在多态中的作用
- MRO(Method Resolution Order):在多重继承中,Python 按照 MRO 决定调用哪个类的方法。
- 多态与 MRO:当子类继承多个父类,且父类中有同名方法时,MRO 决定了方法的调用顺序。
示例:
1 | class A: |
输出:
1 | Method from B |
- 解释:
C
继承了B
和A
,由于B
在前,调用同名方法时,B
的方法优先。
介绍一下Python的自省特性
Python 的自省(Introspection)特性指的是程序在运行时动态检查和获取自身对象信息的能力。借助自省特性,Python 程序可以在执行过程中了解对象的类型、属性、方法以及内存位置等信息,这对于调试、动态操作、元编程等场景非常有用。
自省的主要用途
动态获取对象类型
- 使用
type()
函数获取对象的类型。 - 使用
isinstance()
判断对象是否属于某种类型。
1
2
3x = 10
print(type(x)) # <class 'int'>
print(isinstance(x, int)) # True- 使用
检查对象的属性和方法
dir()
函数用于列出对象的所有属性和方法。
1
2
3
4
5
6
7
8class Person:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, {self.name}")
p = Person("Alice")
print(dir(p)) # 输出 p 对象的属性和方法列表1
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'greet', 'name']
获取对象的属性值
- 使用
getattr()
、setattr()
和hasattr()
动态获取、设置和检查对象属性的值。
1
2
3print(getattr(p, "name")) # 获取属性 'name' 的值
setattr(p, "age", 25) # 动态设置一个新的属性 'age'
print(hasattr(p, "age")) # 检查是否有属性 'age'- 使用
函数与可调用对象检查
callable()
用于检查对象是否是可调用的(如函数、类实例等)。
1
print(callable(p.greet)) # True, 因为 greet 是可调用的
模块与类的自省
- 使用
__name__
获取模块名,__class__
获取对象的类。 __dict__
列出对象的所有属性和方法。
1
2print(p.__class__) # 输出 <class '__main__.Person'>
print(p.__dict__) # 输出 {'name': 'Alice', 'age': 25}- 使用
内置库
inspect
inspect
模块提供了更强大的自省功能,如获取函数签名、源代码、调用层次等信息。
1
2
3
4
5
6
7import inspect
def my_function(x):
return x + 1
print(inspect.getmembers(my_function)) # 列出函数的所有成员
print(inspect.signature(my_function)) # 获取函数签名
自省的应用场景
- 调试与日志记录:可以在运行时动态检查对象的类型和属性值,有助于快速调试和生成详细的日志。
- 元编程:在 Python 中使用装饰器、动态类等元编程技巧时,自省特性提供了关键支持。
- 自动化测试:通过自省可以检查测试对象的结构、属性和方法,帮助自动生成和执行测试用例。
- 动态操作:在框架设计中,如序列化/反序列化,依赖自省来动态获取对象信息和自动处理数据。
Python中使用async def定义函数有什么作用?
一、async def
的作用
async def
是 Python 中定义异步函数的关键字,用于声明一个协程(coroutine)。它的核心作用是:
- 非阻塞并发:允许在等待 I/O 操作(如网络请求、文件读写)时释放 CPU,让其他任务运行。
- 提升效率:适合高延迟、低计算的场景(如 Web 服务器处理请求),通过事件循环(Event Loop)管理多个任务的切换。
与同步函数的区别:
- 同步函数遇到 I/O 时会“卡住”整个线程,直到操作完成。
- 异步函数遇到
await
时会暂停,让事件循环执行其他任务,直到 I/O 完成再恢复。
通过合理使用 async def
,可以在不增加硬件成本的情况下显著提升AI系统吞吐量和响应速度。
二、生动例子:餐厅服务员点餐
假设一个餐厅有 1 个服务员和 3 个顾客:
- 同步场景:服务员依次为每个顾客点餐,必须等当前顾客完全点完才能服务下一个。
- 异步场景:服务员在顾客看菜单时(等待时间)去服务其他顾客,最终总时间更短。
代码实现:
1 | import asyncio |
输出:
1 | 顾客 Alice 开始看菜单... |
三、在 AIGC 中的应用
场景:同时处理多个用户的文本生成请求。
案例:使用异步框架(如 FastAPI)处理 GPT 请求,当一个请求等待模型生成时,处理另一个请求。
1 | from fastapi import FastAPI |
四、在传统深度学习中的应用
场景:异步加载和预处理数据,减少训练时的等待时间。
案例:使用 aiofiles
异步读取文件,同时用多进程进行数据增强。
1 | import aiofiles |
五、在自动驾驶中的应用
场景:实时处理多传感器(摄像头、雷达、LiDAR)的输入数据。
案例:异步接收传感器数据并并行处理。
1 | async def process_camera(frame): |