できる 仕事がはかどるPython自動処理 全部入り。を執筆しました

インプレスから「できる」シリーズの Python 版として発売されました。著者の1人として執筆に関わったので紹介します。

book.impress.co.jp

名前が長いので本稿では本書のことを「できるPy」と呼びます。誰も聞いていませんが、ハッシュタグ#dekiru_py です。

経緯

2018年3月頃に知人からビジネスパーソンを対象に本書の監修として企画・構成をしてほしいといった依頼をいただきました。そのとき、書き手はやる気のある若い人たちがたくさんいるからと、私が目次を作って全体の構成をまとめたら誰かがコンテンツを書いてくれて、私は出来上がったコンテンツをレビューするのが主な役割になるのかなと安易に考えていました。

結果的には私が他の執筆者全員に声をかけて書いてもらうようにお願いして、私自身も著者の1人として少なくない割合を執筆しました。

詳細な目次

インプレスサイトでは目次が公開されていません。Amazon のサイトにも章レベルの目次しかありません。店頭で目次チェックするのも面倒だと思うので詳細な目次を書いておきます。目次のタイトルは編集者さんが対象とする読者層、ビジネスパーソンな方々がわかりやすいようにつけてくれました。

次のような「ここもポイント」という見出しでちょっとした小話や発展的なヒントをいくつか書いています。その目次も一緒に含めておきます。

f:id:t2y-1979:20190525120231p:plain:w480

Chapter 1 Pythonのプログラムを利用する前に … 011

Pythonの紹介

  • 001 Pythonがビジネスに役立つってホント? … 012
    • ここもポイント | インデントルールの背景 … 014

実行環境の構築

実行環境の注意点

  • 004 OSによる違いを把握する … 032

Chapter 2 コマンドラインインターフェース … 035

コマンドラインインターフェース

インタラクティブシェル

Chapter 3 サードパーティライブラリのインストール … 059

パッケージマネージャ

  • 011 パッケージマネージャを使ってライブラリをインストールする … 060
    • ここもポイント | その他の処理系 … 061
  • 012 condaを使ったパッケージのインストール … 062
  • 013 pipを使ったパッケージのインストール … 067

Chapter 4 Pythonのおさらい … 069

プログラミング用語

  • 014 覚えておきたいPython&プログラミングの基本用語 … 070

用語と文法

  • 015 Pythonプログラミングの基本 … 074

基本テクニック

Chapter 5 ファイルの操作と圧縮・展開 … 097

ファイル一覧の取得

  • 018 フォルダ内のファイルを一覧表示する … 098
  • 019 特定種類のファイルだけを表示する … 101

ファイルの圧縮/展開

  • 020 ZIPファイルを作成/展開する … 103
  • 021 ZIPファイルに含まれるファイル名を文字化けさせずに表示する … 107
  • 022 ZIPファイルに含まれるファイル名を文字化けさせずに展開する … 116

Chapter 6 画像の加工 … 119

Pillowによる画像加工

  • 023 Pillowを使った画像処理の基本 … 120
  • 024 画像のサイズ変更と切り抜きを行う … 123
  • 025 画像を回転する … 125
  • 026 画像をモノクロにする … 126

画像の連続処理

  • 027 画像のサイズをまとめて調整する … 127

piexif

Chapter 7 CSVファイルの処理 … 137

CSVファイルの読み込み

  • 029 CSVファイルを読み込む … 138
    • ここもポイント | ちょっとした集計にも役立つ関数 … 142
  • 030 読み込んだCSVファイルを1行ずつ処理する … 143
  • 031 ヘッダー行がないCSVファイルを読み込む … 145
  • 032 文字コードを指定してCSVファイルを読み込む … 146

CSVファイルの書き込み

  • 033 CSVファイルを書き込む … 148
  • 034 列の順番を指定してCSVファイルを書き込む … 151
  • 035 インデックス列を出力せずにCSVファイルを書き込む … 152
  • 036 項目(値)にクォートを付けてCSVファイルを書き込む … 153
  • 037 タブ区切りのデータとしてファイルを書き込む … 158
  • 038 読み込んだCSVファイルに列を追加して書き込む … 159
  • 039 JSONデータを読み込んでCSVファイルを書き込む … 160

CSVビューアーの作成

  • 040 横に長いデータを縦向きで表示する … 163
  • 041 複数の値を扱うときに便利な組み込み関数 … 171
  • 042 指定した列番号の情報だけを表示する … 173
    • ここもポイント | 大きなプログラムを作るときのコツ … 174

Chapter 8 テキストデータの処理 … 175

文字列操作

  • 043 文字列の基本的な操作 … 176

正規表現

  • 044 正規表現を使った文字列の扱い … 184
  • 045 正規表現にマッチするすべての文字列を取り出す … 189
  • 046 HTMLファイルからタグを取り除く … 191
  • 047 非構造化テキストをディクショナリにする … 193

テキスト抽出

  • 048 Microsoft Wordからテキスト抽出 … 196
  • 049 Microsoft PowerPointからテキスト抽出 … 202
  • 050 PDFからテキスト抽出 … 207
  • 051 Markdown形式のドキュメントをHTMLに変換する … 215

形態素解析

  • 052 テキストから重要語句を抜き出す … 219

Chapter 9 Microsoft Excelとの連携 … 225

Excelとopenpyxl

ワークブックの操作

  • 054 ワークブックを扱う … 228
  • 055 既存のExcelファイルを読み込む … 230

セルの操作

  • 056 セルを扱う … 234

グラフの作成

  • 057 CSVファイルを読み込んでグラフを作成する … 239

条件付き書式

  • 058 条件付き書式を扱う … 247
  • 059 値や数式を使った条件付き書式を設定する … 254
  • 060 設定済みの条件付き書式を調べる … 257

Chapter 10 Webスクレイピング … 261

Webスクレイピングの概要

Beautiful Soup

  • 062 Webページから要素を取り出す … 264
  • 063 Webページ内の画像を取り出す … 272

Selenium

  • 064 Webブラウザを自動で制御するには … 276
  • 065 WebページをSeleniumで操作する … 279
    • ここもポイント | SeleniumとBeautiful Soupの違いと使い分け … 283

feedparser

  • 066 RSSを利用してニュースを取得する … 284

Chapter 11 Web API … 287

Web APIの概要

  • 067 Web APIとは … 288
    • ここもポイント | Web APIは変化する … 289

Google Sheets API

  • 068 Google Sheets APIの利用準備をする … 292
    • ここもポイント | 鍵ファイルの管理 … 297
  • 069 Googleスプレッドシートを操作する … 299
    • ここもポイント | 鍵ファイルとprepare_credentials関数 … 301

Google Calendar API

複数のWeb APIの組み合わせ

退屈なことはPythonにやらせよう ――ノンプログラマーにもできる自動化処理プログラミング

www.oreilly.co.jp

オライリー・ジャパンさんから2017年6月に出版されています。

本書の依頼をうけたときに出版社の編集者さんから「退屈なことはPythonにやらせよう」がビジネスパーソン向けに人気があるというお話を伺いました。2017年6月の出版で半年しか販売期間がなかったにも関わらず、次の2018年の技術書ランキングではトップ10内に君臨していてその人気ぶりが伺えます。

gihyo.jp www.shoeisha.co.jp

私も技術書ランキングなどで取り上げられているのを見聞きしていたので本の名前は知っていました。しかし、実際に読んではいませんでした。そこで「できるPy」の依頼を受けたときに購入してどのような内容かを確認しました。

「退屈なことはPythonにやらせよう」は616ページあります。これは Python 入門と実践的なプログラミングの2つの題材が1冊になっているからです。これから初めてプログラミングを学ぶ人にとっては入門から始めてステップアップしながら1冊でたくさんのことを学べます。インタラクティブシェルを使って実際に1行ずつコードを入力・実行しながらその結果を確認して理解していくといったスタイルで書かれています。実際に手を動かしながら学べることから初学者向けとしてとてもよい本だと私は思います。

書店巡り

執筆はそれなりに苦労したにも関わらず、もしくは苦労した甲斐もあって実際に書籍としてリアルな本を手に取ったとき、なんとなく嬉しくなって舞い上がってしまいました。コアタイムが過ぎたら退勤して、近くの書店巡りをして実際に置かれているか調べに行きました。

f:id:t2y-1979:20190523160422j:plain:w1024
丸善ジュンク堂さんの棚

f:id:t2y-1979:20190523162648j:plain:w1024
紀伊国屋さんの棚

私が巡った書店ではどちらも本書の師匠である「退屈なことはPythonにやらせよう」の隣に並べられていました。お客さんにとってどちらが良いかを比較して選びやすい最適な棚配置だと私は思います。

「できるPy」と「退屈なことはPythonにやらせよう」のどちらを選べばよいですか?

と質問をしたくなるでしょう。

もし質問者がこれからプログラミングを学び始めようという方であれば、私は迷わず「退屈なことはPythonにやらせよう」をお勧めします。

「退屈なことはPythonにやらせよう」は素晴らしい内容ですし、扱っている題材も多くすべての章節を読まなくても読者の興味がある分野をみつけるときに「できるPy」では扱っていない題材をみつけることもできます。

じゃあ「できるPy」は発売時点でオワコン?

いえいえ。そんなことはありません。

プログラミングの勉強に限った話ではありませんが、勉強というのは結局のところ独学が基本です。もちろん勉強会へ行ってモチベーションをあげたり、セミナーに通って講師に指導を受けたり、友だちにわからないところを教えてもらったり、いろんな勉強のスタイルはあります。しかしながら、ある程度のスキルを身に着けるには独学でたくさんの時間をかけて学ぶ必要があります。

書籍というのは最も身近で安価に独学を助ける教材の1つです。たくさんの書籍がある理由の1つとして、人それぞれに趣味趣向があり、本人が読みやすい分かりやすいと思う書籍の内容、構成、分量、体裁は異なると私は考えています。本書は熱意のある編集者さんが体裁を整えて校正してくれたおかげで初学者にとってかなり親切な内容になっていると私は思います。いくら詳細に説明されていても読まない本より、基本的なことしか書いていなかったとしても読む本の方が独学の助けになります。

大事なことは自分にとって気に入った本を選ぶこと、独学を続けられそうな本を選ぶことだと私は思います。その視点から「できるPy」が「退屈なことはPythonにやらせよう」よりも勝るかもしれない点をいくつかあげます。

  • 書籍の値段
  • コンテンツの見た目
  • 技術的詳細よりも動くコード

書籍の値段

とてもわかりやすい比較指標です。

  • 「退屈なことはPythonにやらせよう」: 3,996 円 (税込)
  • 「できるPy」: 2,484 円 (税込)

単純な値段の比較で「退屈なことはPythonにやらせよう」が割高であると私は思いません。こちらは Python 入門についても丁寧に説明されているので分量が多くなっています。むしろ内容の充実度からすれば割安といえます。しかし、いまや数多くの Python 入門書が出版されていることから読者によっては他の書籍で Python 入門を終えていて、入門についての内容は不要という人もいるでしょう。

Python 入門が不要、且つ読者が読みたい題材を「できるPy」が扱っているのであれば、値段の安い方を選択するのもよいでしょう。

コンテンツの見た目

私は本を選ぶときに見た目を気にする方です。漫画も好きでよく読む方なのですが、絵柄の雰囲気が好みかどうかがその連載を読み続けるかどうか、時間をかけて読むかどうかに大きく影響します。そのため、自分にとって見やすいかどうかをぱらぱらページをめくりながら判断します。

もちろん知りたい内容がその本しか扱っていないのであればその本を購入するしかありません。しかし、「できるPy」のような初学者向けの本は上位互換として「退屈なことはPythonにやらせよう」がありますし、私が知らない同コンセプトの本もたくさんあるでしょう。読者が見やすい、読みやすいと感じる本を選ぶとよいと思います。

参考までにそれぞれの本の見た目を紹介します。

f:id:t2y-1979:20190525042314p:plain:w1024
退屈なことは Python にやらせよう -14 章 CSV ファイルとJSON データ-

f:id:t2y-1979:20190525042318p:plain:w1024
できるPy -029 CSV ファイルを読み込む-

