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

リスト内包のひみつ

こちらのTweetが Python.jp slack でちょっと話題になっていた。

次のようなコードだ

>>> a = [lambda: print(i) for i in range(3)]
>>> for i in a: i()
2
2
2

結論としては cocoatomo さんの書かれているように、変数の評価タイミングの問題で、

対処としては、次のように、出力する値を、関数の実行時ではなく、関数の作成時に決定する必要がある。

>>> a = [lambda x=i: print(x) for i in range(3)]
>>> for i in a: i()
0
1
2

もともとの問題はこれで解決するのだが、このコード、単純だがPython2とPython3では動作が異なっている。

Pythonの動作を理解するのに良い教材だと思うので、ちょっと解説してみよう。

Python2の場合

Python2では、先程のコードは、次のようなループとして実行される。

>>> from __future__ import print_function
>>> a=[]
>>> for i in range(3):
...     f = lambda : print(i)
...     a.append(f)
...
>>> for i in a: i()
2
2
2

リスト内包式で使われているループ変数 i は、グローバル変数 i として、リスト内包式を終了した後も参照できる。

i の値は、必ず 2 となる。

>>> a = [lambda: print(i) for i in range(3)]
>>> i
2

この場合、lambda 関数の print(i) という式は、グローバル変数i を参照して、出力している。

したがって、 lambda 関数を実行する前に i の値を変更すると、出力も変化する。

>>> a = [lambda: print(i) for i in range(3)]
>>> i = 10000000
>>> for u in a:u()
...
10000000
10000000
10000000

Python3の場合

Python3では、リスト内包は次のように展開され、関数呼び出しとして実行される。

>>> _it = range(3)
>>> def _listcomp(_it):
...     ret = []
...     for i in it:
...         ret.append(lambda : print(i))
...     return ret
...
>>> a = _listcomp(_it)
>>> for i in a: i()
2
2
2

Python2と違って、Python3ではループ変数 i は外部からは参照できない。リスト内包は関数内で実行され、 iグローバル変数ではなくローカル変数となるためだ。

>>> a = [lambda: print(i) for i in range(3)]
>>> i
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'i' is not defined

Python2 では lambda 関数の print(i) は、グローバル変数 i を参照するが、Python3 では、lambda 式はローカル変数 i を参照するクロージャとなる。

クロージャとは、Pythonの関数のなかに別の関数を作成した時、親となる関数のローカル変数を子となる関数が参照する仕組みのことだ。

クロージャの仕組みは、以前

atsuoishimoto.hatenablog.com

に書いた。

したがって、Python2のように、後から i の値を変更することはできない。

>>> a = [lambda: print(i) for i in range(3)]
>>> i = 10000000
>>> for u in a:u()
...
2
2
2

Python2/3の違い

Python2 と Python3 では、リスト内包の実現方法に上記のような違いがある。これは、Python2 では

>>> i = 10000
>>> a = [lambda: print(i) for i in range(3)]
>>> print(i)
3

のように、リスト内包式があると他のローカル変数を上書きしてしまい、わかりにくいという問題を解決するためだ。

この経緯は

python-history-jp.blogspot.jp

でも解説されているので、参照されたい

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

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

第3章

P.50

>>> 1 + 1j  # 整数 + 複素数
(1+2j) # 結果は複素数となる
>>> 1 + 1j  # 整数 + 複素数
(1+1j) # 結果は複素数となる

第4章

P.113

文字列が空でなく、全て数を表す文字なら

文字列が空でなく、全ての文字が数を表す文字なら

P.118

>>> ' スパムハム '.find(' ハム ', 4)
-1
>>> ' スパムハム '.rfind(' ハム ', 4)
-1

検索失敗がエラーとなるよ うなケースでは index() を使い

検索失敗がエラーとなるよ うなケースでは rindex() を使い

第6章

P.201

スクリプトファイルやディレクトリを格納した zip ファイル

スクリプトファイルやディレクトリを格納した Zip ファイル

P.206

$ zip -r spam.zip __main__.py
$ zip  spam.zip __main__.py

P.210

図中

Func_spam関数

func_spam関数

第7章

P.240

x | ファイルを新しくファイルを作成し......

x+ | ファイルを新しくファイルを作成し......

