撤销十年的单例模式和类级配置

Posted

技术标签:

【中文标题】撤销十年的单例模式和类级配置【英文标题】:Undoing a decade of singleton pattern and class-level configuration 【发布时间】:2021-04-20 12:45:20 【问题描述】:

概述

我需要复制整个类的继承树。简单地深度复制类对象是行不通的;正确的工厂模式涉及大量代码更改;我不确定如何使用元类来实现这一点。

背景

我开发的软件实现了对专用外部硬件的支持,通过 USB 连接到主机。许多年前,人们假设一次只能使用一种类型的硬件。因此,hardware 对象被用作单例。多年来,次要课程是根据当前活动的硬件课程配置的。

目前,无法同时将此库用于两种类型的硬件,因为无法同时为两种硬件配置类对象。

近年来,我们通过为每个硬件创建一个 python 进程来避免这个问题,但这已经变得站不住脚了。

这是一个极其简化的架构示例:

# ----------
# Hardware classes
class HwBase():
    def customizeComponent(self, compDict):
        compDict['ComponentBase'].hardware = self

class HwA(HwBase):
    def customizeComponent(self, compDict):
        super().customizeComponent(compDict)
        compDict['AnotherComponent'].prop.configure(1,2,3)

class HwB(HwBase):
    def customizeComponent(self, compDict):
        super().customizeComponent(compDict)
        compDict['AnotherComponent'].prop.configure(4,5,6)


# ----------
# Property classes
class SpecialProperty(property):
    def __init__(self, fvalidate):
        self.fvalidate = fvalidate
        # handle fset, fget, etc. here.
        # super().__init__()

# ----------
# Component classes
class ComponentBase():
    hardware = None

    def validateProp(self, val):
        return val < self.maxVal

    prop = SpecialProperty(fvalidate=validateProp)


class SomeComponent():
    """Users directly instantiate and use this compoent via an interactive shell.
    This component does complex operations with the hardware attribute"""

    def validateThing(self, val):
        return isinstance(val, ComponentBase)

    thing = SpecialProperty(fvalidate=validateThing)


class AnotherComponent():
    """Users directly instantiate and use this compoent via an interactive shell
    This component does complex operations with the hardware attribute"""
    maxVal = 15


# ----------
# Initialization



def initialize():
    """ This is only called once perppython instance."""
    #activeCls = HwA
    activeCls = HwB

    allComponents = 
            'ComponentBase': ComponentBase,
            'SomeComponent': SomeComponent,
            'AnotherComponent': AnotherComponent
    

    hwInstance = activeCls()
    hwInstance.customizeComponent(allComponents)

    return allComponents

components = initialize()

# ----------
# User code goes here
someInstance1 = components['SomeComponent']()
someInstance2 = components['SomeComponent']()

someInstance1.prop = 10
someInstance2.prop = 10

首要目标是同时与 HwA 和 HwB 交互。由于大多数交互是通过组件而不是硬件对象本身完成的,我相信解决方案涉及拥有多个版本的组件,例如:两个独立的继承树,总共 6 个最终组件,为每个硬件配置一个树/集。这是我需要帮助的。


可能的解决方案

考虑到我配置了大约数十种不同的硬件。此外,还有数百个不同的叶组件类,还有许多额外的基类和 mixin 类。

在组件的init方法中移动所有配置步骤

由于使用属性而无法实现;这些需要在类上设置。

深拷贝类对象

复制所有类对象,换入适当的__bases__。可变类变量需要小心处理。但是,我不确定如何处理这个属性,因为属性对象(例如 fvalidate)中的类体引用需要更新为复制的类。

这需要大量的人工干预才能工作。并非不可能,但从长远来看容易破裂。

工厂模式

将所有组件定义包装在一个工厂函数中:

def ComponentBaseFactory(hw):
    class SomeComponent(cache[hw].ComponentBase):
        pass

并拥有某种组件缓存,可以在 initialize() 期间处理创建所有类对象

这是我认为在架构上最正确的可用选项。由于类体被重新执行 在每次工厂调用时,属性的属性都会引用相应的类对象。

缺点:巨大的代码足迹。我熟悉通过 sed 或 python 脚本进行代码库范围的更改,但这会很多。

在组件上添加元类

我不知道该怎么做。基于 python data model (py3.7),在类创建时会发生以下情况(在类定义缩进结束后立即发生):

    MRO 条目已解决; 已确定适当的元类; 类命名空间已准备好; 类体被执行; 类对象已创建。

我需要在定义类后重做这些步骤(就像工厂函数!),但我不确定如何重做第 4 步。具体来说,python 文档在第 3.3.3.5 节中指出该类body 被执行为“特殊?” exec() 内置函数的形式。 如何使用一组不同的本地/全局变量重新执行类主体? 即使我使用 inspect 恶作剧访问类主体的代码,我也不确定我是否能够正确重现模块环境。

即使我弄乱了__prepare____new__,我也看不出如何修复在类代码块中引入的关于属性实例化的交叉引用。

组件作为元类

元类是类工厂,就像类是对象工厂一样。 SomeComponent 和 AnotherComponent 可以声明为元类,然后在 initialize() 期间使用 Hw 对象进行实例化:

SomeComponent = SomeComponentMeta(hw)

