2018/06/06

Stack Overflow Developer Survey 2018 の Python 関連データまとめ



開発者向け Q&A サイトの Stack Overflow が 2011 年以降毎年開発者向けの調査を行いその結果を公表しています。 公式のコメントによると、 2018 年の調査ではなんと 10 万人を越える開発者が調査に協力したそうです。


こういう調査の結果は、驚きの情報が得られるわけではありませんが、世間のトレンドや自分の立ち位置を確認するのによいですよね。

今回は、この 2018 年の調査結果データのうち Python 開発者に関係する部分を抽出してグラフ化してみました。周りに Python 開発者があまりいない方等にとってはとてもおもしろい調査結果なのではないかと思います。興味のある方はぜひご覧ください :)

  • 集計方法
  • 注意点
  • 調査結果グラフ
  • 終わりに

集計方法


Stack Overflow が公開している結果データ csv のうち、次の質問の選択肢で Python にチェックが入った人の回答のみを集計しました。

Which of the following programming, scripting, and markup languages have you done extensive development work in over the past year, and which do you want to work in over the next year?

意訳:

過去 1 年に主に使用した言語と、先 1 年に使用したい言語はどれですか?

数としては、 csv ファイルに含まれる回答全 98,855 件のうちおよそ 30% の 30,359 件が該当しました。

グラフ作成において、回答の選択肢が多い質問では、すべての回答をグラフにすると細かい部分が見づらくなるので、比率の少ない選択は「 others 」という項目に集約しています。

注意点


グラフを見る上での注意点をいくつかあげます。

  • 2018 年の調査結果( csv )が使用されています。
  • この調査の回答が Pythonista の母集団をどれだけ代表しているかは不明なので、この結果を Pythonista 全体にそのまま一般化することはできません(とはいえ、ヒントにはなるかと思います)。
  • Python ユーザと他の言語のユーザの比較は行っていないため、 Python だからこの結果なのか Stack Overflow の調査だからこの結果だからなのかは不明です。
  • グラフ化しているのは調査結果のうちごく一部です。
  • 単一選択の質問は円グラフ、複数選択可の質問は棒グラフでそれぞれ表しています。

では順に見ていきましょう。

調査結果グラフ


DevType (開発者のタイプ)



Which of the following describe you? Please select all that apply.

「開発者としての属性を教えてください」という質問です(複数選択可)。

バックエンド開発者が 58% 、フルスタック開発者が 45% と、回答者の大半はバックエンドの開発を行っている開発者です。学生が 22% 、デザイナー 11% と、本職の開発者以外の人の割合が高いのは Stack Overflow のサービスが Q&A サイトだからでしょうか。

データサイエンティストとマシンラーニングスペシャリストが多いのは Python ならではな気がします。ただ、 Python Software Foundation が 2017 年に行った別の調査ではデータ解析やマシンラーニングをやっている人の割合がこれよりもずっと多かったので、この調査の回答者は「ウェブ開発者が多めに答えている」と考えるのがよさそうです( Stack Overflow のサービスの性質上、それは当然な気もします)。

FormalEducation (最終学歴)



Which of the following best describes the highest level of formal education that you’ve completed?

「最終学歴を教えてください」という質問です(単一選択)。

学部( Bachelor )卒が 43% 、修士( Master )卒が 24% とのことです。修士の割合を他の似た言語と比較すると、 JavaScript は 21% 、 PHP は 18% でした。 Python でよく行われるマシンラーニングを原理を理解して行うには最低でも学部レベルの数学は必要ですし、 Python の利用者に修士が多いのは Python らしいと言えるかもしれません。

ちなみに、国内に関して言うと、文部科学省の資料によると 2012 年時点で学部生 256 万人に対して大学院生は 26 万人だそうです。ざっくり平均で学部生は 4 年、大学院は 3 年在籍すると考えると、学部卒と院卒の割合は学部卒 10 人に対して院卒 1.0〜1.6 人程度になるでしょうか。アメリカやヨーロッパの院卒比率が一般に日本よりも高いことを考慮しても、この調査の回答者は修士卒比率が非常に高いことがわかります(というわけで、この回答者群は全 Pythonista の母集団をよく代表していないと言えます(笑))。


UndergradMajor (学生時代の専攻)



You previously indicated that you went to a college or university. Which of the following best describes your main field of study (aka 'major')

「専攻を教えてください」という質問です(単一選択)。

コンピュータ・サイエンスやソフトウェア・エンジニアリングといった「ソフトウェア関連」が専攻の人たちが最も多く全体の 62% を占めます。 3 位の情報システム・情報技術とあわせると 68% にもなります。 Pythonista にかぎらない全体の回答でも傾向はほぼ同じですが、 Pythonista の場合はその他の工学系・生物学・物理学・数学等の専攻の割合が回答全体よりも少し高いのが特徴です。これは、 Python が広い分野で「プログラミングの非専門家」にもよく使われているという事実を裏付けているといえるでしょう。

世界的には(日本以外では)、最低でも工学系の学士を持っていることが開発者になる最低条件となっている国が多く、文系でも開発者になれる日本の状況は世界ではむしろ例外とも聞きます。この結果からはそのあたりのところも読み取れます。

