Python的dynamicinheritance:如何在创build实例时select基类?

介绍

我在编程工作中遇到了一个有趣的例子,需要我在python中实现dynamic类inheritance机制。 当我使用“dynamicinheritance”这个术语时,我的意思是不能从任何基类inheritance的类,而是在实例化时select从几个基类之一inheritance,这取决于某些参数。

因此,我的问题是:在我将要介绍的情况下,通过dynamicinheritance实现所需的额外function的最佳,最标准和“pythonic”方式是什么?

以简单的方式总结这个例子,我将举一个例子,用两个代表两种不同图像格式的类: 'jpg''png'图像。 然后我会尝试添加支持第三种格式的能力: 'gz'图像。 我意识到我的问题并不那么简单,但是我希望你已经准备好和我一起做更多的事情了。

这两个图像的例子

该脚本包含两个类: ImageJPGImagePNG ,都从Image基类inheritance。 为了创build一个图像对象的实例,用户被要求用文件path作为唯一的参数调用image_factory函数。

这个函数然后猜测path中的文件格式( jpg或者png )并且返回相应类的一个实例。

这两个具体的图像类( ImageJPGImagePNG )都能够通过其data属性解码文件。 两者都以不同的方式做到这一点。 但是,都要求Image基类为一个文件对象才能做到这一点。

UML图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) 

压缩图像的例子

基于第一个图像示例案例,人们希望添加以下function:

应该支持额外的文件格式, gz格式。 而不是一个新的图像文件格式,它只是一个压缩层,一旦解压缩,显示一个jpg图像或png图像。

image_factory函数保持它的工作机制,只是在给定一个gz文件的时候,试着创build一个具体图像类ImageZIP的实例。 给定一个jpg文件时,完全一样,它会创build一个ImageJPG的实例。

ImageZIP类只是想重新定义file_obj属性。 它绝不想重新定义data属性。 问题的关键在于,根据隐藏在zip压缩文件中的文件格式, ImageZIP类需要从ImageJPG或从ImagePNGdynamic地inheritance。 只有在path参数被parsing时才能在类创build时确定正确的inheritance类。

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

显然,在这个例子中ImageZIP类是不起作用的。 这段代码需要Python 2.7。

UML图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) 

可能的解决scheme

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

 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') 

结论

请记住,如果你想提出一个解决scheme,目标不是改变image_factory函数的行为。 这个function应该保持不变。 理想的目标是构build一个dynamic的ImageZIP类。

我只是不知道最好的办法是什么。 但是对于我来说,这是学习Python的一些“黑魔法”的最佳时机。 也许我的答案在于创build之后修改self.__cls__属性的策略,或者可能使用__metaclass__类属性? 或者,也许有关特殊的abc抽象基类可以帮助这里? 还是其他未开发的Python领土?

那么在函数级定义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) 

我会赞成这里的inheritance组合。 我认为你目前的inheritance层次似乎是错误的。 有些东西,比如使用gzip打开文件,与实际的图像格式没什么关系,可以在一个地方很容易地处理,而你想分离使用特定格式自己的类的细节。 我认为使用组合可以委托实现特定的细节,并有一个简单的常见的图像类,而不需要元类或多重inheritance。

 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") 

我只testing了几个本地的PNG和JPEG文件,但希望它说明了另一种思考这个问题的方式。

如果你需要“黑魔法”,首先要考虑一个不需要它的解决scheme。 你可能会发现更好的工作,并导致需要更清晰的代码。

图像类构造函数可能会更好地采取一个已经打开的文件,而不是一个path。 然后,你不限制在磁盘上的文件,但你可以使用urllib,gzip等类似文件的对象。

而且,既然你可以通过查看文件的内容来从PNG中看出JPG,而对于gzip文件,你还是需要这个检测,所以我build议你根本不要看扩展名。

 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. 

对于黑魔法,请参阅什么是Python中的元类? ,但在使用之前要三思,特别是在工作中。

你应该在这种情况下使用组合,而不是inheritance。 看看装饰devise模式 。 ImageZIP类应该用所需的function修饰其他图像类。

随着装饰,你会得到一个非常dynamic的行为取决于你创build的组成:

 ImageZIP(ImageJPG(path)) 

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

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

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