このBlogは移転しました。今後は aish.dev を御覧ください。

Python 3.6の概要 (その4 - クラス定義)

クラス定義のカスタマイズ

これまで、Pythonのクラス定義をカスタマイズする手段として、メタクラスが使われてきた。しかし、メタクラスを利用したカスタマイズは、Pythonのオブジェクトモデルや型システムの知識が必要で実装が難しく、また複数のメタクラスを同時に使用するのが難しい、などの問題点があった。そこで、PEP 487 -- Simpler customisation of class creation では、メタクラスを使わずにクラスをカスタマイズする手段を提供している

init_subclass() メソッド

クラスのサブクラスが作成されたときに呼び出され、引数として、派生クラスと、クラス定義の引数が渡される。__init_subclass__()メソッドは、自動的にクラスメソッドとなる。

class Spam:
    def __init_subclass__(cls, **kwargs):
         super().__init_subclass__()
         print('Spam:', cls, kwargs)

class Ham(Spam, bacon=1):
    def __init_subclass__(cls, **kwargs):
         super().__init_subclass__(**kwargs)
         print('Ham:', cls, kwargs)

class Egg(Ham, cheese=1):
    def __init_subclass__(cls, **kwargs):
         super().__init_subclass__(**kwargs)
         print('Egg:', cls, kwargs)

# prints
# Spam: <class '__main__.Ham'> {'bacon': 1}
# Spam: <class '__main__.Egg'> {'cheese': 1}
# Ham: <class '__main__.Egg'> {'cheese': 1}

ディスクリプタset_name()メソッド

従来のディスクリプタインターフェース__set_name__() メソッドが追加された。__set_name__()はクラスオブジェクトの作成時に呼び出され、引数としてクラスと属性名を受け取る。

class Descr:
    def __get__(self, instance, owner):
        return f'Descr.__get__({instance}, {owner})'

    def __set_name__(self, owner, name):
        print(f'Descr.__set_name__({owner}, {name})')

class Spam:
    descr = Descr()

のように、クラス Spamディスクリプタ Descrインスタンスを属性として作成すると、クラス Spam 作成時に Descr.__set_name__(Spam, 'descr') が呼び出される。

属性の作成順

メタクラスの一般的な使い方として、クラスの属性を、作成された順番にしたがって処理する、というケースがある。この場合、メタクラスでクラスの名前空間に使用する辞書として、collections.OrderedDict()を使用するようにカスタマイズするのが一般的だが、Python 3.6からは名前空間辞書はデフォルトで登録順序を保存するようになったため、このようなカスタマイズは不要となった。

次のように、クラスの名前空間辞書 __dict__ の要素を取得すると、常に属性が登録された順番で返される。

>>> class Spam:
...     a=1
...     b=2
...     c=3
...
>>> Spam.__dict__.keys()  # 常に同じ順番で取得できる  
dict_keys(['__module__', 'a', 'b', 'c', '__dict__', '__weakref__', '__doc__'])

Python 3.6の概要 (その3 - async関連)

非同期ジェネレータ

現在のPythonでは、ジェネレータを使って、とてもお手軽にイテレータを作成できる。例えば、奇数列を生成するジェネレータは、次のように書ける。

def odds():
    i = 1
    while True:
        yield i
        i += 2

しかし、ジェネレータが存在しなかった頃のPythonでは、わざわざ__iter__メソッドなどの特殊メソッドを実装したクラスを定義し、

class Odds:
    def __init__(self):
        self._cur = 1

    def __iter__(self):
        return self

    def next(self):
        ret = self._cur
        self._cur += 2
        return ret

などと書かなければならなかった。

Python3.5で導入された コルーチン は、イテレータと同様な概念として 非同期イテレータ をサポートしているが、ジェネレータに相当する機能が存在しないため、コルーチンを使用したイテレータを作成するのは非常に面倒な作業となっていた。

このため、Python3.6ではあらたに非同期イテレータを作成する、非同期ジェネレータが提供され、コルーチン内でも普通の関数と同様に yield 式を使って非同期ジェネレータを作成できるようになった PEP 525 -- Asynchronous Generators

