单例设计模式

上下文

我最近一直在重构我过去几年一直在从事的项目的一些主要组件,Proxima。它是一种在多台机器上为 DaVinci Resolve 并行编码代理的工具。我从 Python 的入门级知识开始,不得不多次改变规则并使用我不理解的代码来获得我想要的行为。一个例子是使用单例。我最近看到一个不鼓励使用单例的视频,并且很想了解更多关于它们的信息。所以我挖掘了我的旧代码来尝试弄清楚。

注意:这是我“公开学习”。我研究了一个我感兴趣的话题,并尝试通过解释](https://e-student.org/feynman-technique/)来了解更多信息。请对您在这里阅读的所有内容持保留态度;单击链接或搜索自己,但不要相信我的话。我绝不是专家!如果你是,请发表评论并随时向我学习,我总是渴望了解更多。

这是什么?

单例设计模式允许传统类在应用程序范围内具有全局状态的超能力。每个实例化都在后台返回相同的实例。想象一个连接起来很昂贵的数据库。您可能希望将与它的连接限制为仅一个。单例使这很容易。对于我的项目,我有一个复杂的配置文件,在使用之前需要对其进行解析和验证。我不希望每次使用它时都会发生这种情况。

有几种方法可以在 Python 中构造单例类。这是一个使用元类实现的例子:

class Singleton(type):
    _instances = {}
    def __call__ (cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls). __call__ (*args, **kwargs)
        return cls._instances[cls]

class Cake(metaclass=Singleton):
    def __init__ (self):
        # combine dry ingredients
        # combine wet ingredients
        # bake!
        pass

进入全屏模式 退出全屏模式

元类本质上是一个类的一个类,可以用来定义其子类的实例化。当调用一个类并选择Singleton作为其元类时,Singleton定义该类的实例化。在这种情况下,如果在_instances中已经存在Cake实例,则将其返回给调用者。

在此处阅读有关元类的更多信息:https://realpython.com/python-metaclasses/

并在 creating Python singletons 上查看此 SO 线程:http://stackoverflow.com/questions/6760685/ddg#6798042

应该什么时候使用?

单例模式的一般良好候选者是:

  • 不可变数据源,解析缓慢/昂贵(不变异的数据不会变得不一致)

  • 像记录器或控制台接口这样的数据“接收器”(数据不影响内部应用程序状态,并且记录器配置可以在不配置每个实例的情况下保持一致)

为什么它被认为是反模式?

当其全局状态被滥用时,单身人士可能会导致问题。如果您在多个地方需要相同的数据和功能,单例可以轻松实现。如果该数据是复杂的、可变的数据,则出现状态问题的可能性会高得多。

从本质上讲,单例在现代开发中被认为是一种反模式,因为它们:

  • 具有隐式全局状态

  • 难以进行单元测试

  • 多线程难用

  • 经常打破“单一责任”规则

压倒性的互联网舆论是为了避免它们。

其他意见见下文:

  • https://sites.google.com/site/steveyegge2/singleton-considered-stupid

  • https://www.linkedin.com/pulse/singleton-anti-pattern-manik-jain

  • https://stackoverflow.com/questions/12455164/is-singleton-an-anti-pattern

zoz100037https://krakendev.io/blog/antipatterns-singletons

关于反模式的简短咆哮

人类喜欢极端。他们不喜欢灰色地带。我们倾向于光谱的不同端。我相信这些黑白趋势之一是将事物标记为“反模式”。 “不要使用/做___这是一种反模式。” '纳夫说。我认为这样非黑即白的事情是不明智的。上下文是揭示灰色区域的关键;你一个人知道你的代码和用例;只有您知道潜在的利弊将如何影响您。我相信,如果您可以避免缺点并且无法以更好的方式获得优点,则可以很好地使用“反模式”。以果子来判断;它如何影响您的代码。不是别人的流行观点。

为什么我开始在 Python 中使用 Singletons

我的编程之旅始于我在一些语法上不太受欢迎的语言中蹦蹦跳跳,在工作中寻找快速的宏片段和自动化,只是让它们几乎不做我需要的事情。我真的不想学任何东西,我只是想要捷径。但我渐渐爱上了它。从那以后我不得不变得更有耐心,但一些坏习惯仍然存在,不幸的是我错过了一些基础知识。 “类”是这些基础之一。当我开始接触 Python 并开发更复杂的自动化程序时,我意识到我已经达到了玩对象/函数乒乓球的极限。我已经走了这么远没有他们,我开始犹豫不决。我什至首先考虑了函数式编程。最终,我承认了自己的否认,并同意我需要先了解规则,然后才能放弃它们。所以我参加了 Python 课程 101... 之类的。

我的配置管理器用例

我的第一堂课不是“hello world”课,它真的应该是。它是一个配置管理器。和每个有抱负的程序员一样,我写了自己的想法,认为那里没有“适合我的需要”的配置管理器,所以我构建了我的怪物并用以下功能重载了它:

  • 将 YAML 文件解析为整洁、可读的配置

  • 通过基于模式的类型验证来抵消 YAML 的随和性

  • 检测丢失的配置文件并提示用户修改复制的默认值

  • 在保留 YAML 注释、顺序和嵌套的同时插入更新(yikes)

  • 检查配置中是否缺少键并提示用户使用默认值或自定义值进行替换

  • 警告用户关于杂散不支持的键

当时,我认为我现在很受欢迎,因为我已经整齐地组织了课程,而不是在评论围栏中分组的功能。我的配置管理器正在工作,所以我将它导入到我所有需要访问配置对象的模块中并且它工作正常!嗯,大部分。

慢初始化

不幸的是,事实证明我的新课程不会赢得任何基准。不仅仅是在每次初始化时解析 YAML 都会减慢速度,而是所有的数据验证体操。在应用程序启动时运行解析和验证是必要的,但是在每次导入设置对象时重新初始化不是......并且很少有模块没有导入配置,因为应用程序日志记录级别是在 YAML 中配置的。诚然,我可能会写一些东西来提高性能......

它不仅速度慢,而且我已经添加了很酷的彩色日志消息,所以每次执行上下文更改时,我都会弹出“检查设置...”几秒钟。就像我自己的程序在羞辱我一样。这是我难以坚持的另一件事:功能优先于形式;在获得 MVP 之前,我会被细节冲昏头脑。先吃蔬菜,再吃甜点。

我给了自己一点恩典,因为我是第一次探索 OOP 的内城,并寻找解决方案。很快我偶然发现了单例并且不知道它们是如何工作的,所以 yeet -> 删除了我的旧代码并 yank -> 谢谢我在 Stack Overflow 上。问题解决了。

简而言之,使用单例允许我的班级跳过为每个实例运行__init__。在纸面上,这正是我所需要的。事实上,一个缓慢的只读配置管理器是单例的更好用例之一。但我知道将来我可能会进行跨模块设置更新,并且需要可靠的单元测试管道。这些在传统单身人士的缺点列表中相当高。另外,我现在全神贯注于 Python,我想学习推荐的方式。

“最 Pythonic”的方式

虽然可以在 Python 中创建和使用传统的单例,但存在更多“Pythonic”替代方案。如果您的整个团队由第一次使用 Python 的 Java 开发人员组成,他们创建了一个不需要单元测试的一次性应用程序,那么在 Python 中使用传统的单例可能不会那么糟糕。

有两种“pythonic”方法可以做到这一点。后者实际上只是前者的变体:

  • 模块级代码

  • 级代码

如果您不清楚脚本、模块和包之间的区别,请看这里:https://realpython.com/lessons/scripts-modules-packages-and-libraries/

模块式“单例”

这是一个脚本。 请注意,所有内容都发生在一个文件中。我们只需运行它就可以得到我们的蛋糕。

# bake_cake.py script

def combine_dry_ingredients(dry:List):
    return "-".join(dry)

def combine_wet_ingredients(wet:List):
    return "-".join(wet)

def bake(dry:str, wet:str):
    return dry + wet

if __name__ == " __main__":

    dry = ["flour", "baking powder", "cocoa powder"]
    wet = ["eggs", "milk", "vanilla essence"]

    combined_dry = combine_dry_ingredients(dry)
    combined_wet = combine_wet_ingredients(wet)
    baked_cake = bake(combined_dry, combined_wet)

    print(f"Baked cake!\n{baked_cake}")

进入全屏模式 退出全屏模式

这里是作为一个模块。 我们已经初始化了所有变量,但我们没有将它们用于任何事情。

# bake_cake.py

def combine_dry_ingredients(dry:List):
    return "-".join(dry)

def combine_wet_ingredients(wet:List):
    return "-".join(wet)

def bake(dry:str, wet:str):
    return dry + wet

dry = ["flour", "baking powder", "cocoa powder"]
wet = ["eggs", "milk", "vanilla essence"]

combined_dry = combine_dry_ingredients(dry)
combined_wet = combine_wet_ingredients(wet)
baked_cake = bake(combined_dry, combined_wet)

进入全屏模式 退出全屏模式

如果我们导入该模块,所有这些变量都可以像 this 一样访问:

import cake

print(cake.combined_dry)
print(cake.combined_wet)
print(baked_cake)

进入全屏模式 退出全屏模式

这里有几个缺点。您可能不想在导入模块时运行和缓存所有内容。如果干湿成分不是硬编码列表,而是从 .csv 文件或 API 读取,我们可能希望在调用时等待,而不是导入。这称为“延迟加载”。 PyPi 上有一些库和设计模式,您可以使用这些库在功能级别实现延迟加载。如果您需要基于类的实现,请查看@property装饰器。

请参阅 Real Python 的很棒的教程。

包式“单例”

您可以使用包进一步采用模块方法。蛋糕的例子并不完全现实,但希望它仍然能说明这一点。这与使用模块的过程非常相似,但全局变量位于包__init__.py文件中。

这是一些额外的设置,但您可以获得:

  • 更大的逻辑和状态分离

  • 不需要模棱两可的“主”模块或函数

  • 没有强制单一实例的缓存的所有好处

  • 简单的单元测试:只需单独导入模块而不是整个包

这使得单元测试变得微不足道。如果我们想测试单个模块和功能,我们不必与强制的单个实例或全局状态抗衡,我们只需单独导入模块而不是整个包。

# cake.py

def combine_dry_ingredients(dry:List):
    return "-".join(dry)

def combine_wet_ingredients(wet:List):
    return "-".join(wet)

def bake(dry:str, wet:str):
    return dry + wet


# ingredients.py

import os
def get_env_ingredients():
    dry = os.environ.get("CAKE_DRY_INGREDIENTS")
    wet = os.environ.get("CAKE_WET_INGREDIENTS")
    return dry, wet

def get_csv_ingredients():
    pass

def fetch_api_ingredients():
    pass


# __init__.py

import package.cake
import pakage.ingredients

dry, wet = package.get_env_ingredients(dry, wet)
c_dry = package.cake.combine_dry_ingredients(dry)
c_wet = package.cake_combine_wet_ingredients(wet)
package.cake.bake(c_dry, c_wet)

进入全屏模式 退出全屏模式

总结

除非您需要使用元类化强制执行单个类实例,否则使用模块或包来缓存数据可能会取代您对单例的任何需求。当然,在修复未损坏的部分之前,请考虑熟悉 Python 方法的成本和重构的成本。最正确的并不总是最好的。如果你能想到一个 Python 中的单例用例,它支持模块和包,我很想听听!我当然不是 Python 大师,也不是任何其他语言。

为什么Module方式不流行?

不幸的是,因为单例模式存在于许多语言中,所以当您尝试描述重新初始化问题时,Python 模块方法并不是 Google 搜索的首选。那些在接触 Python 之前在其他语言中使用过单例的人可能会搜索“如何在 Python 中做单例”。

也许这就是抢先一步并为一般问题找到一般解决方案的一部分。这是深潜语言课程真正获得回报的地方。

一些替代品

以下是我在此过程中发现的其他几个替代方案。这里没有一对一的功能重叠,但他们试图解决单例的一些缺点。

单态模式

单态设计模式试图通过允许多个实例化来解决单例的一些问题,但在幕后这些实例之间共享静态数据。

它们比单例更容易继承、修改和单元测试:

  • https://stackoverflow.com/questions/63251354/python-problem-in-understanding-monostate-design-pattern-code

  • https://pypi.org/project/monostate/

对象池

当您想要设置大于 1 的实例化限制并且不处理隐式全局状态时,这是另一种设计模式。这非常适合作为资源的硬限制。谢谢_ArjanCodes_https://youtube.com/watch?vu003dRm4JP7JfsKY

Arjan 还更详细地解释了为什么他认为单例是 Python 中的反模式。

更多链接

  • https://stackoverflow.com/questions/3171291/alternatives-for-the-singleton-pattern

  • https://www.amazon.com/o/asin/0201633612

希望这有帮助!

Logo

学AI,认准AI Studio!GPU算力,限时免费领,邀请好友解锁更多惊喜福利 >>>

更多推荐