個人的には、日本の、努力次第で誰にでも職業プログラマへの道が開かれている(=学歴軽視な)状況は素晴らしいと思う反面、基礎知識や適正が無い人もかんたんにプログラマになれてしまって(=実戦投入されてしまって)ろくなキャリアが築けなくても自己責任なのは社会全体で見て非効率でよくないなぁと思います。とはいえ、弁護士や博士のように、政府が無理やり開発者を増やしてもうまく行かなさそうなので、なかなか難しいですね。

Age (年齢)



What is your age? If you prefer not to answer, you may leave this question blank.

「何歳ですか」という質問です(単一選択)。

25 - 34 歳が最も多くほぼ半数の 47% 、続いて 18 - 24 歳が 28% となっています。

Stack Overflow のような Q&A サイトは熟練者よりも経験の少ない人の方が比較的よく使うと思うので、これはそれを反映しているものと思います。とはいえ、 18 - 34 歳が大多数を占めるというこのデータを見ると、高齢国日本と世界の大きなギャップを感じます。

LanguageWorkedWith (使用言語)



Which of the following programming, scripting, and markup languages have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the language and want to continue to do so, please check both boxes in that row.)

「過去 1 年に使用した主な言語は何ですか」という質問です(複数選択可)。

この質問で Python にチェックが入った回答のみを使っているので Python 使用率は当然 100% です。続く言語は次のとおりとなっています。

  • JavaScript 69%
  • HTML 68%
  • CSS 65%
  • SQL 58%
  • Bash/Shell 57%
  • Java 50%
  • C++ 35%
  • C 33%
  • PHP 31%
  • C# 29%

JavaScript / HTML / CSS / SQL / Shell あたりはウェブ開発で欠かせないので、このあたりの使用率が高いのは当然といえば当然かもしれません。個人的には、 C の 33% が意外に高くて驚きです。約 10% が R や Matlab を併用しているのは、データサイエンスその他の科学でよく使われる Python ならではのような気がします。

LanguageDesireNextYear (希望言語)



Which of the following programming, scripting, and markup languages have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the language and want to continue to do so, please check both boxes in that row.)

「次の 1 年に使いたい言語は何ですか」という質問です(複数選択可)。

上位はおおむね上の「使用言語」と同様の顔ぶれです。「使用言語」との対比でいうと、 Go / Kotlin / Rust あたりが「現在使っていないけれど新たに使いたい言語」として人気なようです。逆に人気が無い言語は PHP あたりでしょうか。

DatabaseWorkedWith (使用データベース)



Which of the following database environments have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the database and want to continue to do so, please check both boxes in that row.)

「過去 1 年に使用した主なデータベースは何ですか」という質問です(複数選択可)。

MySQL / PostgreSQL あたりが一番人気なのは統計を取らなくてもなんとなくわかりますね。個人的には、 SQL Server / MongoDB / Elasticsearch あたりが意外と高くて驚きです。世間ではよく使われているんですねぇ。

DatabaseDesireNextYear (希望データベース)



Which of the following database environments have you done extensive development work in over the past year, and which do you want to work in over the next year? (If you both worked with the database and want to continue to do so, please check both boxes in that row.)

「次の 1 年に使いたいデータベースは何ですか」という質問です(複数選択可)。

上位は「使用データベース」とあまり変わらないのでコメントがしづらい感じです。クラウドのデータベースについては一様に新たに使ってみたいと考えている人が多いようですね。

PlatformWorkedWith (使用プラットフォーム)



Which of the following platforms have you done extensive development work for over the past year? (If you both developed for the platform and want to continue to do so, please check both boxes in that row.)

「過去 1 年に使用した主なプラットフォームは何ですか」という質問です(複数選択可)。

作業マシンに加えてウェブサーバとして使うマシンの OS も含まれているようで、 Linux が Windows を上回っています。また、 AWS が 26% と、クラウドの中では AWS が抜群によく使われているようです。

WordPress が選択肢に含まれていることが少し不思議な感じがしますが、 WordPress は 13% と非常に高くなっています。これは、 WordPress が iOS 等に並ぶほどよく使われているということでしょうか。 CMS の中では一人勝ちに近い WordPress 恐るべしです。

PlatformDesireNextYear (希望プラットフォーム)



Which of the following platforms have you done extensive development work for over the past year? (If you both developed for the platform and want to continue to do so, please check both boxes in that row.)

「次の 1 年に使いたいプラットフォームは何ですか」という質問です(複数選択可)。

「使用プラットフォーム」との比較で言うと、 Windows の人気が低いのが目立ちます。一方、人気が高いのは Raspberry Pi 、 Serverless 、 Amazon Echo あたりです。 Raspberry Pi の人気が高いのは「 Python だから」というのもありそうですが、日本国内に比べて海外の Raspberry Pi の人気が高そうなことが伺えます。

FrameworkWorkedWith (使用フレームワーク)



Which of the following libraries, frameworks, and tools have you done extensive development work in over the past year, and which do you want to work in over the next year?

「過去 1 年に使用した主なフレームワークは何ですか」という質問です(複数選択可)。