x | 新しくファイルを作成し......

x+ | 新しくファイルを作成し......

P.261

イテレータオブジェクトから値を取得すると

このように、ジェネレータが作成したイテレータオブジェクトから値を取得すると

P.262

>>> spam.send('One')    # 次の yield 式まで実行
One
2
>>> spam.send('Two')   # 最後まで実行
Two
>>> gen.send('One')    # 次の yield 式まで実行
One
2
>>> gen.send('Two')   # 最後まで実行
Two

P.267

インスタンスから属性の値を取得・設定するとき、クラスの属性としてディスクリプタという種 類のオブジェクトを登録すると、

クラスの属性としてディスクリプタという種 類のオブジェクトを登録すると、インスタンスから属性の値を取得・設定するとき、

索引

P.309

(追加)

dict.values() -------------------- 159

Pythonのfor文は遅い?

bicycle1885.hatenablog.com

こちらの記事を拝見していて、ちょっと気になったので注釈。

PythonやRを使っている人で、ある程度重い計算をする人達には半ば常識になっていることとして、いわゆる「for文を使ってはいけない。ベクトル化*1しろ。」という助言があります。 これは、PythonやRのようなインタープリター方式の処理系をもつ言語では、極めてfor文が遅いため、C言語Fortranで実装されたベクトル化計算を使うほうが速いという意味です。

昔からよくこういう言い方がよくされるが、本当にPythonのfor文は遅いのだろうか。

聞くところによるとRのfor文はガチで遅いそうだが、Pythonの計算が遅いのはインタープリタ方式だからでも、for文が遅いからでもない。もちろん、Pythonインタープリタなので遅いし、for文だって極めて遅い。しかし、これはPythonの計算が遅い要因の一部でしかない。

まずは手元の環境(Macbook Air 2015, Python 3.6)で速度を測ってみよう。以下のコードはすべて Jupyter Notebookで実行している。

import numpy as np
a = np.ones(100000)
b=np.ones(100000 )

def dot(a, b):
    s = 0
    for i in range(len(a)):
        s += a[i] * b[i]
    return s

timeit dot(a, b)
41.6 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

timeit np.dot(a, b)
62 µs ± 5.6 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Pythonのループを使った演算と、numpyを使った演算ではパフォーマンスに大きな差がある。これは for文が遅いから なのだろうか?

試しに、演算をせずにforループだけを実行してみよう。

def loop(a, b):
    s = 0
    for i in range(len(a)):
        pass
    return s

timeit loop(a, b)
3.44 ms ± 129 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

演算を行わず、forループを実行するだけなら、全体の10%以下しかかかっていない。まあ、forループ遅いが、全体の遅さの主犯ではないようだ。

では、Pythonの遅さの残り9割はどこからくるのだろう?

ここからは、Cythonを使って原因を探っていこう。

まず、Cythonで dot()C言語に変換する。

%%cython
def dot_cython(a,b):
    s = 0
    for i in range(len(a)):
        s += a[i] * b[i]
    return s

timeit dot_cython(a,b)
21.9 ms ± 717 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

単純にC言語に変換しただけでは、それほど変化はない。

ここでは、dot_cython()for ループは、CythonによってC言語のループに展開されており、Pythonのようなループによるオーバーヘッドはなくなっている。

また、Pythonバイトコードを経由せずに実行しているため、ループ以外のPythonインタープリタのオーバヘッドもなくなっている。処理時間が 41.6 ms -> 21.9 ms と約半分になっているが、これはほぼインタープリタのオーバヘッドが解消したためだ。

ここでわかるのは、単純にPythonと同じ処理をC言語で書き直すだけでは、numpyの62µsという圧倒的な速度には遠くおよばない、ということだ。 Pythonインタープリタである、というのも、Pythonの遅さの原因の一部でしかないのである。

インタープリタ言語というと、何もかもがコンパイル型言語より数百倍遅くなるようなイメージがあるかもしれないが、それほど極端な差はつかないものだ。

Pythonの遅さの別の原因として、Pythonが静的な型定義を持たない、という点がある。

