目次


Python 3

第 2 回: 高度な話題

メタクラス、修飾子、その他の変わった機能

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Python 3

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Python 3

このシリーズの続きに乞うご期待。

Python のバージョン 3 (Python 3000 または Py3K として知られています) に関する前回の記事では、新しい print() 関数や bytes データ型、そして string 型に対する変更など、Python での基本的な変更のいくつかを紹介し、そうした変更によって後方互換性が失われていることを説明しました。今回の記事は第 2 回として、より高度なトピック、つまり抽象基底クラス (ABC: Abstract Base Class)、メタクラス、関数アノテーションと修飾子、整数リテラルのサポート、数値の型階層、例外の発生と捕捉に対する変更などについて説明します。これらの機能の大部分もバージョン 2.x シリーズとは後方互換性がありません。

クラス修飾子

これまでのバージョンの Python でメソッドを変換する場合には、そのメソッドを定義した後で行う必要がありました。この要件があるため、長いメソッドの場合には、そのメソッドを定義するための重要なコンポーネントが PEP (Python Enhancement Proposal) 318 (「参考文献」のリンクを参照) で提供される外部インターフェースの定義から遠く離れた場所に置かれてしまいました。変換に関するこの要件の一例を示したものが下記のスニペットです。

リスト 1. バージョン 3 以前の Python でのメソッドの変換
def myMethod(self):
    # do something

myMethod = transformMethod(myMethod)

こうした状況のコードがもっと読みやすくなるように、また同じメソッド名を何度も記述しなくて済むように、Python のバージョン 2.4 ではメソッド修飾子が導入されました。

修飾子というのは、他のメソッドを修飾し、メソッドまたは呼び出し可能な別のオブジェクトを返すメソッドです。修飾子であることを表すには、その修飾子の名前の前に「@」記号を付けます (これは Java™ のアノテーションの構文と似ています)。リスト 2 は修飾子の実際を示しています。

リスト 2. 修飾子メソッド
@transformMethod
def myMethod(self):
    # do something