余談ですが、CSV ファイルを扱うのに「退屈なことはPythonにやらせよう」は標準ライブラリの csv モジュールを、「できるPy」ではサードパーティライブラリの pandas を紹介しています。Python のインストールを Anaconda を使って行うとデフォルトで pandas もインストールされます。本書はビジネスパーソン向けということもあり、標準ライブラリよりも便利であれば、サードパーティライブラリを優先して紹介するようにしています。

また執筆はリブロワークスさんの MDBP という Atom プラグインを使いました。Markdown で原稿を書いて CSS でスタイル設定して Atom で実際の書籍のデザインに近い見た目で確認しながら行いました。とてもよくできたプラグインで公開されています。興味のある方は Atom と MDBP プラグインをインストールして試してみるとよいでしょう。

libroworks.co.jp

技術的詳細よりも動くコード

本書を執筆するにあたり、マーケティング業務に携わっている同僚にヒアリングしていたときにこんな話を聞きました。

プログラミング言語の文法がどうこうとか、仕様が云々とか、そういうのは全く興味がありません。サンプルコードの一部の処理やパラメーターを直せばよいというのさえわかれば、適当に変更して実行して、それで目的が達成できればよいです。

プログラマーにはない感覚です。目的が明確なので手段はどうでもよく、目的を達成するコードが動けばそれで満足だというのです。本書を執筆する過程でこのことは私の中で葛藤と逡巡をもたらしました。確かに勘のよい人であれば Python のコードを読んでいるうちに規則性や要点をなんとなく掴んでパラメーターや必要な箇所のみを書き換えてプログラミングできるかもしれません。プログラミングに慣れるという最初の取っ掛かりとしてはそれでよい場合もあるでしょう。

本書は実務で使えるサンプルコードを提供するという目的がありました。サンプルコードはサンプルコードでしかなく、実務というのはそれぞれの業務に特化した個別の事情や要件があり、どういう仕組みや理屈で動いているかをわからずに書き換えて通用するかという懸念があります。しかし、ちゃんと解説したところで詳細に興味がなくて読まない人たちもいるかもしれないというので困ってしまいました。どこまで詳細を説明するか、あるいは説明しないかを考えて悩みながら書いたのが本書になります。

例えば、本書の中で意図的にサンプルコードの詳細説明を省いたのが Chapter 8 の「050 PDFからテキスト抽出」の項目です。余談ですが、本書では PDF からのテキスト抽出に pdfminer.six · PyPI というライブラリを使っています。一方で「退屈なことはPythonにやらせよう」では PyPDF2 · PyPI というライブラリを使っていて異なる点の1つです。これは私の手元にあった PDF ファイルをいくつか PyPDF2 でテキスト抽出したところ、日本語の PDF ファイルのテキスト抽出ができませんでした。pdfminer.six は正常にテキスト抽出できたのでそちらを採用しました。pdfminer.six は開発者が日本人なのでテストデータとして日本語の PDF ファイルも使って検証しながら開発されたのだと推測します。

閑話休題。私の知る限り、pdfminer.six についてのドキュメントは次になります。

ドキュメントをみる限り、pdfminer.six のモジュール構造は PDF のデータ構造に大きく影響を受けているため、複数のモジュールを組み合わせて PDF からテキスト抽出する仕組みとなっています。そのため、このモジュールはどういった機能をもっているかを解説するには PDF のデータ構造について言及する必要があります。PDF ファイルからテキスト抽出するという目的に対して、PDF のデータ構造の詳細に踏み込むのは難し過ぎると私は思いました。そこで pdfminer.six のモジュール構造の説明は行わずにサンプルコードの使い方のみを説明しました。

逆の例として概要だけでも説明した章もあります。Chapter 5 の 「021 ZIPファイルに含まれるファイル名を文字化けさせずに表示する」で文字化けの概要を説明しています。もしかしたら読者層のビジネスパーソンな方々には全く興味のない話かもしれません。しかし、直接的に役に立たなくても文字化けが発生する仕組みがわかれば、なにかの機会に知識として役立つこともあるのでは?と考えてできるだけ簡潔に説明しました。

本書では実務で役立つサンプルコードを提供しつつ、その技術的詳細は最小限しか説明しないという、プログラマー視点からみるともやっとする微妙なバランスで書いています。そのため、本書で提供しているサンプルコードが読者のやりたい自動化処理に近ければ近いほどやりたいことを達成する労力を削減しやすいとも言えます。

まとめ

出版を契機に久しぶりに書店へ行ってたくさんの Python 本が置かれている棚をみました。私は10年前ぐらいから Python を学び始めました。当時と比べて、たくさんの入門本、データ分析や機械学習に関連した Python の本が敷き詰められていて驚くべき状況です。

数多ある入門本の末席の1つとして、本書をきっかけにプログラミングに慣れ親しむ人が増えて、世の中の業務のいくらかが自動化されて効率化されて誰かの役に立てば幸いです。

本書の紹介記事

本書を紹介してくれた方々のブログの記事をまとめます。

shinyorke.hatenablog.com

イベント登壇

medium.com

リファレンス

出版にあたり、共著者やアドバイスしていただいた方々やレビューをしていただいた方々の記事も紹介します。関係者の方々のおかげで出版できたことに感謝します。

xaro.hatenablog.jp

rokujyouhitoma.hatenablog.com

できる 仕事がはかどるPython自動処理 全部入り。 (「できる全部入り。」シリーズ)

できる 仕事がはかどるPython自動処理 全部入り。 (「できる全部入り。」シリーズ)

これからはじめる SQL 入門

レビューに参加した経緯で 技術評論社 さんから献本していただきました。ありがとうございます!

SQL とはなにか

本書は「データベースを操作するための言語」として、RDBMS とその実行環境として PostgreSQL を使って SQL 入門を行います。私自身、これまでおもに RDBMS を使ったシステム開発に携わってきたことから「データベース」と聞くと RDBMS を真っ先にイメージしてしまいます。本書ではローカル環境にインストールされたデータベースに対して実際に SQL を実行して、その結果を確認するといったようにインタラクティブに操作しながら学習を進めていきます。

一方で「Chapter 1 データベースと SQL」をレビューしていて私自身の「データベース」の背景についての誤解に気付いたり、SQL とはなにかと考えるよい機会となりました。もちろん本書は入門者向けに簡潔に説明されているので、データベースそのものやそのモデルの背景や歴史的経緯などについては説明されていませんし、これから SQL を学ぶ人たちが最初にそういったことを気にする必要もありません。

SQL は1980年代に生まれた技術であるそうです。古くからある技術がいまもなお使われ続けているということから多くの歴史的経緯や変遷を辿っていることが伺えます。いまも SQL が使われる分野は拡大されていて、仕様も拡張され続けています。直近で拡張された規格を3つあげると、2008年、2011年、2016年になるそうです。

例えば SQL:2016 を調べてみると、json を扱う機能であったり、Row Pattern Recognition という正規表現を使って特定の行グループを絞り込む (ログのような時系列なデータからグループ分割したものに条件指定して取り出す?) ための機能であったりが追加されています。その時代で求められる機能や要件を取り込みながら SQL は進化しています。「SQL とはなにか」という問いへの答えも、その時代でよく使われている用途に応じて変わっていくように私は思います。

本章の最後に「クラウドコンピューティングとデータベース」というコラムがあります。そのコラムではクラウド環境で利用される RDBMS の紹介に加え、DWH (データウェアハウス) のプロダクトの問い合わせにも SQL のサブセットが使えることが紹介されています。DWH も SQL を使うプロダクトの1つとして大きな領域です。ここでさらに wikipedia の Database#Examples をみると、データベースを分類する方法として3つの方法があると記載されています。

データベースを分類する方法として、1つ目は書誌、文書、統計、マルチメディアといったその内容の種類から、2つ目は会計、音楽、映画、銀行、製造業、保険といったアプリケーションの分野から、3つ目はデータベース構造やインターフェースの種類といった技術的な側面から分類されます。

これらの分類方法から wikipedia では DWH 以外にも様々なデータベースが紹介されています。記載されているもののうち、私が見聞きしたことのあるものだけを取り上げてみても以下のようなものがあります。

  • インメモリデータベース
  • クラウドデータベース
  • 分散データベース
  • ドキュメント指向データベース
  • 組込みデータベース
  • グラフデータベース
  • ナレッジベース

もちろん、関係モデルをデータ構造として扱うデータベースであれば、このような分類の違いをあまり意識することはないかもしれませんし、全く独自のクエリ言語を提供しているものもあるかもしれません。ついつい身近に使っているものだけをデータベースであるかのように錯覚してしまいがちですが、データベースには多くの種類があります。そして、その多くで SQL (またはそのサブセット) を使ってデータを操作できることは、ライブラリの再利用性、システムの相互運用性、学習コストの削減など、多くのメリットがあります。

今後も新しいデータベースが現れてくるでしょう。しかし、そのプロダクトが SQL (またはそのサブセット) を提供する限り、その分類に関係なくそのデータの操作ができます。SQL を使えるということは、IT エンジニアだけではなく、業務においてデータに関わるすべての職種、営業であったりマーケティングであったり、カスタマーサポートであったり、それ以外の多くの職種の人にとって重要なスキルとなるでしょう。

SQL を手打ちして慣れる

本書をレビューしていて過去のことを思い出しました。昔、私は SIer である基幹システムのヘルプデスクのような業務に携わっていました。お客さんから基幹システムのデータや処理についての問い合わせがあったときに調査して回答するといったような業務です。

基幹システムのデータベースにはお客さんの PC からもアクセスできる状態でした。データを直接みたいときにお客さんは Microsoft Access を使ってデータを確認していました。特定テーブルのデータをみる分にはそれで十分であったかもしれませんが、様々な調査のために複数テーブルのデータを結合し、ある条件でデータを絞り込みたいときなど、私宛に連絡がきて、私が SQL を使って該当データを調査して回答するといったことが日常でした。

Chapter 3の最後に「SQL を身に付けるには」というコラムがあります。

筆者の経験を踏まえて、1つアドバイスがあります。それは コピー&ペーストに頼らず、自分の手でタイプして体になじませる というものです。

このコラムに書かれている著者の経験に私は全く同意するところです。私自身も SQL に慣れるきっかけになったのが、日常的にお客さんから問い合わせを受けて、必要なデータを取得する SQL を手入力して調べたりしていました。

そして、SQL に慣れてきたときに運用工数を削減することを目的に、お客さんにも SQL を覚えてもらおうとしていた時期もありました。当時やり取りしていた情報システム部の担当者も自身のスキルアップを目的に SQL を学習したいと仰っていました。そこで問い合わせの回答をするときにそのデータを調査したときの SQL も添付して回答していました。そんなことを半年ぐらい行っていたのですが、残念ながらお客さんの方が SQL はやはりわからないと学習を断念してしまいました。

いま思い返すと、そのときに本書があったらまた違った結果になっていたのではないかと悔しく思います。まさに私がお客さんに提供していたのはコピー&ペーストできる SQL であって、お客さんが自分で手打ちして SQL を覚えるための仕掛けとして不十分であったことが本書を読んでいて実感した次第です。これから SQL を学習される方は、本書を読み進めながらコピー&ペーストせずに手打ちしながら SQL に慣れていくのを実践してみてください。

本文とコラムとのバランス

冒頭の「はじめ」のところで以下のように書かれています。

本書は、著者が初心に戻り「自分が SQL を学び始めたときにこのような解説書があればよかった」と思える一冊に仕上げようという思いで筆を執ったものです。

著者が IT エンジニアであるため、システム開発する上で迷うところやつまづきそうな落とし穴、有用な情報を「コラム」として所々で補足しています。SQL を学ぶという視点からは確かに副次的な情報であるため、そういった情報は本文に含めずにコラムとしてうまくバランスをとっているように思います。というのは、本書の読者は IT エンジニアだけではないと想定しているのだと思います。私がレビューしていて、こういった情報もあった方が良いのではないかと指摘したこと・しようと思ったことの大半がコラムで簡潔にまとめられているので親切な入門書になっていると思います。

コラムの目次がないのはもったいないと思ったので以下にまとめてみました。

Chapter 1 データベースとSQL

Chapter 2 PostgreSQL環境の準備

  • Column: 環境構築やツールに関する補足情報 ・・・ 42

