Python dinámica de la herencia: ¿Cómo elegir la base de la clase sobre la creación de la instancia?

Introducción

Me he encontrado con un caso interesante, en mi programación de trabajo que me obliga a implementar un mecanismo de la dinámica de la herencia de clases en python. A lo que me refiero cuando uso el término «dinámica de la herencia» es una clase que no hereda de ninguna clase base en particular, sino que elige a heredar de una de varias clases base en la creación de instancias, dependiendo de algunos parámetros.

Mi pregunta es, pues, el siguiente: en el caso que se presente, ¿cuál sería la mejor, la más estándar y «python» camino de la implementación de la necesaria funcionalidad extra a través de dinámicas de herencia.

Para resumir el caso en el punto de una manera simple, voy a dar un ejemplo con dos clases que representan dos diferentes formatos de imagen: 'jpg' y 'png' imágenes. A continuación, voy a tratar de agregar la capacidad de soporte de un tercer formato: la 'gz' de la imagen. Me doy cuenta de que mi pregunta no es simple, pero espero que usted está listo para llevar conmigo un par de líneas más.

Las dos imágenes de ejemplo el caso

Este script contiene dos clases: ImageJPG y ImagePNG, tanto heredar
desde el Image de la clase base. Para crear una instancia de un objeto de imagen, el usuario se le pide que llame a la image_factory función con una ruta de archivo como el único parámetro.

Esta función se adivina el formato de archivo (jpg o png) de la ruta de acceso y
devuelve una instancia de la clase correspondiente.

Concretos de la imagen de clases (ImageJPGy ImagePNG) son capaces de decodificar
de archivos a través de su data de la propiedad. Tanto hacer esto de una manera diferente. Sin embargo,
tanto pedir la Image clase base para un objeto de archivo con el fin de hacer esto.

Python dinámica de la herencia: ¿Cómo elegir la base de la clase sobre la creación de la instancia?

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)

La imagen comprimida caso de ejemplo

Edificio en la primera imagen de caso de ejemplo, a uno le gustaría
agregar la funcionalidad siguiente:

Un extra de formato de archivo debe ser compatible, la gz formato. En lugar de
un nuevo formato de archivo de imagen, es simplemente una capa de compresión que,
una vez descomprimido, revela un jpg imagen o un png imagen.

La image_factory función mantiene su mecanismo de trabajo y
simplemente tratamos de crear una instancia de la imagen concreta de la clase ImageZIP
cuando se da un gz archivo. Exactamente en la misma manera que lo haría
crear una instancia de ImageJPG cuando se le da un jpg archivo.

La ImageZIP clase sólo quiere redefinir el file_obj de la propiedad.
En ningún caso se desea redefinir el data de la propiedad. El quid
el problema es que, dependiendo de qué formato de archivo es ocultar
dentro del archivo zip, el ImageZIP clases de necesidades para heredar
desde ImageJPG o de ImagePNG de forma dinámica. La clase correcta para
heredar de sólo puede ser determinada al momento de la clase en la creación de la path
parámetro analizado.

Por lo tanto, aquí es el mismo guión con el extra ImageZIP clase
y una sola línea agregada a la image_factory función.

Obviamente, la ImageZIP clase no es funcional en este ejemplo.
Este código requiere Python 2.7.

Python dinámica de la herencia: ¿Cómo elegir la base de la clase sobre la creación de la instancia?

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 manera de conseguir el comportamiento deseado mediante la interceptación de la __new__ llamada en la ImageZIP de clase y el uso de la type función. Pero se siente torpe y sospecho que podría ser una mejor manera el uso de algunos de Python o las técnicas de diseño de patrones que todavía no conoce.

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

Tener en cuenta si usted quiere proponer una solución que el objetivo no es cambiar el comportamiento de la image_factory función. Que función debe permanecer intacta. El objetivo, lo ideal es construir una dinámica ImageZIP clase.