修飾子は純粋なシンタックス・シュガーです (シンタックス・シュガーとは、ウィキペディアによれば「コンピューター言語の機能には影響を与えないものの、人間がその言語を使いやすくなるようにコンピューター言語の構文に追加されるもの」です。修飾子は静的なメソッドにアノテーションを付けるためによく使われます。リスト 1 とリスト 2 は等価ですが、リスト 2 の方が読みやすくなっています。

修飾子の定義方法は、以下に示すように他のメソッドの場合とまったく同様です。

def mod(method):
    method.__name__ = "John"
    return method

@mod
def modMe():
    pass

print(modMe.__name__)

Python 3 では一層便利になり、メソッドだけではなくクラスにも修飾子を使うことができるようになりました。したがって、次のようなコードを使う代わりに、

class myClass:
    pass

myClass = doSomethingOrNotWithClass(myClass)

以下のコードを使うことができます。

@doSomethingOrNotWithClass
class myClass:
    pass

メタクラス

メタクラスというのは、インスタンスが他のクラスであるクラスです。Python 3 には組み込みの metaclass 型があります (metaclass 型は他のメタクラスを作成するために、あるいは実行時に動的にクラスを作成するために使われます)。次のような構文は相変わらず有効です。

>>>aClass = type('className', 
   (object,), 
   {'magicMethod': lambda cls : print("blah blah")})

この構文は引数として、クラス名としてのストリング、継承されたオブジェクトのタプル (空のタプルの場合もあります)、そして追加対象のメソッドを含むディクショナリー (このディクショナリーも空の場合があります) を受け付けます。もちろん、下記のように型から継承して独自のメタクラスを作成することもできます。

class meta(type):
    def __new__(cls, className, baseClasses, dictOfMethods):
        return type.__new__(cls, className, baseClasses, dictOfMethods)

注意: 上の 2 つの例の意味がまったく理解できない場合には、ぜひ David Mertz と Michele Simionato によるメタクラスのシリーズ記事を読むことをお薦めします。「参考文献」のリンクを参照してください。

Python 3 のクラス定義では、基底クラスが並べられた後にキーワード引数を指定できるようになっていることに注目してください (一般的な形式は class Foo(*bases, **kwds): pass です)。また便利なことに、メタクラスをクラス定義に渡すには、キーワード引数 metaclass を使うことができます。その一例は次のとおりです。

>>>class aClass(baseClass1, baseClass2, metaclass = aMetaClass): pass

メタクラスに関する古い構文では、以下のように組み込みの __metaclass__ 属性にメタクラスを割り当てます。

class Test(object):
    __metaclass__ = type

また、__prepare__ という新しい属性が追加されています。この属性を使うと、新しいクラスの名前空間に対するディクショナリーを作成することができます。この属性はクラス本体が評価される前に呼び出されます (リスト 3)。

リスト 3. __prepare__ 属性を使った単純なメタクラス
def meth():
    print("Calling method")

class MyMeta(type):
    @classmethod
    def __prepare__(cls, name, baseClasses):
        return {'meth':meth}

    def __new__(cls, name, baseClasses, classdict):
        return type.__new__(cls, name, baseClasses, classdict)

class Test(metaclass = MyMeta):
    def __init__(self):
        pass

    attr = 'an attribute'

t = Test()
print(t.attr)

もっと興味深い例が、PEP 3115 からそのまま引用したリスト 4 です。この例で作成されているメタクラスはメソッドの名前を一覧として持っており、そのクラス・メソッドが宣言される順番は維持されています。

リスト 4. クラスのメンバーの順序を保持するメタクラス
# The custom dictionary
class member_table(dict):
    def __init__(self):
        self.member_names = []

    def __setitem__(self, key, value):
        # if the key is not already defined, add to the
        # list of keys.
        if key not in self:
            self.member_names.append(key)

        # Call superclass
        dict.__setitem__(self, key, value)

# The metaclass
class OrderedClass(type):
    # The prepare function
    @classmethod
    def __prepare__(metacls, name, bases): # No keywords in this case
        return member_table()

    # The metaclass invocation
    def __new__(cls, name, bases, classdict):
        # Note that we replace the classdict with a regular
        # dict before passing it to the superclass, so that we
        # don't continue to record member names after the class
        # has been created.
        result = type.__new__(cls, name, bases, dict(classdict))
        result.member_names = classdict.member_names
        return result

メタクラスに関して変更が加えられた理由はいくつかあります。オブジェクトはそのオブジェクトのメソッドをディクショナリーに保存しますが、ディクショナリーには順序がありません。しかし宣言されたクラスのメンバーの順序を保持した方が便利ないくつかのケースがあります。クラスのメンバーの順序を保持するには、クラス作成の最初の方で、その情報がまだ利用可能な段階 (例えば C の構造体を作成する際に役立つ時点) でメタクラスを「含め」ます。また、この新しい仕組みによって、今後は他にも興味深い機能を実装することができるようになります (例えば、クラスを作成する際に作成されるクラス名前空間本体の中にシンボルを挿入し、シンボルの参照を転送するなど)。PEP 3115 では構文の変更の理由として見た目の美しさにも触れていますが、見た目の美しさに関する問題は客観的に解決することはできません。(「参考文献」に挙げた PEP 3115 へのリンクを参照。)

抽象基底クラス (ABC: abstract base class)

このシリーズの前回の記事で触れたように、ABC はインスタンス化できないクラスです。Java 言語や C++ 言語を使った経験があるプログラマーには、この概念はおなじみのはずです。Python 3 では、ABC を扱うための abc という新しいフレームワークが追加されています。

abc モジュールにはメタクラス (ABCMeta) と修飾子 (@abstractmethod@abstractproperty) があります。ABC に @abstractmethod または @abstractproperty がある場合には、その ABC をインスタンス化することはできず、サブクラスの中でオーバーライドする必要があります。例えば次のようなコードは適切ですが、

>>>from abc import *
>>>class C(metaclass = ABCMeta): pass
>>>c = C()

次のようにしてはいけません。

>>>from abc import *
>>>class C(metaclass = ABCMeta): 
...    @abstractmethod
...    def absMethod(self):
...        pass
>>>c = C() 
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class C with abstract methods absMethod

より適切なものにするためには次のコードを使います。

>>>class B(C):
...    def absMethod(self):
...        print("Now a concrete method")
>>>b = B()
>>>b.absMethod()
Now a concrete method

ABCMeta クラスは __instancecheck__ 属性と __subclasscheck__ 属性をオーバーライドし、組み込みの isinstance() 関数と issubclass() 関数をオーバーロードできるようにします。ABC に仮想サブクラスを追加するためには ABCMeta に用意されている register() メソッドを使用します。下記は簡単な例ですが、

>>>class TestABC(metaclass=ABCMeta): pass
>>>TestABC.register(list)
>>>TestABC.__instancecheck__([])
True

isinstance(list, TestABC) を使うことと等価です。皆さんも気付いたかもしれませんが、Python 3 では __isinstance__ ではなく __instancecheck__ を使い、また __issubclass__ ではなく __subclasscheck__ を使いますが、Python 3 の方法の方が自然に見えます。その理由は、例えば isinstance(subclass, superclass) の引数を外に出して superclass.__isinstance__(subclass) にすると混乱を生ずる可能性があるからです。したがって Python 3 では superclass.__instancecheck__(subclass) という構文を使用します。

collections モジュールの中では、あるクラスが特定のインターフェースを提供しているかどうかを、次のようにいくつかの ABC を使ってテストすることができます。

>>>from collections import Iterable
>>>issubclass(list, Iterable)
True

表 1 は collections フレームワークの ABC を示しています。

表 1. collections フレームワークの ABC
ABC継承するもの
Container
Hashable
Iterable
IteratorIterable
Sized
Callable
SequenceSized, Iterable, Container
MutableSequenceSequence
SetSized, Iterable, Container
MutableSetSet
MappingSized, Iterable, Container
MutableMappingMapping
MappingViewSized
KeysViewMappingView, Set
ItemsViewMappingView, Set
ValuesViewMappingView

ABC の型階層

Python 3 では数値クラスを表現する ABC の型階層をサポートしています。これらの ABC は numbers モジュールの中にあり、NumberComplexRealRationalIntegral を含んでいます。図 1 は数値の型階層を示しています。もちろん、こうした階層を使えば独自の数値型や他の数値型 ABC を実装することができます。

図 1. 数値の型階層
数値の型階層

fractions という新しいモジュールは Rational という数値型 ABC を実装します。このモジュールによって有理数の計算をサポートすることができます。dir(fractions.Fraction) を使用してみると、imagreal__complex__ などの属性があることがわかります。これは numeric tower の場合と同様、RationalReal から継承し、RealComplex から継承するからです。

例外の発生と捕捉

Python 3 では except 節が変更され、構文上の曖昧さの問題に対処できるようになっています。これまで Python バージョン 2.5 では、try . . . except による次のような構成体がある場合、

>>>try:
...    x = float('not a number')
... except (ValueError, NameError):
...    print "can't convert type"

誤って次のように記述されることがありました。

>>> try:
...    x = float('not a number')
... except ValueError, NameError:
...    print "can't convert type"

後者の形式の問題は、NameError 例外が決して捕捉されないことです。なぜならインタープリターは ValueError を捕捉すると、その例外オブジェクトを NameError という名前にバインドするからです。これは次の例を見るとわかります。

>>> try:
...    x = float('blah')
... except ValueError, NameError:
...    print "NameError is ", NameError
...
NameError is invalid literal for float(): not a number

そこでこの曖昧さの問題に対処するため Python 3 では、例外オブジェクトを別の名前にバインドしたい場合にはカンマ (,) を as というキーワードに置き換えます。複数の例外を捕捉したい場合には括弧 (()) が必要です。リスト 5 のコードは Python 3 で許可される 2 つの正式な例を示しています。

リスト 5. Python 3 での例外処理
# bind ValueError object to local name ex
try:
    x = float('blah')
except ValueError as ex:
    print("value exception occurred ", ex)
 
# catch two different exceptions simultaneously
try:
    x = float('blah')
except (ValueError, NameError):
    print("caught both types of exceptions")

例外の処理に関するもう 1 つの変更が例外チェーンです。例外チェーンは暗黙的な場合も明示的な場合もあります。リスト 6 は暗黙的な例外チェーンの例です。

リスト 6. Python 3 での暗黙的な例外チェーン
def divide(a, b):
    try:
        print(a/b)
    except Exception as exc:
        def log(exc):
        fid = open('logfile.txt') # missing 'w'
        print(exc, file=fid)
        fid.close()

        log(exc)

divide(1,0)

divide() メソッドでは、ゼロによる割り算を実行しようとすると ZeroDivisionError という例外が発生します。さらに except 節では、ネストされた log() メソッド内で、書き込み用にまだ開かれていないファイルに対して print(exc, file=fid) によって書き込もうとしています。この場合、Python 3 ではリスト 7 のような例外が発生します。

リスト 7. 暗黙的にチェーンされた例外のトレースバックの例
Traceback (most recent call last):
  File "chainExceptionExample1.py", line 3, in divide
  print(a/b)
ZeroDivisionError: int division or modulo by zero

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "chainExceptionExample1.py", line 12, in <module>
    divide(1,0)
  File "chainExceptionExample1.py", line 10, in divide
    log(exc)
  File "chainExceptionExample1.py", line 7, in log
    print(exc, file=fid)
  File "/opt/python3.0/lib/python3.0/io.py", line 1492, in write
    self.buffer.write(b)
  File "/opt/python3.0/lib/python3.0/io.py", line 696, in write
    self._unsupported("write")
  File "/opt/python3.0/lib/python3.0/io.py", line 322, in _unsupported
    (self.__class__.__name__, name))
