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

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 の方が優れていると思う。

Python文法詳解(一刷) 正誤表

以下の正誤表は、一刷のものです。二刷の正誤表は Python文法詳解(二刷) 正誤表 - atsuoishimoto's diary を参照してください。

第1章

P.1

次のようなというカテゴリーになるでしょう

次のようなカテゴリーになるでしょう

第2章

P.18

>>> 10 // 3
3.0
>>> 10.0 // 3.0
3.0

P.21

>>> S[1:4]
'bcd
>>> S[1:4]
'bcd'

第3章

p.48

>>> a = 1
>>> 100 and x == 1 or 200   # x if y else z と同じ式
>>> x = 1
>>> x == 1 and 100 or 200   # x if y else z と同じ式
100

p.52

base - 文字列を整数に変換する際の基数を指定し、省略時は10進数として変換します 0 の場合は、Python の整数リテラルとして変換します。

base - 文字列を整数に変換する際の基数を指定し、省略時は10進数として変換します。0 の場合は、Pythonの整数リテラルとして変換します。

p.53

>> int('0b100', 0) # 2進数
100
>> int('0b100', 0) # 2進数
4

第4章

P.72

>>> L[8:2:-1]    # 9番目から3番目まで取得
['8', '7', '6', '5', '4', '3']
>>> L[10:1:-3]   # 10番目から1番目まで、後ろから2つおきに取得
['9', '6', '3']
>>> L[8:2:-1]    # 9番目から4番目まで取得
['8', '7', '6', '5', '4', '3']
>>> L[10:1:-3]   # 10番目から3番目まで、後ろから2つおきに取得
['9', '6', '3']

P.74

>>> slice(1, 10)    # [1:10] と同じ
slice(None, 10, 3)
>>> slice(1, 10)    # [1:10] と同じ
slice(1, 10, None)

P.75

>>> L[3:7] = ['a', 'b', 'c', 'd']   # 4番目から7番目までの要素を置き換え
>>> L
[0, 1, 2, 'a', 'b', 'c', 'd', 7, 8, 9]
>>> L[1:4] = ['a', 'b', 'c', 'd']   # 2番目から4番目までの要素を置き換え
>>> L
[0, 'a', 'b', 'c', 'd', 4, 5, 6, 7, 8, 9]

P.88

start、endを指定したときは、スライス演算と同じ規則で指定する範囲から、subを検索します。

start、endを指定したときは、スライス演算と同じ規則で指定する範囲から、valueを検索します。

P.92

c = a;
b = a;
a = c;
c = b;
b = a;
a = c;

P.96

>>> hello = '今日は'
>>> hello.decode('utf-8')
b'\xe4\xbb\x8a\xe6\x97\xa5\xe3\x81\xaf'
>>> hello = '今日は'
>>> hello.encode('utf-8')
b'\xe4\xbb\x8a\xe6\x97\xa5\xe3\x81\xaf'

P.97

>>> s = 'とても
... 長くて
... 一行に収まらない
... 文字列'
>>> s = 'とても\
... 長くて\
... 一行に収まらない\
... 文字列'

P.112

>>> 'spam'.isalnum()
True
>>> 'スパムハム卵'.isalnum()    # 日本語の文字もTrue
True
>>> 'spam'.isalpha()
True
>>> 'スパムハム卵'.isalpha()    # 日本語の文字もTrue
True

P.121

