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

Введение

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

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

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

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

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

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

Оба конкретных класса изображения ( ImageJPG и ImagePNG ) способны декодировать файлы через их свойство data . Оба делают это по-другому. Однако для этого Image базовый класс 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 сохраняет свой рабочий механизм и просто попытается создать экземпляр конкретного класса изображений ImageZIP когда ему задан gz файл. Точно так же он создавал бы экземпляр ImageJPG при заданном jpg файле.

Класс ImageZIP просто хочет переопределить свойство file_obj . Он ни в коем случае не хочет переопределять свойство data . Суть проблемы в том, что в зависимости от того, какой формат файла скрывается внутри zip-архива, классы ImageZIP должны наследовать либо из ImageJPG либо из ImagePNG динамически. Правильный класс для наследования может быть определен только при создании класса при path параметра path .

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

Очевидно, что в этом ImageZIP класс 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) 

Возможное решение

Я нашел способ получить желаемое поведение, перехватив вызов ImageZIP классе 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__ класса __metaclass__ ? Или может быть, что-то связанное со специальными абстрактными базовыми классами abc могло бы помочь здесь? Или другая неисследованная территория Питона?

  • Python dictionary: TypeError: unhashable type: 'list'
  • Представление не возвратило объект HttpResponse. Вместо этого он вернул None
  • Импорт и использование стандартного модуля Python из внутреннего расширения Python C
  • ValueError: не удалось преобразовать строку в float: id
  • Python - анализировать IPv4-адреса из строки (даже при цензуре)
  • как добавить дюжину тестовых примеров в тестовый набор автоматически в python
  • Как закрепить два списка списков в Python?
  • PyAudio IOError: нет устройства ввода по умолчанию
  • 4 Solutions collect form web for “Динамическое наследование Python: как выбрать базовый класс при создании экземпляра?”

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

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

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

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

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

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

     ImageZIP(ImageJPG(path)) 

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

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

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

    Interesting Posts

    Matplotlib, так что ось журнала имеет только незначительные метки меток в указанных точках. Также измените размер ярлыков меток в colorbar

    Python Pandas DataFrame считывает точный заданный диапазон в листе excel

    DRF: Простое назначение внешнего ключа с вложенными сериализаторами?

    Python и объект / класс attrs – что происходит?

    Почему я должен использовать метод __prepare__ для получения пространства имен класса?

    Извлечение XML в фрейм данных с родительским атрибутом в качестве заголовка столбца

    Tkinter Изменить размер текста

    Регулярное выражение для возврата текста между скобками

    Python: Как получить ссылку на модуль внутри самого модуля?

    Отображение номеров строк в IPython / Jupyter Notebooks

    Является ли '#! / Usr / bin / python' перед каждым скриптом Python обязательным?

    Как распечатать пары ключ-значение словаря в python

    Индикатор выполнения в Google App Engine

    Значение курсора Matplotlib с двумя осями

    Python argparse: как вставить новую строку в текст справки?

    Python - лучший язык программирования в мире.