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'
图像。我知道我的问题并不是那么简单,但我希望你准备好忍受我再多写几行。
两张图片示例案例
此脚本包含两个类:ImageJPG
和 ImagePNG
,均继承
来自Image
基类。要创建图像对象的实例,要求用户调用image_factory
函数,并以文件路径作为唯一参数。
此函数然后从路径中猜测文件格式(jpg
或 png
)并
返回对应类的实例。
两个具体的图像类(ImageJPG
和ImagePNG
)都能够解码
通过他们的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
类需要继承
动态地来自ImageJPG
或ImagePNG
。正确的类
继承自只能在类创建时确定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动态继承:如何在创建实例时选择基类?的主要内容,如果未能解决你的问题,请参考以下文章