clock2021.12.13 09:24update
SERVICE
home

プログラミング歴40年のおじさんが初めて本格的なPythonプログラムを書いてみた

AUTHOR :   ギックス

2.4k

この記事は GiXo アドベントカレンダー の 11 日目の記事です。
昨日は、「非エンジニアばかりの分析チームで1年間 Dataform を運用してみた」でした。

Technology Div. の三ツ井です。たまたま機会があって Python でプログラムを書くことになりました。プログラミングには長い間携わってきましたが、Python は実際に hello world 以上のプログラムを書くのはこれが初めてです。書き始めから数ヶ月経った今に至るまで、書き方の作法やツールの使い方、Python への認識など少しづつ変化してきています。本記事では、その体験をまとめてみたいと思います。長文です。

はじめに自己紹介

プログラミングは、学生の頃が最初でおよそ40年余り。と言ってもずっとプログラミングだけをしてきたわけではありません。キャリアの中で自身でプログラミングをしていた時間は実はそんなに長くはなく、特に後半の20年は立場上いろいろな責務があり、プログラミングしたくても実際に手を動かす時間はなかったという感じではありました。そうは言ってもソフトウェア工学の領域やソフトウェア開発ツール開発の活動が長かったので、効率の良いプログラム開発手法やプログラミング言語の進化にはずっと興味を持っています。Pyhon も含め、最近話題のプログラミング言語や開発ツールの進歩については深く体感するという意味ではキャッチアップできていなかったので、機会があれば試してみたいとは思っていました。

最初に本格的なプログラムを書いたのは Lisp [1] で、大学の研究室では CLU [2] という言語の処理系が稼働していました。その辺りから影響を受けていることは確かです。職業プログラマとしては C++, Java がメインでした。

現プロジェクトの紹介

Python を使うきっかけとなったのは、現在手入力しているデータ入力の業務の効率を機械学習手法によってどの程度向上できるか検証するというものでした。最初は Cross-entropy を使った単純な決定木の生成を試し、その後、LightGBM [3] 技術を使い機械学習による推定精度を検討しました。ここではオープンソースの Python ライブラリが活用できました。

このプロジェクトでは、機械学習を使った自動化の検討だけでなくユーザ自身があるデータから別のデータへの変換ルールを定義することで手作業を減らすというアプローチも同時に検討しています。ユーザは変換ルールを定義するためにプログラムを書くわけではなく、Excel ワークシートでルールを記述します。変換ルールは、単純な値対値の対応表形式の場合もあれば、論理式や集約計算式を使って値を導出する複雑なものもあります。ユーザがルール記述をしたり動作確認するので、それ支援する仕組みが必要となります。この、言わば「エンドユーザ向け変換ルールシステム」の開発では、Python の構文解析ライブラリ、表形式データを扱えるPandas ライブラリ[4] 、Excel の操作をするための openpyxl ライブラリ [5] などが使われています。このシステムは、Google Cloud Platform (GCP) 上に構築されています。

本記事ではこのプロジェクトを通しての Python の使用経験について書きますが、使っている Python のバージョンは Python 3.9 、開発ツールは VS Code 1.62.3。これが前提となります。

動的言語ではあるが静的型付けを行う言語のように使える

一番使用経験が長いのは Java なので、あえて言うなら自分は Java プログラマです。Java プログラマとして Python のような動的言語に対する懸念として感じていたのは、規模の大きなプログラムを型宣言無しに書けるものだろうかということでした。プログラムを読むときは、自分で書いたものであっても型情報を頼りに読んでいきます。コメントも最小限書き込みますが、型情報がほとんど強制的にプログラムの正しい注釈になっているのに対して、コメントは努力を怠るとプログラムとすぐに乖離してしまいます。

Python プログラムを書き始める当初から型ヒントが書けることは知っていましたが、必須ではないこともあり最初はプログラムを実行して結果を得ることを優先的に進めていたこともあり、型ヒントをどの程度つけるかどんな場合にクラスを定義するかについては、色々な書き方を試しているという状態でした。(後でよく調べたら解決できたのですが)型をつけた時に名前の前方参照を文字列でしなければならないとか、型チェッカーのエラーがうまく消せないなど型の使用がしっくりこない感じもあり、当初は自分としての書き方も揺れている状態でした。

正直 Python は自由度が高く色々な書き方ができるため、逆にどう書くべきかを自分なりに腹落ちさせるには時間がかかったように思います。もちろん Effective Python [6] などの書籍やネット上での情報は大変有効ではありました。ただ、腹落ちさせるとなるとこのような知見と自分で試し納得することの両方で進めていくしかないように思えます。