Chapter 3 データの取得と絞り込み(SELECT)

  • Column: ORDER BY 句を省略した場合の並び順 ・・・ 57
  • Column: OFFSET 句の代替 ・・・ 82
  • Column: SQL を身に付けるには・・・ 84

Chapter 4 データの作成・変更(INSERT,UPDATE,DELETE)

  • Column: CRUDSQL ・・・ 102

Chapter 5 データ型

  • Column: 文字型をどう使い分けるか? ・・・ 114
  • Column: 符号なし整数を扱うデータ型は? ・・・ 118
  • Column: シーケンスの重複を自動で防ぐには? ・・・ 125
  • Column: もっと複雑な配列も扱える ・・・ 135
  • Column: そのほかのデータ型 ・・・ 138
  • Column: JSON 型の使いどころ ・・・ 143

Chapter 8 テーブルの結合

  • Column: AS も省略できる ・・・ 205

Chapter 9 サブクエリ

  • Column: IN と EXISTS の違い、JOIN への書き換え ・・・ 240

Chapter 10 一歩進んだ SQL

  • Column: 全文検索システム ・・・ 251
  • Column: WHERE 句で関数や演算子を使う際の注意 ・・・ 252
  • Column: WITH 句を使った SELECT 文 ・・・ 263
  • Column: ほかの RDBMS での UPSERT ・・・ 266
  • Column: オートコミット ・・・ 273

Chapter 11 データベースとテーブルの操作

  • Column: 複数の制約を組み合わせる ・・・ 287
  • Column: ほかの RDBMS では列の場所を指定できる? ・・・ 287
  • Column: VIEW の変更 ・・・ 314
  • Column: データベースと日本語 ・・・ 315

まとめ

本書は SQL を段階的に学んでいく上で丁寧な解説と構成になっています。そのため、どちらかと言えばシステム開発に直接関わらない人向けにとってよい入門になると思います。冒頭でも述べた通り、データベースの重要性や SQL の応用範囲は広がる一方です。本書がデータを扱うすべての職種の人にとって SQL を学ぶきっかけになればと思います。

これからはじめる SQL入門

これからはじめる SQL入門

達人プログラマーの新装版がでたので読み直してみた

オーム社 さんから献本して頂きました。ありがとうございます!

原著は1999年10月20日に出版されたそうです。このブログ記事を書いているのが2016年11月なので17年前に書かれたことになります。元の出版社であるピアソンエデュケーション社が日本から撤退したことによりオーム社さんに移管され、その機会に翻訳も全面的に見直されたようです。

本書を読む動機付け

変化の速い IT 業界において17年前に書かれた本をいま読む価値があるのか?この問いに答えるのは難しいです。

開発プロジェクトという日々の業務では、様々な要件や移り変わる状況の中で何かしら制約がありつつも判断を下さなければなりません。それはアーキテクチャの策定であったり、開発方法論の実践だったり、個々の技術の選定だったりします。そこで判断を下し、そのときの判断が正しかったのかどうか、本当の意味でその正否はその後の歴史でしか分かりません。さらに何をもって正否と判断するのか。この正否の判断自体も多様な価値観で行われるものですが、達人プログラマーの原題である The Pragmatic ProgrammerPragmatic (実践的) という意味を借りて、広く浸透している考え方やうまく運用がまわっている仕組みをここでは正しいとします。その考え方が本当に広く浸透しているのか、その仕組みが多くの現場で使われているのか、それを私が保証することはできませんが、私の周りで見聞きする分にはそうみえるということで話を進めていきます。

閑話休題。本書を読むことで17年前に書かれた考え方や仕組みが 実践的に 正しかったのかどうかの一端を知ることができます。ともすれば、IT 業界というのは新しい技術や方法論が毎年たくさん出てきて、生き残るものもあれば消えていくものもまた多いです。そんな業界で17年も生き残っているものがあるとしたら、それは普遍的なものだと言ってしまっても過言ではないかもしれません。目新しいものに注意を取られがちな日常で何が本質かを考えたときにその基礎を教えてくれる、歴史を経た 達人プログラマー は単にプラクティスをまとめた本ではなくなっているように私は思います。

ある程度、業務でプログラマー経験のある方は私のような読み方もできますが、一方で経験の少ない若い方にとってはどうでしょうか。もちろん若い方へもお勧めしますが、本書の中で特定技術を例として紹介されているものの中には、いまは廃れてしまって現状にそぐわないものもあります。そのため、若い方が聞いたことのない特定技術が出てきたときは、その技術については自分で調べ直してみることが必要になります。訳注で補足をしている箇所もいくつかあるのであわせて確認してみてください。

達人プログラマーの所感

私が印象に残った節についていくつか抜粋しながら簡単に所感を書いていきます。

4 十分によいソフトウェア

この節ではソフトウェアのリリースをどのタイミングで行うかといった考え方を書いています。内容自体はプロジェクトマネージャーやプロダクトマネージャー向けに書かれているものですが、私は読んでいて、一般の開発者がいつプルリクエスト (以下PR) を送るかというタイミングを考えるときに似ているように思いました。

コードレビューをするのが当たり前のワークフローになりつつある昨今、どのタイミングで PR を送るか悩む人もいると思います。私は自分の中で8割ぐらいの品質になったら気軽に PR を送る方です。言わば、いくつかツッコミ所を残した状態で PR を送っています。その理由は次になります。

  • その PR の課題に対する解決策として設計や考え方があっているかどうかを判断してもらう
  • 全体からすると些細な内容でどう対応するかを自分の中でも迷いがあるので意見がほしい
  • 自分が書いたコードの意図が他人に伝わるかどうかをみてみる

そしてレビューしてもらって設計や考え方があっていないのであれば、やはり気軽に PR を取りやめて再設計します。どんなに時間をかけて品質をあげても他人の視点をもつことはできません。もちろん自分の中ではこの設計方針がベストだと考えて PR を送るわけですが、それまで自分が考えていなかった懸念や概念を他人から与えられることによって、他のやり方が妥当だと思うこともあります。

せっかく書いたコードを捨てるというのは悔しい行為だと思います。時間をかければかけるほど、サンクコストも気になってしまいます。時間 (コスト) のかかる機能開発だと進捗を小まめにレビューしてもらって手戻りを少なくする工夫をすると良いと思います。

7 二重化の過ち

いわゆる DRY原則 を紹介しながら、いろんな状況で二重化が起きることを説明しています。システム開発をしていて最も身近な問題の1つと言っても良いかもしれません。

プログラミングを始めた人にとって、おそらく初期段階でそこそこの規模のアプリケーションを実装していると実感すると思います。引き継いだ既存のアプリケーションの品質がよくないとそういった重複コードに悩むこともあります。

原則はあくまで原則でしかなく、結局のところその場その状況において何が最善であるかを選択する必要がでてきます。メリット/デメリットを考慮した上でやむを得ず二重化するという判断も現実にはあるでしょうが、二重化には多くの弊害があるという事実を知っておくことが重要です。そして、そうならないようにどうすれば良いかを考えていくことも重要です。そういう考え方をしているうちに、言語機能、設計手法や開発方法論で解決しようとしていることへの理解にもつながっていくように私は思います。

8 直交性

ときどき聞く言葉なのに私はちゃんと定義を把握していなかったので再学習しました。

「直交性」とは幾何学の分野から拝借してきた用語です。(中略)。この用語はコンピューティングの分野では、ある種の独立性、あるいは分離性を表しています。2つ以上のものごとで、片方を変更しても他方に影響を与えない場合、それらは直交していると呼ぶわけです。

直交性は DRY の原則とも密接に関係しています。またこの節では設計、ツール、コーディング、テスト、ドキュメントにおいても直交性の概念が適用できると紹介しています。

優れたフレームワークやライブラリ、良いプラクティスが共有されやすい昨今だと、直交性は当たり前過ぎてあまり意識することはないかもしれません。そういった背景にある概念を学ぶ良い機会にも思いました。

12 専用の言語

いまの言葉にすると ドメイン固有言語 (以下DSL) に相当すると思います。Tips としては以下のように書かれています。

問題領域に近いところでプログラミングを行うこと

この概念自体は適切だと思いますが、その事例の1つとしてミニ言語を実装する事例が紹介されています。またそれが必要な根拠として sendmail の設定は複雑なので DSL により制御を容易にする例を紹介しています。

達人プログラマーに書かれているほとんどの内容は、小さい開発プロジェクトにも大きい開発プロジェクトにも両方適用できるように書かれていますが、DSL に関して私は慎重派なので反対の立場にたってその理由を書いてみます。

もちろん巨大で複雑なプロジェクトや十分に成熟したアプリケーションに対して DSL が大きな価値を提供する場面があることは私も同意します。あえて反論しようと思ったのは DSL を開発するデメリットもあるからです。

  • 開発者にとって DSL の開発・保守コストがかかる
  • ユーザーにとって DSL の学習コストがかかる
  • ユーザーにとって DSL で書かれた機能や設定の保守コストがかかる
  • アプリケーション本体の機能と DSL が提供する機能の間にズレが生じる可能性がある
  • アプリケーション本体と DSL の依存関係を管理する必要がある

ざっと思いついたものをあげてみました。自分が取り組んでいる開発プロジェクトでこれらのコストを負担しても DSL を使うメリットがある場合は取り組んでも良いでしょう。逆にこれらのコストを考えずに安易に DSL を使うと将来の技術的負債となることもあると思います。

14 プレインテキストの威力

シンプル且つ意味深なように思えてなぜか印象に残りました。

知識はプレインテキストに保存すること

テキストの利点として以下をあげています。

  • 透明性が保証される
  • さまざまな活用ができる
  • テストが容易になる

いまどきの利点をさらにあげるとバージョン管理システムで差分管理しやすいという点も追加したいです。

これはこれで言っていることは正しいのですが、言わばテキストなら後から何とでもできるということでしかありません。もし特定用途に使いたいとしたらデータを正規化して扱いたくなるので、テキストデータは逆に曖昧で扱いにくいものです。例えば、HTTP/2 でバイナリープロトコルを採用した理由として、解析が容易でエラーチェックが厳密である利点があげられています。

18 デバッグ

私はこの節が好きです。残念ながらデバッグの手法は昔と比べてほとんど進歩がないように思います。大きく分けると以下の2つでしょう。

もちろん言語処理系の型システムが強力になったり、ツール/ライブラリが拡張されてより便利になったりはしています。しかし、エラーが発生して人間がソースコードを読みながら解析して原因を追求していくという行為自体はほとんど変わっていません。この節ではそのための心構えや戦略が丁寧に説かれています。

1つ私も過去にやっていた実践的な手法を紹介します。

問題の原因を探し出すための非常に簡単で効果的なテクニックとして、「誰かに説明する」という手法があります。

これはデバッグに限らず、コードレビューをレビューツール上だけでなく、レビューアに対面で説明するときにも有効です。説明しているうちに自分でもっと良い方法を思いついたりすることもあります。おそらく人間の思考として、ものごとを多面的にみる上でアウトプットの方法を変える手法が強力なんだと思います。

21 契約による設計

この節はどうなんでしょうね?あらかじめ要件や仕様を厳密にしやすい業務系ではこういった設計が定着しているのでしょうか?私は経験がなくて実践的にこの手法が使われているのかどうか知りません。

Web 業界ではまだまだここまで厳格な設計手法は定着していないように私は思います。また別の節にある表明プログラミングもそうですが、厳密さを保証するためのオーバーヘッド (実行効率や保守性など) もかかることから敬遠されがちなところもあると思います。

そう思ってはいるものの、最近契約による設計に関する記事をみかけたので以下を紹介しておきます。

28 時間的な結合

マルチスレッドを用いた並列処理について説明されています。さすがにこの節の内容や例は古くなってしまっているように感じます。非同期/並行処理 *1 は、プログラミング、システム設計、いろんな状況で本質的に難しい課題です。

この節ではワークフロー、アーキテクチャ、設計、インターフェースについて説明されていますが、内容がいまどきの Web 開発の問題とはややあっていないかもしれません。もちろん本書は Web 業界向けに書かれたものではないと思いますが、それでも昨今のクラウド化や開発方法論の変化との乖離により違和感があるのは仕方ないように思います。