Yo no sé realmente cuál es la mejor manera de hacer esto es. Pero esta es una ocasión perfecta para mí para aprender más acerca de algunos de Python «magia negra». Tal vez mi respuesta se encuentra con estrategias como la modificación de la self.__cls__ atributo después de la creación o tal vez el uso de la __metaclass__ atributo de clase? O tal vez algo que ver con el especial abc clases base abstractas podría ayudar aquí? O otros inexplorado Python territorio?

  • Creo que eres la imposición de una restricción artificial que debe ser de una clase hereda de sus tipos existentes. Creo que una fábrica de función o una clase que encapsula uno de sus tipos es más Python. Para el caso, creo que sería mejor tener un genérico Image clase, con las funciones o classmethods para la carga de diferentes formatos.
  • Todo @Thomas dice que es correcto. Si usted necesita, usted ha estructurado su herencia mal. Llame a la Image constructor con un «tipo de datos» argumento es la manera obvia; hay otros. Además, tenga en cuenta, en lugar del tipo() se puede llamar la __new__ métodos de la base correspondiente a las clases en el orden correcto.
  • Yo no se el problema, usted puede hacer su ejemplo muy fácilmente con la ImagePNG, ImageJPG, CompressedFile clases y les pegan juntos con la herencia múltiple es decir class CompressedPNG(ImagePNG, CompressedFile) y escribir una simple image_from_path función.
  • Si usted desea ayudar, por favor no le pida a sus preguntas como a las cuatro de la página largo de los ensayos.
  • también, basándose en la extensión del archivo para detectar el tipo mime es realmente una mala práctica, una mejor solución sería utilizar el archivo de la magia de bytes (se puede hacer con el magic módulo)
  • Son en realidad el trabajo con imágenes o se trata simplemente de un ejemplo? Hay mejores maneras para confirmar que un archivo de imagen es de un tipo específico y las bibliotecas como PIL (pythonware.com/products/pil) vale la pena investigar en lugar de rodar su propia solución.
  • El tipo de archivo de la materia que está pensado como un ejemplo. Sin embargo, yo no usaría la magia de la biblioteca como las costuras de la PyPI versión costuras a ser mantenido. Una nueva versión que existe en algún otro lugar, pero no se agrega PyPI…
  • Tan sólo es concebido como un ejemplo para intentar crear una situación similar a la que estoy experimentando. No estoy trabajando con imágenes.
  • Lo siento, pareció difícil de explicar y he preferido entrar en todos los detalles que a riesgo de ser malinterpretado.
  • Que significa escribir una clase para (cada tipo de archivo) X (cada formato de compresión). La idea es ser capaz de añadir nuevos formatos y formatos de compresión sin demasiado caldera placa de código.
  • Por cierto, ¿cómo se crean los diagramas de clase ? Me haría falta un programa como este. Gracias !
  • He utilizado este sitio web: yuml.me

InformationsquelleAutor xApple | 2011-08-14

