私のプログラミングジョブで、Pythonに動的クラス継承のメカニズムを実装する必要がある興味深いケースに遭遇しました。 「動的継承」という用語を使用する場合の意味は、特に基本クラスから継承しないクラスで、インスタンス化時にいくつかのパラメーターに応じて、いくつかの基本クラスの1つから継承することを選択します。
したがって、私の質問は次のとおりです。私が提示する場合、動的継承を介して必要な追加機能を実装するための最良の、最も標準的で「Python的な」方法は何でしょうか。
事例を簡単な方法で要約するために、2つの異なる画像形式を表す2つのクラスを使用する例を示します:'jpg'
および'png'
画像。次に、3番目の形式である'gz'
画像をサポートする機能を追加してみます。私の質問はそれほど単純ではないことに気づきましたが、さらに数行にわたって私と一緒に耐えられる準備ができていることを願っています。
このスクリプトには、2つのクラスImageJPG
およびImagePNG
が含まれており、どちらもImage
基本クラスから継承しています。画像オブジェクトのインスタンスを作成するには、ファイルパスを唯一のパラメーターとしてimage_factory
関数を呼び出すように求められます。
次に、この関数はパスからファイル形式(jpg
またはpng
)を推測し、対応するクラスのインスタンスを返します。
両方の具象画像クラス(ImageJPG
およびImagePNG
)は、data
プロパティを介してファイルをデコードできます。どちらも異なる方法でこれを行います。ただし、どちらもImage
基本クラスにファイルオブジェクトを要求してこれを実行します。
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が必要です。
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領土?
関数レベルで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)
ここでは、継承よりも構成を優先します。現在の継承階層は間違っているようです。 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ファイルでのみテストしましたが、うまくいけば、この問題についての別の考え方を示しています。
「黒魔術」が必要な場合は、まずそれを必要としないソリューションについて考えてみてください。あなたはよりよく機能し、より明確なコードを必要とする何かを見つける可能性があります。
画像クラスのコンストラクターは、パスの代わりにすでに開かれているファイルを取るほうがよい場合があります。そうすれば、ディスク上のファイルだけでなく、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のメタクラスとは にアクセスしますが、特に仕事でそれを使用する前によく考えてください。
この場合、継承ではなくコンポジションを使用する必要があります。 デコレータのデザインパターン を見てください。 ImageZIP
クラスは、他の画像クラスを必要な機能で装飾する必要があります。
デコレーターを使用すると、作成する構成に応じて非常に動的な動作が得られます。
ImageZIP(ImageJPG(path))
また、より柔軟で、他のデコレータを使用できます。
ImageDecrypt(password, ImageZIP(ImageJPG(path)))
各デコレータは、追加する機能をカプセル化し、必要に応じて構成済みクラスに委任します。