例えば、dot() では s += a[i] * b[i]という式を実行しているが、この中の X*Y のような乗算処理では、次のような処理が行われる。

  1. X が乗算をサポートしているかチェックする
  2. X の乗算関数を取得する
  3. Y が被乗数として適切なデータかチェックする
  4. X の値と Y の値を取り出し、乗算する
  5. 乗算の結果から新しい浮動小数点数オブジェクトを作成する

しかし、C や Javaのような、静的な型定義をもつプログラミング言語では、そもそも乗算を行えないような処理はコンパイルエラーとなるため、 上記の1. 〜 3. の処理の必要がなく、さまざまな最適化を行って処理を高速化できる。

Cythonでは、明示的にC言語のデータ型を指定して値を変換できる。まず、数値演算処理の部分にデータ型を宣言し、高速化してみよう。

%%cython
def dot_typed(a,b):
    cdef double s = 0.0
    for i from 0 <= i < len(a):
        s += <double>(a[i]) * <double>(b[i])
    return s

timeit dot_typed(a,b)
11.8 ms ± 189 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

この宣言により、次のような高速化が行われる。

  1. 上記の1. 〜 3. の処理は必要ない。
  2. 上記の 4. の演算処理は、Pythonの加算処理と乗算処理ではなく、CPUの機能で高速に演算されるようになる。
  3. 上記の 5. の浮動小数点数オブジェクトを生成せず、ハードウェアがサポートしている浮動小数点数が作成される。

だいぶ速くなったが、やはりまだ Numpy には及ばない。

dot_typed() では、演算中に <double>(a[i]) のようにして、Numpyの配列から要素を取得して、C言語double 型に変換している。実は、これもかなり複雑な処理なのだ。

  1. a が添字によるインデックスをサポートしているかチェックする
  2. a のインデックス関数を取得する
  3. 添字 i が添字として適切かチェックする
  4. i の整数値を取得する
  5. a から i 番目の値を取得する
  6. 取得した値で、numpy.float64 オブジェクトを作成する
  7. numpy.float64 オブジェクトを double 型に変換可能かチェックする
  8. numpy.float64 オブジェクトの変換関数を取得する
  9. double 型に変換する

この処理をスキップして、Numpy配列からデータを直接取得してみよう。

実は、Numpy配列には double 型のデータが格納されており、適切なデータ型を指定して直接参照してしまえば、変換は一切必要なくなってしまう。Numpy内部では、このような形式で要素を参照して効率的に処理を行えるようになっている。

Cythonには、Numpyなどのバッファを直接参照する、Typed Memoryview 型が備わっている。この機能で、単なる double 型データの入った配列としてアクセスできるようにしてみよう。

%%cython
def dot_view(double[:] a, double[:] b):
    cdef double s = 0.0
    for i from 0 <= i < len(a):
        s += a[i] * b[i]
    return s

timeit dot_view(a,b)
152 µs ± 6.32 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

おお。速くなった。データをPythonプロトコルを使って取り出す部分が最後のボトルネックだったわけで、この部分を最適化することで、Numpyの4割ぐらいのパフォーマンスまで迫ることができた。

のこりの速度差は、おそらくNumpy内部の内積処理の、高度な最適化によるものだと思う。もう一段階せまってみようかと思ったが、面倒なのでやめた。

つまり

  • Pythonを使った処理は遅くなるが、インタープリタだから、というのは実はそれほど大きな要因ではない。

  • Pythonの演算が遅い最大の要因は、Pythonが静的な型宣言を行わない言語で、型推論JITもなく、常に動的にオブジェクトの演算を行う、という点にある場合がほとんどだ。

  • Numpyでは、配列をすべて同じデータ型しか格納できない、 Homogeneous なコンテナとすることで、効率的に計算を行えるようにしている。

  • Numpy 「は?型推論?ぜんぶfloatにすればよくね?」

ついでに

Pythonが遅い原因として、GIL(Global Interpreter Lock) によってマルチコアをうまく使えないから、と言われることもある。

これもまあ、なくはない。

しかし、仮にGILがなくとも、Pythonの演算はせいぜいCPU数分しか速くならない。CPUが16個あってもたかだか16倍になるにすぎない。これではとてもNumpyには対抗できないのである。

おまけに

この根本的な原因は、今のJulia(v0.3.7)には破壊的な演算子が無いため、いつでも新しい配列を確保してしまう点にあります。

