Python文法詳解(二刷) 正誤表
第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文は遅い?
こちらの記事を拝見していて、ちょっと気になったので注釈。
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
のような乗算処理では、次のような処理が行われる。
X
が乗算をサポートしているかチェックするX
の乗算関数を取得するY
が被乗数として適切なデータかチェックするX
の値とY
の値を取り出し、乗算する- 乗算の結果から新しい浮動小数点数オブジェクトを作成する
しかし、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. 〜 3. の処理は必要ない。
- 上記の 4. の演算処理は、Pythonの加算処理と乗算処理ではなく、CPUの機能で高速に演算されるようになる。
- 上記の 5. の浮動小数点数オブジェクトを生成せず、ハードウェアがサポートしている浮動小数点数が作成される。
だいぶ速くなったが、やはりまだ Numpy には及ばない。
dot_typed()
では、演算中に <double>(a[i])
のようにして、Numpyの配列から要素を取得して、C言語の double
型に変換している。実は、これもかなり複雑な処理なのだ。
a
が添字によるインデックスをサポートしているかチェックするa
のインデックス関数を取得する- 添字
i
が添字として適切かチェックする i
の整数値を取得するa
からi
番目の値を取得する- 取得した値で、
numpy.float64
オブジェクトを作成する numpy.float64
オブジェクトをdouble
型に変換可能かチェックするnumpy.float64
オブジェクトの変換関数を取得する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が静的な型宣言を行わない言語で、型推論も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されているが、まだ発生するようだ。
回避方法は以下の通り。
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 を試してみた
まだ開発中のフレームワークだが、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/#
を開く。
作成した APIが表示され、INTERACT
をクリックするとAPIを実行できる。
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.py
の create_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サイトなどでよくある、サイドバーに「最近の記事ボックス」などを表示する機能の実装が難しい。
サイトのデザインテーマを指定して既存のデザインを共有する機能があるが、テーマとして指定できるのは一つだけしかなく、指定したテーマ以外からテンプレートやCSS、Javascriptの共有手段する手段がない。
目次の作成機能がない
などだ。
それでは、ということで独自に静的サイトジェネレータを開発した。
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には組み込みのテーマとして
miyadaiku.themes.base - 必要最低限のHTMLテンプレートと、Google Analytics や Open Graph などの、汎用的なマクロを提供
miyadaiku.themes.pygments - Pygmentsによる構文ハイライト用CSS
などがある。
また、別パッケージとして、次のようなテーマを提供している。
- miyadaiku.themes.jquery - プロジェクトに jQueryを組み込む
- miyadaiku.themes.bootstrap4 - プロジェクトにBootstrap4を組み込む
- miyadaiku.themes.fontawesome - プロジェクトにFontawesomeを組み込む
テーマは通常のPythonパッケージと同様に pipなどでインストールし、同時に複数のテーマを利用できる。また、テーマが利用しているテーマは自動的にインポートされる。
API
Jinja2テンプレートからコンテンツの表示や検索を行うAPIにアクセスできるので、独自のページ構成や目次などを自由に作成できる。
コンテンツオブジェクトはアーティクルなどのコンテンツや画像などの素材ファイルなどにアクセスするオブジェクトで、タイトルやカテゴリなどのメタデータの参照や、ページへのリンク作成などを行える。
コンテンツコレクション は、すべてのコンテンツを管理するオブジェクトで、条件を指定してコンテンツを検索できる。検索条件として、プロパティやタグなどのメタデータや、格納ディレクトリなどを指定できる。