Python を使っている開発者であれば Django 使用率が最も高いのかと思いきや、 JavaScript ・ Node 関連のツールが Django と同等かそれ以上の人気のようです。回答者全体の割合でも Node や Angular ・ React の割合が非常に高いので、 JavaScript は今や開発者の必修言語と言ってもよいかもしれません。

ちなみに、 Vue.js や Flask は回答の選択肢に入っていないためこれらの使用状況はわかりません。

FrameworkDesireNextYear (希望フレームワーク)



Which of the following libraries, frameworks, and tools have you done extensive development work in over the past year, and which do you want to work in over the next year?

「次の 1 年に使いたいフレームワークは何ですか」という質問です(複数選択可)。

「使用フレームワーク」との比較で言うと、マシンラーニング用の TensorFlow の人気がとにかく高いことがわかります。 Torch/Pytorch についても 10% の人が「使いたい」と考えており、マシンラーニング用のツールが全般的に注目を集めているようです。

IDE (統合開発環境)



Which development environment(s) do you use regularly? Please check all that apply.

「開発環境として何を使っていますか」という質問です(複数選択可)。

回答者全体の Vim の使用率は 25.8% なので、 Vim の使用率が高いのは Python 開発者ならではです。また、 VS Code や Visual Studio よりも Sublime Text の使用率が高いのも Python 開発者ならではと言えそうです。

個人的には、(私は Notepad++ を使わないので) Notepad++ が Sublime Text や VS Code に並ぶぐらい多く使われていることに驚きです。本格的な IDE のカテゴリでは JetBrains 社の IDE が非常に強いこともわかります。 IPython / Jupyter が 17% も使われているのも Python ならではですね。

OperatingSystem ( OS )



What is the primary operating system in which you work?

「メインで使う OS は何ですか」という質問です(単一選択)。

Windows / Linux / その他がそれぞれ 3 分の 1 ずつとなっています。 Linux が結構よく使われているんですね。 Vagrant 等の影響でしょうか。

CommunicationTools (コミュニケーションツール)



Which of the following tools do you use to communicate, coordinate, or share knowledge with your coworkers? Please select all that apply.

「一緒に働く人たちとのコミュニケーションツールとして何を使っていますか」という質問です(複数選択可)。

個人的には、 Slack と Office が多いのは納得ですが、 Jira や Confluence が多いのは驚きです。私の印象では日本ではあまり使われていないのですが、海外では(なのか日本でもそうなのかわかりませんが)結構使われているのですね。

また、チャットといえば Skype が Google Hangouts と同じくらいは使われているのではないかと思っていましたが、回答の選択肢にそもそも無かったので、開発者にはあまり使われていないのかもしれませんね。

NumberMonitors (モニタの数)



How many monitors are set up at your workstation?

「モニタは何枚使っていますか」という質問です(単一選択)。

51% という約半数の人たちが 2 枚使っています。 1 枚・ 2 枚以外の選択肢は 3 枚以上です。回答者全体の割合もこれとほぼ同じなので、使えるモニタが 1 枚しか無い職場で働いている人は「海外も含めて世界の 7 割以上の開発者は自分よりも恵まれた環境でコーディングしているんだ」と考えてよいでしょう。

モニタのサイズ等にもよるので一概には言えませんが、個人的にはモニタは 3 枚ぐらいまでなら増やせば増やすほど生産性が上がるので、モニタの追加は費用対効果がとてもよい設備投資だと思います。逆に、モニタ 1 枚だけで開発をするのはシューズを片方しか履かずにマラソンを走るようなものなので、当然のようにモニタ 1 枚しか用意しないような会社には開発者は関わってはいけません(個人の見解です)。

Methodology (開発手法)



Which of the following methodologies do you have experience working in?

「どのような開発手法の経験がありますか」という質問です(複数選択可)。

アジャイル( Agile )とスクラム( Scrum )がそれぞれ約半数で、カンバン( Kanban )とペアプログラミング( Pair programming )が 4 分の 1 程度でそれに続きます。日本はアジャイル手法の広まりが遅れていると言われることがありますが、それを実感するような結果ですね。「約半数がアジャイルやスクラムの経験がある」と言われると、日本とは完全に別世界の話に思えます。

とはいえ、回答全体で見ると、アジャイル 85.4% 、 Scrum 62.7% と Python 開発者に限定した使用率よりも高いので、(私の集計が間違っていなければ) Python 開発者はアジャイル手法をあまり使っていないと言えそうです。

このあたりの国内外ギャップにはさまざまな原因があるかと思いますが、「日本では請負が主流で、欧米は(にかぎらず海外全般も?)インハウスの開発が主流」と聞くので、そのあたりの商習慣や業界構造が大きく影響していそうです。また、日本ではおそらくコンピュータ・サイエンスやソフトウェア工学を専門的に学んだ人の割合が少なく、各手法を正しく理解して実践できる人が単純に不足しているというのも要因のひとつではないかと思います。

VersionControl (バージョン管理)



What version control systems do you use regularly? Please select all that apply.

「ふだんバージョン管理システムとして何を使っていますか」という質問です(複数選択可)。

