Введение
Я столкнулся с интересным случаем в моей работе по программированию, который требует от меня реализации механизма динамического наследования классов в Python.Под термином «динамическое наследование» я подразумеваю класс, который не наследует от какого-либо конкретного базового класса, а предпочитает наследовать от одного из нескольких базовых классов при создании экземпляра, в зависимости от какого-либо параметра.
Таким образом, мой вопрос заключается в следующем: в случае, который я представлю, каков будет лучший, самый стандартный и «питонический» способ реализации необходимой дополнительной функциональности с помощью динамического наследования.
Подведем итогЯ просто приведу пример, используя два класса, которые представляют два разных формата изображения: 'jpg'
и 'png'
изображения.Затем я попытаюсь добавить возможность поддерживать третий формат: изображение 'gz'
.Я понимаю, что мой вопрос не так прост, но я надеюсь, что вы готовы выдержать еще несколько строк.
Пример с двумя изображениями case
Этот сценарий содержит два класса: 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
.Точно так же он будет создавать экземпляр ImageJPG
, если ему будет предоставлен файл jpg
.
Класс ImageZIP
просто хочет переопределить свойство file_obj
.Ни в коем случае он не хочет переопределять свойство data
.Суть проблемы в том, что, в зависимости от того, какой формат файла скрыт внутри zip-архива, классы ImageZIP
должны наследоваться либо от ImageJPG
, либо от ImagePNG
динамически.Правильный класс для наследования может быть определен только при создании класса при разборе параметра path
.
Следовательно, здесь тот же сценарий с дополнительным классом ImageZIP
и единственной добавленной строкой к image_factory
function.
Очевидно, класс 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)
Возможное решение
Я нашел способ получитьжелаемое поведение, перехватывая вызов __new__
в классе ImageZIP
и используя функцию type
.Но это кажется неуклюжим, и я подозреваю, что мог бы быть лучший способ использовать некоторые методы Python или шаблоны проектирования, о которых я еще не знаю.
Помните, что если вы хотите предложить решение, цель которого не состоит в том, чтобы изменить поведение функции image_factory
.Эта функция должна оставаться нетронутой.В идеале цель состоит в том, чтобы создать динамический класс ImageZIP
.
Я просто не знаю, как лучше всего это сделать. Но для меня это прекрасный повод узнать больше о «черной магии» Python. Может быть, мой ответ заключается в таких стратегиях, как изменение атрибута self.__cls__
после создания или использование атрибута класса __metaclass__
? Или, может быть, здесь поможет что-то, связанное со специальными abc
абстрактными базовыми классами? Или другая неисследованная территория Python?