这类似于工厂模式,但也需要进行大量代码更改:必须将大量类代码移动到元类__init__

【问题讨论】:

我只是重新阅读了这个问题 - 如果您的叶子类将具有所有 SpecialProperty 属性的独立实例,它会为您工作吗?可以编写一个元类来自动处理这个问题。但我还不确定这是否是你所需要的(如果这就是你所需要的)。 我想过,是的,它会起作用。问题是设置它。拥有 SpecialProperty 类属性后,如何将 fvalidate 映射回父级属性?这需要了解 SpecialProperty 对象。并非不可能,但这最终需要高维护,因为有许多不同的属性对象。 复制或深度复制是否适用于属性对象?您是否在其中实施__set_name__? (由metaclass.__new__调用) (好的 - 我在这里做了一个实验,发现 SpecialProperty 的实例不能通过常规的 copy.copy 或 copy.deepcopy 复制。我将编写代码来复制它们,而无需求助于那些,然后你应该测试它。如果你可以添加SpecialProperty类的完整代码,那么我写的任何东西都会更确定) (eeek - 我将在我的答案中加入这一点 - 但也发现了这一点:如果您在属性的子类中使用 .setter.deleter 来设置这些方法,您生成他们自己的一个新实例,并且不要复制原始实例的__dict__。这意味着如果一个人像通常的属性一样使用@SpecialProperty().setter,那么.fvalidate 属性将会丢失。你的代码库肯定考虑到这一点,否则它甚至不会起作用。 【参考方案1】:

我必须在这里花费更多时间才能正确理解您的需求,但如果您的“TL;DR”使用不同的全局/非局部变量执行类主体是底线,工厂方法是正如您所考虑的那样,这是一种非常干净易读的方式。

起初,我认为元类在这里不是一个好方法——尽管它可以用来定制你的特殊属性(在我的第一次阅读中,我无法弄清楚它们实际上做了什么,以及它们应该如何做最终课程之间的差异)。如果作为类工厂的函数可以专门化您的属性,那么它仍然可以工作。

如果您需要的是 HwaHwB 的属性是独立的,就像在 HwA 中访问与在 HwB 中访问的不同的列表对象一样,是的,元类可以通过自动重新创建任何创建子类时的属性(这样属性对象本身就不会与超类和整个层次结构共享)。 如果那是您需要的,请发表评论,我可以编写一些概念验证代码。

无论如何,可以创建一个元类,在实例化子类时,它将查看所​​有SpecialProperty 的层次结构并为子类创建新实例 - 以便保留在超类上设置的基值对子类有效,但是当配置运行时,每个类都会有一个独立的配置。 (事实证明,不需要元类:__init_subclass__ 涵盖了我们)

要注意的另一件事是,属性的​​子类不能简单地使用 Python 的copy.copy(经过经验测试)进行复制,因此我们需要一种方法来创建这些属性的可靠副本。我在下面包含了一个函数,但它可能需要改进才能与实际的 SpecialProperty 类一起使用。

from copy import copy

def copy_property(prop):
    cls = prop.__class__
    new_prop = cls.__new__(cls)
    # Initialize the attributes that can't be set from Python code, inplace:
    property.__init__(new_prop, prop.fget, prop.fset, prop.fdel) 
    if hasattr(prop, "__dict__"): # only exists for subclasses of property
        # Possible adaptation needed: it may be that for some attributes of
        # SpecialProperty, a deepcopy would be needed.
        # But for the given example attribute of "fvalidate" a simple copy is better:
        new_prop.__dict__ = copy(prop.__dict__)
    return new_prop



    
# Python 3.6 introduced `__init_subclass__` which is called at subclass _creation_
# time. With it, the logic can be inserted in ComponentBase and there is no need for
# a metaclass.
    
class ComponentBase():
    
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        for attrname in dir(cls):
            attr = getattr(cls, attrname)
            if not isinstance(attr, SpecialProperty):
                continue
            new_prop = copy_property(attr)
            setattr(cls, attrname, new_prop)
        
    
    
    hardware = None
    ...

正如您所见,由于您的项目选择了子类化property,因此必须采取一些变通方法。我在这里留下这句话作为余数,除非property 满足一个确切的需求,否则编写一个实现Descriptor Protocol 的新类更干净——只需直接实现__set____get____delete__

【讨论】:

(否则,我认为与您实时聊天将是获得最佳解决方案的更好方式 - 如果您不介意,我的个人资料中有一些联系信息) 1.原始帖子中的属性只是一个示例。我用它们来捕获与不同类变量的关系:这里,属性直接引用validate 方法。在实践中,我们有各种属性、用于验证的工厂方法、对某些基类的引用等。类主体中发生的事情太多了,但是,嘿,遗留代码:) 2.就我而言,工厂方法是最干净的方法,但涉及大量代码更改。如果我介绍它,我可能会添加很多错误。如果可以完成一个包含良好的元类实现,那么它的工作量就会减少并且不易出错,即使元类(在我看来)天生就更难被新的维护者掌握。

以上是关于撤销十年的单例模式和类级配置的主要内容,如果未能解决你的问题,请参考以下文章

python中的单例模式

单例模式

python中的单例模式

python中的单例模式

几种常见的单例模式

单例模式