import asyncio

# spam()は非同期ジェネレータ
async def spam(): 
    await asyncio.sleep(1)
    yield 'spam1'
    await asyncio.sleep(2)
    yield 'spam2'

async def spam_restrant():
    async for s in spam():  #非同期forループ
        print(s)

loop = asyncio.get_event_loop()
loop.run_until_complete(spam_restrant())

通常のジェネレータオブジェクトは、send(), throw(), close() メソッドで通信できるが、非同期ジェネレータでも同様にasend(), athrow(), aclose() メソッドを使用できる。

非同期内包

リスト内包などの内包式で、非同期for ループを指定できるようになった。PEP 530 -- Asynchronous Comprehensions

import asyncio
async def spam(n):
    for i in range(n):
       yield f'spam{i}'

async def spam_restrant():
    print([s async for s in spam(3)])

loop = asyncio.get_event_loop()
loop.run_until_complete(spam_restrant())

また、内包式中に、 await 式を指定できるようになった。

import asyncio
async def spam(n):
    await asyncio.sleep(0.1)
    return f'spam:{n}'

async def pred(n):
    return n % 2

async def spam_restrant():
    print({n:await spam(n) for n in range(5) if await pred(n)})

loop = asyncio.get_event_loop()
loop.run_until_complete(spam_restrant())

Python3.6の概要(その2 - 変数アノテーション)

変数アノテーション

PEP 484 -- Type Hintsで導入された型ヒントが、モジュールのグローバル変数や、クラスのインスタンス変数などにも指定できるようになった。PEP 526 -- Syntax for Variable Annotations

変数の型アノテーションは、mypy などではコメントとして指定するようになっていたが、Python3.6以降では、正式なPythonの構文で指定できるようになった。

>>> SPAM:str = 'global spam string'  # SPAM は str型グローバル変数

>>> class Ham:
...     EGG: int = 100  # EGGはint型クラス変数

この例では、代入文で変数を作成するのと同時に型を宣言しているが、変数の値を指定せずに、変数の型だけでも宣言できる。この場合は代入文では使用せずに、

>>> SPAM:str  # SPAM は str型グローバル変数

>>> class Spam:
    num: int   # Spam.numはint型

と指定する。デフォルト値の存在しない、クラスのメンバ変数などはこの形式で指定すると良いだろう。

値を指定せずに型だけを指定した場合、変数としては存在しないので実際に代入するまでは参照できない。

>>> HAM:int  # HAMの型宣言。宣言だけで、まだ変数は作成されていない
>>> print(HAM)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  NameError: name 'HAM' is not defined

>>> HAM = 100  # 代入文で変数名を作成
>>> print(HAM)
100

指定した型情報は、モジュールやクラスの__annotations__メンバ変数で参照できる。

>>> class Egg:
...     num: int
...
>>> Egg.__annotations__
{'num': <class 'int'>}

関数内でローカル変数にアノテーションを指定してもエラーとはならないが、指定した結果はどこにも格納されていないようだ。ローカル変数のアノテーションは、単に無視されるだけようだ。

Python 3.6の概要 (その1 - f文字列)

f文字列

これまで、Pythonで文字列に変数を埋め込む方式にはいくつかあったが、ついに式を文字列中に直接記述できるようになった。 (PEP 498 -- Literal String Interpolation)

式を埋め込んだ文字列はf文字列(formatted string:フォーマット済み文字列)と呼ばれ、raw文字列の r と同じように、先頭に f を指定する

f文字列は通常の文字列と同じようも使えるが、文字列内の {} で囲まれた部分は、Pythonの式として評価し、その結果を文字列として出力する。

>>> f'hello'   # 式を含まないf文字列
'hello'

>>> f'100+1={100+1}'  # 式を評価
'100+1=101'

>>> order={'spam':100, 'ham':200}
>>> f'spam: {order["spam"]}, ham: {order["ham"]}' # 変数を参照
'spam: 100, ham: 200'