Git が 88% とほぼ 9 割です。全体の回答でもほぼ同じ割合なので、これは Python 開発者にかぎりません。 Git も今や開発者の必須スキルと言えるでしょう。

以上です。

終わりに


今回は私が興味のあるところに絞って Python 限定でデータを抽出してみましたが、他の項目や、全体の傾向、他の言語の傾向を見てみるのもおもしろいと思います。興味がある方はオリジナルの方もご覧になってみてください。


例えば、 「 Most Loved, Dreaded, and Wanted ... 」(最も好きな・嫌いな・やりたい○○は何ですか?)という質問 はとてもおもしろいです。 dreaded なものには「確かに!」と声に出して言いたくなります。

ちなみに、 csv のデータはカラム数が非常に多く行数も約 10 万行あるので Excel 等でそのまま集計するのはつらいかもしれません。

また、今回集計とグラフ作成に使用した Jupyter notebook を GitHub に置きました。興味のある方はぜひ自由に見たり再利用したりしてみてください。


ちなみに使用したライブラリは次のとおりです。

  • jupyter
  • matplotlib
  • numpy
  • pandas

参考


Stack Overflow の他に Python Software Foundation と JetBrains が行った Python 公式の調査もあります。 Pythonista の傾向等に興味のある方はそちらもご覧になるとよいかもしれません。

2018/05/29

yield の使い方

Python のキーワード yield の使い方について説明します。


目次


  • yield とは
  • yield でジェネレータを作る
  • yield でコンテキストマネージャを作る
  • yield from でジェネレータを入れ子にする
  • その他の使い方


yield とは


yield は英語で「生み出す」「生む」「起こす」といった意味の単語とのことで、 Python における「 yield 」は、 コードを構成する構成要素(「キーワード」)のひとつで、 yield 式を作るためのもの です。

def get_abc():
    yield 'a'
    yield 'b'
    yield 'c'

print(list(get_abc()))  # => ['a', 'b', 'c']
print(list(get_abc()))  # => ['a', 'b', 'c']

yield 」は、関数(あるいはメソッド)の中にのみ記述することができるもので、 yield が記述された関数をジェネレータ関数化するものです。

ジェネレータ関数 」とは、関数の中でも少し特殊な関数で、通常の関数が 1 つの値を return で返すのに対して、ジェネレータ関数はジェネレータオブジェクトを返します。

ジェネレータオブジェクト 」(もしくは「ジェネレータイテレータ」(ジェネレータイテレータは『ゼニヤッタ・モンダッタ』みたいで語感がいいですね))とは、ジェネレータによって生成されたイテレータオブジェクトです。

イテレータオブジェクト 」とは、 for ループや組み込みの next() 関数に渡せば値を 1 つずつ返してくれるオブジェクトのことです(厳密には、 __iter__() __next__() の 2 つのメソッドを持っていて、 __iter__() メソッドの戻り値が self (自分自身)のオブジェクトです。参考: Iterator Types )。

for item in イテレータ:
    print(item)

まとめます。

  • yield キーワード: 関数の中で yield 式として使われ、関数をジェネレータ関数にするもの。
  • ジェネレータ関数: 「ジェネレータオブジェクト」を返すもの。
  • ジェネレータオブジェクト: ジェネレータ関数の呼び出しによって生成されたイテレータ。


少し余談ですが、このあたりの用語に関してややこしいと私が思うのは、 Python のプログラムの中では「ジェネレータオブジェクト」に「 generator (ジェネレータ)」という表記がされていて、その一方で、 Python.org の公式ドキュメントでは「ジェネレータ関数」のことを「ジェネレータ」と呼んでいるところです。

ジェネレータオブジェクトのことを「ジェネレータ」と呼ぶのであればそれを生成する関数の方は「ジェネレータ関数」、ジェネレータ関数のことを「ジェネレータ」と呼ぶのであればそれが生成するものは「ジェネレータオブジェクト」と呼ぶ、というように、人と話をするときにはどちらかに統一してから話をしないと混乱しそうです。

この節の最後に、公式のドキュメントからジェネレータの説明に関する部分を抜粋してご紹介します。

Generators are a special class of functions that simplify the task of writing iterators. Regular functions compute a value and return it, but generators return an iterator that returns a stream of values.

翻訳: ジェネレータ(訳注: このジェネレータは「ジェネレータ関数」の意味です)はイテレータをシンプルに書けるようにしてくれる特殊な関数です。 通常の関数はある値を計算して返しますが、ジェネレータは値のストリームを返すイテレータを返します

When you call a generator function, it doesn’t return a single value; instead it returns a generator object that supports the iterator protocol.

翻訳: ジェネレータ関数を呼ぶと、単一の値を返す代わりに、イテレータプロトコルをサポートするジェネレータオブジェクトを返します(訳注: これは上の「値のストリームを返すイテレータ」と同じ意味です)。

The big difference between yield and a return statement is that on reaching a yield the generator’s state of execution is suspended and local variables are preserved. On the next call to the generator’s __next__() method, the function will resume executing.

