Python动态继承:如何在创建实例时选择基类?

Posted

技术标签:

【中文标题】Python动态继承:如何在创建实例时选择基类?【英文标题】:Python dynamic inheritance: How to choose base class upon instance creation? 【发布时间】:2011-10-26 18:53:25 【问题描述】:

简介

我在编程工作中遇到了一个有趣的案例,需要我在 python 中实现动态类继承机制。我在使用术语“动态继承”时的意思是一个类,它不特别从任何基类继承,而是在实例化时选择从几个基类之一继承,具体取决于某些参数。

因此,我的问题如下:在我将介绍的情况下,通过动态继承实现所需额外功能的最佳、最标准和“pythonic”方式是什么。

为了以简单的方式总结这个案例,我将给出一个示例,使用代表两种不同图像格式的两个类:'jpg''png' 图像。然后我将尝试添加支持第三种格式的功能:'gz' 图像。我知道我的问题并不是那么简单,但我希望你准备好忍受我再多写几行。

两张图片示例案例

此脚本包含两个类:ImageJPGImagePNG,均继承 来自Image 基类。要创建图像对象的实例,要求用户调用image_factory 函数,并以文件路径作为唯一参数。

此函数然后从路径中猜测文件格式(jpgpng)并 返回对应类的实例。

两个具体的图像类(ImageJPGImagePNG)都能够解码 通过他们的data 属性文件。两者都以不同的方式做到这一点。然而, 两者都向 Image 基类请求文件对象以执行此操作。

import os

#------------------------------------------------------------------------------#
def image_factory(path):
    '''Guesses the file format from the file extension
       and returns a corresponding image instance.'''
    format = os.path.splitext(path)[1][1:]
    if format == 'jpg': return ImageJPG(path)
    if format == 'png': return ImagePNG(path)
    else: raise Exception('The format "' + format + '" is not supported.')

#------------------------------------------------------------------------------#
class Image(object):
    '''Fake 1D image object consisting of twelve pixels.'''
    def __init__(self, path):
        self.path = path

    def get_pixel(self, x):
        assert x < 12
        return self.data[x]

    @property
    def file_obj(self): return open(self.path, 'r')

#------------------------------------------------------------------------------#
class ImageJPG(Image):
    '''Fake JPG image class that parses a file in a given way.'''

    @property
    def format(self): return 'Joint Photographic Experts Group'

    @property
    def data(self):
        with self.file_obj as f:
            f.seek(-50)
            return f.read(12)

#------------------------------------------------------------------------------#
class ImagePNG(Image):
    '''Fake PNG image class that parses a file in a different way.'''

    @property
    def format(self): return 'Portable Network Graphics'

    @property
    def data(self):
        with self.file_obj as f:
            f.seek(10)
            return f.read(12)

################################################################################
i = image_factory('images/lena.png')
print i.format
print i.get_pixel(5)

压缩图像示例案例

在第一个图像示例案例的基础上,人们想 添加以下功能:

应该支持一种额外的文件格式,gz 格式。代替 作为一种新的图像文件格式,它只是一个压缩层, 解压后,显示jpg 图像或png 图像。

image_factory 函数保持其工作机制,并将 只需尝试创建具体图像类的实例ImageZIP 当它被赋予gz 文件时。完全一样 在给定jpg 文件时创建ImageJPG 的实例。

ImageZIP 类只是想重新定义file_obj 属性。 在任何情况下,它都不想重新定义 data 属性。症结所在 问题在于,取决于隐藏的文件格式 在 zip 存档中,ImageZIP 类需要继承 动态地来自ImageJPGImagePNG。正确的类 继承自只能在类创建时确定path 参数被解析。

因此,这是带有额外 ImageZIP 类的相同脚本 并在image_factory 函数中添加了一行。

显然,ImageZIP 类在此示例中不起作用。 此代码需要 Python 2.7。

import os, gzip

