【发布时间】: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 抱歉,这很难解释,我更愿意深入了解所有细节,而不是冒被误解的风险。
标签: python oop design-patterns inheritance