io.UnsupportedOperation: BufferedReader.write() not supported

2 つの例外が処理されていることに注目してください。これまでのバージョンの Python であれば、最初の例外 ZeroDivisionError は処理されることはなかったはずです。では、どのようにして 2 つの例外が処理されているのでしょう。Python 3 では、すべての例外オブジェクトに ZeroDivisionError のような__context__ 属性があります。この場合には、発生した IOError__context__ 属性の中に ZeroDivisionError が「保持」されます。

例外オブジェクトには、__context__ 属性の他に、必ず None に初期化される __cause__ 属性があります。この属性によって、例外の原因を明示的に記録することができます。__cause__ 属性を設定するためには下記の構文を使います。

>>> raise EXCEPTION from CAUSE

これは次のようにコーディングした場合とまったく同じです。

>>>exception = EXCEPTION
>>>exception.__cause__ = CAUSE
>>>raise exception

ただし後の方法の方がずっとスマートです。リスト 8 は明示的な例外チェーンの例です。

リスト 8. Python 3 での明示的な例外チェーン
class CustomError(Exception): 
    pass

try:
    fid = open("aFile.txt") # missing 'w' again
    print("blah blah blah", file=fid)
except IOError as exc:
    raise CustomError('something went wrong') from exc

前の例と同じように、このファイルは書き込み用に開かれていなかったため、print() 関数を実行することによって例外が発生します。リスト 9 は例外のトレースバックです。

