Динамическое наследование Python: как выбрать базовый класс при создании экземпляра? - PullRequest
34 голосов
/ 14 августа 2011

Введение

Я столкнулся с интересным случаем в моей работе по программированию, который требует от меня реализации механизма динамического наследования классов в Python.Под термином «динамическое наследование» я подразумеваю класс, который не наследует от какого-либо конкретного базового класса, а предпочитает наследовать от одного из нескольких базовых классов при создании экземпляра, в зависимости от какого-либо параметра.

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

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

Пример с двумя изображениями case

Этот сценарий содержит два класса: 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 динамически.Правильный класс для наследования может быть определен только при создании класса при разборе параметра path.

Следовательно, здесь тот же сценарий с дополнительным классом ImageZIP и единственной добавленной строкой к image_factory function.

Очевидно, класс 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 или шаблоны проектирования, о которых я еще не знаю.

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

Я просто не знаю, как лучше всего это сделать. Но для меня это прекрасный повод узнать больше о «черной магии» Python. Может быть, мой ответ заключается в таких стратегиях, как изменение атрибута self.__cls__ после создания или использование атрибута класса __metaclass__? Или, может быть, здесь поможет что-то, связанное со специальными abc абстрактными базовыми классами? Или другая неисследованная территория Python?

Ответы [ 4 ]

18 голосов
/ 14 августа 2011

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

10 голосов
/ 14 августа 2011

Как насчет определения класса 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)
4 голосов
/ 14 августа 2011

Если вам когда-нибудь понадобится «черная магия», сначала попробуйте придумать решение, в котором оно не требуется.Скорее всего, вы найдете что-то, что работает лучше и требует более ясного кода.

Для конструкторов классов изображений может быть лучше взять уже открытый файл вместо пути.Тогда вы не ограничены файлами на диске, но вы можете использовать файловые объекты из 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?, но подумайте дважды, прежде чем использовать это, особенно на работе.

2 голосов
/ 14 августа 2011

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

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

ImageZIP(ImageJPG(path))

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...