web-dev-qa-db-ja.com

Python動的継承:インスタンス作成時に基本クラスを選択する方法?

前書き

私のプログラミングジョブで、Pythonに動的クラス継承のメカニズムを実装する必要がある興味深いケースに遭遇しました。 「動的継承」という用語を使用する場合の意味は、特に基本クラスから継承しないクラスで、インスタンス化時にいくつかのパラメーターに応じて、いくつかの基本クラスの1つから継承することを選択します。

したがって、私の質問は次のとおりです。私が提示する場合、動的継承を介して必要な追加機能を実装するための最良の、最も標準的で「Python的な」方法は何でしょうか。

事例を簡単な方法で要約するために、2つの異なる画像形式を表す2つのクラスを使用する例を示します:'jpg'および'png'画像。次に、3番目の形式である'gz'画像をサポートする機能を追加してみます。私の質問はそれほど単純ではないことに気づきましたが、さらに数行にわたって私と一緒に耐えられる準備ができていることを願っています。

2つの画像の例のケース

このスクリプトには、2つのクラスImageJPGおよびImagePNGが含まれており、どちらもImage基本クラスから継承しています。画像オブジェクトのインスタンスを作成するには、ファイルパスを唯一のパラメーターとしてimage_factory関数を呼び出すように求められます。

次に、この関数はパスからファイル形式(jpgまたはpng)を推測し、対応するクラスのインスタンスを返します。

両方の具象画像クラス(ImageJPGおよびImagePNG)は、dataプロパティを介してファイルをデコードできます。どちらも異なる方法でこれを行います。ただし、どちらもImage基本クラスにファイルオブジェクトを要求してこれを実行します。

UML diagram 1

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)


圧縮画像の事例

最初の画像の例のケースに基づいて、次の機能を追加したいと思います。

追加のファイル形式、gz形式をサポートする必要があります。新しい画像ファイル形式ではなく、単に圧縮レイヤーであり、圧縮解除すると、jpg画像またはpng画像のいずれかが表示されます。

image_factory関数は、その機能メカニズムを維持し、ImageZIPファイルが指定されたときに具象イメージクラスgzのインスタンスを作成しようとします。まったく同じ方法で、ImageJPGファイルを指定すると、jpgのインスタンスが作成されます。

ImageZIPクラスは、file_objプロパティを再定義したいだけです。 dataプロパティを再定義する必要はありません。問題の核心は、Zipアーカイブ内に隠されているファイル形式に応じて、ImageZIPクラスがImageJPGまたはImagePNGから動的に継承する必要があることです。継承する正しいクラスは、pathパラメータが解析されるときのクラス作成時にのみ決定できます。

したがって、ここに、追加のImageZIPクラスとimage_factory関数に1行追加された同じスクリプトがあります。

明らかに、この例ではImageZIPクラスは機能していません。このコードにはPython 2.7が必要です。

UML diagram 2

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)


可能な解決策

ImageZIPクラスの__new__呼び出しをインターセプトし、type関数を使用することで、必要な動作を取得する方法を見つけました。しかし、それは不器用に感じ、私はまだ知らないいくつかのPythonテクニックまたはデザインパターンを使用するより良い方法があるかもしれないと思います。

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')


結論

image_factory関数の動作を変更することが目的ではないという解決策を提案する場合は、注意してください。その機能はそのままにする必要があります。理想的には、動的なImageZIPクラスを作成することが目標です。

これを行うための最良の方法が何であるか私は本当に知りません。しかし、これは私がPythonの「黒魔術」について学ぶための絶好の機会です。たぶん私の答えは、作成後にself.__cls__属性を変更したり、多分__metaclass__クラス属性を使用したりするような戦略にありますか?または、特別なabc抽象基本クラスと何か関係がある場合は、ここで役立つでしょうか?または他の未踏のPython領土?

37
xApple

関数レベルでImageZIPクラスを定義するのはどうですか?
これにより、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)

編集:変更する必要なし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)
12
Niklas R

ここでは、継承よりも構成を優先します。現在の継承階層は間違っているようです。 or gzipでファイルを開くなどのいくつかのことは、実際の画像形式とはほとんど関係がなく、特定の形式の独自のクラスでの作業の詳細を分離したいときに、1か所で簡単に処理できます。構成を使用すると、メタクラスや多重継承を必要とせずに、実装固有の詳細を委任でき、単純な共通のImageクラスを持つことができると思います。

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")

私はこれをいくつかのローカルpngおよびjpegファイルでのみテストしましたが、うまくいけば、この問題についての別の考え方を示しています。

18
stderr

「黒魔術」が必要な場合は、まずそれを必要としないソリューションについて考えてみてください。あなたはよりよく機能し、より明確なコードを必要とする何かを見つける可能性があります。

画像クラスのコンストラクターは、パスの代わりにすでに開かれているファイルを取るほうがよい場合があります。そうすれば、ディスク上のファイルだけでなく、urllibやgzipなどのファイルのようなオブジェクトを使用できます。

また、ファイルの内容を確認することでPNGからJPGを識別でき、gzipファイルの場合はとにかくこの検出が必要なので、ファイル拡張子をまったく確認しないことをお勧めします。

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.

黒魔術については Pythonのメタクラスとは にアクセスしますが、特に仕事でそれを使用する前によく考えてください。

4
Petr Viktorin

この場合、継承ではなくコンポジションを使用する必要があります。 デコレータのデザインパターン を見てください。 ImageZIPクラスは、他の画像クラスを必要な機能で装飾する必要があります。

デコレーターを使用すると、作成する構成に応じて非常に動的な動作が得られます。

ImageZIP(ImageJPG(path))

また、より柔軟で、他のデコレータを使用できます。

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

各デコレータは、追加する機能をカプセル化し、必要に応じて構成済みクラスに委任します。

2
Jordão