programacion - Herencia dinámica de Python: ¿cómo elegir la clase base en la creación de la instancia?
polimorfismo en python (4)
¿Qué hay de definir la clase ImageZIP
nivel de función?
Esto habilitará su 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)
Editar : sin necesidad de cambiar 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)
Introducción
He encontrado un caso interesante en mi trabajo de programación que me obliga a implementar un mecanismo de herencia dinámica de clases en Python. Lo que quiero decir cuando uso el término "herencia dinámica" es una clase que no hereda de ninguna clase base en particular, sino que elige heredar de una de varias clases base en la instanciación, dependiendo de algún parámetro.
Mi pregunta es la siguiente: en el caso que presentaré, cuál sería la mejor manera, la más estándar y la "pitónica" de implementar la funcionalidad extra necesaria a través de la herencia dinámica.
Para resumir el caso de manera simple, daré un ejemplo utilizando dos clases que representan dos formatos de imagen diferentes: imágenes ''jpg''
y ''png''
. Luego intentaré agregar la capacidad de admitir un tercer formato: la imagen ''gz''
. Me doy cuenta de que mi pregunta no es tan simple, pero espero que estés listo para soportar algunas líneas más.
El ejemplo de dos imágenes
Este script contiene dos clases: ImageJPG
e ImagePNG
, ambas ImagePNG
de la clase base de la Image
. Para crear una instancia de un objeto de imagen, se le pide al usuario que llame a la función image_factory
con una ruta de archivo como único parámetro.
Esta función luego adivina el formato de archivo ( jpg
o png
) de la ruta y devuelve una instancia de la clase correspondiente.
Ambas clases de imágenes concretas ( ImageJPG
e ImagePNG
) pueden decodificar archivos a través de su propiedad de data
. Ambos hacen esto de una manera diferente. Sin embargo, ambos piden a la clase base de la Image
un objeto de archivo para poder hacer esto.
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)
El caso de ejemplo de imagen comprimida
Partiendo del primer caso de ejemplo de imagen, a uno le gustaría agregar la siguiente funcionalidad:
Se debe gz
formato de archivo extra, el formato gz
. En lugar de ser un nuevo formato de archivo de imagen, es simplemente una capa de compresión que, una vez descomprimida, revela una imagen jpg
o una imagen png
.
La función image_factory
mantiene su mecanismo de trabajo y simplemente intentará crear una instancia de la clase de imagen concreta ImageZIP
cuando se le dé un archivo gz
. Exactamente de la misma manera crearía una instancia de ImageJPG
cuando se le dé un archivo jpg
.
La clase ImageZIP
solo quiere redefinir la propiedad file_obj
. En ningún caso, quiere redefinir la propiedad de los data
. La clave del problema es que, dependiendo de qué formato de archivo se esconde dentro del archivo comprimido, las clases de ImageZIP
deben heredarse de ImageJPG
o de ImagePNG
dinámicamente. La clase correcta para heredar solo se puede determinar en la creación de clase cuando se analiza el parámetro de path
.
Por lo tanto, aquí está el mismo script con la clase extra ImageZIP
y una sola línea agregada a la función image_factory
.
Obviamente, la clase ImageZIP
no es funcional en este ejemplo. Este código requiere 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)
Una posible solución
He encontrado una forma de obtener el comportamiento deseado mediante la interceptación de la llamada __new__
en la clase ImageZIP
y el uso de la función de type
. Pero se siente torpe y sospecho que podría haber una forma mejor de usar algunas técnicas de Python o patrones de diseño que aún no conozco.
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'')
Conclusión
Tenga en cuenta que si desea proponer una solución, el objetivo no es cambiar el comportamiento de la función image_factory
. Esa función debe permanecer intacta. El objetivo, idealmente, es crear una clase dinámica ImageZIP
.
Simplemente no sé cuál es la mejor manera de hacerlo. Pero esta es una ocasión perfecta para que aprenda más sobre la "magia negra" de Python. Tal vez mi respuesta self.__cls__
estrategias como modificar el atributo self.__cls__
después de la creación o tal vez usar el __metaclass__
clase __metaclass__
? ¿O tal vez algo que ver con las clases básicas abstractas abc
especiales podría ayudar aquí? U otro territorio de Python inexplorado?
Debe usar la composición en este caso, no la herencia. Eche un vistazo al patrón de diseño del decorador . La clase ImageZIP
debería decorar otras clases de imágenes con la funcionalidad deseada.
Con los decoradores, obtienes un comportamiento muy dinámico según la composición que crees:
ImageZIP(ImageJPG(path))
También es más flexible, puedes tener otros decoradores:
ImageDecrypt(password, ImageZIP(ImageJPG(path)))
Cada decorador simplemente encapsula la funcionalidad que agrega y delega a la clase compuesta según sea necesario.
Preferiría la composición sobre la herencia aquí. Creo que su jerarquía de herencia actual parece incorrecta. Algunas cosas, como abrir el archivo con o gzip, tienen poco que ver con el formato de imagen real y pueden manejarse fácilmente en un solo lugar, mientras que usted desea separar los detalles del trabajo con un formato específico de clases propias. Creo que al usar la composición, puede delegar detalles específicos de la implementación y tener una clase simple de imagen común sin requerir metaclases o herencia múltiple.
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")
Solo probé esto en algunos archivos locales png y jpeg, pero espero que ilustre otra forma de pensar sobre este problema.
Si alguna vez necesitas "magia negra", primero intenta pensar en una solución que no lo requiera. Es probable que encuentre algo que funcione mejor y dé como resultado un código más claro.
Puede ser mejor para los constructores de la clase de imagen tomar un archivo ya abierto en lugar de una ruta. Entonces, no está limitado a los archivos en el disco, pero puede usar objetos similares a archivos de urllib, gzip y similares.
Además, como puedes distinguir JPG de PNG mirando el contenido del archivo, y para el archivo gzip necesitas esta detección de todos modos, recomiendo no mirar la extensión del archivo.
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.
Para la magia negra, ve a ¿Qué es una metaclase en Python? , pero piénselo dos veces antes de usarlo, especialmente en el trabajo.