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

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

第1章

P.1

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

第4章

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関数

P.240

第7章

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

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

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

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

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 - 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

静的サイトジェネレータ Miyadaiku

ここ数年、www.python.jp は、 Pelican を使って構築していた。

Pelican は実績のある静的サイトジェネレータで使いやすくはあるが、基本的にはBlogサイトの構築ツールであり、あまり柔軟性や拡張性には重点を置かれていないように感じていた。www.python.jp 以外でもいくつかのサイト構築に使用したが、以下のような不満を感じていた。

  • アーティクルに Jinjaテンプレートを書きたい

    reStructuredTextやMarkdown には、定型文などを記述するため手段として、エクステンションやディレクティブなどを開発して組み込む仕組みがあるが、開発・管理はそれなりに面倒で、そう気軽には作れない。Jinjaのマクロ機能などを使って、手軽に拡張できる仕組みがほしい。

  • アーティクル全体を検索するAPIがない。このため、Blogサイトなどでよくある、サイドバーに「最近の記事ボックス」などを表示する機能の実装が難しい。

  • サイトのデザインテーマを指定して既存のデザインを共有する機能があるが、テーマとして指定できるのは一つだけしかなく、指定したテーマ以外からテンプレートやCSSJavascriptの共有手段する手段がない。

  • 目次の作成機能がない

  • 画像ファイルなどを、専用のディレクトリに保存しなければならない。アーティクルと同じディレクトリに保存したい。

などだ。

それでは、ということで独自に静的サイトジェネレータを開発した。

miyadaiku.github.io

Jinja2テンプレート

Miyadaikuでは、reStructuredTextとMarkdown用にJinja2を記述するための 拡張 を提供しており、テンプレートだけでなく、コンテンツ中でもJinja2でHTMLを生成できる。

Sample Miyadaiku article
---------------------------

This is a *plain* reST article.

.. jinja::
    {% for i in range(10) %}
      <p>{{ i }}</p>
    {% endfor %}

テーマ

作成したテンプレートやCSS、画像などの素材・コンテンツからPythonのパッケージを作成し、テーマとして再利用できる。Miyadaikuには組み込みのテーマとして

などがある。

また、別パッケージとして、次のようなテーマを提供している。

テーマは通常のPythonパッケージと同様に pipなどでインストールし、同時に複数のテーマを利用できる。また、テーマが利用しているテーマは自動的にインポートされる。

API

Jinja2テンプレートからコンテンツの表示や検索を行うAPIにアクセスできるので、独自のページ構成や目次などを自由に作成できる。

コンテンツオブジェクトはアーティクルなどのコンテンツや画像などの素材ファイルなどにアクセスするオブジェクトで、タイトルやカテゴリなどのメタデータの参照や、ページへのリンク作成などを行える。

コンテンツコレクション は、すべてのコンテンツを管理するオブジェクトで、条件を指定してコンテンツを検索できる。検索条件として、プロパティやタグなどのメタデータや、格納ディレクトリなどを指定できる。