1つだけ補足しておくと、昨今はそれぞれのプログラミング言語が非同期/並行処理をサポートする機能を提供していたりします。例えば、Python では asyncio というライブラリが標準で提供されています。そのライブラリはプログラマーが陥りがちな落とし穴を回避してくれます。そのため、自分で一から考えて作り込むのではなく、その言語やライブラリが提供する機能や仕組みに沿って開発するのがいまどきのプラクティスになるのではないかと思います。

31 偶発的プログラミング

本書を読みながらツイートしていて最も Impressions が高かったのがこの節でした。開発者が自分で実装しているコードをあまり理解せずに実装を進めてしまい、あるとき動かなくなってしまうときの背景や状況について説かれています。

いまはプログラミングを学ぶドキュメントがインターネット上にたくさんあっても、おそらくこの問題は昔よりも現在の方が起こりがちではないかと想像します。そして私自身も程度の違いはあれど実際にやっていると思います。誰かが作った優れたフレームワークやライブラリもこのことを助長します。

さらに何らかの問題や分からないことがあっても、大抵は stackoverflowqiita がその解決策として検索にヒットします。ほとんどの場合において、そこに書かれている内容を試せばうまくいくと思います。コードをそのままコピペする人もいるでしょう。それを繰り返した結果、簡単に偶発的プログラミングに陥ってしまいます。

以前ドワンゴの川上さんが仰っていた 膨大な数の二流のウェブエンジニア という言葉がずっと頭の中に残っています。

36 要求の落とし穴

この節も私は好きです。

要求は拾い集めるものではなく、掘り起こすものである

この言葉は的を射ています。要件定義はスキルの有無や頭の回転の速さに関係なく、その人の当事者意識の在り方に影響すると私は過去の経験則から考えています。つまり話を取りまとめて形式化するのが上手なことと、適切な要件を掘り起こすこととは違うということです。その当事者意識とはどうやって得られるのかの最たることも Tips に書かれています。

ユーザーの視点に立つには、ユーザーと働くこと

実際に業務でできるかと言うと難しい状況もあります。開発者がユーザーの気持ちを理解することが重要なのは誰しも経験から分かってくるように思います。ドッグフードを食べるとも言われたりしますが、可能であれば自分が開発しているアプリケーションやシステムを自分で積極的に使うことが最初の一歩になるはずです。

38 準備ができるまでは

この節では自分の勘と経験で直感的におかしいと感じたら一旦立ち止まることの大事さについて書かれています。そして、そのときにプロトタイピングがその不安材料を洗い出すことに役立つともあります。

ちょっと精神的な話になるかもしれませんが、私は無意識に考えることを意識的に活用するときがあります。難しいバグに悩まされている最中、帰ってきて寝て起きたら手がかりを閃いたという経験がある人も多いと思います。

未経験のものごとに取り組んでいると心理的に不安に思ってしまうのは仕方ないことです。そのときは分からなくてもずっと考え続けていることで、あるとき閃くんじゃないかと楽観的に信じ続けることが良い方向に働くときもあります。勉強するときもこのことは役に立ちます。いま分からなくてもずっと勉強しているとあるとき分かるんじゃないかと諦めてしまわない根拠に使えます。

誰しも分からないという状態は辛いです。その分からないストレスを下げる工夫にもなるのではないかと私は思います。

読みながらそのとき思ったこと・考えたこと

