MelonBlog

python中优雅的使用单例模式

习惯用面相对象的方式来 coding 的朋友,写 python 的时候一定也是喜欢先通过 class 来建模,然后再通过 method 来实现各种逻辑。用这种方式来 coding 一定会频繁的创建类实例,频繁的重复创建对象开销比较大,单例模式可以降低这部分开销。

使用单例模式

简单的使用

最简单的使用单例方式实际上就是在包内就创建好实例,然后外部的包直接使用这个创建好的实例

栗子🌰:

假设 mapper 包的 lookup_mapper.py 是用来查询 lookup 表的一个类,其他模块要通过这个类来查询数据

class LookupMapper(BaseMapper):
    def find(self, lookup_type: str, code: str) -> Lookup | None:
        c = self.conn.cursor()
        try:
            result = c.execute(
                'select id,type,code,value,status from lookup where type = ? and code = ?',
                (lookup_type, code))
            row = result.fetchone()
            if row is None:
                return None
            return Lookup(id=row[0], type=row[1], code=row[2], value=row[3], status=row[4])
        finally:
            c.close()
            
LookupMapperInstance = LookupMapper()

在其他包里直接引用LookupMapperInstance

from mapper.lookup_mapper import LookupMapperInstance
instance1 = LookupMapperInstance
instance2 = LookupMapperInstance
print(instance1 == instance2)

这种方式使用起来非常简单,但是需要自己提前创建好实例,这里会有个问题,外部的包可能绕过这个实例,直接调用 LookupMapper 的构造函数自己创建实例,这样就导致类实例的使用不统一,下面讲一个更优雅且统一的方式使用单例对象。

优雅的使用

为了让用户调用 LookupMapper 类的构造函数也能返回单例对象,就可以完美解决上面说的使用方式不统一的问题

先来看代码:

from functools import wraps
def singleton(orig_cls):
    orig_new = orig_cls.__new__
    instance = None
    @wraps(orig_cls.__new__)
    def __new__(cls, *args, **kwargs):
        nonlocal instance
        if instance is None:
            instance = orig_new(cls, *args, **kwargs)
        return instance
    orig_cls.__new__ = __new__
    return orig_cls

这里定义了一个 singleton 函数,函数的参数为一个类,singleton 函数做的事情就是定义一个新的__new__函数来覆盖类原始的构造函数。

@wraps 的用法可能新手读者不是很了解,它的作用是让被修饰的对象保持原有的信息,例如类名和文档字符串,这里的作用就是保持orig_cls.__new__函数信息,从代码上来看其实核心思路就是让orig_cls.__new__反过来再修饰singleton函数内部的__new__函数


那怎么让 singleton 函数和类结合起来呢,这里可以使用装饰器模式来增强类的原始功能

@singleton
class LookupMapper(BaseMapper):
    def find(self, lookup_type: str, code: str) -> Lookup | None:
        c = self.conn.cursor()
        try:
            result = c.execute(
                'select id,type,code,value,status from lookup where type = ? and code = ?',
                (lookup_type, code))
            row = result.fetchone()
            if row is None:
                return None
            return Lookup(id=row[0], type=row[1], code=row[2], value=row[3], status=row[4])
        finally:
            c.close()
            
mapper1 = LookupMapper()
mapper2 = LookupMapper()
print(mapper1 == mapper2)
print(mapper1.__new__)

上面的代码通过@singleton修饰了原始的 LookupMapper 类。