翻訳: yieldreturn 文の大きな違いは、 yield の場合は処理が yield の行に到達したときにジェネレータの実行状態が一時停止されてローカル変数が保持される点です。ジェネレータの __next__() メソッドが次回呼ばれたときにジェネレータ関数は実行を再開します。

参考

概念的な説明だけではわかりづらいので、つづいてサンプルコードを見ながら説明していきます。


yield でジェネレータを作る


yield 式は上述のとおり、関数の中で使われ、関数をジェネレータ関数に変える働きをします。

def gen_etos():
    ETOS = '子丑寅卯辰巳午未申酉戌亥'
    for eto in ETOS:
        yield eto

この関数を呼び出すと、「ジェネレータオブジェクト」(あるいは「ジェネレータイテレータ」)と呼ばれるオブジェクトが返されます。

g1 = gen_etos()

# ジェネレータ関数の戻り値はジェネレータオブジェクト
type(g1)
# => generator

# ジェネレータかどうかのチェックは types の GeneratorType でできる
import types
isinstance(g1, types.GeneratorType)
# => True

ジェネレータオブジェクトは、 for 文または next() 関数で回すことができます。

g1 = gen_etos()
for value in g1:
    print(value)
# =>
# 子
# 丑
# 寅
# 卯
# 辰
# 巳
# 午
# 未
# 申
# 酉
# 戌
# 亥

g1 = gen_etos()
try:
    while True:
        print(next(g1)) 
except StopIteration:
    pass
# =>
# 子
# 丑
# 寅
# 卯
# 辰
# 巳
# 午
# 未
# 申
# 酉
# 戌
# 亥

ジェネレータ関数は同時に複数のジェネレータオブジェクトを返すことができます。それら複数のジェネレータオブジェクトの内部状態は通常(あえて nonlocal 変数を共有するようなことをしなければ)互いに独立です。

g1 = gen_etos()
g2 = gen_etos()
 
for _ in range(3):
    print(next(g1))
# =>
# 子
# 丑
# 寅

# g2 の内部状態は g1 とは独立
for _ in range(2):
    print(next(g2))
# =>
# 子
# 丑

# g1 の内部状態は g2 とは独立
for _ in range(2):
    print(next(g1))
# =>
# 卯
# 辰

説明をシンプルにするために上の gen_etos() は引数を持たないようにしましたが、ジェネレータ関数も通常の関数と同様に引数を受け取ることができます。実用上は引数を渡すケースの方が多いかと思います。

def get_nonblank_lines(path):
    with open(path) as f:
        for line in f:
            if line.strip():
                yield line

g1 = get_nonblank_lines('sample.dat')
for line in g1:
    print(line, end='')

また上のコードではいずれもわかりやすくするためにジェネレータオブジェクトにあえて g1 という名前を付けましたが、実践ではジェネレータオブジェクトには名前を付けずに、次のように for ループの右側で直接ジェネレータ関数を呼び出す形が多いのではないかと思います。

for line in get_nonblank_lines('sample.dat'):
    print(line, end='')

混乱しやすいのですが、ジェネレータ関数そのものがイテレータなのではなく、ジェネレータ関数の戻り値がイテレータであるという点を覚えておく必要があります。

上の説明には「イテレータ」「 for 文」「 next() 関数」ということばが出てきました。これらの意味を押さえておかないと上の説明を完全に理解するのは難しいので、これらの意味がちょっと怪しいぞという方は一度これらのワードを検索するなり書籍にあたるなりしてからこの記事に戻ってきていただくと意味がスッキリとわかるのではないかと思います。

以上で yield 式とジェネレータの基本の説明は終わりです。続いて、 yield の少し応用的なお話に進みます。

yield でコンテキストマネージャを作る


コンテキストマネージャとは with 式に渡して使えるオブジェクトで、コードブロックに一時的なコンテキストを提供するものです。

with open('sample.log', 'a') as f:
    print('これはファイルに書き込まれます。', file=f)

例えば、組み込み関数の open()with 文に渡すとコンテキストマネージャとしてふるまうオブジェクトを返す関数です。

このコンテキストマネージャの仕組みそのものは yield とは独立のものですが、 yield で作るジェネレータ関数を使って、独自のコンテキストマネージャをかんたんに作ることができます。具体的には、コードブロックを挿入したい位置に yield 文を書いたジェネレータ関数を書き、それを @contextlib.contextmanager でデコレートします。

次のコードは print() の出力先を一時的に指定されたファイルに切り替えるコンテキストマネージャを yield を使って定義した例です。

from contextlib import contextmanager
import sys


@contextmanager
def switch_stdout(path):
    try:
        # print() の出力先を指定されたファイルに切り替える
        sys.stdout = open(path, 'a')
        # with のコードブロックの処理がここで実行される
        yield
    finally:
        # print() の出力先を標準出力に戻す
        # sys.__stdout__ はオリジナルの標準出力を格納したもの
        sys.stdout = sys.__stdout__


print('これは標準出力に出ます。')
with switch_stdout('/tmp/sample.log'):
    print('これは標準出力には出ません。')
    print('これも標準出力には出ません。')
print('これも標準出力に出ます。')

