Python で Unicode 正規化 NFC/NFD の文字列を扱う

先日、ビジネスパーソン向けの Python 本を執筆したことを書きました。

t2y.hatenablog.jp

本稿では本書のことを「できるPy」と呼びます。

Amazon でいくつかカスタマーレビューもいただいて次のコメントをみつけました。

python3.7 対応ということで、pathlib を使ってる点が(古いpython は切り捨てる!的なところは)潔いと言えば潔いし、日本語のファイル名にも気を配っている記述はオライリーに期待するのは酷なところもある。でもこの本でもNFD問題は全くの記述無し。だめだろ、それじゃ。

Amazon CAPTCHA

まさに仰る通りです。執筆時にそのことに気づかずご指摘いただいてありがとうございます。

ここでご指摘されている NFD 問題というのは、ファイル名のみに限った問題ではなく、Unicode文字集合を扱ってエンコード/デコードするときに発生する問題です。ASCII 文字しか扱わない開発者が書いたコンテンツとの差別化を図るという意味でも付加価値になり得るコンテンツだと私も思います。

本稿では「できるPy」に掲載できなかった NFC/NFD 問題について説明しようと思います。私の一存では決められませんが、本書を改訂できるタイミングがあれば、出版社と相談して本稿の内容も追加してもらうように働きかけようと考えています。

本稿で紹介するサンプルデータならびにサンプルコードは次の URL からダウンロードできます。

NFC/NFD 問題とは

Unicode は世界中のすべての言語で使われる文字を扱う文字集合です。Python は内部的に Unicode で文字列を扱います。例えば、UTF-8エンコードされたファイルの内容を読み込むコードが次になります。

import sys

filename = sys.argv[1]
with open(filename, encoding='utf-8') as f:
    for line in f:
        print(line.strip())

Unicode では1つの文字を表すのに複数の文字を組み合わせることができます。そして、同じ文字が別々の表現だと検索や置換などに不都合があるのでどれか1つの表現に統一することを 正規化 と呼びます。その正規化形式の名前の頭文字をとって NFC、NFD と呼びます。

  • NFC: Normalization Form Canonical Composition (合成済みの文字)
  • NFD: Normalization Form Canonical Decomposition (複数文字を結合した文字列)

言葉だけではわかりにくいので実際にその違いをサンプルデータとともに説明します。いま NFC/NFD で正規化されたテキストファイルを前述したプログラムを使って中身を表示してみます。

実行結果。

$ python3 read_file_and_print.py NFC_sample1.txt
プログラミング

$ python3 read_file_and_print.py NFD_sample1.txt
プログラミング

どちらも「プログラミング」と表示されました。OS 環境や OS バージョンによって正しく表示されない可能性もあります。私の環境 (macOS 10.14.4) では見た目上は全く同じに表示されます。

このとき変数 line には UTF-8 でデコードされて Unicode の文字列がセットされます。次に NFC/NFD 問題をイメージしやすいように Unicode の文字列を文字単位で扱うサンプルを紹介します。

import sys
import unicodedata

def show_unicode_name(line):
    for char in line.strip():
        name = unicodedata.name(char)
        space = ' '
        if unicodedata.combining(char) != 0:
            space += ' '
        print(f'{char}{space}: {name}')

filename = sys.argv[1]
with open(filename, encoding='utf-8') as f:
    for line in f:
        text = [char for char in line.strip()]
        print(f'文字単位: {text}, 長さ: {len(text)}')
        show_unicode_name(line)

実行結果。

NFC で正規化されたテキストを文字単位で分割して Unicode データベースに登録されている名前を表示する。

$ python3 read_file_and_show_unicode_name.py NFC_sample1.txt
文字単位: ['プ', 'ロ', 'グ', 'ラ', 'ミ', 'ン', 'グ'], 長さ: 7
プ : KATAKANA LETTER PU
ロ : KATAKANA LETTER RO
グ : KATAKANA LETTER GU
ラ : KATAKANA LETTER RA
ミ : KATAKANA LETTER MI
ン : KATAKANA LETTER N
グ : KATAKANA LETTER GU

NFD で正規化されたテキストを文字単位で分割して Unicode データベースに登録されている名前を表示する。

$ python3 read_file_and_show_unicode_name.py NFD_sample1.txt
文字単位: ['フ', '゚', 'ロ', 'ク', '゙', 'ラ', 'ミ', 'ン', 'ク', '゙'], 長さ: 10
フ : KATAKANA LETTER HU
 ゚  : COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
ロ : KATAKANA LETTER RO
ク : KATAKANA LETTER KU
 ゙  : COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK
ラ : KATAKANA LETTER RA
ミ : KATAKANA LETTER MI
ン : KATAKANA LETTER N
ク : KATAKANA LETTER KU
 ゙  : COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK

NFD で正規化された文字列では濁音や半濁音が別の文字として扱われていて、Unicode の文字として数えたときの文字数も異なっていることがわかります。例えば「プ (U+30D7)」と、「フ (U+30D5)」と合成用半濁点 (U+309A) の組み合わせで表す「プ」の文字は Unicode 上では別の文字なのでこの違いにより、検索や置換などで一致しないということが発生します。

