Python dynamic inheritance: How to choose base class upon instance creation?

2024/11/19 13:38:10

Introduction

I have encountered an interesting case in my programming job that requires me to implement a mechanism of dynamic class inheritance in python. What I mean when using the term "dynamic inheritance" is a class that doesn't inherit from any base class in particular, but rather chooses to inherit from one of several base classes at instantiation, depending on some parameter.

My question is thus the following: in the case I will present, what would be the best, most standard and "pythonic" way of implementing the needed extra functionality via dynamic inheritance.

To summarize the case in point in a simple manner, I will give an example using two classes that represent two different image formats: 'jpg' and 'png' images. I will then try to add the ability to support a third format: the 'gz' image. I realize my question isn't that simple, but I hope you are ready to bear with me for a few more lines.

The two images example case

This script contains two classes: ImageJPG and ImagePNG, both inheriting from the Image base class. To create an instance of an image object, the user is asked to call the image_factory function with a file path as the only parameter.

This function then guesses the file format (jpg or png) from the path and returns an instance of the corresponding class.

Both concrete image classes (ImageJPGand ImagePNG) are able to decode files via their data property. Both do this in a different way. However, both ask the Image base class for a file object in order to do this.

UML diagram 1

import os#------------------------------------------------------------------------------#
def image_factory(path):'''Guesses the file format from the file extensionand 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 = pathdef get_pixel(self, x):assert x < 12return self.data[x]@propertydef file_obj(self): return open(self.path, 'r')#------------------------------------------------------------------------------#
class ImageJPG(Image):'''Fake JPG image class that parses a file in a given way.'''@propertydef format(self): return 'Joint Photographic Experts Group'@propertydef 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.'''@propertydef format(self): return 'Portable Network Graphics'@propertydef 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)


The compressed image example case

Building on the first image example case, one would like to add the following functionality:

An extra file format should be supported, the gz format. Instead of being a new image file format, it is simply a compression layer that, once decompressed, reveals either a jpg image or a png image.

The image_factory function keeps its working mechanism and will simply try to create an instance of the concrete image class ImageZIP when it is given a gz file. Exactly in the same way it would create an instance of ImageJPG when given a jpg file.

The ImageZIP class just wants to redefine the file_obj property. In no case does it want to redefine the data property. The crux of the problem is that, depending on what file format is hiding inside the zip archive, the ImageZIP classes needs to inherit either from ImageJPG or from ImagePNG dynamically. The correct class to inherit from can only be determined upon class creation when the path parameter is parsed.

Hence, here is the same script with the extra ImageZIP class and a single added line to the image_factory function.

Obviously, the ImageZIP class is non-functional in this example. This code requires Python 2.7.

UML diagram 2

import os, gzip#------------------------------------------------------------------------------#
def image_factory(path):'''Guesses the file format from the file extensionand 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 = pathdef get_pixel(self, x):assert x < 12return self.data[x]@propertydef file_obj(self): return open(self.path, 'r')#------------------------------------------------------------------------------#
class ImageJPG(Image):'''Fake JPG image class that parses a file in a given way.'''@propertydef format(self): return 'Joint Photographic Experts Group'@propertydef 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.'''@propertydef format(self): return 'Portable Network Graphics'@propertydef 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 fromImageJPG and at other times inherits from ImagePNG'''@propertydef format(self): return 'Compressed ' + super(ImageZIP, self).format@propertydef file_obj(self): return gzip.open(self.path, 'r')################################################################################
i = image_factory('images/lena.png.gz')
print i.format
print i.get_pixel(5)


A possible solution

I have found a way of getting the wanted behavior by intercepting the __new__ call in the ImageZIP class and using the type function. But it feels clumsy and I suspect there might be a better way using some Python techniques or design patterns I don't yet know about.

import reclass ImageZIP(object):'''Class representing a compressed file. Sometimes inherits fromImageJPG 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)@propertydef format(self): return 'Compressed ' + super(ImageZIP, self).format@propertydef file_obj(self): return gzip.open(self.path, 'r')


