9

我正在寻求有关我的代码设计的建议。

介绍

我有几个类,每个类代表一种文件类型,例如:MediaImageFile、MediaAudioFile 和通用(也是基类)MediaGenericFile。

每个文件都有两个变体:Master 和 Version,所以我创建了这些类来定义它们的特定行为。编辑:版本代表主文件的调整大小/裁剪/修剪/等变体。它主要用于预览。

编辑: 为什么我想动态地做它的原因是这个应用程序应该是可重用的(它是 Django 应用程序),因此它应该很容易实现其他 MediaGenericFile 子类而无需修改原始代码。

我想做的事

  1. 首先,用户应该能够在不影响原始代码的情况下注册自己的 MediaGenericFile 子类。

  2. 文件是版本还是主文件很容易(一个正则表达式)从文件名中识别出来。

    /path/to/master.jpg                   -- master
    /path/to/.versions/master_version.jpg -- version
    
  3. Master/Version 类使用 MediaGenericFile 的一些方法/属性,例如文件名(您需要知道文件名才能生成新版本)。

  4. MediaGenericFile 扩展了 LazyFile,它只是一个惰性 File 对象。

现在我需要把它放在一起……</p>

二手设计

在开始编写“版本”功能之前,我有工厂类 MediaFile,它根据扩展名返回适当的文件类型类:

>>> MediaFile('path/to/image.jpg')
<<< <MediaImageFile 'path/to/image.jpg'>

Master 和 Version 类定义了使用 MediaGenericFile 等的方法和属性的新方法。

方法一

一种方法是动态创建新类型,它继承 Master(或 Version)和 MediaGenericFile(或子类)。

class MediaFile(object):
    def __new__(cls, *args, **kwargs):
        ...  # decision about klass
        if version:
            bases = (Version, klass)
            class_name = '{0}Version'.format(klass.__name__)
        else:
            bases = (Master, klass)
            class_name = '{0}Master'.format(klass.__name__)

        new_class = type(class_name, bases, {})
        ...
        return new_class(*args, **kwargs)

方法二

第二种方法是在 Master/Version 中创建方法“contribute_to_instance”并在创建 new_class 后调用它,但这比我想象的要棘手:

classs Master(object):
    @classmethod
    def contribute_to_instance(cls, instance):
        methods = (...)
        for m in methods:
            setattr(instance, m, types.MethodType(getattr(cls, m), instance))

class MediaFile(object):
    def __new__(*args, **kwargs):
        ...  # decision about new_class
        obj = new_class(*args, **kwargs)
        if version:
            version_class = Version
        else:
            version_class = Master

        version_class.contribute_to_instance(obj)
        ...
        return obj

但是,这不起作用。调用 Master/Version 的方法仍然存在问题。

问题

实现这种多重继承的好方法是什么?

这个问题怎么称呼?:) 我试图找到一些解决方案,但我根本不知道如何命名这个问题。

提前致谢!

答案注释

拉尔斯曼斯

对于我的情况,比较和实例检查不会有问题,因为:

  1. 无论如何都要重新定义比较

    class MediaGenericFile(object):
        def __eq__(self, other):
            return self.name == other.name
    
  2. 我从不需要检查 isinstance(MediaGenericFileVersion, instance)。我正在使用 isinstance(MediaGenericFile, instance) 和 isinstance(Version, instance) 两者都按预期工作。

然而,在我看来,为每个实例创建新类型听起来是相当大的缺陷。

好吧,我可以在元类中动态创建这两种变体,然后使用它们,例如:

>>> MediaGenericFile.version_class
<<< <class MediaGenericFileVersion>
>>> MediaGenericFile.master_class
<<< <class MediaGenericFileMaster>

进而:

class MediaFile(object):
    def __new__(cls, *args, **kwargs):
        ...  # decision about klass
        if version:
            attr_name = 'version_class'
        else:
            attr_name = 'master_class'

    new_class = getattr(klass, attr_name)
    ...
    return new_class(*args, **kwargs)

最终解决方案

最后设计模式是工厂类。MediaGenericFile 子类是静态类型的,用户可以自己实现和注册。Master/Version 变体在元类中动态创建(从几个 mixin 粘合在一起)并存储在“缓存”中,以避免 larsmans提到的危险。

感谢大家的建议。最后我理解了元类的概念。好吧,至少我认为我理解它。推送源主...</p>

4

5 回答 5

4

我当然建议不要在__new__. 它的问题是您为每个实例创建一个新类型,这会导致开销,更糟糕的是,会导致类型比较失败:

>>> Ham1 = type("Ham", (object,), {})
>>> Ham2 = type("Ham", (object,), {})
>>> Ham1 == Ham2
False
>>> isinstance(Ham1(), Ham2)
False
>>> isinstance(Ham2(), Ham1)
False

这违反了最小意外原则,因为这些类可能看起来完全相同:

>>> Ham1
<class '__main__.Ham'>
>>> Ham2
<class '__main__.Ham'>

但是,如果您在模块级别构建类,则可以让方法 1 正常工作MediaFile

classes = {}
for klass in [MediaImageFile, MediaAudioFile]:
    for variant in [Master, Version]:
        # I'd actually do this the other way around,
        # making Master and Version mixins
        bases = (variant, klass)
        name = klass.__name__ + variant.__name__
        classes[name] = type(name, bases, {})

然后,在 中MediaFile.__new__,按名称查找所需的类classes。(或者,将新构造的类设置在模块上而不是 a 中dict。)

于 2012-07-21T11:16:23.490 回答
4

我不确定您希望它有多动态,但是使用“工厂模式”(这里使用类工厂)是相当可读和可以理解的,并且可以做您想做的事情。这可以作为一个基础......MediaFactory可以更聪明,你可以注册多个其他类,而不是硬编码MediaFactoryMaster等......

class MediaFactory(object):

    __items = {}

    @classmethod
    def make(cls, item):
        return cls.__items[item]

    @classmethod
    def register(cls, item):
        def func(kls):
            cls.__items[item] = kls
            return kls
        return func

class MediaFactoryMaster(MediaFactory, Master): pass
class MediaFactoryVersion(MediaFactory, Version): pass

class MediaFile(object):
    pass

@MediaFactoryMaster.register('jpg') # adapt to take ['jpg', 'gif', 'png'] ?
class MediaFileImage(MediaFile):
    pass

@MediaFactoryVersion.register('mp3') # adapt to take ['mp3', 'ogg', 'm4a'] ?
class MediaFileAudio(MediaFile):
    pass

其他可能的 MediaFactory.make

@classmethod
def make(cls, fname):
    name, ext = somefunc(fname)
    kls = cls.__items[ext]
    other = Version if Version else Master
    return type('{}{}'.format(kls.__name__,other.__name__), (kls, other), {})
于 2012-07-21T11:48:37.770 回答
2

你怎么不使用继承而是玩弄__new__

class GenericFile(File):
    """Base class"""

class Master(object):
    """Master Mixin"""

class Versioned(object):
    """Versioning mixin"""

class ImageFile(GenericFile):
    """Image Files"""

class MasterImage(ImageFile, Master):
    """Whatever"""

class VersionedImage(ImageFile, Versioned):
    """Blah blah blah"""

...

目前尚不清楚您为什么要这样做。我认为这里有一种奇怪的代码气味。我建议使用更少的具有一致接口(duck-typing)的类,而不是十几个类,并isinstance检查整个代码以使其全部工作。

也许你可以用你想在你的代码中做的事情来更新你的问题,人们可以帮助识别真正的模式或建议一个更惯用的解决方案。

于 2012-07-21T11:42:15.467 回答
1

您不必为每个实例创建一个新类。不要在创建新类中__new__创建它们__metaclass__。在 base 或 base_module 中定义一个元类。这两个“变体”子类很容易保存为其通用父类的类属性,然后__new__根据自己的规则查看文件名并决定返回哪个子类。

注意__new__在构造函数调用期间返回的类不是“指定”的类。 您可能需要采取措施__init__从 withing中调用__new__

子类要么必须:

  1. 向工厂或父母“注册”自己
  2. import编辑,然后让父或工厂通过递归搜索找到它们cls.__subclasses(每次创建可能必须发生一次,但这可能不是文件处理的问题)
  3. 通过使用“setuptools”entry_points类型的工具找到,但这需要用户付出更多的努力和协调
于 2012-07-21T13:50:50.730 回答
0

您应该问的 OOD 问题是“我提议的继承的各个类是否共享任何属性

继承的目的是共享实例自然共有的公共数据或方法。除了都是文件之外,图像文件和音频文件有什么共同点?如果你真的想扩展你的隐喻,你可以想象AudioFile.view()它可以呈现 - 例如 - 音频数据的功率谱的可视化,但ImageFile.listen()更没有意义。

我认为您的问题避开了这个与语言无关的概念问题,有利于对象工厂的依赖于 Python 的机制。我认为您在这里没有适当的继承案例,或者您未能解释您的 Media 对象需要共享哪些常见功能。

于 2012-07-21T11:42:53.517 回答