リスト 9. 例外のトレースバック
Traceback (most recent call last):
  File "chainExceptionExample2.py", line 5, in <module>
    fid = open("aFile.txt")
  File "/opt/python3.0/lib/python3.0/io.py", line 278, in __new__
    return open(*args, **kwargs)
  File "/opt/python3.0/lib/python3.0/io.py", line 222, in open
    closefd)
 File "/opt/python3.0/lib/python3.0/io.py", line 615, in __init__
    _fileio._FileIO.__init__(self, name, mode, closefd)
IOError: [Errno 2] No such file or directory: 'aFile.txt'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
File "chainExceptionExample2.py", line 8, in <modulei>
  raise CustomError('something went wrong') from exc
__main__.CustomError: something went wrong

トレースバックの中の「The above exception was the direct cause of the following exception (上記の例外が、下記の例外の直接の原因です)」という行に注目してください。この行の後に別のトレースバックが続き、「something went wrong (何かがおかしい)」という CustomError で終わっています。

最後に、例外オブジェクトに追加されたもう 1 つの属性が __traceback__ 属性です。捕捉された例外に __traceback__ 属性がない場合には新しいトレースバックが設定されます。下記はその簡単な例です。

from traceback import format_tb

try:
    1/0
except Exception as exc:
    print(format_tb(exc.__traceback__)[0])

