Подтвердить что ты не робот

Динамическое наследование Python: как выбрать базовый класс при создании экземпляра?

Введение

Я столкнулся с интересным случаем в своем задании программирования, которое требует, чтобы я реализовал механизм динамического наследования классов в python. То, что я имею в виду при использовании термина "динамическое наследование", - это класс, который не наследует от какого-либо базового класса в частности, а скорее предпочитает наследовать от одного из нескольких базовых классов при создании экземпляра в зависимости от некоторого параметра.

Мой вопрос, таким образом, следующий: в случае, который я представлю, будет лучшим, самым стандартным и "питоническим" способом реализации необходимой дополнительной функциональности посредством динамического наследования.

Чтобы резюмировать случай в точке простым способом, я приведу пример, используя два класса, которые представляют два разных формата изображения: 'jpg' и 'png' изображения. Затем я попытаюсь добавить возможность поддержки третьего формата: образ 'gz'. Я понимаю, что мой вопрос не так прост, но я надеюсь, что вы готовы поговорить со мной о еще нескольких строках.


Пример примера двух изображений

Этот script содержит два класса: ImageJPG и ImagePNG, оба наследующие из базового класса Image. Чтобы создать экземпляр объекта изображения, пользователю предлагается вызвать функцию image_factory с пути к файлу в качестве единственного параметра.

Эта функция затем угадывает формат файла (jpg или png) из пути и возвращает экземпляр соответствующего класса.

Оба конкретных класса изображения (ImageJPG и ImagePNG) способны декодировать файлов через их свойство 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 динамически. Правильный класс для inherit from может определяться только при создании класса, когда path параметр анализируется.

Следовательно, здесь тот же script с дополнительным классом 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?

4b9b3361

Ответ 1

Как определить класс 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

Я бы предпочел композицию над наследованием здесь. Я думаю, что ваша нынешняя иерархия наследования кажется неправильной. Некоторые вещи, такие как открытие файла с помощью или 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, но, надеюсь, это иллюстрирует другой способ мышления об этой проблеме.

Ответ 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.

Для черной магии перейдите в Что такое метакласс в Python?, но подумайте дважды, прежде чем использовать это, особенно на работе.

Ответ 4

Вы должны использовать композицию в этом случае, а не наследование. Посмотрите на шаблон дизайна декоратора. Класс ImageZIP должен украсить другие классы изображений желаемой функциональностью.

С декораторами вы получаете очень динамичное поведение в зависимости от создаваемой композиции:

ImageZIP(ImageJPG(path))

Более гибкий, вы можете иметь другие декораторы:

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

Каждый декоратор просто инкапсулирует функциональность, которую он добавляет, и делегирует в составной класс по мере необходимости.