Pythonのインデントは何が正解なのか、BigQueryで調べてみた

More than 1 year has passed since last update.

これは誰が得をするのかわからないエントリーですが、コーヒーブレイクにどうぞ。


発端その1:Pythonのコードスタイル

PythonのコードスタイルといえばPEP8が有名です。スタイルチェックにこれを利用している方も多いのではないでしょうか?こちらでは、インデントに半角スペース4つ使う事がルールとなっています。


Use 4 spaces per indentation level.


さて、ところがこの半角スペース4つって、ちょっと幅を取りすぎな感じがしないでしょうか?でも天下のPEP8で規定されているので、勝手に数を減らすとPython警察が出動しそうです。ここはこちらの意見をサポートする事例が必要です。

Chromium Python Style Guideは、Chromium OSにコントリビュートするときに使われるスタイルガイドです。こちらのインデントはどうでしょうか?


Indentation: use 2, not 4, spaces


半角スペース2つ! ChromiumならPython警察も許してくれるでしょう。しかしこの半角スペース2つのインデント、どれ位普及しているのでしょうか?普及率をサポート資料として添付できれば、鬼に金棒です。


発端その2:Kaggleの乱数シードネタ

先日Kaggleで、一番使われる乱数シードの数値は何か?というタイトルの投稿がありました。そこではBigQueryでGitHubのコードを分析し、乱数シード値を抽出して統計を取るという事が行われていました。結果は以下です。

乱数シード統計

via Kaggle

そんな・・Meaning of Lifeが0や1ごときに負けるとは・・

さておき、BigQueryにはいくつかパブリックデータが用意されていて、GitHubのリポジトリデータもその1つです。これを使えば、インデントの数も統計が取れるのではないでしょうか?


BigQueryでGitHubのリポジトリ解析

先述の通り、BigQueryにはGitHubのリポジトリデータがパブリックデータとして公開されています。ソースコードそのものが入ったテーブルは2TBもあるので、ここはランダムにサンプリングをしてサイズを減らしたテーブルを使います。これは24GBほどあります。


ソースコードをスキャンするクエリ

BigQueryはインデックスを付けること無く、全てのデータ1をフルスキャンしてクエリの結果を返します。先ほどのKaggleの例を見てみましょう。

#standardSQL

SELECT
REGEXP_EXTRACT(content, r'(random_state=\d*|seed=\d*|random_seed=\d*|random_number=\d*)')
FROM
`bigquery-public-data.github_repos.sample_contents`

REGEXP_EXTRACTというのは、正規表現でマッチした文字列を返す関数です。クエリで正規表現が書けるってすばらしいですよね。

ちなみにこのクエリは23.6GBをフルスキャンすることになりますが、8.4秒で終わりました

クエリ結果は次のようになります。

Row
f0_

1
seed=3690

2
seed=1

3
seed=4124

4
seed=

5
seed=

..
...

Kaggleの例では、この後にクエリ結果をPandasに載せ替えて統計を取っているようです。


Pythonのインデントを調べるクエリ

これも上記のようにソースコードを正規表現でマッチングさせればなんとかなる…でしょうか?実際にはソースコード中にインデントミスしている箇所もあるはずなので、”完全に”そのソースコードのインデントは何々であると言い切るのは難しそうです。

ここでは、「行頭からのスペース数で最小のものを、そのソースコードのインデントスタイルとして抽出」してみることにします。

もう少し正確にやるなら、行頭からのスペース数の分布を見て、発生回数がある閾値以上のスペース数の内、最小公倍数となるものとかを抽出すれば良さそうですが、今回は面倒なので止めました。

クエリは次のようになります。

#standardSQL

WITH
lines AS (
SELECT
SPLIT(content, "\n") AS line,
id
FROM
`bigquery-public-data.github_repos.sample_contents`
WHERE
sample_path LIKE "%.py" )
SELECT
space_count,
COUNT(space_count) AS number_of_occurence
FROM (
SELECT
id,
MIN(CHAR_LENGTH(REGEXP_EXTRACT(flatten_line, r'^\s+'))) AS space_count
FROM
lines
CROSS JOIN
UNNEST(lines.line) AS flatten_line
WHERE
REGEXP_CONTAINS(flatten_line, r'^\s+')
GROUP BY
id )
GROUP BY
space_count
ORDER BY
number_of_occurence DESC

ちょっと複雑なので1つずつ解説します。(SQLが得意な方、ベターな方法があればお教えください)



  1. WITH clauseで一行毎のソースを抽出するサブクエリを実行


  2. UNNESTArrayになっているソース文字列をFlatten


  3. GROUP BYid毎(ソースファイル毎)にまとめる


  4. MINで1ソースファイル内の最小スペース文字数を取得


  5. GROUP BYでスペース文字数毎にまとめる


  6. COUNTで発生数をカウント(1ソースファイルで1カウント)

という感じです。結果を見てみましょう。

space_count
number_of_occurence

4
63121

1
9441

2
6725

3
1102

6
145

…うん、

知ってた。

1はなんでしょうね?複数行コメントかな。

コメントいただきました。Tabインデントですね。

しかし、半角スペース2もなかなか健闘していると言えるのではないでしょうか?時系列での推移も示せば、上昇傾向がでるかもしれません。


まとめ

BigQueryすごい。

解析していないデータが眠っているなら、とりあえずBigQueryに入れておくのもよいでしょう。データを入れているだけなら、ストレージのみの課金しか発生しません。クエリは、月1TBまで無料です。





  1. 全てのデータというと語弊がありますが、検索対象となるカラムのデータはスキャンされます。実際にクエリでスキャンするサイズはコンソールに表示されます。