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

时间:2011-08-14 13:19:07

标签: python oop design-patterns inheritance

简介

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

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

为了以简单的方式总结这个案例,我将举例说明使用两个代表两种不同图像格式的类:'jpg''png'图像。然后,我将尝试添加支持第三种格式的功能:'gz'图像。我意识到我的问题并不那么简单,但我希望你已经准备好与我一起承担更多的问题了。

两个图像示例案例

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

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

两个具体的图像类(ImageJPGImagePNG)都能够解码 文件通过data属性。两者都以不同的方式做到这一点。然而, 为了做到这一点,我们都要求Image基类提供文件对象。

UML diagram 1

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文件时。完全按照它的方式 在给出ImageJPG文件时创建jpg的实例。

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

因此,这是与额外ImageZIP类相同的脚本 以及image_factory函数的一个添加行。

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

UML diagram 2

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)


可能的解决方案

我找到了一种通过拦截__new__类中的ImageZIP调用并使用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领域?

4 个答案:

答案 0 :(得分:18)

我赞成在这里继承作文。我认为你当前的继承层次似乎是错误的。有些东西,比如用gzip或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 :(得分:10)

如何在功能级别定义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)

答案 2 :(得分:4)

如果你需要“黑魔法”,首先要考虑一个不需要它的解决方案。你可能会发现一些效果更好的东西,并且需要更清晰的代码。

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

另外,既然您可以通过查看文件的内容来告诉PNG的JPG,而对于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?,但在使用之前请三思而后行,尤其是在工作中。

答案 3 :(得分:2)

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

使用装饰器,您将获得非常动态的行为,具体取决于您创建的构图:

ImageZIP(ImageJPG(path))

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

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

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