Pythonには、破壊的な代入演算子( += など) がある。

石本敦夫氏に聞く、Pythonの歴史とこれから〜Pythonエンジニア列伝 Vol.3 - Python学習チャンネル by PyQ でも話したが、Pythonにこの種の代入演算子が導入されたのは、実はNumpyで使用するためだった。

Python1.5までは、+= の導入には否定的な意見が多かった。これは、

X = [1,2,3]
X += [4,5]

のように、リストなどの更新可能なオブジェクトなら、リストオブジェクト X に新しく要素を追加すればよい。

しかし、同じようなスクリプトでも、

X = (1,2,3)
X += (4,5)

では、Xは更新不可能なタプルオブジェクトなので、要素を追加できない。この場合は、X に要素が追加されるのではなく、新しく (1,2,3,4,5) というタプルオブジェクトが、X に代入されることになる

このような判りにくさから、+= は導入されないという判断がくだされていた。

しかし、Numpyで大きな配列を効率的に演算するため、ということで必要性を認められ、導入されたのである。

GitLab PagesのDeployが失敗する場合の対処方法

GitLab Pages にドキュメント生成するたびに同じ落とし穴にハマるので、ここに記しておく。

GitLab Pagesにドキュメントを生成すると、ビルドが成功しても、その後のDeployが失敗する場合がある。

この問題は次のチケットでGitLabに報告済みで、もうすでにCloseされているが、まだ発生するようだ。

gitlab.com

回避方法は以下の通り。

  • artifacts: に指定するドキュメントの path: は、かならず public という名前のディレクトリとする。
  • public ディレクトリは、かならず新規に作成する。

Sphinxをつかってpdfやドキュメントを生成するときは、次のように public ディレクトリを新規作成し、生成したファイルを public ディレクトリに移動するようにする。

.gitlab-ci.yml の例を以下に示す。

# Sphinxによるドキュメント生成の例

image: atsuoishimoto/ubuntu-latex