>>> f'Hello, {input("名前を入力してください: ")}' # 関数の呼び出し
名前を入力してください: いしもと
'Hello, いしもと'

文字列のformatメソッドと同じように、出力フォーマットも指定できる。

>>> f'{100:0>20f}'
'0000000000100.000000'

f文字列内の式は、Pythonコンパイル時に抽出され、実行時に値を評価して結果をフォーマットしている。式の評価は、文字列の左から順に行われる。

>>> import dis, sys
>>> def f(a, b):
...     return f'A: {a}, B: {b!a:10s}'
...
>>> dis.dis(f)
  2           0 LOAD_CONST               1 ('A: ')
              2 LOAD_FAST                0 (a)
              4 FORMAT_VALUE             0
              6 LOAD_CONST               2 (', B: ')
              8 LOAD_FAST                1 (b)
             10 LOAD_CONST               3 ('10s')
             12 FORMAT_VALUE             7 (ascii, with format)
             14 BUILD_STRING             4
             16 RETURN_VALUE

f文字列内の式は、通常の式と変わりなく評価してスタックに積まれ、FORMAT_VALUEというバイトコードで文字列に変換される。最後にBUILD_STINRGで、文字列を結合する、という仕組みのようだ。現行の文字列フォーマットと比べても、とくにオーバーヘッドが大きい、ということもなさそうだ。

仮想継承とsingledispatch

以前、emitjson というパッケージを公開した。使い方は簡単に こちらに 書いたが、要はfunctools.singledispatch() をちょっと使いやすくしたものにすぎない。

singledispatch() は、Python3の抽象基底クラスと組み合わせると、非常に面白い使い方ができる。emitjson を例にして、使い方を紹介してみたい。

emitjson

簡単に emitjson の使い方を說明しておこう

emitjson は、emitjson という名前だが、JSON を出力するためのモジュールではなく、いろいろなオブジェクトを json として出力可能なオブジェクトに変換するためのリポジトリを作成するモジュールだ。特にJSON専用ということもなく、たとえばCSVファイルを作成する場合などでも利用できる。

例として、datatime.date 型の日付と、PIL.Image.Image 型の画像データを変換するリポジトリを作成してみる。

import emitjson
import datetime
import base64
import PIL.Image

myrepo = emitjson.repository()  # emitjson リポジトリを作成

# date型をISO-8601形式の文字列に変換
@myrepo.register(datetime.date)
def conv_date(obj):
    return obj.isoformat()

# Image型をBASE64形式の文字列に変換
@myrepo.register(PIL.Image.Image)
def conv_date(obj):
    return base64.b64encode(obj.tobytes()).decode('ascii')

# 日付と画像の辞書を作成
src = {
    'date':datetime.date.today(),
    'img': PIL.Image.open('img.png')
}

# 辞書に格納した、dateオブジェクトとImageオブジェクトを変換する
converted = myrepo(src) 

仮想継承

上記の例でわかるように、emitjson は、オブジェクトの型を指定して、その変換用関数を登録する singledispatch 関数だ。変換関数は、指定した型そのものだけではなく、その派生型のオブジェクトにも適用される。

ここで、「派生型」とは、isinstance()issubsclass() で派生型と判定されるオブジェクトである、ということだ。singledispatch 関数は、型から対象の関数を取得するとき、単純に型から関数を取得するのではなく、オブジェクトの継承関係を調べ、もっとも「近い」型の関数を適用するようになっている。

ところで、 Pythonisinstance()issubsclass() が派生クラスと判定するのは、通常の継承関係の他に「仮想継承」がある。仮想継承は、直接の継承関係のない型同士であっても、派生クラスと判定されるように設定する機能だ。これを利用すれば、いろいろと面白い singledispatch の利用方法が考えられる。ちなみに、Pythonの仮想継承はC++言語の同じ用語の機能とはまったく無関係なので注意。

ABCMeta.register()

