IBM®
本文へジャンプ
    Japan [変更]    ご利用条件
 
 
検索範囲検索:    
    ホーム    製品    サービス & ソリューション    サポート & ダウンロード    マイアカウント    
skip to main content

developerWorks Japan  >  Linux | Open source  >

Python 3: 第 2 回: 高度な話題

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

developerWorks
ページオプション

JavaScript を要するドキュメントオプションは表示されません

原文はこちら

原文はこちら


レベル: 中級

Cesar Otero, Consultant, Freelance

2009年 01月 30日

Python 3 は Guido van Rossum による強力な汎用プログラミング言語の最新バージョンです。Python 3 は 2.x シリーズとの後方互換性をなくし、その代わり構文に関するいくつかの問題を解決しています。この記事は前回の記事の続きです。今回はこの 2 回シリーズの記事の第 2 回目として、Python の新しい機能に関する説明を続けるとともに、抽象基底クラス、メタクラス、修飾子における変更点などの、より高度なトピックの詳細について説明します。

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)

キーワードのみの引数

Python 3 では関数の引数を「パラメーター・スロット」に割り当てる方法が変更されています。渡されるパラメーターの中ではアスタリスク (*) を使用し、可変長の引数を受け付けないようにします。名前が付けられた引数は、* の引数よりも後で渡さなければなりません (例えば def meth(*, arg1): pass など)。この詳細については Python のドキュメントや PEP 3102 を参照してください (「参考文献」にリンクがあります)。

注意: 上の 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
Iterator Iterable
Sized
Callable
Sequence Sized, Iterable, Container
MutableSequence Sequence
Set Sized, Iterable, Container
MutableSet Set
Mapping Sized, Iterable, Container
MutableMapping Mapping
MappingView Sized
KeysView MappingView, Set
ItemsView MappingView, Set
ValuesView MappingView
collections フレームワーク

collections フレームワークは、コンテナー・データ型、両端キュー (deque: double-ended queue として知られています)、そしてデフォルトのディクショナリー (defaultdict として知られています) で構成されています。deque は先頭からでも末尾からでもキューへの追加と取り出しを行うことができます。defaultdict コンテナーは組み込みのディクショナリーのサブクラスであり、(Python 3 のドキュメントによれば) 「1 つのメソッドをオーバーライドし、1 つの書き込み可能インスタンス変数を追加します。」それを除けば、defaultdict コンテナーはディクショナリーとまったく同じように動作します。また collections フレームワークには namedtuple() というデータ型ファクトリー関数も用意されています。

ABC の型階層

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


図 1. 数値の型階層

numerical tower

Python の数値の型階層は Scheme 言語の numerical tower をヒントにしたものです。

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 つしかないことに注目してください。

with 文

Python のバージョン 2.6 の時点から with はキーワードになったので、この機能を __future__ によってインポートする必要はありません。




上に戻る


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

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 に追加されたモジュールのうち最も優れているものは antigravity です。Python を起動し、コマンドラインで import antigravity と入力してみてください。きっと良いことがあるはずです。

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

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



参考文献

学ぶために

製品や技術を入手するために


著者について

author photo - cesar otero

Cesar Otero は Java と Python に関するフリーのコンサルタントです。彼は電気工学の学位を持ち、副専攻は数学です。




記事の評価


サイト改善のため、ご意見をお寄せください。こちらのフォームからお願いいたします。



 


 


不充分・不完全である大変素晴らしい
 


この記事を共有する

del.icio.us del.icio.us newsing newsing FC2ブックマーク FC2ブックマーク
Choix! Choix! ニフティクリップ ニフティクリップ Yahoo!ブックマーク Yahoo!ブックマーク
MM/memo MM/memo CZブックマーク CZブックマーク livedoorクリップ livedoorクリップ
はてなブックマーク はてなブックマーク Buzzurl(バザール) Buzzurl(バザール)




上に戻る


IBM、IBM ロゴ、ibm.com、DB2、developerWorks、Lotus、Rational、Tivoli、および WebSphere は、International Business Machines Corporation の米国およびその他の国における商標または登録商標です。これらおよび他の IBM 商標に、この情報の最初に現れる個所で商標表示 (® または ™) が付されている場合、これらの表示は、この情報が公開された時点で、米国において、IBM が所有する登録商標またはコモン・ロー上の商標であることを示しています。このような商標は、その他の国においても登録商標またはコモン・ロー上の商標である可能性があります。IBM が現在所有している米国における商標のリストをご参照ください。 Java およびすべての Java 関連の商標およびロゴは、Sun Microsystems, Inc. の米国およびその他の国における商標です。 Linux は、Linus Torvalds の米国およびその他の国における商標です。他の会社名、製品名およびサービス名等はそれぞれ各社の商標です。 他の会社名、製品名およびサービス名等はそれぞれ各社の商標です。

    日本IBMについて プライバシー お問い合わせ