pages:
  script:
    - make latexpdf
    - make html
    - mkdir public
    - mv _build/latex/*.pdf public/
    - mv _build/html/* public/
  artifacts:
    paths:
    - public
  only:
    - master

ダイクストラおばちゃん

最近、偶然プログラミング初心者に接する機会が続いた。初心者にもいろいろあるが、中でも印象深い女性のことを思い出したので書いておきたい。

大昔、ちょっとした業務改善のシステムを開発することになって、実際にその業務を行っている事務や経理の方々に話を伺ったことがある。

この時お会いした年配の女性が、すさまじいほどのExcelのエキスパートだった。当時のPC環境はまだまだ原始的で、動作も不安定だったが、彼女は独学でExcelマクロを開発し、かなりの業務の自動化に成功していた。

話を聞いてみると、とくにプログラミングの勉強をしたことはなく、本を数冊読んだ程度で、あとはExcelのヘルプだけを頼りにマクロを組み上げたらしい。まだインターネットもさほど普及していない時代だ。ほぼ自分の頭だけで考えて、ここまでたどり着いたのだろう。素晴らしい出来栄えだった。

一番驚いたのは、彼女が重要なソフトウェア工学の原則を独力で再発見していたことだ。「処理は機能ごとの関数に分割するとわかりやすいんですよ」とか、「意味のある名前を付けてあげるのが一番大事なんです」とか、自分の発見を体系化し、明確なルールとして運用していたのだ。私は戦慄した。なんてことだ、このおばちゃんダイクストラやんけ!

この時点で、彼女がExcelを使い始めてからまだ数年。どこまで成長するのだろうか。私は背筋に冷たいものを感じながら彼女に礼を言い、インタビューを終了したのだった。

API star - Python3用 Web API framework

最近何度か名前を目にした Webアプリケーションフレームワーク API Star を試してみた

github.com

まだ開発中のフレームワークだが、Pythonの型アノテーションをうまく利用して、Web APIを簡単に開発できるようになっている。

インストール

githugからソースをダウンロードして実行してみた

$ git clone https://github.com/encode/apistar.git
$ pip install -e .

アプリケーションの作成

apistar コマンドでひな形を作成する。

$ apistar new test
test/app.py
test/tests.py

アプリケーションの実行

作成した app.py を実行する。

$ python app.py start
 * Running on http://127.0.0.1:8080/ (Press CTRL+C to quit)
 * Restarting with fsevents reloader
 * Debugger is active!
 * Debugger PIN: 233-378-852

APIの追加

app.py を次のように修正する

from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls
from apistar import typesystem

def welcome(name=None):
    if name is None:
        return {'message': 'Welcome to API Star!'}
    return {'message': 'Welcome to API Star, %s!' % name}


# ここから追加
class IntProp(typesystem.Integer):
    minimum = 1
    maximum = 5

class EnumProp(typesystem.Enum):
    enum = ['one', 'two', 'three']

class RetType(typesystem.Object):
    properties = {
        'strprop': typesystem.string(max_length=100),
        'intprop': IntProp,
        'enumprop': EnumProp,
    }

def api1(intprop: IntProp, enumprop: EnumProp) -> RetType:
    return RetType(strprop='abc', intprop=intprop, enumprop=enumprop)

# ここまで追加


routes = [
    Route('/', 'GET', welcome),
    Route('/api1', 'GET', api1),   # この行を追加
    Include('/docs', docs_urls),
    Include('/static', static_urls)
]

app = App(routes=routes)


if __name__ == '__main__':
    app.main()

この例では、2つのデータ型を定義して、関数 api1() の引数として指定している。一つは 1 から 5 までの整数値を表す型で、もう一つは文字列のone, two, three のいずれかの文字列の列挙型だ。

app.py を再起動し、ブラウザで http://127.0.0.1:8080/docs/# を開く。

f:id:atsuoishimoto:20170912083047p:plain

作成した APIが表示され、INTERACT をクリックするとAPIを実行できる。

f:id:atsuoishimoto:20170912083326p:plain

Webページの作成

APIだけではなく、Jinjaテンプレートを使って通常のHTMLも作成できる。

def generate_htmlpage(name, templates: Templates):
    templ = templates.get_template('hello.html')    # templates/hello.htmlを使用
    return templ.render(username=name)

RDBの利用

RDBへのアクセス手段として、SQLAlchemyとDjango-ORMを使用できる。

from apistar import Include, Route
from apistar.frameworks.wsgi import WSGIApp as App
from apistar.handlers import docs_urls, static_urls

from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

from apistar.backends import sqlalchemy_backend

Base = declarative_base()

# Testテーブルの定義
class Test(Base):
    __tablename__ = "Test"
    id = Column(Integer, primary_key=True)
    name = Column(String)


def create_test(session: sqlalchemy_backend.Session, name: str):
    # Testテーブルにデータを登録し、idを返す
    test = Test(name=name)
    session.add(test)
    session.flush()
    return {'id': test.id}

routes = [
    Route('/create_test', 'GET', create_test),
    Include('/docs', docs_urls),
    Include('/static', static_urls)
]

# Configure database settings.
settings = {
    "DATABASE": {
        "URL": "sqlite:///test.sqlite",
        "METADATA": Base.metadata
    }
}

app = App(
    routes=routes,
    settings=settings,
    commands=sqlalchemy_backend.commands,  # Install custom commands.
    components=sqlalchemy_backend.components  # Install custom components.
)


if __name__ == '__main__':
    app.main()

この例では、SQLAlchemyとSQLiteを使ってデータを登録している。app.pycreate_tables コマンドでテーブルを作成できる。

$ python app.py create_tables

tse 0.0.15リリース

tse 0.0.15をリリースした。

  • 組み込み関数 P() を追加

Python3以降では、print(...) の代わりに P(...) と書けるようにした。

  • コマンド置換

Python3以降では、`command` で コマンドとして command を実行するようにした。

例:

$ tse -b 'P(`ps`)'
  PID TTY          TIME CMD
18447 pts/1    00:00:00 bash
18487 pts/1    00:00:00 tse
18488 pts/1    00:00:00 sh
18489 pts/1    00:00:00 ps

これだけだとあまり役に立たないが、Python3.6以降では、fプリフィックスをつけて変数を展開できる。

$ ls | tse -s '\.py$' 'P(f`wc {L}`.rstrip())'
       0       0       0 __init__.py
     436    1191   14696 main.py