Введение
Я столкнулся с интересным случаем в своем задании программирования, которое требует, чтобы я реализовал механизм динамического наследования классов в python. То, что я имею в виду при использовании термина "динамическое наследование", - это класс, который не наследует от какого-либо базового класса в частности, а скорее предпочитает наследовать от одного из нескольких базовых классов при создании экземпляра в зависимости от некоторого параметра.
Мой вопрос, таким образом, следующий: в случае, который я представлю, будет лучшим, самым стандартным и "питоническим" способом реализации необходимой дополнительной функциональности посредством динамического наследования.
Чтобы резюмировать случай в точке простым способом, я приведу пример, используя два класса, которые представляют два разных формата изображения: 'jpg'
и 'png'
изображения. Затем я попытаюсь добавить возможность поддержки третьего формата: образ 'gz'
. Я понимаю, что мой вопрос не так прост, но я надеюсь, что вы готовы поговорить со мной о еще нескольких строках.
Пример примера двух изображений
Этот script содержит два класса: 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
динамически. Правильный класс для
inherit from может определяться только при создании класса, когда path
параметр анализируется.
Следовательно, здесь тот же script с дополнительным классом ImageZIP
и одна добавленная линия к функции image_factory
.
Очевидно, что класс 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 или шаблоны проектирования, о которых я еще не знаю.
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?