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

46 投票
4 回答
21257 浏览
提问于 2025-04-16 23:34

介绍

在我的编程工作中,我遇到了一个有趣的案例,需要我在Python中实现一种动态类继承的机制。这里的“动态继承”指的是一个类并不是固定地从某个基类继承,而是在创建实例时,根据某些参数选择从多个基类中的一个继承。

我的问题是:在我接下来要介绍的案例中,如何以最佳、最标准和“Pythonic”的方式通过动态继承实现所需的额外功能。

为了简单明了地总结这个案例,我将用两个类来举例,这两个类分别代表两种不同的图像格式:'jpg''png'。然后,我会尝试添加对第三种格式的支持:'gz'图像。我知道我的问题并不简单,但希望你能耐心看完接下来的内容。

两个图像的示例案例

这个脚本包含两个类:ImageJPGImagePNG,它们都继承自Image基类。用户需要调用image_factory函数,并传入一个文件路径作为唯一参数,来创建一个图像对象的实例。

这个函数会根据路径猜测文件格式(jpgpng),并返回相应类的实例。

这两个具体的图像类(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)


压缩图像的示例案例

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

支持一种额外的文件格式,gz格式。它并不是一种新的图像文件格式,而只是一个压缩层,解压后会显示出一个jpg图像或png图像。

image_factory函数的工作机制保持不变,当传入gz文件时,它会尝试创建ImageZIP类的实例,就像传入jpg文件时创建ImageJPG实例一样。

ImageZIP类只想重新定义file_obj属性,而不想重新定义data属性。问题的关键在于,根据压缩包内隐藏的文件格式,ImageZIP类需要动态地从ImageJPGImagePNG继承。正确的继承类只能在创建类时解析path参数后确定。

因此,这里是同一个脚本,增加了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)


可能的解决方案

我找到了一种方法,通过拦截__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 个回答

5

如果你需要一些“黑魔法”的技巧,首先要想想有没有其他解决办法,不要急着用那些复杂的方法。你可能会发现更简单有效的方案,而且代码也会更清晰。

对于图像类的构造函数,直接传入一个已经打开的文件可能会更好,而不是只给一个文件路径。这样一来,你就不局限于硬盘上的文件,还可以使用像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.

如果你真的需要“黑魔法”,可以去看看这个链接:Python中的元类是什么?,不过在使用之前,特别是在工作中,最好再考虑一下。

20

在这里,我更倾向于使用组合而不是继承。我觉得你现在的继承结构看起来不太对。有些事情,比如打开文件或者使用gzip,这些和实际的图像格式关系不大,可以在一个地方轻松处理。而你应该把处理特定格式的细节分开到自己的类里。我认为通过组合,你可以把实现的具体细节委托出去,这样就能有一个简单的公共图像类,而不需要使用元类或者多重继承。

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文件上测试过这个,但希望它能给你提供一种不同的思考方式来解决这个问题。

15

在函数级别定义 ImageZIP 类怎么样呢?
这样做可以让你实现 动态继承

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)

撰写回答