Введение
Я столкнулся с интересным случаем в своем задании программирования, которое требует, чтобы я реализовал механизм динамического наследования классов в 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?