このような Unicode における正規化の違いによって起こる問題のことを NFC/NFD 問題と呼ばれたりします。

NFC/NFD 問題が発生する状況

OS によって Unicode の正規化形式が異なります。Windows/Linux では NFC を採用しており、昔の macOS (Mac OS X) では NFD を採用していました。同じプラットフォーム上で日本語ファイル名や日本語のテキストを扱っている分には、この問題に遭遇することは少ないかもしれません。

また macOS では NFD を採用しているという記事をみかけますが、最近の macOSiOS では NFC を使うように変わっているそうです。ファイルシステムが HFS+ から Apple File System (APFS) に置き換えられたときに変わったのかもしれません。私はその背景に明るくないため、詳しい方がいましたら教えてください。

私の環境は macOS 10.14.4 ですが、APFS を使っているせいか、NFCUnicode 正規化が行われます。

$ mount
/dev/disk1s1 on / (apfs, local, journaled)

この NFC/NFD 問題は昔の macOS (Mac OS X) で作られた日本語ファイル名や、Unicode文字集合に用いる符号化方式でエンコードされたコンテンツを Windows/Linux 環境で扱うとき、もしくはその逆のときに発生します。一方で NFD を採用していたときの macOS (Mac OS X) であっても、例えば Web ブラウザからコピペしたコンテンツは NFC で扱われたりして、同じプラットフォーム上でも NFD と NFC が混在してしまうといったことも発生するそうです。そして、見た目上は全く同じ文字にみえるため、この問題に気付くのが難しいというのが NFC/NFD 問題の厄介なところです。

Python で NFD を NFC に正規化する

さて本題です。ここまでで NFC/NFD 問題の概要を説明しました。

Python では標準ライブラリで Unicode データベースを扱うライブラリ unicodedata が提供されています。このライブラリを使うことで Unicode 文字の情報を取得したり、読み込んだ文字列に対して NFC/NFD といった正規化形式の変換をやり直すこともできます。

docs.python.org

unicodedata.normalize() の引数に NFC や NFD といった正規化形式と Unicode 文字列を渡すと変換できます。

unicodedata.normalize('NFC', unistr)

また結合文字かどうかは unicodedata.combining() に文字を渡してゼロ以外が返ってくるかどうかで判断できます。

unicodedata.combining(chr)

次のサンプルコードでは、濁音、半濁音などの結合文字があるかどうかを調べて、結合文字がある場合は NFC に変換します。

import sys
import unicodedata

def is_nfd(line):
    for char in line.strip():
        if unicodedata.combining(char) != 0:
            return True
    return False

def show_unicode_name(line):
    for char in line.strip():
        name = unicodedata.name(char)
        space = ' '
        if unicodedata.combining(char) != 0:
            space += ' '
        print(f'{char}{space}: {name}')

filename = sys.argv[1]
with open(filename, encoding='utf-8') as f:
    for line in f:
        text = [char for char in line.strip()]
        print(f'文字単位: {text}, 長さ: {len(text)}')
        show_unicode_name(line)

        if is_nfd(line):
            print('NFD から NFC への変換')
            converted = unicodedata.normalize('NFC', line)
            show_unicode_name(converted)

実行結果。

$ python3 read_file_and_normalize.py NFD_sample1.txt
文字単位: ['フ', '゚', 'ロ', 'ク', '゙', 'ラ', 'ミ', 'ン', 'ク', '゙'], 長さ: 10
フ : KATAKANA LETTER HU
 ゚  : COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK
ロ : KATAKANA LETTER RO
ク : KATAKANA LETTER KU
 ゙  : COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK
ラ : KATAKANA LETTER RA
ミ : KATAKANA LETTER MI
ン : KATAKANA LETTER N
ク : KATAKANA LETTER KU
 ゙  : COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK
NFD から NFC への変換
プ : KATAKANA LETTER PU
ロ : KATAKANA LETTER RO
グ : KATAKANA LETTER GU
ラ : KATAKANA LETTER RA
ミ : KATAKANA LETTER MI
ン : KATAKANA LETTER N
グ : KATAKANA LETTER GU

まとめ

PythonUnicode の文字列が NFC/NFD のどちらの形式で正規化されているか、また別の正規化形式に変換する方法を紹介しました。

最近の macOSNFC を採用していることからいまが過渡期で NFC/NFD 問題は徐々に NFC に統一されていって将来的にはこういった問題が起こらなくなるのかもしれません。一方で NFD という表現形式も Unicode で定義されている仕組みであり、この仕組み自体がなくなるわけではありません。

本稿では濁音・半濁音のみを紹介しましたが、特殊な記号を複数の記号の組み合わせで表現することもできます。Unicode にはこのような表現形式があるというのを覚えておくといつか役に立つことがあるかもしれません。

リファレンス

ja.wikipedia.org

en.wikipedia.org

www.slideshare.net

gihyo.jp

qiita.com

できる 仕事がはかどる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ファイルにして実行するとエラーになったのでそういう作りなんだと思います