>>> 'spam ham egg sausage'.split(maxsplit=2)   # 最大2回分割
['spam, 'ham', 'egg sausage']
>>> 'spam ham egg sausage'.split(maxsplit=2)   # 最大2回分割
['spam', 'ham', 'egg sausage']

P.129

b'\nspam\nham\negg\n\\t'
b'\nspam\nham\negg\n\t'

P.132

>>> bytes.fromhex('73 70 61 6d'')
>>> bytes.fromhex('73 70 61 6d')

P.146

chars を省略した場合、またはNoneを指定した場合は、全ての空白文字を削除します。

bytes を省略した場合、またはNoneを指定した場合は、全ての空白文字を削除します。

P.147

文字列が + か - の符号ではじまる場合、符号をバイト列の先頭に移動します。

バイト列が + か - の符号ではじまる場合、符号をバイト列の先頭に移動します。

P.152

>>> d['spam'] = 'egg' # キー 'ham' の値を `egg` に上書き
>>> d['spam'] = 'egg' # キー 'spam' の値を `egg` に上書き

P.156

逆にnot in 演算子は、左項のキーが、右項の辞書に含まれない場合に、True を返します。not in 演算子は、逆に辞書に左項のオブジェクトと同じ値の要素が含まれない場合に、True を返します。

逆にnot in演算子は、左項のキーが、右項の辞書に含まれない場合に、Trueを返します。

P.158

また、イテレータによるキーの列挙は、列挙中に辞書が変更されると無効になります。

また、イテレータによるキーの列挙は、列挙中に辞書の要素数が変更されると無効になります。

P.159

他の集合やイテラブルオブジェクトと、集合演算も行えます。

>>> d = {'spam':1, 'ham':2, 'egg':3, 'bacon':4}
>>> view = d.keys()
>>> view - ['spam', 'bacon']
{'spam', 'bacon'}

他の集合やイテラブルオブジェクトと、集合演算も行えます。

>>> d = {'spam':1, 'ham':2, 'egg':3, 'bacon':4}
>>> view = d.keys()
>>> view - ['spam', 'bacon']
{'ham', 'egg'}

P.164

集合オブジェクトが返すキーの順番は一定ではなく。

集合オブジェクトが返すキーの順番は一定ではなく、

P.164

要素の列挙は、列挙中に集合が変更されると無効になります。

要素の列挙は、列挙中に集合の要素数が変更されると無効になります。

P.169

>>> s.discad(1)
>>> s.discard(1)

第5章

P.180

start - インデックス値の初期値を指定します。省略時は 1 となります。

step - 数値の増分を指定します。省略時は 1 となります。

start - インデックス値の初期値を指定します。省略時は 0 となります。

P.184

キーボード割り込みを書けられた時に、

キーボード割り込みをかけられた時に、

第6章

P.193

>>> def spam(ham, egg, *args):
...     print('ham={}, egg={}, args={}'.format(ham, egg, args))}
>>> def spam(ham, egg, *args):
...     print('ham={}, egg={}, args={}'.format(ham, egg, args))

P.204

from ..ham.egg import bacon # 親パッケージのham.egg.spam モジュールをインポート
from ..ham.egg import bacon # 親パッケージのham.egg.bacon モジュールをインポート

P.206

$ cat spamp.zip >>spam
$ cat spam.zip >>spam

P.210

from spam import func_spam
import spam
func_ham = spam.func_spam

P.210

# egg モジュールは、ham モジュールをインポートして、func_spam を呼び出す
import ham

ham.func_spam()
# egg モジュールは、ham モジュールをインポートして、func_spam を呼び出す
import ham
ham.func_ham()

P.210

egg モジュールでは、ham モジュールを経由して、ham.func_spam() 関数を呼び出しています。

egg モジュールでは、ham モジュールを経由して、spam.func_spam() 関数を呼び出しています。

P.210

このように、呼び出し方にかかわらず、関数を作成したモジュールが、常にその関数のグローバル名前空間として検索されです。

このように、呼び出し方にかかわらず、関数を作成したモジュールが、常にその関数のグローバル名前空間として検索されます。

P.220

... super().ham() # 基底型の ham() を呼び出す
... super().spam() # 基底型の spam() を呼び出す

P.227

したがって、インスタンスから属性HAMを取得すると、インスタンス名前空間の検索は失敗しますが、失敗しますが、自動的にクラスの名前空間を次に検索し、

したがって、インスタンスから属性HAMを取得すると、インスタンス名前空間の検索は失敗しますが、自動的にクラスの名前空間を次に検索し、

P.229

def ham(self, arg):
    print(arg)
def egg(self, arg):
    print(arg)

P.229

Spam = type('Spam', (), {'ham':method, 'ham':100})
Spam = type('Spam', (), {'ham':100, 'egg':method})

P.230

type.__prepare__(metacls , name , bases , \*\*kwargs)

パラメタ

  metacls - メタクラス名を指定します。
  name - クラス名を指定します。
type.__prepare__(name , bases , \*\*kwargs)

パラメタ

  name - クラス名を指定します。

第7章

P.250

  • 誤 (7.1.6.6項)

raw -読み込みを行う、バッファなしストリームオブジェクトを指定します。

raw -書き込みを行う、バッファなしストリームオブジェクトを指定します。

P.251

raw -読み込みを行う、バッファなしストリームオブジェクトを指定します。

raw -読み書きを行う、バッファなしストリームオブジェクトを指定します。

P.252

line_buffering - True を指定する行バッファリングとなり、改行文字を出力した後に出力バッファをフラッシュします

line_buffering - True を指定すると、行バッファリングとなり、改行文字を出力した後に出力バッファをフラッシュします

P.262

処理を処理を再開します。

処理を再開します。

P.266

>>> list(x+y for x in 'abc'
... for y in '123' if y < '3'} # for のネストと if
>>> list(x+y for x in 'abc'
... for y in '123' if y < '3') # for のネストと if

P.267

print('Spam: {0.spam}, Ham: {0.ham}'.format(self.spam, self.ham))
print('Spam: {0.spam}, Ham: {0.ham}'.format(self))

P.267

... print('Hello {!'.format(', '.join(names))}
... print('Hello {}!'.format(', '.join(names))}

P.271

メソッド呼び出しので第一引数として、インスタンスではなく、常にクラスオブジェクトが渡されるようになります。

メソッド呼び出しの第一引数として、インスタンスではなく、常にクラスオブジェクトが渡されるようになります。

P.273

>>> spam.x
0
>>> spam.__dict__['ham'] = 999  # インスタンスの属性値を変更
>>> spam.x                      # プロパティは影響を受けない
0
>>> spam.ham
0
>>> spam.__dict__['ham'] = 999  # インスタンスの属性値を変更
>>> spam.ham                    # プロパティは影響を受けない
0

P.274

ダック・タイピング(Dock typing)

ダック・タイピング(Duck typing)

P.288

__getattribute__(attr)

__getattribute__(self, attr)

第8章

P.294

object - 参照カウントをオブジェクトを指定します。

object - 参照カウントを取得するオブジェクトを指定します。

P.299

Pythonは動的なプログラミング言語なので、関数を呼び出したり、オブジェクトを属性を参照したとき、

Pythonは動的なプログラミング言語なので、関数を呼び出したり、オブジェクトの属性を参照したとき、

P.299

そのように、オブジェクトを調査することイントロスペクションと呼びます。

そのように、オブジェクトを調査することをイントロスペクションと呼びます。

P.301

>>> type(unknown_obj).__module__.__file__
'/usr/local/lib/python3.4/site-packages/unknown.py
>>> import unknown_module
>>> unknown_module.__file__
'/usr/local/lib/python3.4/site-packages/unknown_module.py

P.302

行番号 バイト ニーモニック 引数 引数が示す値
       コード
       の位置
行番号 バイト 命令コード 引数 引数が示す値
       コード
       の位置

謝辞

本正誤表の作成にあたっては、下記の皆様をはじめ、多くの方々のご協力を頂きました。心よりお礼申し上げます。

(順不同)

tse 0.0.9

tse(Text Stream Editor) 0.0.9 をリリースした。このリリースでは、--begin オプションと、--end オブションを複数指定できるように修正した。

これまでだと、--begin複数行の値を指定するとき、

$ tse --begin 'print(1)' --begin 'print(2)'

複数--begin を記述する必要があったが、0.0.9 以降では

tse --begin 'print(1)' 'print(2)'

のように、--begin オプションに複数行を指定できるようになった。

この修正により、

tse --begin 'print(1)' *.txt

のように、入力ファイル名を --begin/--end の直後に指定していると、ファイル名がPythonスクリプトとして認識され、エラーとなってしまう。

このような場合は、次のようにスクリプトの終わりを -- で明示的に指定する必要がある。

tse --begin 'print(1)' −− *.txt