4 Kommentare

  1. 12

    ¿Qué acerca de la definición de la ImageZIP de la clase en función de nivel ?

    Esto le permitirá a 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)
    • Yo no había pensado acerca de la definición de una clase dentro de una función. Muy buena idea. Pero para que funcione sin necesidad de cambiar el image_factory función, la nueva función tendría que ser llamado «ImageZIP».
    • He editet la respuesta. Esto debe ser lo que usted está buscando.
  2. 18

    Yo estaría a favor de composición a lo largo de la herencia de aquí. Creo que su actual jerarquía de herencia parece mal. Algunas cosas, como la de abrir el archivo con o gzip poco tienen que ver con el actual formato de la imagen y puede ser fácilmente manejado en un lugar mientras que usted desea para separar los detalles del trabajo con un formato específico propio de las clases. Creo que el uso de la composición puede delegar la ejecución de detalles y con una simple Imagen común de la clase sin necesidad de metaclasses o la 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")

    Yo sólo he probado esto en un par de locales png y jpeg, pero espero que ilustra otra forma de pensar acerca de este problema.

    • +1! Tal vez no soy lo suficientemente inteligente como para manejar la «avanzada» de la herencia de los esquemas, pero en casos como este siempre me encuentro composición más fácil pensar y ampliar/debug.
    • Además de la herencia del régimen se establece en la piedra, por así decirlo, así que no puedo componer igual que ahora: la idea de añadir soporte de compresión se presenta como una idea de último momento. Además, ahora, para cada método de f(x) de la que desea agregar a su clase de Imagen que usted tiene que redirigir f(x): self._format.f(x)
    • La función de la sobrecarga de la llamada tiene un coste, pero me dependiendo de la frecuencia de hacer llamadas a la base del formato hay maneras de mitigar el costo. Lamento lo que te herencia esquema no puede ser cambiado.
  3. 4

    Si alguna vez la necesidad de «magia negra», primero trate de pensar en una solución que no la requieren. Usted es probable encontrar algo que funcione mejor y los resultados en las necesidades de un código más claro.

    Puede ser mejor para la imagen de constructores de clase para tomar una ya abierto el archivo de en lugar de una ruta de acceso.
    Entonces, usted no está limitado a los archivos en el disco, pero se puede usar en archivos como objetos de urllib, gzip, y similares.

    También, ya que se puede decir JPG de PNG mirando el contenido del archivo, y para gzip archivo que necesita este detección de todos modos, recomiendo no mirar la extensión de 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, ir a ¿Qué es un metaclass en Python?, pero piense dos veces antes de usar, especialmente en el trabajo.

    • Una vez más, que podría cambiar toda la herencia del esquema del proyecto. Pero en este punto es difícil. El formato y la imagen de adivinar, era tomado como un buen ejemplo de como llamar a un «image_factory» de la función. No estoy realmente tratando con Imágenes. Estoy buscando una manera de conseguir una cierta dinámica de la herencia a resolver mi problema sin refactorización demasiado de lo que ya está en su lugar.
    • Así, se ha descrito que tipo de solución en Una «posible solución» en su pregunta. Como usted dijo, que la solución es torpe, y no hay una forma mejor: la mejor manera es refactorizar el código para que tenga más sentido. Si usted está buscando específicamente para la dinámica de las clases, usted no puede conseguir mucho mejor que las de tipo() llamadas (excepto, tal vez, class definiciones dentro de una función, pero entonces, si usted quiere un significativo nombre de la clase tiene que establecer __name__ después, así que no es mucho más bonito). Lo siento, pero usted tiene una solución de trabajo; no te puedo ayudar más. Sólo ten cuidado de que no se puede subclase su ImageZIP de manera significativa.
  4. 2

    Debe utilizar la composición en este caso, no por herencia. Echa un vistazo a la patrón de diseño decorador. El ImageZIP clase debe decorar otra imagen de clases con la funcionalidad deseada.

    Con decoradores, usted obtiene una gran comportamiento dinámico en función de la composición que cree:

    ImageZIP(ImageJPG(path))

    Es más flexible también, usted puede tener otros decoradores:

    ImageDecrypt(password, ImageZIP(ImageJPG(path)))

    Cada decorador sólo encapsula la funcionalidad se añade y delegados para el compuesto de la clase según sea necesario.

    • He mirado en decoradores de patrones de diseño. No costura para aplicar aquí como un decorador debe tener, como uno de sus atributos, una instancia de la clase que hereda. ¿Qué sería de mi decorador de heredar de aquí ?
    • Su decorador adorna imágenes, por lo que heredan de la base Image clase.
    • Pero, entonces, si mi decorador hereda de la Imagen en lugar de ImageJPEG (o ImagePNG para el caso) de la funcionalidad específica para el formato de falta, y «yo.de datos» se convierte en una propiedad no definida ?
    • El decorador compone la clase específica que decora. Usted va a crear, por ejemplo, como esta en su fábrica: ImageZIP(ImageJPG(path)). También puede definir un data método que acaba de delegados para el compuesto de la instancia.
    • Un ejemplo de código que podría ser interesante. Pero tengo la impresión de que estamos de vuelta a la casilla de salida, y debe incluir la composición de la funcionalidad mediante la refactorización de la image_facotry función.

Kommentieren Sie den Artikel

Bitte geben Sie Ihren Kommentar ein!
Bitte geben Sie hier Ihren Namen ein

Pruebas en línea