#------------------------------------------------------------------------------#
def image_factory(path):
    '''Guesses the file format from the file extension
       and returns a corresponding image instance.'''
    format = os.path.splitext(path)[1][1:]
    if format == 'jpg': return ImageJPG(path)
    if format == 'png': return ImagePNG(path)
    if format == 'gz':  return ImageZIP(path)
    else: raise Exception('The format "' + format + '" is not supported.')

#------------------------------------------------------------------------------#
class Image(object):
    '''Fake 1D image object consisting of twelve pixels.'''
    def __init__(self, path):
        self.path = path

    def get_pixel(self, x):
        assert x < 12
        return self.data[x]

    @property
    def file_obj(self): return open(self.path, 'r')

#------------------------------------------------------------------------------#
class ImageJPG(Image):
    '''Fake JPG image class that parses a file in a given way.'''

    @property
    def format(self): return 'Joint Photographic Experts Group'

    @property
    def data(self):
        with self.file_obj as f:
            f.seek(-50)
            return f.read(12)

#------------------------------------------------------------------------------#
class ImagePNG(Image):
    '''Fake PNG image class that parses a file in a different way.'''

    @property
    def format(self): return 'Portable Network Graphics'

    @property
    def data(self):
        with self.file_obj as f:
            f.seek(10)
            return f.read(12)