abcモジュールABCMeta メタクラスを利用すると、手軽にいろいろな型のグループを定義できる。例えば、次のような、2つのクラスがあった時、

class Spam:
    def __init__(self, n):
        self.spams = 'spam'*n
    def __str__(self):
        return self.spans

class Ham:
    def __init__(self, n):
        self.hams = 'ham'*n
    def __str__(self):
        return self.hams

どちらのクラスのインスタンスJSONに文字列として出力する関数を、次のように定義できる。

import abc

class ToStringTypes(metaclass=abc.ABCMeta):
    pass

# SpamとHamをToStringTypesの仮想サブクラスとして登録する
ToStringTypes.register(Spam)  
ToStringTypes.register(Ham)

# ToStringTypesの変換関数
@myrepo.register(ToStringTypes)
def toStringConv(obj):
    return str(obj)    # objを文字列化

myrepo(Spam())  # toStringConv(Spam()) が呼び出される

ToStringTypesabc.ABCMetaメタクラスとするクラスで、抽象基底クラスと呼ばれる。この「抽象基底クラス」も、C++言語の用語とは切り離して考えていただきたい。

ここでは、抽象基底クラスで利用可能な register() メソッドを使って、Spam クラスと Ham クラスを、ToStringTypes クラスの派生クラスとして登録する。

どちらのクラスも、親クラスとして ToStringTypes クラスを指定していないが、