format_tb がリストを返し、このリストの中には例外が 1 つしかないことに注目してください。

整数リテラルのサポートと構文

Python は、8 進数や、(もちろん) 10 進数、そして 16 進など、さまざまな数を基数とする整数のストリング・リテラルをサポートしていますが、Python 3 では 2 進リテラルも追加されました。また 8 進リテラルの表現方法が変更され、前に 0o または 0O (つまりゼロの後に大文字または小文字の o) を付けて評価対象の 8 進リテラルを表現することになりました。例えば 8 進数の 13、つまり10 進数の 11 は次のように表現します。

>>>0o13
11

新しい 2 進リテラルは前に 0b または 0B (つまりゼロの後に大文字または小文字の b) を付けて表現します。10 進数の 21 は 2 進数で次のように表現することができます。

>>>0b010101
21

oct() メソッドと hex() メソッドは削除されました。

関数アノテーション

関数アノテーションはコンパイル時に関数のさまざまな部分 (例えばパラメーターなど) に式を関連付けます。関数アノテーションは単独では意味を持ちません。つまりサードパーティーのライブラリーが関数アノテーションに対応しない限り関数アノテーションは処理されません。関数アノテーションは、関数のパラメーターや戻り値にアノテーションを付ける方法を標準化することを目的に設けられました。関数アノテーションの構文は次のとおりです。

def methodName(param1: expression1, ..., paramN: expressionN)->ExpressionForReturnType:
    ...

例えば関数のパラメーターに対するアノテーションは次のようになります。

def execute(program:"name of program to be executed", error:"if something goes wrong"):
    ...

下記は関数の戻り値にアノテーションを付ける例です。これは関数の戻り型をチェックする際に便利です。

def getName() -> "isString":
     ...

関数アノテーションに関する完全な文法は PEP 3107 を参照してください (「参考文献」にリンクがあります)。

まとめ

Python 3 の最終リリースは 2008年12月の初めに入手可能となりました。私はそれ以来、ブログをいくつか調べるなどして、後方互換性がない問題に関する他の人達の反応を調べてきました。何らかの手段で正式な合意を得たわけではありませんが、そうしたブログを読む限り意見は分かれるようです。Linux® 開発コミュニティーの一部の人達は、大量のコードをポーティングしなければならないため、バージョン 3 への移行を非常に嫌っているようです。それとは対照的に、多くの Web 開発者は unicode がサポートされるように変更されたというだけの理由でバージョン 3 に移行しようとしています。

私の意見は、判断を下す前に少なくとも PEP や開発メーリング・リストをよく調べ、その後で新しいバージョンにポーティングするかどうかを決めるとよいと思います。PEP には、それぞれの変更に対する根拠と、その変更の結果得られる利点が、実装例とともに説明されています。これらの変更は実によく考えられており、また徹底的に議論されたものです。このシリーズでは、すべての PEP を読まなくても一般的な Python プログラマーが Python 3 での変更の概要をつかめるような内容にしました。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=373843
ArticleTitle=Python 3: 第 2 回: 高度な話題
publish-date=01302009