resueltos programacion polimorfismo otra orientada objetos metodos metodo llamar lista importar herencia ejercicios clases clase python oop design-patterns inheritance

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.