さて、このように最初は作法が定まらず色々な書き方を試していたわけですが、最近ではだいぶ収束しつつあります。例えば今はこのようなスタイルで書いています。

  • 基本的にトップレベルの関数は極力使わずにほぼ全てをクラスとメソッドにする。
  • 簡便に使える名前なしの tuple, 配列, 辞書型は使いすぎないようにする。複雑になりそうならクラスにする。
  • クラス定義では基本的に全て @dataclass アノテーションを使う。
  • クラス変数やインスタンス変数、メソッドのシグネチャには必ず型ヒントを書く。
  • ローカル変数の型宣言は、必ず入れるとは限らないが、冗長過ぎず可読性上あった方が良いと判断する場合は入れる。
  • グローバル変数は出来るだけ使わずクラス変数にする。
  • 型は可能な限り具体的に書く。例えば list ではなく list[dict[str, list[str]] のように書く。
  • 型ヒントで、Optional, cast を使う頻度は高い。
  • Any はできるだけ使わない。Union も基本的には使わない。Union[str, None] (あるいは str | None )と書く代わりに Optional[str] と書く。
  • Optional は None 値を積極的に使いたい場合のみ使用し、型として None 値を含めるべきかの判断は都度意識する。
  • 変数への再代入禁止を表明するために Final を活用する。制約が強い方が意図しない副作用が防げ可読性も高くなると考える。
  • モジュールの先頭には、必ず from __future__ import annotations を入れる。これで型ヒントで名前の前方参照があっても良い感じに記述できる。
  • 1クラス1ファイルという Java のような書き方は今のところしていない。但し、1ファイルに多数のクラスを含めるのはクラス間の結合度を高めてしまっている可能性があるのでファイル分割すべきかは随時注意する。

型検査ツールとしては mypy [7] を VS Code と統合する形で使っていますが、使用感は概ね良好と考えています。型ヒントのない既存ライブラリとの兼ね合いで型エラーが直せずエラーメッセージを抑制するコメントを挿入する場合が稀にありますが、型ヒントを最大限つけていくことが型のミスマッチに由来するエラーをプログラム実行前に早期に検出するのに役立っています。

上記の書き方が「Python らしさ」の観点から全て妥当なものかは確信はないところではありますが、Java プログラマとしては違和感なく以前と同じようなスタイルでプログラム開発ができ、少なくとも型の観点からは当初の大規模開発に関する懸念は大分薄れてきていると感じています。

ツールの支援もなかなか良い。特に、リファクタリング

前述の型ヒントは規模の大きな Python を書く上での重要なプラクティスであり、Linter や型検査ツールはこれを日々正しく進めるために欠かせない道具であると考えています。もう一つ重要なプラクティスであると考えるのがリファクタリングです。アジャイル開発では、機能拡張や変更を受け入れて開発を進めていくため絶えずプログラム構造の見直しと書き直しが必要となります。例えば、プログラムのある部分の振る舞いを少しだけ変えた別のパターンが必要になった時に違いをうまく吸収できるようなパラメタ化を考えることが多いと思います。あるいは単純に関数のサイズが大きくなりすぎた、可読性が落ちている、と感じた時点で論理的な部分に分割しようとするでしょう。

Python は動的な言語で静的な型付けを強制しないため、上記のような書き換えを行うときにプログラマが十分注意しないと、意図せず振る舞いが変わってしまいツールもそのことを検出できず、実行時のエラーやテストフェーズでのエラーの発見なってしまうということが起きやすいと考えます。一般にリファクタリングツールはプログラムの振る舞いを変えない変換をしてくれるのでこのような問題を避けるのに有効です。リファクタリングツールは特に規模の大きなソフトウェア開発では非常に重要な要素の一つだと考えてきました。

Java ではかなり高度なリファクタリングツールが存在していて使用経験があるのですが、「動的な言語である Python ではどの程度リファクタリングツールの機能が使えるのか」というが当初持っていた懸念材料の一つではありました。

幸いにも現在使っている VS Code では以下の Python リファクタリング機能が標準でバンドルされているようです。

  • シンボルの名前変更
  • メソッドの抽出
  • 変数の抽出

「シンボルの名前変更」は、選択されている関数名、クラス名、変数名に対してそれらが出現する場所でプログラムの意味を保つように変更します。同一ファイル中に同じ名前が存在していても、名前が意味するプログラム要素が別であれば別な名前として扱われます。

「メソッドの抽出」は、プログラム中の選択された部分を新しいメソッドまたは関数として括り出します。生成されるメソッドまたは関数の引数や return 文は自動生成されます。これで長くなりすぎた関数を分割できます。

「変数の抽出」は、プログラム中の選択された部分が式であるときに、これを新しい変数とその初期値の代入として挿入し、選択した式が出現する場所をその変数名に置き換えます。

これまで VS Code の Python リファクタリング機能を使ってきた経験から言えるのは、若干の不便や限界はあるものの「そこそこ使える」です。

これまで認識している不便を挙げるとすれば、

  • 抽出したメソッドの引数や戻り値には型ヒントが自動的に付かないため、手作業で型ヒントを加える必要がある。もちろん型ヒントを加えなくても振る舞いは変わっていないので適用前と振る舞いは変わらないが。
  • 抽出したメソッドの先頭にコメント行がある場合にプログラム部分のインデントが狂ってしまう。Python ではこれはエラーとなるため手直ししないと動かない。これはツールのバグだと思われる。
  • 抽出したメソッドの引数の順序は自動的に決まる(指定はできない)。順序が気に入らない場合が多いため、結局手作業で直す。ここで間違えると振る舞いが変わるかエラーになってしまうため注意が必要である。
  • モジュール名(ファイル名)の変更がリファクタリングの対象ではないためモジュール名を変更した場合は、それをインポートしている他のモジュールを全て手で修正する必要がある。

このような不便や手作業があるため、リファクタリング実施後には他のことをする前に入念なテストが必要ですが、それでもリファクタリングツールは予想以上に良い感じで機能して、まあまあ安心してプログラムの書き換えができるので、日々の開発作業の中では欠かせないものとなっています。

ここまで書いてきたことは、一言で言うと「Python プログラミングでも、Java など別の(静的)プログラミング言語のベストプラクティスが違和感なく活かせる」というになるかと思います。次は、Python 言語、Python 環境ならではの戸惑った点や注意点について整理してみたいと思います。

Python らしい書き方

「Python らしさ (Pythonic Thinking)」については Effective Python でも最初の章で取り上げられており、きれいに簡潔にプログラムを書くといった言語独特の世界観があることがわかります。実際、Python プログラムを書き始めた頃はどのようのにプログラムを書くべきかは手探りであり、また言語の全部の機能を記憶して書いているわけではない状態だったため書き方にも揺れやブレがありました。その中で Effective Python や豊富なネット上の記事を参照し実際に試行錯誤を繰り返す中で書き方も定まっていった形です。例えば、リスト型や辞書型データのコンストラクタが括弧記号だけで済むのは簡潔で便利であるし、その内包表記を使えば同じことを for 文や map 関数(と list 関数の組み) で書くよりも簡潔に書け可読性も上がります。

例を一つ挙げると、

これを内包表記を使うと、

のような数学の集合記法に近い形になります。

Effective シリーズは他のプログラミング言語にもある書籍でこれまでも参考にしてきたので Python でも言語の学び始めから使っています。言語仕様を学ぶ教科書ではありませんが、言語独特の使い方のコツやコーディングのベストプラクティスや Python コミュニティについて手早く知るには良い手段の一つだと思います。

気になるパフォーマンス

Python は動的な言語でインタプリタ言語であるのでコンパイル言語に比べ実行速度は劣るわけですが、「実際それがシステムの設計にどのように影響してくるだろうか、処理の並列度を上げることで対処できるだろうか」というのも当初持っていた懸念の一つでした。

並列性

Python にはスレッド機能があり、スレッドプログラミングを行うために必要なライブラリも用意されています。しかし、GIL (Global Interpreter Lock) があるために並列に走りうるスレッドを複数走らせても GIL で処理が直列化されてしまい実際には並列処理にはならずマルチコアCPUのコアを使い切ることができません。処理を並列に行うにはプロセスライブラリを使って Python プロセスを複数生成します。これで複数のコアを使った処理になります。

今のプロジェクトの中でもいくつかの局面ではマルチプロセスの処理をしています。Python のプロセスライブラリはプロセスを跨いだ関数呼び出しが簡潔に書ける機能を提供してくれるため、比較的簡単に記述できます。ただし、シングルプロセスで動いていたプログラムをマルチプロセス対応にするためにはプロセス間で受け渡しするデータがシリアライズ可能でなければならず、多少なりとも書き換える必要はあります。またマルチプロセスの処理になれば使うメモリの総量はマルチスレッド処理に比べるとかなり多くなってしまいます。コア数8、メモリ16GBのPCで開発をしていますが、マルチプロセス処理にしても経験上コアを使い切ることはできずCPUについては50%程度の使用率で頭打ちという感じでした。

もっとも、クラウド環境での実行とスケールアウトを考えるのであれば、分散マルチプロセスでの最適化を最初から考えるべきかもしれません。コアを使い切るという発想自体古いのかもしれません。

ライブラリのパフォーマンス

パフォーマンスについてもう一つ触れておきたいのが、Python ライブラリの使用です。Python ライブラリは全ての部分が Python で書かれているとは限らず、一部が C, C++ などでコンパイルされている場合がありマルチスレッド化されていたりするので、その場合は違ったパフォーマンス体験が得られます。最初に使った機械学習のための LightGBM ライブラリは見事にコアを使い切っていました。本体の多くの部分は C++ で書かれているようです。

表形式のデータを扱う上では Pandas ライブラリ を使っています。Pandas は Cython [8] で書かれて C に変換されるので高速です。但し、残念ながらマルチスレッド化はされていないように見えます。いずれにしても開発中のシステムでは Pandas はパフォーマンスの観点から非常に重要な要素となっています。同じことを Python のプログラムだけで書くとパフォーマンスは劇的に遅くなります。

Pandas を使う場合、その使い方、プログラムの設計では独特の注意が必要となります。Pandas は列指向であり、最大限のパフォーマンスを得るには列単位の処理を意識したプログラミングをすることになります。Pandas では行列(pandas.DataFrame) は列 (pandas.Series) の集まりであり、列に対する処理はほとんどCで実装されているため非常に高速です。Pandas を使って行単位の繰り返し処理を書くことも可能ですが、列指向のデータ構造から行データを構成するオーバーヘッドとループの中にPythonプログラムによる処理が入ってしまうことからパフォーマンスに相当な差が出てきます。

pandas.Series クラスでは条件判定や数値計算、文字列計算などのメソッドが豊富に用意されているのでそれを組み合わせて様々な処理を列指向の作法で書くことができます。感覚的には SQL で処理を書いているのに近いかもしれません。

以下の例は3行2列の行列を作って特定の条件を満たす行を取り出すというプログラムを列指向で記述しています。

角括弧の表記に比較演算子や論理演算子が掛かっているところは特殊メソッドに慣れていないと一瞬戸惑います(最初私は戸惑いました)。このプログラムは、まず行列 (pandas.DataFrame) から列 (pandas.Series) オブジェクトを取り出し、その列データに対して列単位でまとめて比較メソッドや論理演算メソッドを呼び出して真偽値からなる列オブジェクトを計算し、最後に得られた真偽値列オブジェクトを用いて行列から特定の行を取り出します。ここで 「for ループ文が無い」ことに注目してください。この一連の処理はかなり高速です。

同じことを行指向で書くと for 文を使った以下のようになります。

データの規模が大きいとこの両者には大きなパフォーマンスの差が出てきます。結局、繰り返し回数の多いループを出来るだけ Python プログラムとしては書かないようにし、効率の良いライブラリが使えるならそれを探して活用する努力をすべきです。

おわりに

振り返ると、「Java プログラマが Python を書き始める」という状況において、色々調べる必要はあったものの思った以上に違和感の少ないスムーズな出発点だったように思います。Python は頻繁にバージョンアップデートがなされていて、例えば型ヒントも継続的な改善がなされているため、数年前であればもう少し違った感覚を持ったかもしれません。リファクタリングツールも同様で新しいバージョンリリースの度に改善されていて今後の進展が期待されます。

私の Python プログラミングはまだ学習途上ではあり、本記事の記述にも不明瞭だったり十分正確でない点があるかもしれませんが、同じようなプログラマの皆さんに少しでも有用な情報が提供できたならば幸いです。


明日は「BigQueryスロット利用量の数え方・考え方」を公開予定です。

参考文献

[1] https://ja.wikipedia.org/wiki/LISP

[2] https://ja.wikipedia.org/wiki/CLU 

[3] https://lightgbm.readthedocs.io/ 

[4] https://pandas.pydata.org/

[5] https://openpyxl.readthedocs.io/ 

[6] Brett Slatkin. Effective Python, 2nd Edition. Addison-Wesley Professional, 2019. URL: https://effectivepython.com/.

[7] https://mypy.readthedocs.io/

[8] https://ja.wikipedia.org/wiki/Cython 


Kinichi Mitsui
Data-Informed 事業本部 / Technology Div. 所属
外資系IT企業においてソフトウェア開発技術の研究および製品開発、クラウドサービス開発に従事。定年退職後にGiXoに入社。

SERVICE