このコードを実行すると、 with のコードブロック内の 2 つの print() 関数の文字列は /tmp/sample.log に出力されます。一方、 with のコードブロックの外(=前後)にある print() 関数の文字列は通常どおり標準出力に出力されます。

ちなみに、この switch_stdout() と同等の機能を持つ @contextlib.redirect_stdout が標準ライブラリにすでに用意されています。


上の例のコンテキストマネージャでは yield 式に値を渡していませんが、 yield に値を渡すこともできます。コンテキストマネージャとなるジェネレータ関数の yield に渡された値は、利用時に with 文の as で受け取ることができます。

例えば、次のような中身の bookmarks テーブルを持つ SQLite のデータベースファイルがあるものとします。

url,title 
"https://www.google.co.jp","Google"
"https://www.instagram.com","Instagram"

$ sqlite3 db.sqlite3 >> EOS
CREATE TABLE bookmarks (url text, title text);
INSERT INTO bookmarks VALUES ("https://www.google.co.jp", "Google");
INSERT INTO bookmarks VALUES ("https://www.instagram.com", "Instagram");
EOS

このデータベースのアクセスするためのコンテキストマネージャとして次のようなものを作ることができます。ここで yield に渡されたコネクションオブジェクトは withas で取得・利用することが可能です。

from contextlib import contextmanager
import sqlite3


@contextmanager
def sqlite_conn(path):
    try:
        conn = sqlite3.connect(path)
        # コネクションオブジェクトをコンテキストに渡す
        yield conn
    finally:
        if 'conn' in locals():
            conn.close()

# コネクションオブジェクトを as で受け取る
with sqlite_conn('db.sqlite3') as conn:
    cursor = conn.cursor()
    cursor.execute('SELECT * FROM bookmarks')
    for row in cursor:
        print(row)

このように、通常のジェネレータ関数の yield とコンテキストマネージャの yield は意味合い・使い方が大きく異なるので注意が必要です。ジェネレータ関数の yield 式は「何らかの値を受け取ること」や「繰り返し呼ばれること」が前提で使われてますが、コンテキストマネージャの yield 式は値を返すとはかぎらず、原則一度しか呼ばれません。

他のスクリプト言語に馴染みのある方には、 Python のコンテキストマネージャは、 Ruby であれば「ブロックを受け取るメソッド」、 JavaScript であれば「無名のコールバック関数」に近いものと考えるとわかりやすいかと思います。


yield from でジェネレータを入れ子にする


Python 3.3 以降では yield from 文で、ジェネレータ関数の入れ子がかんたんに実現できるようになっています。

次のジェネレータ関数 gen_numbers_all() は、ジェネレータ関数 gen_numbers_1()gen_numbers_2() のジェネレータオブジェクトをそのまま結合したようなふるまいをします。

def gen_numbers_1():
    for n in 'AKQJ':
        yield n

def gen_numbers_2():
    for n in '98765432':
        yield n

def gen_numbers_all():
    # 他のジェネレータ関数で作られたジェネレータオブジェクトが 
    # yield した値をそのまま yield する
    yield from gen_numbers_1()
    yield from gen_numbers_2()

print(list(gen_numbers_1()))
# =>
# ['A', 'K', 'Q', 'J']

print(list(gen_numbers_2()))
# =>
# ['9', '8', '7', '6', '5', '4', '3', '2']

print(list(gen_numbers_all()))
# =>
# ['A', 'K', 'Q', 'J', '9', '8', '7', '6', '5', '4', '3', '2']

この yield from 式については、ジェネレータオブジェクトの内部状態を変更する send() メソッド等を使った使い方等もう少し複雑な使い方の例も Python.org の公式ドキュメントでは紹介されています。 yield from についてもっと詳しく知りたい方は公式のドキュメントにも目を通してみてください。



その他の使い方


上で見た使い方の他に、 asyncio で利用するコルーチンを定義するためにも yield 式は利用することができます。ただ、この使い方は用途が限られる&私は asyncio を使う必要に迫られず経験が不足しているので、ここで語るのはやめておきます。

気になる方は公式のページ等をご覧ください。


以上、 Python の yield の使い方についてでした。


参考


他サイト

ブログ内

2018/05/23

Python Tips: Python の例外システムを活用したい

Python 3 の例外システムを活用する上で押さえておきたいポイントをまとめてみます。

例外システムに関しては過去に「 Python の例外処理」という記事も書いています。この記事と内容が重複しますが、例外に興味のある方はよろしければそちらもご覧ください。


目次


本記事の目次です。

  • 基本形を押さえる
  • finally を押さえる
  • else を押さえる
  • 組み込みの例外クラスのツリーを押さえる
  • except のパターンを押さえる
  • 例外オブジェクトのアトリビュートを利用する
  • コンテキストマネージャを使う

順に見ていきましょう。

基本形を押さえる


Python 3 の例外処理の基本形は次のとおりです。

import sys

FILE_IN = 'sample.dat'

try:
    # 例外が起こる可能性のある処理
    file = open(FILE_IN)
except Exception:
    # 例外が起こったときの処理
    print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
    sys.exit(1)

キーワード tryexcept を組み合わせて使います。

