Introduction
J'ai rencontré un cas intéressant dans mon travail de programmation qui me demande d'implémenter un mécanisme d'héritage dynamique de classes en python. Ce que j'entends par "héritage dynamique" est une classe qui n'hérite pas d'une classe de base en particulier, mais qui choisit plutôt d'hériter d'une ou plusieurs classes de base à l'instanciation, en fonction d'un paramètre.
Ma question est donc la suivante : dans le cas que je vais présenter, quelle serait la meilleure façon, la plus standard et la plus "pythonique" d'implémenter la fonctionnalité supplémentaire nécessaire via l'héritage dynamique.
Pour résumer le cas de figure de manière simple, je vais donner un exemple en utilisant deux classes qui représentent deux formats d'image différents : 'jpg'
y 'png'
des images. J'essaierai ensuite d'ajouter la possibilité de prendre en charge un troisième format : le 'gz'
image. Je me rends compte que ma question n'est pas si simple, mais j'espère que vous êtes prêts à me supporter pendant quelques lignes supplémentaires.
Le cas d'exemple des deux images
Ce script contient deux classes : ImageJPG
y ImagePNG
tous deux héritant de de la Image
classe de base. Pour créer une instance d'un objet image, on demande à l'utilisateur d'appeler la fonction image_factory
avec un chemin de fichier comme seul paramètre.
Cette fonction devine ensuite le format du fichier ( jpg
o png
) du chemin et renvoie une instance de la classe correspondante.
Les deux classes d'images concrètes ( ImageJPG
y ImagePNG
) sont capables de décoder via leur data
propriété. Les deux le font d'une manière différente. Cependant, les deux demandent au Image
pour un objet fichier afin de faire cela.
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)
Le cas de l'exemple d'image compressée
En se basant sur l'exemple de la première image, on voudrait ajouter la fonctionnalité suivante :
Un format de fichier supplémentaire doit être pris en charge, le gz
format. Au lieu de un nouveau format de fichier image, il s'agit simplement d'une couche de compression qui, une fois décompressée, révèle soit un jpg
ou une png
image.
El image_factory
conserve son mécanisme de fonctionnement et simplement essayer de créer une instance de la classe d'image concrète ImageZIP
lorsqu'il reçoit un gz
fichier. Exactement de la même manière qu'il créer une instance de ImageJPG
lorsqu'on lui donne un jpg
fichier.
El ImageZIP
veut simplement redéfinir la classe file_obj
propriété. En aucun cas, il ne veut redéfinir la propriété data
propriété. Le nœud du problème du problème est que, en fonction du format de fichier qui se cache dans l'archive zip, la propriété ImageZIP
doit hériter de soit de ImageJPG
ou de ImagePNG
de façon dynamique. La classe dont il faut hériter ne peut être déterminée qu'au moment de la création de la classe, lorsque l'élément path
est analysé.
Ainsi, voici le même script avec le supplément ImageZIP
classe et une seule ligne ajoutée à la image_factory
fonction.
De toute évidence, le ImageZIP
est non fonctionnelle dans cet exemple. Ce code nécessite 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)
Une solution possible
J'ai trouvé un moyen d'obtenir le comportement désiré en interceptant l'appel à l'aide. __new__
dans l'appel ImageZIP
et en utilisant la classe type
fonction. Mais cela semble maladroit et je soupçonne qu'il pourrait y avoir une meilleure façon de procéder en utilisant des techniques Python ou des modèles de conception que je ne connais pas encore.
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')
Conclusion
Gardez à l'esprit, si vous souhaitez proposer une solution, que l'objectif n'est pas de modifier le comportement de l'utilisateur. image_factory
fonction. Cette fonction doit rester intacte. L'objectif, dans l'idéal, est de construire une fonction dynamique ImageZIP
classe.
Je ne sais pas vraiment quelle est la meilleure façon de le faire. Mais c'est une occasion parfaite pour moi d'en apprendre davantage sur la "magie noire" de Python. Peut-être que ma réponse se trouve dans des stratégies comme la modification de la fonction self.__cls__
après la création ou peut-être en utilisant l'attribut __metaclass__
attribut de classe ? Ou peut-être quelque chose à voir avec l'attribut spécial abc
Les classes de base abstraites pourraient aider ici ? Ou d'autres territoires Python inexplorés ?
6 votes
Je pense que vous imposez une contrainte artificielle selon laquelle il doit s'agir d'une classe héritant de vos types existants. Je pense qu'une fonction de fabrique ou une classe encapsulant un de vos types est plus pythonique. D'ailleurs, je pense qu'il serait encore mieux d'avoir une fonction générique
Image
avec des fonctions ou des méthodes de classe pour le chargement à partir de différents formats.2 votes
Tout ce que @Thomas dit est vrai. Si vous avez besoin de ça, vous avez mal structuré votre héritage. Appelez le
Image
avec un argument "datatype" est le moyen le plus évident ; il en existe d'autres. Gardez également à l'esprit qu'au lieu de type(), vous pouvez simplement appeler la fonction__new__
des classes de base appropriées dans le bon ordre.2 votes
Je ne comprends pas le problème non plus, vous pouvez faire votre exemple assez facilement avec
ImagePNG, ImageJPG, CompressedFile
classes et les coller ensemble avec l'héritage multiple ieclass CompressedPNG(ImagePNG, CompressedFile)
et écrire un simpleimage_from_path
fonction.0 votes
Si vous voulez de l'aide, ne posez pas vos questions sous forme d'essais de quatre pages.
1 votes
De plus, se baser sur l'extension du fichier pour détecter le mime-type est vraiment une mauvaise pratique, une meilleure solution serait d'utiliser les octets magiques du fichier (ce qui peut être fait avec la commande
magic
module)0 votes
Travaillez-vous réellement avec des images ou est-ce juste un exemple ? Il existe de meilleures façons de confirmer qu'un fichier image est d'un type spécifique et des bibliothèques comme PIL ( pythonware.com/produits/pil ) valent la peine d'être étudiées plutôt que d'élaborer votre propre solution.
0 votes
@MatToufoutu Le type de fichier n'est qu'un exemple. Cependant, je n'utiliserais pas la bibliothèque magique car il semble que la version PyPI ne soit pas maintenue. Une nouvelle version existe quelque part ailleurs mais n'est pas ajoutée à PyPI...
0 votes
@Mike Il s'agit simplement d'un exemple pour essayer de créer une situation similaire à celle que je rencontre. Je ne travaille pas avec des images.
6 votes
@Glenn Désolé, ça semblait difficile à expliquer et je préférais entrer dans tous les détails que de risquer d'être mal interprété.
0 votes
@Jochen Cela signifierait écrire une classe pour (chaque type de fichier) X (chaque format de compression). L'idée est de pouvoir ajouter de nouveaux formats et de nouveaux formats de compression sans trop de code passe-partout.
0 votes
Comment avez-vous créé les diagrammes de classe ? J'aurais besoin d'un tel programme. Merci !
0 votes
@Niklas J'ai utilisé ce site web : yuml.me