本書を読みながらツイートした内容です。余談ですが、先の所感を紹介する節を選定するときに自分のツイート分析から多くの人が関心をもってそうなものも考慮しました。ハッシュタグをつけておけば良かったと後で後悔しました (´・ω・`)

リファレンス

新装版 達人プログラマー 職人から名匠への道

新装版 達人プログラマー 職人から名匠への道

*1:この節では並列と並行の詳細には触れないとあるので、ここでも並列と並行の違いについては触れません。私の感覚的なもので非同期と並行をセットで考えるのでそのように書いています

Python と型ヒント (Type Hints) と #pyconjp

先週末は PyCon JP 2015 に参加してきました。

どうでも良いことですが、たまたま会社がシルバースポンサーになっていましたが、参加そのものは個人でした。結果的には会場に会社ブースもあったため、そこでお手伝いもしつつの参加となりました。私以外にはどうでも良いことですね。

会社のブログにもイベント参加の所感を書いたので興味があればそちらもどうぞ。

型ヒントの発表

年明けから余裕があったので型ヒントの調査をしてきました。私自身、難しいことは分からないのですが、型システムに興味が出てきたところでいろんな言語の型システムをみてコードを書いたり、その特徴を調べたりするのがいまはおもしろいです。最近は Go 言語を主に書いていて型推論を伴う静的型付け言語の強力さを実感したりしています。

そして動的型付け言語に型ヒントを入れるという逆のアプローチもやはり興味深いです。Python では mypy が成功を収め、その型アノテーションの構文が PEP 484 で標準化されました。これまでも何度か mypy についての記事を書いてきました。細かいところは変わってしまっていますが、経緯や背景は変わっていないので興味のある方は以下も参考にしてください。

そして、これまで私が調べたことの集大成として PyCon JP 2015 で発表してきた次第です。

togetter はこちら。

聴講してくれたのは100人前後だと思うのですが、発表者と聴いている人たちとの距離感が近かったので私の中では発表しやすかったように思います。

発表直前、小山さん (@) が私の真前のスペースで

ここ地べたに座って聴いても良いっすか?

と声を掛けてくれて適当に「良いですよー」と答えながら、なんか少し緊張がほぐれて発表を開始しました。

何度か聴衆席に質問を投げかけながら進めました。Pythonのパラドックス を知らない人が大半だったりと世代が変わっているなぁと実感しました。発表の中で Python3 のパラドックスみたいな冗談も言っていますが、意図としては Python3 への移行を阻むものはほとんどないはずなので新しいものを作るときは Python3 を使おうよという呼びかけです。

移行が着実に進んでいるというの以下のアンケートなどから伺えます。

型ヒントについての詳細は PEP 484 型ヒントの翻訳 を眺めてもらうのが一番良いと思います。それなりの分量がありますが、興味のあるところから読み進めるのでも良いでしょう。サンプルコードも付いているので分かりやすいです。

型チェッカー

本節では発表のときにあまり触れなかった、型チェッカーとしての mypy を使ってみた私の所感を書きます。PEP 484 では型アノテーションの構文の標準化のみで型チェッカーは付属していません。そのため、依然として mypy は自分でインストールしなければなりません。

結論から先に書くと、型チェッカーとしての mypy はまだ実用レベルとは言えないです。github の mypy プロジェクトをみても250以上の issue が報告されており、期待する動作には至っていないようにみえます。

ちなみに mypy の PyPI 上のパッケージ名は mypy-lang というパッケージ名です。mypy という名前のパッケージが既に使われているためです。ちなみにこれは wsgi フレームワークだそうです。インストールするときには間違えないようにご注意を。

mypy の最新バージョンは 2015-04-05 に 0.2 というバージョンがリリースされています。この時点では PEP 484 のドラフト状態であり、そのときに決まっていた内容、おそらくは PyCon US 2015 で一定の合意を得た内容だったのではないかと推測します。

その後、PEP 484 が正式に認可されたのが 2015-05-22 です。

なぜか7月前後の master ブランチの開発は停滞していますが、その後8月頃からまた開発がアクティブになっています。それでも Python 3.5 と同じタイミングで 0.3 をリリースできてなく、また雰囲気的にも近々リリースするようにみえません。

github から mypy のソースをクローンしてきて、発表前にいくつかサンプルコードの型チェックなどを試したりしてみました。そのときにちょっと触って気付いたこと = うまく動かないところ = 既知の issue をいくつか見つけました。

やっぱり型チェッカーって大変なんだなぁというのが素朴な感想です。issue のページで label:pep484 でフィルターしても現時点で17個あります。mypy の 0.3 がリリースされるまでは型チェッカーは様子見といったところかもしれません。

今朝 git pull して動かそうとしたら bultins が見つからないというエラーになりました。

(mypy)$ mypy tutorial.py 
tutorial.py:1: error: Could not find builtins
...

パスの問題のようです。

いま mypy にコントリビュートするチャンスがたくさんありそうです。

型チェックは必要?

発表を聴いてくれていた同僚のデータサイエンティストに発表後どんな印象を受けましたか?と尋ねてみました。すると、やはり難しいと答えが返ってきました。ジェネリクスとか、これは Python なの?といった印象を受けたそうです。ジェネリクスの話をする際、会場で Java やったことがある人?と尋ねたら8割以上、手が挙がったので「ジェネリクスとは」みたいな話を省いてどんどん話を進めたのもあるかもしれません。

発表後の質疑応答においても、例えば 共変性と反変性 とか、Python はそういうことを考えずに簡単に使えて良いのに、、、といった質問もありました。私も説明が難しいからこの内容は発表から省いたのですが、確かに型ヒントをちゃんと書こうと思ったら分かっている人しか書けない、直感的に書ける類のものではないという意味で難しいです。

型チェックを誰が必要としているか?というと、その回答は学習コストやメンテナンスコストを払ってでも実行前に型チェックしたいかどうかの動機次第かなと思います。以下の清水川さん (@) の記事で Guido 自身も PyCharm や Google でも似たようなスタブファイルを作っていたから標準化することに意義があるんだと語っています。

私の経験からだと、実行前にエラーが知りたいケースは高い SLA が要求されるアプリ、または大規模なアプリだと思います。チーム開発で日々コードを書いてコミットしていると、他の人が何をやっているか分からないために認識の違いによるバグは常に入ってしまいます。

そういったバグを見つけるために動的型付け言語はテストをたくさん書くことで一定の品質を担保してきたわけですが (もちろん静的型付け言語でもテストは必要ですが)、その規模拡大に伴ってもうしんどくなってきたというのが現状ではないかと私は思います。アプリの規模が大きくなればなるほど、テストで品質を担保するのが難しくなります。

  • 全てのテストケースを網羅できない
  • テストのメンテナンスコストがかかる
  • テストの質が開発者のスキルに依存する

型チェックはテストなのか?と言うと議論はありそうですが、型レベルの操作において不整合がないことは、人間がプログラムを書く上で失敗しやすいミスを防いでくれます。

  • 人間はコードを書き間違える (typo)
  • 人間は全てのソースコードを把握してコードを書けない
  • 人間は時間が経つとそのソースコードを書いたときの経緯を忘れる

これらの失敗に対して型チェックは有効です。自分が全てを把握できていないコードベースにコードを追加していくとき、エラーを未然に防ぐことへの安心感をプログラマーは求めている気がします。

まとめ

型ヒントを扱う型チェッカーや IDE がどのぐらい普及するか、またはさらに便利な用法が出てくるか、まだまだこの先どうなるかは分かりません。とは言え、型ヒントそのものはあくまでオプションの位置付けなので悪い作用をもたらすことは何もないと思います。私ももう少し mypy が安定してきたら、それを使うテストツールなどを作ってみようと考えています。

Python とマクロ、代数的データ型

前回はマクロの概要と Python でマクロを実装するための仕組みについて説明しました。

Python とマクロ、インポートフックと抽象構文木 - forest book

動作原理を理解した上で実際にマクロでどういったことができるのか、MacroPy というライブラリで提供されている機能をみながら考察してみます。

MacroPy の概要

Python でのマクロ実装の1つです。インポートフックでモジュール内のマクロ機能を AST 変換することで動作します。MacroPy で提供されているマクロ機能は以下のデコレーターを使って実装されています。

  • @macros.expr
  • @macros.block
  • @macros.decorator
  • @macros.expose_unhygienic

これらの仕組みを使って自分でマクロを実装することもできます。それにより Python の意味論 (semantics) の拡張を簡単にします。

リポジトリには python3 ブランチがあり、Python 3 対応が試みられているようですが、正常に動作しない機能もあるため、Python 2.7 を使う方が無難だと思います。

MacroPy が提供するマクロや機能は多岐に渡ります。またドキュメントもしっかりしているので学習にはとても良さそうです。ただ、全てを README に記載しているので目を通すだけでもなかなか大変です。以下は MacroPy の README から目次を抜き出したものです。

機能

マクロ

ユーザー定義マクロ

リファレンス

項目がたくさんあるので興味のあるところから読み進めると良いと思います。

今回は README の前半部によく登場する Case クラスという機能を提供するマクロとその背景について解説します。

Case クラス

Scala から Case クラスという機能を提供するマクロです。Case クラス について MacroPy のドキュメントで以下が引用されています。

Case Classes Are Cool - Code Commit

私が Scala をよく知らないため、この記事を読んでもよく分からなくて最初からつまづきました。Case クラスについて調べていると Effetive Scala に Case クラスについて説明があるのを見つけました。

代数的データ型としてのケースクラス

ケースクラス (case class) は、代数的データ型 (algebraic data type) をエンコードする: ケースクラスは数多くのデータ構造をモデリングするのに役に立ち、強力な不変式を簡潔なコードとして提供する。ケースクラスは、パターンマッチと共に利用すると特に有用だ。パターンマッチの解析器は、さらに強力な静的保証を提供する包括的解析 (exhaustivity analysis) を実装している。

Effective Scala 関数型プログラミング-代数的データ型としてのケースクラス

( ゜Д゜)

説明が簡潔過ぎてもっと分からなくなってしまいました。

代数的データ型 (Algebraic data type)

wikipedia:代数的データ型 という用語が新たに出てきました。Case クラスを理解する前にこの型が何なのかを調べることにしましょう。

余談ですが、以前、Python と型ヒント について調べていたときに Alex Gaynor 氏が Python の型システムについての懸念を表明し、その中で代数的データ型がないといったことも挙げられていました。

Python's type system isn't very good. It lacks many features of more powerful systems such as algebraic data types, interfaces, and parametric polymorphism. Despite this, it works pretty well because of Python's dynamic typing. I strongly believe that attempting to enforce the existing type system would be a real shame.

[Python-ideas] Proposal: Use mypy syntax for function annotations

そういったやり取りも記憶に残っていて代数的データ型に興味がありました。大雑把にいまの自分の理解で要約しますが、厳密な定義は原典を参照してください。

代数的データ型とは、一般的に関数型言語にみられるデータ型で、具体的には直積型、直和型、列挙型や再帰型といったデータ型を指します。関数型言語では、これらのデータ型を使ったプログラミングが一般的であり、パターンマッチングと共にそのデータ型の表現や操作について簡潔、且つ強力に扱えます。

代数的データ型と簡潔な表現

例えば、なぜ次に学ぶ言語は関数型であるべきか - YAMAGUCHI::weblog の記事で紹介されている Boolean 式を表す型と、それらの式を評価する関数の定義が以下になります。

  • OCamlでの表現型と評価器
type 'a expr = | True 
               | False 
               | And  of  'a expr * 'a  expr 
               | Or   of  'a expr * 'a  expr 
               | Not  of  'a expr 
               | Base of  'a  
 
let  rec eval eval_base expr  = 
   let  eval' x = eval eval_base x in 
   match expr with 
   | True  -> true 
   | False -> false 
   | Base base  -> eval_base base 
   | And  (x,y) -> eval' x && eval' y  
   | Or  (x,y)  -> eval' x || eval' y 
   | Not  x     -> not (eval' x) 

これと同等のことを Python で実装してみたのが以下の記事になります。

代数的データ型とオブジェクト指向プログラミングと

代数的データ型の直和型 (後述) に相当するものは、オブジェクト指向言語においてもクラスの継承や列挙型 (Enum) で表現できます。以下は列挙型を使って表現したコードです。

# -*- coding: utf-8 -*-
from abc import ABCMeta, abstractmethod

from extenum import ConstantSpecificEnum

class Evaluator(metaclass=ABCMeta):
    @abstractmethod
    def evaluate(self, value): pass

class MyEvaluator(Evaluator):
    def evaluate(self, value):
        return bool(value)

class Expr(ConstantSpecificEnum):

    TRUE = 1
    FALSE = 2
    BASE = 3
    AND = 4
    OR = 5
    NOT = 6

    @overload(TRUE)
    def eval(self, evaluator, *args):
        return True

    @overload(FALSE)
    def eval(self, evaluator, *args):
        return False

    @overload(BASE)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0])

    @overload(AND)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0]) and evaluator.evaluate(args[1])

    @overload(OR)
    def eval(self, evaluator, *args):
        return evaluator.evaluate(args[0]) or evaluator.evaluate(args[1])

    @overload(NOT)
    def eval(self, evaluator, *args):
        return not evaluator.evaluate(args[0])

ぱっと見た直感で随分とコード量が増えて、コードの見た目 (表現) が冗長になってしまっているというのに気付くと思います。

関数型言語で代数的データ型の定義とそれを扱う処理はとても簡潔に書ける (表現できる) のに対して、代数的データ型をサポートしないオブジェクト指向言語でそういった処理を実装するのは冗長で複雑になりがちであるというのを実感する例です。

私が調べた中では、代数的データ型について語るときに言語機能としてそういったデータ型を簡潔に強力に表現できるかどうかということが論点の1つとして語られているように思います。

余談ですが、直和型を簡潔に表現するために Python 標準の enum モジュールだと機能不足だったので extenum というパッケージを作りました。extenum の機能と用途について簡単にまとめた記事が以下になります。

enum を拡張する extenum パッケージを作りました

標準の enum モジュールにはない以下の機能を提供しています。

  • 定数固定メソッド実装
  • 暗黙の列挙型メンバー
  • EnumSet

代数的データ型とパターンマッチング

代数的データ型の文脈で使われるデータ型がいくつかあります。Python におけるそれらを考察したのが以下の記事になります。

代数的データ型とパターンマッチングと

  • 直積型
  • 直和型
    • 前節の Ocaml のコードのように関数型言語では簡潔に表現できる
    • Python ではサポートしていない、継承や enum で代替できるが簡潔ではない
  • 列挙型
    • Python 3.4 から標準ライブラリとして提供されている
  • 再帰
    • データ型を定義するときに再帰的に扱う、前方参照 (forward reference) が必要

さらに代数的データ型とパターンマッチングは表裏一体な機能と言っても良さそうなので、一緒に考察することでその利点がより分かりやすくなります。

上記の記事で直積型は namedtuple に相当すると説明しています。

ここでようやく元の話に戻ってきましたが、この直積型に相当するものが Case クラスです。MacroPy では以下のように定義します。

@case
class Point(x, y): pass

MacroPy では Case クラスは以下の機能をもつと説明されています。Case クラスは @macros.decorator で実装されています。

ざっくり言うと、様々な特殊メソッド (機能) をもつクラスを自動生成してくれます。こういった何かの機能を自動生成するものをボイラープレート (boilerplate) と呼んだりするようです。詳細は README にあるサンプルコードを参照してください。

先の記事を書いた後で id:podhmoPythonのnamedtupleについて見過ごしてきたこと で namedtuple はタプルであって型ではないと言及しているのに気付きました。

これは結局、namedtupleは名前の通りtupleでしかないせいです。tupleなので型名を持っていません。したがって、同じ順序で同じ値が渡されていたものは比較でTrueになるというわけです。

先の記事で namedtuple が直積型に相当すると書いたのは厳密には間違っていて、Case クラスの __eq__ ではクラスのチェックも実装されているため、Case クラスが直積型に相当すると言った方が適切でしょう。

    def __eq__(self, other):
        try:
            return self.__class__ == other.__class__ \
                and all(getattr(self, x) == getattr(other, x) for x in self.__class__._fields)
        except AttributeError:
            return False

さて、前述した OCamlでの表現型と評価器を、MacroPy の Case クラスとパターンマッチングを使って実装してみます。

# -*- coding: utf-8 -*-
from macropy.case_classes import macros, case
from macropy.experimental.pattern import macros, ClassMatcher, _matching, switch

@case
class Expr:  # Algebraic data type
    class True_: pass
    class False_: pass
    class Base(value): pass
    class And(expr1, expr2): pass
    class Or(expr1, expr2): pass
    class Not(expr): pass

def eval_(expr):  # Pattern Matching
    with switch(expr):
        if Expr.True_():
            return True
        elif Expr.False_():
            return False
        elif Expr.Base(value):
            return bool(value)
        elif Expr.And(expr1, expr2):
            return eval_(expr1) and eval_(expr2)
        elif Expr.Or(expr1, expr2):
            return eval_(expr1) or eval_(expr2)
        elif Expr.Not(expr):
            return not eval_(expr)

def test():
    True_, False_ = Expr.True_, Expr.False_
    Base, And, Or, Not = Expr.Base, Expr.And, Expr.Or, Expr.Not
    assert eval_(Not(True_())) is False
    assert eval_(Or(And(Base(3), False_()), Not(False_()))) is True

列挙型で実装したコードよりも随分とすっきりしましたね。

Case クラスの継承 によると、@case で生成したクラスでは、継承関係をネストした内部クラスとして表現できるようです。この例では、Expr の内部クラスは Case クラスを継承することになります。

そして eval_() 関数が MacroPy の パターンマッチング のマクロで実装されています。こういった with 文と一緒に使うマクロは @macros.block で実装されています。

もはや Python の意味論ではないので何ともコメントが難しいですが、OCaml のコードによく似た表現になっていることが伺えます。そして、それでもまだ冗長であるのも否めない気はします。

感覚的なものですが、構文を変えずに意味論だけの拡張で他言語の概念を取り入れるというものの限界というのか境界というのか、そういったものが見え隠れしている気がします。

代数的データ型の補足

Python のようなオブジェクト指向言語ではほぼ馴染みがないため、最初から関数型言語で学習する方が良いとは思いますが、PyAlgebraicDataTypes というライブラリが代数的データ型とパターンマッチングの学習向けに分かりやすいと思います。以下に簡単なチュートリアルを書きました。

代数的データ型と FizzBuzz と

他にも代数的データ型そのものの概要やそれに関するデータ構造について以下の記事が参考になりました。

The Algebra of Data, and the Calculus of Mutation

代数表現とデータ型の表現の概念から始まり、直積型と直和型、再帰型の説明、後半に wikipedia:en:Zipper_(data_structure) や One-Hole Contexts といった話題も出てきます。

まとめ

MacroPy のマクロ機能と関数型言語における代数的データ型の概念について紹介しました。

  • MacroPy
    • Case クラス
    • パターンマッチング
  • 代数的データ型
    • 直積型
    • 直和型
    • 列挙型
    • 再帰

MacroPy を使って代数的データ型を表現してみました。

ボイラープレートとしての Case クラス、Case クラスを型とみなしたパターンマッチングにより、関数型言語のそれに近い表現で実装することはできました。マクロを使うことで Python の意味論を拡張できるというのを実感するには分かりやすい例でした。

Python とマクロ、インポートフックと抽象構文木

どちらがきっかけだったのか忘れてしまいましたが、wikipedia:メタプログラミングwikipedia:抽象構文木 について調べているうちに マクロ が出てきました。

私の中では、マクロと聞くと、C 言語の、プリプロセッサ (コンパイルの前処理) でコードに置き換えるものを漠然とイメージします。改めてマクロって何だったっけ?何が嬉しいのだっけ?と考えてみると、基本的なことが分かっていないことに気付いたのでマクロについて調べ直してみました。

マクロとは

wikipedia からマクロの定義を引用します。

A macro (short for "macroinstruction", from Greek μακρο- 'long') in computer science is a rule or pattern that specifies how a certain input sequence (often a sequence of characters) should be mapped to a replacement output sequence (also often a sequence of characters) according to a defined procedure. The mapping process that instantiates (transforms) a macro use into a specific sequence is known as macro expansion. A facility for writing macros may be provided as part of a software application or as a part of a programming language. In the former case, macros are used to make tasks using the application less repetitive. In the latter case, they are a tool that allows a programmer to enable code reuse or even to design domain-specific languages.

wikipedia:en:Macro_(computer_science)

ざっくり意訳すると、

コンピューターサイエンスで言うところのマクロ ("マクロ命令" の省略形) とは、定義された手続きに従い、特定の入力シーケンス (文字列のシーケンス) が出力シーケンス (文字列のシーケンス) に置き換えられる方法を指定するルールやパターンのことです。特定のシーケンス内にインスタンス化する (変換する) 対応付け処理は、マクロ展開として知られています。マクロを書くための機構 (facility) は、ソフトウェアアプリケーションの一部、もしくはプログラミング言語の一部として提供されるかもしれません。前者は簡潔な表現でそのアプリケーションを使うタスクを作るために使われます。後者はプログラマーにとってコードを再利用させたり、ドメイン特化言語を設計することさえ可能にするといったツールになります。

大雑把に要約すると、既定のコードを置き換えるルールやパターンを作ることで簡潔な表現やコードの再利用性をもたらすといったことが嬉しそうですね。

また、マクロと言えば Lisp 系の言語の特徴的な機能のように私はよく見聞きしていました。

S式は言ってみれば言語の構文木そのものです。普通の言語では、処理系のフロントエンドにある構文解析器が、「人間に優しい」文法を「機械が理解しやすい」 構文木に変換します。

Lisp:S式の理由

wikipedia:S式 という表現方法、プログラムのコードそのものをデータとして扱えるという特性により、普通の言語 *1 で必要な構文解析や抽象構文木を操作するといった処理が簡単になり、その結果としてマクロがより身近で強力なものになるのではないかと推測します。

参考までに Clojure でのマクロを使う動機付けについて書かれた記事を紹介します。


閑話休題。前置きが長くなってしまいました。先の wikipedia の続きの説明によると、Python も立派にマクロをサポートしています。

While syntactic macros are often found in Lisp-like languages, they are also available in other languages such as Prolog, Dylan, Scala, Nemerle, Rust, Haxe,[5] and Python.[6]

Syntactic macros

あれ!?そうだったっけ?と思う方もいるかもしれません。普通に Python でマクロを書いたりすることはないのでイメージできないかもしれません。

その根拠として Python でマクロを提供するライブラリとして MacroPy が紹介されています。このライブラリは様々な機能がマクロとして実装されていて、Python でマクロをどう実装するかの参照実装の1つとして良いと思います。念のため、初学者向けに断っておくと、Python における一般的なアプリ開発の用途でマクロを使う必要性は全くありません。本稿ではマクロという概念そのものを学ぶことが目的です。またマクロはその特性上、その言語におけるメタプログラミングを提供する仕組みとも密接な関係があります。そのため、マクロを学ぶことは Python におけるメタプログラミングを学ぶ上で良い題材とも言えるでしょう。

Python におけるマクロの概説

MacroPy の概要説明に分かりやすい図があるのでそこから引用します。


MacroPy は大まかに次のように動作します。

1. モジュールのインポートをフックする (インターセプトする)
2. モジュールのソースコード構文解析して AST (抽象構文木) に変換する
3. AST をトラバースして、見つけたマクロを展開する
4. 改変した AST をコンパイルして、モジュールの読み込みを再開する

(注) あるモジュール内でマクロが使われているとき、そのモジュールを直接実行することはできない (マクロが有効にならない) 。

MacroPy 30,000ft Overview

横文字がたくさん出てきました。まず用語が分からないとそれだけで嫌になってきます。それぞれの用語を1つずつ整理しながら意図している内容を噛み砕いていきましょう。

Python のモジュール

通常 モジュール は、Python のプログラム (ソースコード) を記述した xxx.py というファイルです。ここでは Python でインポートできる最小単位がモジュールであることを覚えておいてください。モジュールも Python の世界の中で扱えるオブジェクトの1つです。

歴史的に Python のインポートの API が貧弱だったことからいまの状況はやや混沌としています。私はよく知らないので簡単な紹介しかできませんが、いまは3つの方法があります。

1. __import__() 関数

import 文で呼ばれる組み込み API といったものでしょうか。インポートを制御する低レベルのインターフェースです。昔からのライブラリなどで動的にモジュールをインポートするプログラムでしばしば見かけたりします。昔は __import__ を使うしかなかったのですが、いまは importlib.import_module() を使うようにとドキュメントで推奨されています。

2. imp ライブラリ

このライブラリがいつからあるのか、どういった変遷を経たのか私はよく知りませんが、PEP 302 の仕組みを提供するライブラリの1つです。但し、ドキュメントによると 3.4 で撤廃とあるので今後は importlib へ移行されていくようです。

PEP 302 の Python バージョンが 2.3 (2002年) と明記されています。それなりに昔からある仕組みのようです。この PEP には後述するインポートフックの仕様についても記述されています。

3. importlib ライブラリ

Python 3.1 から導入されたインポートを扱う新たなライブラリです。一部 2.7 にもバックポートされています。

What’s New In Python 3.1 を眺めていて1つ気付くのは、importlib はインポート文の pure Python な参照実装だとあります。imp の C 拡張 (CPython) なところを取り除いていって、インタープリター間の移植性を高めたいといったところも狙いのようです。

と、考察した後になってから importlib の冒頭にその目的が書いてあることに気付きました。

The purpose of the importlib package is two-fold.
One is to provide the implementation of the import statement (and thus, by extension, the __import__() function) in Python source code.
(...snip...)
Two, the components to implement import are exposed in this package, making it easier for users to create their own custom objects (known generically as an importer) to participate in the import process.

importlib 31.5.1 はじめに

また後述するインポートフックのところでも出てきますが、以下の内容も頭の片隅に入れておいてください。

モジュールには、関数定義に加えて実行文を入れることができます。これらの実行文はモジュールを初期化するためのものです。これらの実行文は、インポート文の中で 最初に モジュール名が見つかったときにだけ実行されます。

6.1. モジュールについてもうすこし
インポートフック

Python のモジュールをインポートするときの処理に割り込んでごにょごにょするといったことをインポートフックと呼びます。

先ほどの PEP 302 で導入された仕組みによると、sys.meta_pathfinder オブジェクトを登録することにより、デフォルトの sys.path よりも先にその finder.find_module() が呼ばれます。そして、finder.find_module()loader オブジェクトを返し、loader.load_module() がモジュールオブジェクトを返します。

実際に試してみましょう。

$ vi run.py 
# -*- coding: utf-8 -*-
def main():
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

適当なパッケージとモジュールを作り、

$ tree .
.
├── run.py
└── test
    ├── __init__.py
    ├── t1.py
    ├── t2.py
    ├── t3.py

$ head test/*.py
==> test/__init__.py <==

==> test/t1.py <==
print('I am t1')

==> test/t2.py <==
print('I am t2')

==> test/t3.py <==
print('I am t3')

インポート時に標準出力するだけのプログラムを用意します。

$ python run.py 
I am t1
I am t2
I am t3

インポートフックを実装するための finder/loader の両方の機能をもつ ImportHook クラスを定義し、インポート前に sys.meta_path に登録します。ImportHookインスタンスfind_module() が loader としての自分自身を返し、load_module() が呼ばれます。

# -*- coding: utf-8 -*-
import imp
import sys

class ImportHook:

    def find_module(self, mod_name, path=None):
        if mod_name == 'test.t2':
            print('find_module:', mod_name, path)
            return self

    def load_module(self, mod_name):
        print('load_module:', mod_name)
        path = mod_name.split('.')
        mod = imp.load_module(mod_name, *imp.find_module(path[-1], path[0:-1]))
        return mod

def main():
    sys.meta_path.insert(0, ImportHook())
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

このプログラムを実行すると以下の出力になります。

$ python3.4 run.py  # python2.7 でも実行可
I am t1
find_module: test.t2 ['/Users/t2y/work/external-repo/python/learn/import-hook/test']
load_module: test.t2
I am t2
I am t3

インポートフックが呼ばれてモジュールの検索と読み込みが行われているのが確認できました。

余談ですが、ドキュメントを見ていて sys.path_hooks というのもあるようです。この例では sys.meta_path に finder オブジェクトを登録しましたが、その finder オブジェクトを生成する呼び出し可能オブジェクトのリストを登録するようです。さらにもう1つ前の段階でもフックできるようですね。

リファレンス:

抽象構文木 (Abstract Syntax Tree)

抽象構文木構文解析構文木とデータ構造の中間的なものとして使用される。さらにコンパイラインタプリタなど(プログラミング言語処理系)でのプログラムの中間表現として使われ、コンパイラ最適化やコード生成はその上で行われる。抽象構文木のとりうる構造は抽象構文で記述されている。

wikipedia:抽象構文木

Pythonソースコード構文解析して、抽象構文木 (以下 AST) を扱うために ast モジュールという標準ライブラリがあります。ast モジュールのヘルパー関数を使うと、簡単にソースコードを AST のノードオブジェクトに変換できます。

ソースコードを見た方が分かりやすいので簡単なサンプルを紹介します。

# -*- coding: utf-8 -*-
import ast
import inspect
import sys

def func():
    for i in range(3):
        if i == 2:
            print('Value is {}'.format(i))

source = inspect.getsource(func)
tree = ast.parse(source)
print(ast.dump(tree))

実行すると以下のような AST のノードオブジェクトの dump が出力されます。手で整形するのも難しかったのでちょっと見辛いですが、どういったオブジェクト表現かという雰囲気は掴めます。後ほど、マクロ展開について考察するときのために Python の AST 表現は (知らない人には) 訳が分からない程度に覚えておいてください。

$ python3.4 ast_sample.py 
Module(
body=[FunctionDef(name='func',
           args=arguments(args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]),
  body=[For(target=Name(id='i', ctx=Store()),
             iter=Call(func=Name(id='range', ctx=Load()),
             args=[Num(n=3)], keywords=[], starargs=None, kwargs=None),
    body=[If(test=Compare(left=Name(id='i', ctx=Load()), ops=[Eq()], comparators=[Num(n=2)]), 
      body=[Expr(value=Call(func=Name(id='print', ctx=Load()),
                 args=[Call(func=Attribute(value=Str(s='Value is {}'), attr='format', ctx=Load()),
                   args=[Name(id='i', ctx=Load())],
                   keywords=[], starargs=None, kwargs=None)],
                 keywords=[], starargs=None, kwargs=None))],
    orelse=[])],
  orelse=[])],
decorator_list=[], returns=None)]
)

実際に ast モジュールを使ったことがなかったのと公式ドキュメントでは使い方がよく分からなかったので以下に簡単な入門記事を書きました。

ast モジュールの使いどころとしては、Python コードを処理するテンプレート、コード解析、マクロ、そういった類のツールなどが一般的です。分かりやすいサンプルとして、以下にデコレートされている関数やメソッドを調べるといったことをやってみました。

ソースコードの AST 変換とモジュール読み込み

ここまででインポートフックと AST について分かりました。

前々節のインポートフックのソースコードを修正して、あるモジュールのソースコードを読み込んで AST に変換・改変した後、その AST をコンパイルして新規にモジュールを作成するのをやってみましょう。

AST のノードオブジェクトに変更を加えるには ast モジュールの NodeTransformer を使います。ast.parse() でファイルのソースコードを AST オブジェクト (Module ノード) に変換し、NodeTransformer を継承したクラスを設けてトラバースするのが簡単です。ここでは文字列のノードオブジェクトの値を書き換えています。

class StringTransformer(ast.NodeTransformer):
    def visit_Str(self, node):
        node.s = 'I am StringTransformer'
        return node

...
    def handle_ast(self, file_, mod_name):
        source = file_.read()
        tree = ast.parse(source)
        transformed_tree = StringTransformer().visit(tree)
        code = compile(transformed_tree, mod_name, 'exec')
        return code

NodeTransformer は Visitor パターンで処理を実装します。Visitor パターンって何だったっけ?という方は以下の記事で復習しましょう。

そして、NodeTransformer で変更を行った AST の Module ノードオブジェクトを compile() することでコードオブジェクトを取得します。

次に置き換え用のモジュールオブジェクトを新規に生成し、先ほど変更を加えてコンパイルしておいたコードオブジェクトをそのモジュールの名前空間exec() します。これはモジュールのところでインポート時に実行して初期化されるという処理に相当します。

    def create_module(self, mod_name, file_name, code):
        mod = imp.new_module(file_name)
        sys.modules[mod_name] = mod
        exec(code, mod.__dict__)
        return mod

Python でインポートしたときに行われる処理、sys.modules への登録やコード実行などを行っています。本来は __file____package__ といった属性にも適切な値を設定すべきですが、単純なサンプルなのでここでは省きます。

前々節のインポートフックのソースコードに修正を施したものが以下になります。

# -*- coding: utf-8 -*-
import ast
import imp
import sys

class StringTransformer(ast.NodeTransformer):
    def visit_Str(self, node):
        node.s = 'I am StringTransformer'
        return node

class ImportHook:
    def find_module(self, mod_name, path=None):
        if mod_name == 'test.t2':
            print('find_module:', mod_name, path)
            return self

    def load_module(self, mod_name):
        print('load_module:', mod_name)
        package_name, file_name = mod_name.split('.')
        file_, pathname, desc = imp.find_module(file_name, [package_name])

        # read source and transform ast
        code = self.handle_ast(file_, mod_name)

        # create new module and exec it
        mod = self.create_module(mod_name, file_name, code)
        return mod

    def handle_ast(self, file_, mod_name):
        source = file_.read()
        tree = ast.parse(source)
        print('AST:', tree)
        transformed_tree = StringTransformer().visit(tree)
        code = compile(transformed_tree, mod_name, 'exec')
        print('compiled:', code)
        return code

    def create_module(self, mod_name, file_name, code):
        mod = imp.new_module(file_name)
        sys.modules[mod_name] = mod
        exec(code, mod.__dict__)
        return mod

def main():
    sys.meta_path.insert(0, ImportHook())
    from test import t1
    from test import t2
    from test import t3

if __name__ == '__main__':
    main()

実行結果です。

$ python3.4 run.py 
I am t1
find_module: test.t2 ['/Users/t2y/work/external-repo/python/learn/import-hook/test']
load_module: test.t2
AST: <_ast.Module object at 0x1053c0518>
compiled: <code object <module> at 0x105313ae0, file "test.t2", line 1>
I am StringTransformer
I am t3

"I am t2" の文字列出力を AST のレイヤーで書き換えて実行することができました。

ここまでのサンプルコードではマクロ展開以外の動作、つまり MacroPy の概要説明にあるモジュール読み込みのワークフローの流れを確認しました。

マクロ展開について少し

前節のサンプルコードでは、実際にマクロを定義したわけではありませんが、StringTransformer で AST のノードオブジェクトの文字列の値を直接変更しました。冒頭で紹介した、マクロが既定コードの置き換えを目的としているといった内容を思い出してください。マクロ展開というのは、マクロというルールやパターンから AST のレイヤーで変更を行うことに相当します。つまり、マクロで実現したいことは AST で実現できるということであり、AST で実現できないことはマクロで実現できないということでもあります。

ここで Python の構文とその AST のオブジェクト表現を見比べてみます。

>>> import ast
>>> ast.dump(ast.parse("2 + 2"))
'Module(body=[Expr(value=BinOp(left=Num(n=2), op=Add(), right=Num(n=2)))])'

ある記事の言葉を引用すると、

この結果にはかなりがっかりした。Lisp なら、これと同等の構文木は (+ 2 2) である。この構文木なら誰にでも使えるが、 Python のこんな構文木をいじれるのは本当のエキスパートだけだろう。

Python にはマクロがない。

Python の構文と AST の表現が全く違うことから、マクロを書く・展開する・適用する = AST を操作するということが誰にでも実装できない、ひいては普通にマクロを書くことはないということにつながるように思います。

Karnickel

いきなり MacroPy を読み進めようとすると、複雑過ぎて挫折してしまうかもしれません。私は挫折しました。

学習向けにもっと簡単なものとして Karnickel という小さいライブラリがあります。これは Sphinx の作者としても有名な Georg Brandl 氏 *2ast モジュールとインポートフックのデモとして作ったライブラリのようです。

MacroPy の概要説明の図を使いたかったために紹介する順序があべこべになってしまいましたが、KarnickelMacroPy 同様のモジュール読み込みフローをもちます。インポートフック、NodeTransformer によるマクロ展開といった一通りのサンプルを含み、1ファイル300行ちょっとのコード量です。私は最初に Karnickel のライブラリを読み進めました。

Karnickel のマクロ

マクロ実装のデモ向けライブラリのため、マクロとして提供されている機能そのものにあまり意味はありません。

また使い方の説明が分かりにくいため (インポートフックを知っている前提)、マクロ展開のフローが分かっていないと、試しに実行するところから戸惑うかもしれません。簡単に使い方を紹介します。ちなみに KarnickelPython 3 では動きません (Python 2.6+) 。

example.macros にサンプルのマクロが定義されています。マクロは @macro デレコーターで定義するようです。

from karnickel import macro

@macro
def add(i, j):
    i + j

@macro
def assign(n, v):
    n = v

@macro
def custom_loop(i):
    for __x in range(i):
        print __x
        if __x < i-1:
            __body__

実行するときはこのマクロ定義モジュール以外に、インポートフックがある実行用ファイル (run.py) とマクロをインポートして使うファイル (example/test.py) の2ファイルが必要です (たぶん) *3

# -*- coding: utf-8 -*-
import karnickel
karnickel.install_hook()

from example.test import usage_expr, usage_block, usage_3

print 'usage_expr():', usage_expr()
print 'usage_block():', usage_block()
print 'usage_3():'
usage_3()

実行結果。

$ python run.py 
usage_expr(): 22
usage_block(): 1
usage_3():
0
loop continues...
1
loop continues...
2
loop continues...
3
loop continues...
4
loop continues...
5
loop continues...
6
loop continues...
7
loop continues...
8
loop continues...
9

詳細は追いませんが、NodeTransformer を使ってマクロ展開を実装しているのが karnickel.py で確認できます。

class Expander(ast.NodeTransformer):
    """
    AST visitor that expands macros.
    """

    def __init__(self, module, macro_definitions=None, debug=False):
        ...

まとめ

Python におけるマクロを提供する仕組み、言わばメタプログラミングについて紹介しました。

  • Python におけるマクロ
    • インポートフック
    • 抽象構文木
    • AST 変換 (マクロ展開)

インポートフックを実装してみると、インポート周りの API が分かりにくいと思うかもしれません。良く言えば癖がある、悪く言えば使いにくいといった感じでしょうか。歴史的経緯や互換性もあるでしょうし、メタプログラミングが本質的に難しいということなのかもしれません。以前からインポート周りは Python の良くないところの1つに挙げられている発表を私はいくつか見たことがあります。importlib の冒頭にも書かれているよう、もっと使いやすくなるように今後も改善されていくと思います。

次回は MacroPy で提供されているマクロの機能についても見ていきましょう。

*1:この文脈では一般的な手続き型プログラミング言語を指していると思われます

*2:基調講演者 - PyCon APAC 2013

*3:これらを1ファイルにして実行するとエラーになったのでそういう作りなんだと思います

Python と型ヒント (Type Hints)

先日、Python の静的型チェッカーとして mypy を紹介しました。

私には難しくてまとめきれないため、Guido が参照している漸進的型付け (Gradual Typing) も含め、また別の機会に、、、。

とか言っているうちに1ヶ月ほど経ってしまいました。

そうこうしているうちに PEP のドラフトも出てきたので区切りとしてまとめておきます。一通り調べたことを基にして書いていますが、私の誤解や勘違いもあるでしょうから怪しいところがあったら調べ直してみてください。もちろんツッコミも大歓迎です。

型ヒント (Type Hints) を導入するという提案

現時点の PEP の内容をみて簡単にまとめます (これらはまだドラフトなので今後も内容が更新される可能性があります) 。

PEP 483 では、ここで言う型ヒントは、漸進的型付け (Gradual Typing) という型システムの理論体系に基づくものであり、その型システムがどういったものかという概要を説明しています。この PEP は漸進的型付けを導入するにあたっての原則や動機付け、その参照実装として mypy の構文を使った型アノテーションの具体例を紹介しています。型システムとしての側面に着目してその背景を整理したものにみえます。

そして、PEP 484 は PEP 483 の理論を実現するための型ヒントの標準化、それらの構文を提案するもののようです。ジェネリクスや直和型など動的型付けな型システムにはなかった概念や、プラットフォーム (OS やバージョン) に特化したチェックについての提案もあったりします。この PEP は先週に提案されたばかりというのもあり、メーリングリストgithub でどういった構文や表現方法を受け入れるべきかといった議論がまさにいまも活発に行われています。

mypy から強く触発されているという冒頭の言葉もありますが、現時点の mypy では提供されていない Optional[T]Union[T, None] と見なすといった記述もあります。mypy プロジェクトのブログによると、

作者である Jukka Lehtosalo 氏もその標準化の作業にコントリビュートしていると言っています。おそらくは、型ヒントを表現するために必要なものの大半は mypy に含まれる typing モジュールに実装し、そのモジュールを Python 3.5 から標準ライブラリのような形態で提供するように推測されます。念のために補足すると、これらの PEP で提案しているのは Python の型システムそのものに大きな変更を加えるのではなく、型ヒントに必要なものをなるべく Python モジュールで実現しましょうといった取り組みです。

PEP 484 を実現するために必要な issue が mypy のリポジトリにも登録されています。

これらの PEP を読んだ方の中には、型ヒントの標準化についての提案はあるけれど、それらが型チェッカーでどう扱われるかについては何も書いていないと思われた方もいるでしょう。mypy の作者の記事によると、これらの PEP は型ヒントの標準化を推めるものであって、それをどう扱うかは依然として型チェッカー (mypy も含む) や IDEJIT コンパイラーといったサードパーティツールやプロダクトに委ねるといった方針のようです。

PEP の最後にさらっとこんなことも書いてあります。

Is type hinting Pythonic?

Type annotations provide important documentation for how a unit of code should be used. Programmers should therefore provide type hints on public APIs, namely argument and return types on functions and methods considered public. However, because types of local and global variables can be often inferred, they are rarely necessary.


The kind of information that type hints hold has always been possible to achieve by means of docstrings. In fact, a number of formalized mini-languages for describing accepted arguments have evolved. Moving this information to the function declaration makes it more visible and easier to access both at runtime and by static analysis. Adding to that the notion that “explicit is better than implicit”, type hints are indeed Pythonic .

PEP 484 - Type Hints | Python.org

翻訳するとこんな感じでしょうか。

型ヒントは Pythonic か?

アノテーションは、あるコードがどう使われるべきかという重要なドキュメントを提供します。それ故に、プログラマーはパブリックな API に型ヒントを提供すべきです。すなわちパブリックとみなす関数やメソッドの引数と返り値についてです。とはいえ、ローカル変数やグローバル変数の型は推論されるため、それらについての必要性はめったにないでしょう。


型ヒントのような類の情報は docstring で実現することも可能でしょう。実際のところ、受け取る引数を記述する形式化されたミニ言語もいくつか開発されました。この情報を関数定義へもっていくことは、実行時と静的解析時の両方においてアクセスしやすく、より見通しの良いものにします。さらに "暗黙よりも明示が良い" という考えからも、型ヒントはまさに Pythonic だと言えるわけです。

The Zen of Python からの引用はややこじつけな感もありますが、型ヒントもまた Pythonic という文化やイディオムを支えるものになっていくのかもしれません。

漸進的型付け (Gradual Typing) という型システム

PEP で参照されている漸進的型付けについてみてみましょう。Jeremy Siek 氏による 漸進的型付け の入門記事から要点をまとめます。

  • 静的型付けと動的型付けの良いとこ取りをしようといった型システムである
  • 漸進的型付けを備えた型システムでは、型を書く書かないを同一言語内で選択できる
  • アノテーションを記述すると型チェッカーにより型エラーを捕捉できる

これは前述の PEP においては以下のように説明されています。

Summary of gradual typing

We define a new relationship, is-consistent-with, which is similar to is-subclass-of, except it is not transitive when the new type Any is involved. (Neither relationship is symmetric.) Assigning x to y is OK if the type of x is consistent with the type of y. (Compare this to "... if the type of x is a subclass of the type of y," which states one of the fundamentals of OO programming.) The is-consistent-with relationship is defined by three rules:

  • A type t1 is consistent with a type t2 if t1 is a subclass of t2. (But not the other way around.)
  • Any is consistent with every type. (But Any is not a subclass of every type.)
  • Every type is a subclass of Any . (Which also makes every type consistent with Any , via rule 1.)

That's all! See Jeremy Siek's blog post What is Gradual Typing for a longer explanation and motivation. Note that rule 3 places Any at the root of the class graph. This makes it very similar to object . The difference is that object is not consistent with most types (e.g. you can't use an object() instance where an int is expected). IOW both Any and object mean "any type is allowed" when used to annotate an argument, but only Any can be passed no matter what type is expected (in essence, Any shuts up complaints from the static checker).

PEP 483 - The Theory of Type Hints | Python.org

翻訳すると、

漸進的型付けの概要

我々は is-consistent-with という新しい関係を定義します。それは新たな型 Any が適用されるときに推移的 (transitive)ではないという点を除けば、is-subclass-of の関係によく似ています。(これらの関係に対称性はありません。) もし x の型が y の型と一貫性がある (consistent) なら x を y に割り当てられます。(これを "もし x の型が y のサブクラスであるなら ..." という仮定に置き換えると、オブジェクト指向プログラミングの基礎の1つを述べています。) この is-consistent-with という関係は次の3つの規則で定義されます。

  • 型 t1 が型 t2 のサブクラスなら型 t1 は 型 t2 と一貫性がある。(但し、その逆は成り立たない)
  • Any は全ての型と一貫性がある。(但し、Any は全ての型のサブクラスではない)
  • 全ての型は Any のサブクラスである。(規則1により、全ての型は Any と一貫性があるともみなせる。)

これが全てです!詳細な説明と動機付けは Jeremy Siek 氏のブログ記事 What is Gradual Typing を参照してください。規則3はクラスグラフの根 (root) に Any を置くということに注意してください。これは object にとてもよく似ています。その違いは object がほとんどの型と一貫性がないという点のみです (例えば、int が期待されるところで object() のインスタンスは使えません) 。言い換えると、Anyobject の両方とも、ある引数をアノテートするときに使うには "任意の型を許容する" ことを意味しますが、どの型が期待されるかに関わらず引数に渡せるのは Any のみです (本質的には、Any は静的チェッカーからのメッセージを止める) 。

一貫性がある (is-consistent-with) という関係と Any という型が登場しています。この規則によると、全ての型の基底クラスであり、全ての型と一貫性のある任意の型として Any という型を定義しましょうとあります。ここで、この規則をよく見直してみると、型としての objectAny をあえて分けているのは何のためだろう?という疑問が出てきました。一見すると型としての objectAny の条件を満たせそうにもみえます。PEP には object との違いは一貫性の有無しか書いていません。

メーリングリストでのやり取りを検索してみたところ、おそらくは以下の操作に対する扱いが最も大きな違いではないかと推測します。

Also, consider the important difference between Any and object. They are both at the top of the class tree -- but object has *no* operations (well, almost none -- it has repr() and a few others), while Any supports *all* operations (in the sense of "is allowed by the type system/checker"). This places Any also at the *bottom* of the class tree, if you can call it that. (And hence it is more a graph than a tree -- but if you remove Any, what's left is a tree again.)

[Python-ideas] Type Hinting Kick-off

つまり、object 型はほとんどの操作をサポートしないけれど、Any は全ての操作をサポートするという点です。

def func(x: object) -> int:
    return x + 1  # object 型は + (__add__) という操作をサポートしない

こういったアノテーションの型チェックを行うときに具象型でもある object は型チェッカーからみたら扱いにくいのかもしれません。

>>> object() + 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'object' and 'int'

漸進的型付けの入門記事では、漸進的型チェッカーが不明な型を 動的型 として扱うときに、そういった動的型に対するアップキャスト (派生クラスから基底クラスへのキャスト) とダウンキャスト (基底クラスから派生クラスへのキャスト) の両方を許容するというのが際立った特徴であると説明しています。そのため、サブクラスの関係は 動的型 をアップキャスト・ダウンキャストすることにより同じ型にキャストされてしまい、型チェックが機能しなくなるとあります。

そこで is-consistent-with という関係をもって型チェックするために上述した3つの規則を定義しました。ある型に対するサブクラスと Any は一貫性があると定義することにより、この問題を回避するというのが狙いです。そして object 型は全ての型の基底クラスとなるので一貫性がないという規則が導かれる、ということでしょうかね。

漸進的型付けを導入している言語として wikipedia には以下が紹介されています。ActionScript がこのカテゴリーに含まれるんだというのを知るとそう目新しいものでもないんだなと自身の勉強不足を実感しました。

Examples of gradually typed languages include ActionScript, Dart, Dylan, Hack, Perl 6, Typed Racket, Typed Clojure, TypeScript, and mypy (a static type checker for Python).

wikipedia:en:Gradual_typing

ちょうど Dylan という言語コミュニティでも漸進的型付けについての記事が投稿されていました。

Python の型システムに対する懸念

Flask などの開発者として知られる Armin Ronacher 氏の記事から紹介します。

彼はフレームワークやライブラリの開発経験から API の設計において型システムがどういったものであることを望むかという視点を述べています。いまの動向から未来のプログラミング言語は、強力な型システムを備えていて、なお柔軟性や生産性を保持するといったものになると考察しています。Python という言語は、そのインタープリターの実装である CPython の最適化や歴史的経緯により課題を抱えています。具体的には C 言語側か Python 側かのどちらで実装されるかで型意味論が異なるという状況になっており、そのことは PyPy といった他処理系の実装や API 設計に悪影響を及ぼすと懸念を抱いています。

過去または現在も抱えている CPython の型システムの不明瞭さから、Python に型アノテーションを導入することよりも型システムをより強力なものに改善すべきだというのが彼の想いのようです。とはいえ、これから Python の型システムを改善しようというのは多大な時間と労力がかかることから現実的には無理だろうというのも理解しています。mypy や漸進的型付けへの言及はあまりないことから、型アノテーションの導入には興味がないといったようにみえます。

以前の mypy を紹介した記事においても PyPy のコア開発者である Alex Gaynor 氏も同様のことを示唆していました。

PyPy で良いことがあるんじゃないかと妄想しますが、PyPy のコア開発者である Alex Gaynor は、型アノテーションが PyPy にとって価値がないと断言しています。彼は型アノテーションの導入よりも、Python の型システムを改善しようと提案していますが。

PS: You're right. None of this would provide *any* value for PyPy.

[Python-ideas] Proposal: Use mypy syntax for function annotations
mypy で静的型付け Python プログラミング - forest book

Python の作者である Guido van Rossum 氏からのメーリングリストでのやり取りをみる限り、互換性を崩さずに型アノテーションを導入していこうといった姿勢が伺えました。現在の Python ほどの規模のユーザーコミュニティになると、Armin 氏や Alex 氏が指摘するような型システムの改善という、影響範囲の大きそうな改善を期待するのは難しいのかもしれません。その視点からも mypy で実装された型アノテーションが既存の Python 3 の構文としてそのまま実行可能であるというのは特筆すべきことなのだと思います。

アノテーションと Composability の考察

Andrew Montalenti 氏による記事を紹介します。

彼は、関数アノテーションが導入された当時からアノテーションに対する Composability *1 が欠けていると指摘しています。関数アノテーションには階層化の機構がなかったため、フレームワークによって記述方法が異なったり、そのことが可読性を落とすものになり得ると考えているようです。

例えば、以下のような関数アノテーションの定義方法を比較した場合、

def foo(
    *args: {"doc": "arguments", "type": list}, 
    **kwargs: {"doc": "keyword arguments", "type": dict}): \
    -> {"doc": "a bar instance", "type": Bar}

アノテーションデコレーター を使う Pyanno: Python Annotations の例を紹介しながら

from geometry import Point

@returnType(float)
@parameterTypes(Point, Point)
def getDistance(p1, p2):
    """
    getDistance() calculates the distance between two points.
    """
    ...

一目瞭然であると説明しています。

彼の提案としては、アノテーションを階層化する仕組みと基本的な慣習を提供するというものであったようです。(アノテーションの) 構文を変えずにその上に composition 層を設けるというのであれば、それはコードの明瞭さにはつながらない。そして、いまはアノテーションの用途をドキュメント利用に限定した方が良いのではないかと指摘しながら、mypy の構文は簡潔で表現力のあるものだという点も認めていて、議論の開始点としては良さそうだとも述べています。

最後に

But let’s remember: simple is better than complex — and practicality beats purity!

» Python annotations and type-checking

という The Zen of Python からの言葉で締めくくっています。

Composability という概念

Andrew 氏の記事に対する lawrence 氏のトラックバックも読んでみました。彼は Composability が指すものはこういうものではないかと考察しています。

Python に対する mypy は、実行時にプログラムの動作に影響を与えないため、Clojure に対する Typed Clojure とよく似ています。動的型付き言語に型チェックを追加することは、形式的ではなくユニットテストに必要なもののを多くをアサートできるのに加え、ドキュメントとして役立つので、一定の間違いを防げるというのが彼の経験談のようです。さらに composable とはどういうものだろう?という問いに Shen という実験的言語の機能から、真に composable な型システムは、ある型の規則が別の新たな型定義の一部に使えるものだと説明しています。

最後に Andrew 氏の意味する composable を誰にでも分かるように言い換えると、Clojure ではデータ型の定義とドキュメントとしてのアノテーションの定義は別になっており、その両方を兼ねようとしている Python の型アノテーションは悪い考えだと締めくくっています。

型システムにまつわる用語

型システムについて調べているときに用語が分からなくて苦労したので少し整理しておきます。

選択的型付け (Optional Typing, Optional type systems)

漸進的型付けと関連して似たような意図を表すのに使われるそうですが、厳密には違う定義のようです。基本的には wikipedia の受け売りです。

大きな違いは漸進的型付けは同一言語内で型の有無を選択しようという意図に対して、選択的型付けは型システムの選択と言語の選択を独立させて、必要に応じて言語内にモジュールであるかのように型システムを組み込むことを示唆しています。但し、現実的には型がその言語の動作に影響しないという要件を実現するのは難しく、例えば、クラスベースの継承はできないことになってしまうとあります。

型ヒント (Type Hinting, Type Hints)

私がググった限りでは、厳密な定義をみつけることはできませんでした。おそらくは言葉通りの用語の意味でしかないと思います。

Clojure が型ヒントと選択的型付け (または漸進的型付け) という2つの用語を使い分けています。

Clojure には言語機能として型ヒントの仕組みがあります。これは実行時のリフレクションを避けることでプログラムの最適化を行うことを目的としていて、型エラーを捕捉するといった用途には使えません。

(defn len [x]
  (.length x))
 
(defn len2 [^String x]
  (.length x))
 
user=> (time (reduce + (map len (repeat 1000000 "asdf"))))
"Elapsed time: 3007.198 msecs"
4000000
user=> (time (reduce + (map len2 (repeat 1000000 "asdf"))))
"Elapsed time: 308.045 msecs"
4000000

Python の PEP でも型ヒントという用語を使っていますが、いまのところ、最適化には使われません。型アノテーションをドキュメントもしくは型チェッカーのための用途だと明言して型ヒントと呼んでいるため、Clojure で言う型ヒントと Python で言う型ヒントは異なる用途を指す用語となっています。

そして、選択的型システム (または漸進的型付け *2 ) を導入する仕組みとして Typed Clojure (Python でいう mypy) があります。ann というマクロで関数アノテーションを指定できるようです。

(ann add [Number -> Number])
(defn add [x]
  (+ x 1))

リファレンス:

まとめ

まとめられないですね ... (´・ω・`)

今回扱った話題の中には、型システム、構文、コミュニティなどプログラミング言語に関するいろんな知見を含んでいました。いくつか記事を読んだだけでは自分の中に明確なモデルを構築できなかったのと、自分の解釈が正しいかどうかを検証するのが難しいというのも分かりました。それでも良かったことは、他のプログラミング言語について知るきっかけになったことです。

Python へ型ヒントのための構文や仕組みを導入すること自体は、私が調べた限りでは、一定数の支持は得ていて積極的に進められているようにみえます。Python の型システムに懸念があるというのは、おそらくはその通りなのでしょうけれど、型ヒントの導入とは直接関係するものではないので分けて考えた方が良いように思いました。

*1:一般的には式や関数を組み合わせてプログラムを作るという特性を指すようです。

*2:README には Gradual typing in Clojure, as a library. とあるので厳密な定義を意図しているわけではなさそうです。