他の言語を知る方だと try catch のパターンに馴染みのある方が多いでしょうか。 Python の場合は catch の代わりに except を使用します。

except 句は一般に次のいずれかのパターンで書きます。

except [キャッチしたい例外のクラス]:  
except [キャッチしたい例外のクラス] as [キャッチされた例外オブジェクトにつける名前]: 

finally を押さえる


tryexcept の後には finally という句を繋げることができます(厳密にいうと except がなくても大丈夫です)。

import sys

FILE_IN = 'sample.dat'

try:
    # 例外が起こる可能性のある処理
    file = open(FILE_IN)
    file.close()
except Exception:
    # 例外が起こったときの処理
    print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
    sys.exit(1)
finally:
    # 例外の発生有無にかかわらず最後に実行したい処理
    print('finally です。')
# =>
# ファイル sample.dat を開くことができません。
# finally です。

finally 句は例外が発生してもしなくても必ず実行されます。

finally はよくできていて、処理がどの経路を通っても必ず実行され、かつ、なるだけ遅いタイミングで実行されるようになっています。異なる経路のパターン a) - e) をあげて、どのタイミングで finally 句が実行されるのかを以下に見ていきましょう。

a) try の中で例外が発生しなかった場合 → try を抜けるときに実行される:
try:
    print('try です。')
finally:
    print('finally です。')
# =>
# try です。
# finally です。

b) try の中で例外が発生し、 except 句のひとつでキャッチされ、その中で例外があげられなかった場合 → except 句の後に実行される:
try:
    value = 1 / 0
except ZeroDivisionError:
    print('ZeroDivisionError です。')
finally: 
    print('finally です。')
# =>
# ZeroDivisionError です。
# finally です。

c) try の中で例外が発生し、 except 句のひとつでキャッチされ、その中で例外があげられた場合 → except 句内で例外があげられる直前で実行される:
try:
    value = 1 / 0
except ZeroDivisionError as e:
    print('ZeroDivisionError です。')
    raise e
finally: 
    print('finally です。')
# =>
# ZeroDivisionError です。
# finally です。
# Traceback (most recent call last):
#   ...
# ZeroDivisionError: division by zero

d) try の中で例外が発生し、どの except 句でもキャッチされなかった場合 → finally 句が実行された後に例外があがる(公式ドキュメントでは「 re-raised 」という表現が使われています):
try:
    value = 1 / 0
except StopIteration:
    print('StopIteration です。')
finally: 
    print('finally です。')
# =>
# finally です。
# Traceback (most recent call last):
#   ...
# ZeroDivisionError: division by zero

e) try 内で return 文や break 文が来て try 句を抜ける場合 → returnbreak の後に実行される:
def sample_exception():
    try:
        print('try です。')
        return True
    except:
        return False
    finally:
        print('finally です。')

result = sample_exception()
print(result)
# =>
# try です。
# finally です。
# True

このように finally 句はどんな場合でも必ず実行されるので、 try 句の中の処理の成否によらずに必ず実行したい処理――例えばデータベース等の外部リソースの開放等をするのに有用です。

else を押さえる


try except 句の後には else という句を繋げることができます。 else 句は try 句の中で 例外が起こらなかったときにのみ 実行されます。

import sys

FILE_IN = 'sample.dat'

try:
    # 例外が起こる可能性のある処理
    file = open(FILE_IN)
    file.close()
except Exception:
    # 例外が起こったときの処理
    print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
    sys.exit(1)
else:
    # 例外が起こらなかったときの処理
    print('file.closed: {}'.format(file.closed))
# =>
# file.closed: True

例外が起こらなかったときにのみ実行されると聞くと、「 else 句の中身を try の中に書けば else 句は要らないんじゃないか」と思えますが(私は思いました)、 elsetry の中身を最小限に留め思わぬ例外処理が起こってしまうのを防ぐのに有効です。 else 句が便利なパターンとして例えば次のような使い方が考えられます。

try:
    # データベースに変更を加える処理
except:
    # データベースの処理が失敗したのでトランザクションをロールバックする
else:
    # 処理が成功した場合にログを残す

個人的には、この「 try の処理が成功した場合に実行される句」に else という名前を使うのは違和感があります。 then 等の名前の方が直感的でわかりやすい気がしますが、このあたりは「予約語をなるべく少なくする」ことを優先した判断の結果なのですかね。

組み込みの例外クラスのツリーを押さえる