Conclusion

Bear in mind if you want to propose a solution that the goal is not to change the behavior of the image_factory function. That function should remain untouched. The goal, ideally, is to build a dynamic ImageZIP class.

I just don't really know what the best way to do this is. But this is a perfect occasion for me to learn more about some of Python's "black magic". Maybe my answer lies with strategies like modifying the self.__cls__ attribute after creation or maybe using the __metaclass__ class attribute? Or maybe something to do with the special abc abstract base classes could help here? Or other unexplored Python territory?

Answer

I would favor composition over inheritance here. I think your current inheritance hierarchy seems wrong. Some things, like opening the file with or gzip have little to do with the actual image format and can be easily handled in one place while you want to separate the details of working with a specific format own classes. I think using composition you can delegate implementation specific details and have a simple common Image class without requiring metaclasses or multiple inheritance.

import gzip
import structclass ImageFormat(object):def __init__(self, fileobj):self._fileobj = fileobj@propertydef name(self):raise NotImplementedError@propertydef magic_bytes(self):raise NotImplementedError@propertydef magic_bytes_format(self):raise NotImplementedErrordef 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 Truereturn Falsedef get_pixel(self, n):# ...passclass 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 = pathself._file = self._open()self._format = self._identify_format()@propertydef format(self):return self._format.namedef get_pixel(self, n):return self._format.get_pixel(n)def _open(self):opener = openif self.path.endswith(".gz"):opener = gzip.openreturn opener(self.path, "rb")def _identify_format(self):for format in self.supported_formats:f = format(self._file)if f.check_format():return felse:raise ValueError("Unsupported file format!")if __name__=="__main__":jpeg = Image("images/a.jpg")png = Image("images/b.png.gz")

I only tested this on a few local png and jpeg files but hopefully it illustrates another way of thinking about this problem.

https://en.xdnf.cn/q/26432.html

Related Q&A

Python: efficiently check if integer is within *many* ranges

I am working on a postage application which is required to check an integer postcode against a number of postcode ranges, and return a different code based on which range the postcode matches against.E…

How to pass on argparse argument to function as kwargs?

I have a class defined as followsclass M(object):def __init__(self, **kwargs):...do_somethingand I have the result of argparse.parse_args(), for example:> args = parse_args() > print args Namespa…

How do you check whether a python method is bound or not?

Given a reference to a method, is there a way to check whether the method is bound to an object or not? Can you also access the instance that its bound to?

Most Pythonic way to declare an abstract class property

Assume youre writing an abstract class and one or more of its non-abstract class methods require the concrete class to have a specific class attribute; e.g., if instances of each concrete class can be …

PyQt on Android

Im working on PyQt now, and I have to create the application on Android, Ive seen the kivy library, but its too crude.Is there any way now to run an application on Android made on PyQt?

How to elementwise-multiply a scipy.sparse matrix by a broadcasted dense 1d array?

Suppose I have a 2d sparse array. In my real usecase both the number of rows and columns are much bigger (say 20000 and 50000) hence it cannot fit in memory when a dense representation is used:>>…

Do I have to do StringIO.close()?

Some code:import cStringIOdef f():buffer = cStringIO.StringIO()buffer.write(something)return buffer.getvalue()The documentation says:StringIO.close(): Free the memory buffer. Attempting to do furtherop…

Python: ulimit and nice for subprocess.call / subprocess.Popen?

I need to limit the amount of time and cpu taken by external command line apps I spawn from a python process using subprocess.call , mainly because sometimes the spawned process gets stuck and pins the…

array.shape() giving error tuple not callable

I have a 2D numpy array called results, which contains its own array of data, and I want to go into it and use each list: for r in results:print "r:"print ry_pred = np.array(r)print y_pred.sh…

Python unittest - Ran 0 tests in 0.000s

So I want to do this code Kata for practice. I want to implement the kata with tdd in separate files:The algorithm:# stringcalculator.py def Add(string):return 1and the tests:# stringcalculator.spec.…