#------------------------------------------------------------------------------#
class ImageZIP(### ImageJPG OR ImagePNG ? ###):
    '''Class representing a compressed file. Sometimes inherits from
       ImageJPG and at other times inherits from ImagePNG'''

    @property
    def format(self): return 'Compressed ' + super(ImageZIP, self).format

    @property
    def file_obj(self): return gzip.open(self.path, 'r')

################################################################################
i = image_factory('images/lena.png.gz')
print i.format
print i.get_pixel(5)

可能的解决方案

我找到了一种通过拦截ImageZIP 类中的__new__ 调用并使用type 函数来获得所需行为的方法。但感觉很笨拙,我怀疑可能有更好的方法使用一些我还不知道的 Python 技术或设计模式。

import re

class ImageZIP(object):
    '''Class representing a compressed file. Sometimes inherits from
       ImageJPG and at other times inherits from ImagePNG'''

    def __new__(cls, path):
        if cls is ImageZIP:
            format = re.findall('(...)\.gz', path)[-1]
            if format == 'jpg': return type("CompressedJPG", (ImageZIP,ImageJPG), )(path)
            if format == 'png': return type("CompressedPNG", (ImageZIP,ImagePNG), )(path)
        else:
            return object.__new__(cls)

    @property
    def format(self): return 'Compressed ' + super(ImageZIP, self).format

    @property
    def file_obj(self): return gzip.open(self.path, 'r')

结论

请记住,如果您想提出一个目标不是改变image_factory 函数的行为的解决方案。该功能应保持不变。理想情况下,目标是构建一个动态的ImageZIP 类。

我真的不知道最好的方法是什么。但这对我来说是一个了解更多 Python 的“黑魔法”的绝佳机会。也许我的答案在于创建后修改self.__cls__ 属性或使用__metaclass__ 类属性等策略?或者也许与特殊的 abc 抽象基类有关的东西可以在这里提供帮助?还是其他未开发的 Python 领域?

【问题讨论】:

我认为你是在强加一个人为的约束,它必须是一个继承自现有类型的类。我认为封装您的一种类型的工厂函数或类更 Pythonic。就此而言,我认为最好还是有一个通用的Image 类,其中包含用于从不同格式加载的函数或类方法。 @Thomas 所说的一切都是正确的。如果你需要这个,你的继承结构是错误的。使用“数据类型”参数调用Image 构造函数是显而易见的方法;还有其他人。另外,请记住,您可以按正确的顺序调用适当基类的 __new__ 方法,而不是 type()。 我也不明白这个问题,你可以用ImagePNG, ImageJPG, CompressedFile类很容易地做你的例子,并将它们与多重继承结合在一起,即class CompressedPNG(ImagePNG, CompressedFile)并编写一个简单的image_from_path函数。 另外,依靠文件扩展名来检测 mime 类型确实是一种不好的做法,更好的解决方案是使用文件的魔法字节(可以使用 magic 模块完成) @Glenn 抱歉,这很难解释,我更愿意深入了解所有细节,而不是冒被误解的风险。 【参考方案1】:

在这里,我更喜欢组合而不是继承。我认为您当前的继承层次结构似乎是错误的。有些事情,例如使用或 gzip 打开文件与实际图像格式几乎没有关系,并且可以在一个地方轻松处理,而您希望将使用特定格式的细节分开处理自己的类。我认为使用组合可以委托实现特定的细节并拥有一个简单的通用 Image 类,而无需元类或多重继承。

import gzip
import struct


class ImageFormat(object):
    def __init__(self, fileobj):
        self._fileobj = fileobj

    @property
    def name(self):
        raise NotImplementedError

    @property
    def magic_bytes(self):
        raise NotImplementedError

    @property
    def magic_bytes_format(self):
        raise NotImplementedError

    def check_format(self):
        peek = self._fileobj.read(len(self.magic_bytes_format))
        self._fileobj.seek(0)
        bytes = struct.unpack_from(self.magic_bytes_format, peek)
        if (bytes == self.magic_bytes):
            return True
        return False

    def get_pixel(self, n):
        # ...
        pass


class JpegFormat(ImageFormat):
    name = "JPEG"
    magic_bytes = (255, 216, 255, 224, 0, 16, 'J', 'F', 'I', 'F')
    magic_bytes_format = "BBBBBBcccc"


class PngFormat(ImageFormat):
    name = "PNG"
    magic_bytes = (137, 80, 78, 71, 13, 10, 26, 10)
    magic_bytes_format = "BBBBBBBB"


class Image(object):
    supported_formats = (JpegFormat, PngFormat)

    def __init__(self, path):
        self.path = path
        self._file = self._open()
        self._format = self._identify_format()

    @property
    def format(self):
        return self._format.name

    def get_pixel(self, n):
        return self._format.get_pixel(n)

    def _open(self):
        opener = open
        if self.path.endswith(".gz"):
            opener = gzip.open
        return opener(self.path, "rb")

    def _identify_format(self):
        for format in self.supported_formats:
            f = format(self._file)
            if f.check_format():
                return f
        else:
            raise ValueError("Unsupported file format!")

if __name__=="__main__":
    jpeg = Image("images/a.jpg")
    png = Image("images/b.png.gz")

我只在几个本地 png 和 jpeg 文件上对此进行了测试,但希望它能说明另一种思考这个问题的方式。

【讨论】:

+1!也许我只是不够聪明,无法处理“高级”继承方案,但在这种情况下,我总是发现组合更容易思考和扩展/调试。 好吧,继承方案可以说是一成不变的,所以我现在不能那样编写:添加压缩支持的想法是事后才想到的。另外,现在,对于您要添加到 Image 类的每个方法 f(x),您必须重定向 f(x):self._format.f(x) 函数调用开销确实有成本,但我取决于您调用底层格式的频率,有办法降低该成本。得知您的继承方案无法更改,我深感遗憾。【参考方案2】:

如何在函数级别定义ImageZIP 类? 这将启用您的dynamic inheritance

def image_factory(path):
    # ...

    if format == ".gz":
        image = unpack_gz(path)
        format = os.path.splitext(image)[1][1:]
        if format == "jpg":
            return MakeImageZip(ImageJPG, image)
        elif format == "png":
            return MakeImageZip(ImagePNG, image)
        else: raise Exception('The format "' + format + '" is not supported.')

def MakeImageZIP(base, path):
    '''`base` either ImageJPG or ImagePNG.'''

    class ImageZIP(base):

        # ...

    return  ImageZIP(path)

编辑:无需更改image_factory

def ImageZIP(path):

    path = unpack_gz(path)
    format = os.path.splitext(image)[1][1:]

    if format == "jpg": base = ImageJPG
    elif format == "png": base = ImagePNG
    else: raise_unsupported_format_error()

    class ImageZIP(base): # would it be better to use   ImageZip_.__name__ = "ImageZIP" ?
        # ...

    return ImageZIP(path)

【讨论】:

我没有想过在函数中定义一个类。好主意。但要让它在不改变 image_factory 函数的情况下工作,新函数必须被称为“ImageZIP”。 @xApple 我已经编辑了答案。这应该是您要搜索的内容。 是的,今天这对我有帮助。非常感谢。【参考方案3】:

如果您需要“黑魔法”,请首先尝试考虑不需要它的解决方案。您可能会找到效果更好的东西,并且需要更清晰的代码。

图像类构造函数采用已经打开的文件而不是路径可能会更好。 然后,您不仅限于磁盘上的文件,还可以使用来自 urllib、gzip 等类似文件的对象。

另外,由于您可以通过查看文件的内容来区分 JPG 和 PNG,而对于 gzip 文件,无论如何您都需要这种检测,我建议您根本不要查看文件扩展名。

class Image(object):
    def __init__(self, fileobj):
        self.fileobj = fileobj

def image_factory(path):
    return(image_from_file(open(path, 'rb')))

def image_from_file(fileobj):
    if looks_like_png(fileobj):
        return ImagePNG(fileobj)
    elif looks_like_jpg(fileobj):
        return ImageJPG(fileobj)
    elif looks_like_gzip(fileobj):
        return image_from_file(gzip.GzipFile(fileobj=fileobj))
    else:
        raise Exception('The format "' + format + '" is not supported.')

def looks_like_png(fileobj):
    fileobj.seek(0)
    return fileobj.read(4) == '\x89PNG' # or, better, use a library

# etc.

对于黑魔法,请访问What is a metaclass in Python?,但在使用之前请三思而后行,尤其是在工作中。

【讨论】:

再一次,我可以更改项目的整个继承架构。但在这一点上是困难的。格式和图像猜测实际上只是一个很好的例子,因为它需要一个“image_factory”函数。我实际上并没有处理图像。我只是在寻找一种方法来获得一些动态继承来解决我的问题,而无需过多地重构已经存在的东西。 嗯,您已经在问题的“可能的解决方案”中描述了这种解决方案。正如您所说,该解决方案很笨拙,并且有更好的方法:更好的方法是重构代码,使其更有意义。如果您专门寻找动态类,那么没有比 type() 调用更好的了(除了函数内的 class 定义,但是如果您想要一个有意义的类名,则必须在之后设置 __name__ ,所以它不是更好)。抱歉,您有一个可行的解决方案;我不能再帮你了。请注意,您不能对 ImageZIP 进行有意义的子类化。【参考方案4】:

在这种情况下,您应该使用组合,而不是继承。看看decorator design pattern。 ImageZIP 类应该用所需的功能装饰其他图像类。

使用装饰器,您可以根据您创建的组合获得非常动态的行为:

ImageZIP(ImageJPG(path))

它也更灵活,你可以有其他装饰器:

ImageDecrypt(password, ImageZIP(ImageJPG(path)))

每个装饰器只是封装它添加的功能,并根据需要委托给组合类。

【讨论】:

我看过装饰器设计模式。它们并不适合在这里应用,因为装饰器必须具有作为其属性之一的它所继承的类的实例。我的装饰器会从这里继承什么? 你的装饰器装饰图像,所以它会继承自 Image 基类。 但是,如果我的装饰器继承自 Image 而不是 ImageJPEG(或 ImagePNG),那么特定于格式的功能就会丢失,并且“i.data”成为未定义的属性? 装饰器组成它所装饰的特定类。您将创建它,例如,在您的工厂中像这样:ImageZIP(ImageJPG(path))。您还将定义一个 data 方法,该方法仅代表组合实例。 一个代码示例可能很有趣。但我的印象是,我们又回到了第一步,必须通过重构 image_facotry 函数来包含合成功能。

以上是关于Python动态继承:如何在创建实例时选择基类?的主要内容,如果未能解决你的问题,请参考以下文章

python:元类与抽象基类

Python中静态方法和类方法的区别

从抽象基类构造函数创建继承类的实例

Python 多重继承:选择要调用的基类方法

Python 类:继承与实例化

从设计基类及其派生类看继承关系