解决Python项目部署的时候有个对象吃GPU的问题

88 阅读5分钟

问题描述

在zdppy_api多个woker部署的时候,有多个进程,其中有一个类,在创建实例的时候会很吃GPU,但是实际上这个类全局只需要创建一次就行,而多进程中,这个类会被创建多次,也就是会多次消耗GPU,从而导致容器在启动时就崩溃,无法正常启动。

解决方案

让这个吃GPU的类,变成单例,这样的话,全局就只有一个对象,只会被创建一次。

参考代码

import threading

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

    def __new__(cls, *args, **kwargs):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super(Singleton, cls).__new__(cls)
        return cls._instance

    def __init__(self):
        print("aaaaaaaaaaaaa")

# 使用
instance1 = Singleton()
instance2 = Singleton()
print(instance1 is instance2)  # 输出 True

单例能够保证_instance只有一个,也就是instance1 is instance2的结果是True,但是不会保证__init__只执行一次,__init__每次创建对象都会执行。所以控制台,会输出两次print的结果。

__new____init__的区别

在Python中,__new____init__是两个特殊的方法,它们都在创建类的新实例时被调用,但它们的用途和行为有所不同。

__new__

  • __new__是一个静态方法,也就是说它不需要在实例上被调用,它的作用是创建一个新实例。
  • __new__必须返回一个实例对象,这个对象可以是类本身的实例,也可以是其他类的实例。
  • __new__通常用于继承不可变类型(如intstrtuple等)或者实现单例模式。
  • 如果__new__方法在派生类中被重写,那么它必须调用父类的__new__方法,并且通常使用super().__new__(cls, ...)来完成。

__init__

  • __init__是一个实例方法,它在__new__之后被调用,用于初始化新创建的实例。
  • __init__不返回任何值,或者更准确地说,它返回None
  • __init__用于设置实例变量和执行其他初始化操作。
  • 如果__init__在派生类中被重写,它不需要(也不应该)调用父类的__init__方法,除非有特定的初始化需要。

区别

  • 调用顺序__new__首先被调用以创建实例,然后__init__被调用以初始化这个实例。
  • 返回值__new__必须返回一个对象,而__init__不返回任何值。
  • 用途__new__用于控制对象的创建过程,而__init__用于对象的初始化。

示例

class MyClass:
    def __new__(cls, *args, **kwargs):
        print("Creating an instance of MyClass")
        instance = super().__new__(cls)
        return instance

    def __init__(self, value):
        print("Initializing the instance")
        self.value = value

# 创建实例
obj = MyClass(10)

在这个例子中,当你创建MyClass的一个实例时,首先会打印出"Creating an instance of MyClass",然后打印出"Initializing the instance"。这展示了__new____init__的调用顺序和它们在对象创建过程中的不同作用。

总结来说,__new__负责创建对象,而__init__负责初始化对象。理解这两个方法的区别对于深入掌握Python面向对象编程非常重要。

抽象可能有问题代码

A是需要消耗GPU的那个类,B是调用A的类。

class A:
    def __init__(self):
        print("a need gpu")

    def fa(self):
        print("fa")


class B:
    def __init__(self):
        self.a = A()

    def __call__(self):
        self.a.fa()
        print("ba....")


b = B()
b()

b1 = B()
b1()

在原本代码中,B里面的a并不是单例的,所以每次执行,都会创建新的对象,每次创建新的对象,都会消耗较高GPU,一旦对象创建过多,GPU不够,程序就会崩溃。

__call__可以让类对象当成方法使用

在Python中,__call__ 方法是一个特殊方法,它允许一个实例变得可调用,即可以像函数那样被调用。当一个类的实例被用作函数时,Python会自动调用这个方法。

__call__ 的作用

  1. 使实例可调用: 当你想要让你的类的实例表现得像一个函数时,你可以实现__call__方法。这在创建需要延迟计算或需要缓存结果的自定义对象时非常有用。
  2. 工厂模式__call__方法常用于工厂模式,其中类的实例负责返回其他类的实例。
  3. 装饰器: 在装饰器模式中,__call__方法用于包装函数,并在调用被装饰函数前后执行额外的代码。
  4. 元类: 在创建类的类(即元类)时,__call__方法用于创建类的实例。

以下是一些__call__方法的使用示例:

示例1:使实例可调用

class Greeting:
    def __init__(self, name):
        self.name = name

    def __call__(self):
        return f"Hello, {self.name}!"

g = Greeting("Kimi")
print(g())  # 输出: Hello, Kimi!

在这个例子中,Greeting类的实例g表现得像一个函数,当调用g()时,实际上是调用了g__call__方法。

示例2:工厂模式

class Greeter:
    def __call__(cls, name):
        return cls().get_greeting(name)

    @staticmethod
    def get_greeting(name):
        return f"Hello, {name}!"

# 使用
greeting = Greeter()
print(greeting("Kimi"))  # 输出: Hello, Kimi!

在这个例子中,Greeter类有一个__call__方法,它接受一个名字并返回一个问候语。

示例3:装饰器

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Something is happening before the function is called.")
        result = func(*args, **kwargs)
        print("Something is happening after the function is called.")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("Kimi")

虽然这个例子没有直接使用__call__,但my_decorator函数返回的wrapper函数实际上是一个可调用对象,它在被调用时执行额外的代码。这是装饰器模式的一个典型应用,__call__方法在内部被用来使wrapper可调用。

__call__方法是Python中一个非常强大的特性,它提供了极大的灵活性,允许你以函数的方式使用对象。

最终的解决方案

import threading

class A:
    def __init__(self):
        print("a need gpu")

    def fa(self):
        print("fa")


class B:
    _a = None
    _lock = threading.Lock()

    def __init__(self):
        with B._lock:
            if B._a is None:
                B._a = A()

    def __call__(self):
        self._a.fa()
        print("ba....")


b = B()
b()

b1 = B()
b1()

这里核心是对B进行了改造。

  • 当创建对象的时候,就判断a的实例是否已经被创建了
  • 如果被创建了,就直接啥也不干
  • 如果没被创建,就创建对象并赋值
  • 这样,在__call__,调用类的_a对象的时候,始终是唯一的
  • 最终的结果就是,need_gpu只输出了一次