Python 3 には組み込みの例外クラスが多数用意されています。子クラスの例外は親クラスの except 句でキャッチできるので、適切な粒度で例外をキャッチして正しく例外処理を行うために、このツリー構造を押さえて必要があります。

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      |    +-- ModuleNotFoundError
      +-- LookupError
      |    +-- IndexError
      |    +-- KeyError
      +-- MemoryError
      +-- NameError
      |    +-- UnboundLocalError
      +-- OSError
      |    +-- BlockingIOError
      |    +-- ChildProcessError
      |    +-- ConnectionError
      |    |    +-- BrokenPipeError
      |    |    +-- ConnectionAbortedError
      |    |    +-- ConnectionRefusedError
      |    |    +-- ConnectionResetError
      |    +-- FileExistsError
      |    +-- FileNotFoundError
      |    +-- InterruptedError
      |    +-- IsADirectoryError
      |    +-- NotADirectoryError
      |    +-- PermissionError
      |    +-- ProcessLookupError
      |    +-- TimeoutError
      +-- ReferenceError
      +-- RuntimeError
      |    +-- NotImplementedError
      |    +-- RecursionError
      +-- SyntaxError
      |    +-- IndentationError
      |         +-- TabError
      +-- SystemError
      +-- TypeError
      +-- ValueError
      |    +-- UnicodeError
      |         +-- UnicodeDecodeError
      |         +-- UnicodeEncodeError
      |         +-- UnicodeTranslateError
      +-- Warning
           +-- DeprecationWarning
           +-- PendingDeprecationWarning
           +-- RuntimeWarning
           +-- SyntaxWarning
           +-- UserWarning
           +-- FutureWarning
           +-- ImportWarning
           +-- UnicodeWarning
           +-- BytesWarning
           +-- ResourceWarning

# ZeroDivisionError は親クラスの ArithmeticError でキャッチできる
try:
    value = 1 / 0
except ArithmeticError as e:
    print('例外が起こりました。')
    print('{}: {}'.format(type(e).__name__, str(e)))
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero

except のパターンを押さえる


try 句の後に書く except 句はさまざまなパターンで記述することができます。

ひとつの except 句で複数の例外をキャッチする:
try:
    value = 1 / 0
except (OverflowError, ZeroDivisionError) as e:
    print('例外が起こりました。')
    print('{}: {}'.format(type(e).__name__, str(e)))
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero

複数の except 句をひとつの try 句につける:
try:
    value = 1 / 0
except OverflowError as e:
    print('OverflowError です。')
except ZeroDivisionError as e:
    print('ZeroDivisionError です。')
except Exception as e:
    print('Exception です。')
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero

except 句を複数連ねるときの注意点は、例外の場合分けを適切にするためには「 小さい例外を前に、大きな例外を後に書かなくてはいけない 」ことです。

上のサンプルではこのとおりに「小さい例外を先、大きな例外を後」にして書いていますが、これを変えて Exceptionexcept 句を先頭に持ってくると、そこですべての例外がキャッチされてしまうので OverflowErrorZeroDivisionErrorexcept 句には到達することがありません。これでは複数の except 句を書いた意味がなくなるので、 except 句は必ず、小さい例外(例外クラスの継承ツリーで枝葉の方にある例外)から先に書く必要があります。

例外オブジェクトのアトリビュートを利用する


例外もひとつのオブジェクトなので、自由にアトリビュートを付けることができます。

例えば、 Requests ライブラリ の例外には、リクエストオブジェクトを格納した request という名前のアトリビュートが付けられています。利用者は except 句の中でそのアトリビュートに自由にアクセスすることができます。

import requests


try:
    requests.get('http://www.yahoo.co.jp', timeout=0.001)
except requests.exceptions.ConnectTimeout as e:
    # ライブラリ側で用意してくれている e.requst アトリビュートが利用できる
    print('URL "{}" へのリクエストがタイムアウトしました。'.format(e.request.url))

規模の大きめのライブラリでは、独自の例外クラスを用意していて、アトリビュートにさまざまな情報を提供していることがあります。ライブラリの独自の例外クラスを取り扱う場合にはどんなアトリビュートがあるのかをチェックしておくとよいでしょう。

コンテキストマネージャを使う


with 文で利用できる Python のコンテキストマネージャは、特定のコードブロックに対して「コンテキスト」を提供できる仕組みです。さまざまな利用方法がありますが、代表的な使い方のひとつに「特定の例外処理パターンを再利用しやすくする」というものがあります。

サンプルとして、 CSV ファイルを読み込んで各行を返すリーダーオブジェクトを提供するコンテキストマネージャの例を見てみましょう。

from contextlib import contextmanager
import csv


@contextmanager
def read_csv(path):
    '''CSV ファイルの読み込みを行うコンテキストマネージャ'''
    try:
        f = open(path)
        reader = csv.reader(f)
        yield reader
    finally:
        if 'f' in locals():
            f.close()


# コンテキストマネージャを使って CSV ファイルを読み込む
with read_csv('sample1.csv') as reader:
    print([row[0] for row in reader])
# 出力例:
# => 
# ['アレックス', 'マーティ', 'メルマン', 'グロリア']

with read_csv('sample2.csv') as reader:
    print([row[0] for row in reader])
# 出力例:
# => 
# ['ポー', 'タイガー', 'ヘビ', 'カマキリ', 'ツル', 'モンキー']

read_csv() を定義することで、 tryfinally を使った例外処理のパターンをシンプルな with 文で再利用できるようになります。

コードの中でよく似た例外処理のパターンが繰り返し出てきた場合は、共通の例外処理パターンをコンテキストマネージャ化することによって、無用なコードの重複を減らすことができます。

...

以上、 Python の例外システムを活用する上で押さえておきたいポイント集でした。

以上のことがひととおりきちんと押さえられれば、 Python の例外処理については「正しく使えている」と自信を持ってよいのではないでしょうか。


参考