isinstance(Spam(), ToStringTypes)`

や、

issubsclass(Ham, ToStringTypes)

は、どちらもTrue を返すようになる。

したがって、ToStringTypes オブジェクトの変換関数として toStringConv() を登録すると、Spam クラスと Ham クラスのインスタンスは、toStringConv() で文字列に変換されるようになる。

この方式では、Spam クラスと Ham クラスに一切手を入れずに、変換関数だけを定義できる。Pythonの標準オブジェクトや、サードパーティ製ライブラリのオブジェクトなどでも、ソースを修正せずに柔軟な変換関数を定義できるというのがポイントだ。

subclasshook()

ABCMeta.register() を使って明示的に仮想サブクラスを登録するのではなく、特定の条件のクラスをサブクラスとして扱うようにもできる。

class HasToDict(metaclass=abc.ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        return hasattr(subclass, 'toDict')

この抽象基底クラス HasToDict は、__subclasshook__()メソッドを定義し、toDictという名前の属性をもつ型ならなんでも派生クラスとなるようにしている。HasToDict クラスを利用して、

class Spam:
    def toDict(self):
        return {'spam':100}

@myrepo.register(HasToDict)
def conv_to_dict(obj):
    return obj.toDict()

のように、toDict() メソッドをもつオブジェクトはすべて辞書に変換する、という変換関数を定義できる。

Python3用パッケージってどのぐらい増えただろ?

Python3を使ってても、PyPIのパッケージがPython3対応かどうか、あんまり心配しなくなった気がする今日このごろです。

体感的にはPython3であまり不自由はない感じになってきたが、実際問題、どの程度Python3対応が進んでいるのか、気になったので簡単に調べてみた。

PyPIの情報は xmlrpcインターフェース で簡単に取得できる。あまり速くないので、全件取得するとかなり時間がかかるが。。。

最新パッケージの情報を取得し、Programming Language として Python 3 を明記しているパッケージの件数をカウントした。もちろん、メタ情報を記述していないパッケージもあるので誤差はあるが、それほど多くはないだろう。

で、2016年6月18日時点でダウンロード可能なパッケージ数は 70524件。そのうち、Python 3 と明示的に表記しているパッケージは。。。

19182件

だった。

全体の約27%。四分の一といったところだ。まだまだPython2の遺産は大きい。

ここで気になるのは、Python3用にリリースされるパッケージは、現在でもPython2用よりもずっと少ないのだろうか?という点だ。

Python2と3の差はこのまま拡大し続けるのだろうか?それともPython3用パッケージのリリースが順調に増えていくのだろうか?

現在のPythonエコシステムは以前に比べて成熟しており、いわゆる「定番」が決まってきている。昔のように、似たようなライブラリが乱立するということも少なくなっている気もしていて、そうなると、新規に公開されるパッケージの数は減ってくるのかもしれない。単純に数だけの比較にこだわってもしかたがないが、目安の一つにはなるだろう。

というわけで、現在ダウンロード可能な全パッケージの最新更新日と、Python3対応状況をグラフにしてみた。

f:id:atsuoishimoto:20160719152844p:plain

活動が活発なパッケージでは、Python3対応がどんどん増えている様子が見て取れる。

現在の最新版が2016年5月1日以降に更新されているパッケージは 8582件。このうち、Python3対応パッケージは 3868件で、約45%となる。現在では、PyPIにリリースされるパッケージの約半数は Python3対応ということだ。

Python3対応のパッケージが全体の半数を超えるには、まだあと数年は必要だろうか。

にもあるように、今がいろいろな点でPythonPython2と3の切り替えが目に見えていくポイントなのではないかと思う。

emitjson

emitjson というPythonモジュールを公開した。

pypi.python.org

emitjson という名前にしたが、じつはJSONは出力しない。 任意のオブジェクトを、json モジュールがサポートしているオブジェクトに変換するときに使うユーティリティだ。

例えば、日付とバイナリデータが入った辞書のリストからJSONを作成しようと思うと、だいたいこんな感じになる。

values = []
for d in src_vlaues:
   date = d['date'].isoformat()
   bin = base64.b64encode(d['bin'])
   values.append({'date':date, 'bin':bin})

json.dumps(values)

意外とめんどくさい。このぐらいなら「めんどくさい」で済むが、要素の数が多いとめんどくささが半端なくなってくる。 json.JSONEncoder をカスタマイズする手はあるが、それもなかなか面倒だ。

そこで、自前でデータを変換する仕組みを考えてみた。collections.abc と、Python3.4で導入された、functools.singledispatch を使えば、簡単に拡張性の高い変換ユーティリティを作れそうだ。

作ってみると、あっさりといい感じに仕上がった。上の処理は次のように書ける。

import emitjson

myrepo = emitjson.repository()  # emitjson リポジトリを作成

# リポジトリに bytes型の変換関数を登録
@myrepo.register(bytes)
def conv_bytes(obj):
    # bytes型をBase64文字列に変換
    return base64.b64encode(obj).decode('ascii')

json.dumps(myrepo(src_vlaues))

emitjson では、リストの要素や辞書のキー・値を走査し、登録した種類のオブジェクトがあれば、自動的に変換を行う。デフォルトでは シーケンス型・マッピング型の操作をサポートしており、datetime.date, datetime.datetime の要素があれば ISO 8601形式の文字列に変換する。

emitjson.repository() をデコレータとして、独自の変換関数を登録できる。上の例では、conv_bytes() 関数は、bytes型のオブジェクトを文字列に変換する。

また、DjangoやSQLAlchemyのモデルなどを辞書オブジェクトに変換する処理は、次のように定義できる。

class Test:
    def __init__(self):
        self.prop1 = 'spam'
        self.prop2 = 'ham'

# Test1クラスを変換する処理の定義
@myrepo.register(Test1)
class Test1Converter(emitjson.ObjConverter):
    prop1 = attr    # obj.prop1 を取得
    prop_a = attr('prop2')    # obj.prop2 を取得

json.dumps(myrepo([Test()]))

同じように、オブジェクトをJSON化可能な形式に変換するモジュールに、bpmappers がある。

bpmappers では主にクラスインスタンスを辞書に変換する機能が主となっているが、emitjson では簡単に任意のオブジェクトを変換できるようになっている。また、コンテナオブジェクトを変換するとその内容を自動的にスキャンし、再帰的に変換を行う点も大きな違いだ。ただし、 emitjson は Python3.4 以降でなければ動かない。Python3.4より前、とくにPython2.xで利用する場合は bpmappers が必須となる。測定はしていないが、おそらくパフォーマンスもbpmappers の方が優れていると思う。