Open10

Pythonを使い始めて引っかかった所

Web開発者としてPHP、JavaScript、Rubyを2年ずつぐらい経験してきたあと初めてPythonを触って、引っかかったところをまとめていく。主にRuby on Rails・TypeScriptと比べた印象になる。

本稿は、私とは逆の立場である下記の記事にインスパイアされている。
Python使いがRubyを触るために理解するべきこと - Qiita

gitignoreが長い

github/gitignoreのリポジトリにあるPython.gitignoreは 138行 ある。

https://github.com/github/gitignore/blob/b0012e4930d0a8c350254a3caeedf7441ea286a3/Python.gitignore

それだけ何らかの慣習等でignoreしなければならないファイルの種類が多くてまとまっていないということだと思う。特に lib/ が ignore されるので、他の言語のフレームワークと同じノリで lib/ というディレクトリを作ってしまい、git管理されずデプロイされなくて詰まったりした。

なお、長いと思っていたRails.gitignoreが69行で、逆にNode.gitignoreが118行と結構多めだった。
普段gitignoreは頻繁に触ることは無いので多少長くても気にならないはずだけど、上記のlib/のような問題に直面したので気になるようになったのかもしれない。

パッケージマネージャがまだ戦国時代

RubyだったらBundler、Node.jsでもnpmyarnを使うのが一般的だと断言してしまっていいと思う。一方Pythonのパッケージマネージャは現状コレだと言えるものが無い。なので参照する文献によって環境構築方法もまちまちだったりする。

RubyやJavaScriptに慣れた人だったら poetry を使うのが最も使い勝手が良いと思う。私も基本 poetry を使っている。しかしインストールに時間がかかる時がある問題があったり、最近は突如インストールが通らなくなったりと、これらのツールの中ではまだ枯れていない印象を受ける。

さらに、そもそもPythonの実行環境としてKaggleやGoogle Colaboratoryも一般的だったりして、そうなるとまたパッケージマネージャの事情も変わってくる。混沌としている。

as でパッケージ名を過度に省略する

たとえば pandas という便利なライブラリがある。これの使い方を調べると、おそらく下記のようなコードに遭遇すると思う。

import pandas as pd

pd.DataFrame(hoge)

なぜ as pd と略す必要があるのか分からない。pandasのまま使うのでは良くないのか。
でも、どんな文献でもだいたいみんな省略して使っている。これはpandasだけではなく、他のライブラリでもimport matplotlib.pyplot as pltimport numpy as npなどどこでもよく見かけるので、これはPythonという言語自体にライブラリの名前を省略する文化があるんだと思う。
いちいち省略名をライブラリ名と一緒に頭に入れるのが面倒なのでやめてほしい。

elif

TypeScriptは else if

// TypeScript
if (false) {
  console.log("hoge");
} else if (true) {
  console.log("fuga");
}

PHPは elseif

<?php
// PHP
if (false) {
  echo "hoge";
} elseif (true) {
  echo "fuga";
}

Rubyは elsif

# Ruby
if false
  puts "hoge"
elsif true
  puts "fuga"
end

そしてPythonは elif

# Python
if False:
    print("hoge")
elif True:
    print("fuga")

これはPythonだけが変だという話ではないけど、いちいち言語ごとに変えないでほしい。個人的には造語を増やしてほしくないのでTypeScriptのelse ifが好み。

join

配列を連結するメソッドjoinは、RubyとJavaScriptでは配列オブジェクトのインスタンスメソッドになっている。

# Ruby
["hoge","fuga","piyo"].join(",")
// TypeScript
["hoge","fuga","piyo"].join(",");

対してPythonは逆で、joinは文字列オブジェクトのインスタンスメソッドでiterableを引数に取る、という扱いになっている。

# Python
",".join(["hoge","fuga","piyo"])

ちなみにPHPはまた異なっていて、join(implode)は標準の関数であり、第1引数に文字列、第2引数に配列を取る。

// PHP
join(",", ["hoge","fuga","piyo"]);

文字列と数値の積

Pythonは以下のようになる。特にRubyをやった人間だと、3行目3 * "3"の結果が333になるのには違和感を覚えると思う。

# Python
print( 3  *  3)  #=> 9
print("3" *  3)  #=> 333
print( 3  * "3") #=> 333
print("3" * "3") #=> TypeError: can't multiply sequence by non-int of type 'str'

Pythonの公式ドキュメントには以下の説明がある。

引数は、両方とも数値であるか、片方が整数で他方がシーケンスかのどちらかでなければなりません。前者の場合、数値は共通の型に変換された後乗算されます。後者の場合、シーケンスの繰り返し操作が行われます。

Rubyだと以下のようになる。2行目"3" * 3333になるのは、数値同士の計算の時のようにIntegerオブジェクトをレシーバとして*(multiple)メソッドを呼んでいるのではなく、Stringオブジェクトをレシーバとして*(multiple)メソッドを呼んでいるため。

# Ruby
puts  3  *  3  #=> 9
puts "3" *  3  #=> 333
puts  3  * "3" #=> String can't be coerced into Integer (TypeError)
puts "3" * "3" #=> no implicit conversion of String into Integer (TypeError)

なおPHPやJavaScriptでは暗黙の型変換が行われて数値の算術計算が行われる。

<?php
// PHP
echo  3  *  3;  //=> 9
echo "3" *  3;  //=> 9
echo  3  * "3"; //=> 9
echo "3" * "3"; //=> 9
// TypeScript
console.log( 3  *  3 ); //=> 9
console.log("3" *  3 ); //=> 9 ※
console.log( 3  * "3"); //=> 9 ※
console.log("3" * "3"); //=> 9 ※

※TypeScriptでは警告The left-hand(right-hand) side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.が出る。

個人的には、Rubyの挙動が多少クセはあるもののプログラミング言語の仕様として筋が通っていて一番しっくり来る。でも「*は何らかのクラスのインスタンスメソッドではなく、代数学的な意味での二項演算である」と捉えると、Pythonの挙動は交換法則を満たすのであるべき姿に近いのかもしれない。

プライベート変数が無い

https://docs.python.org/ja/3/tutorial/classes.html#private-variables

オブジェクトの中からしかアクセス出来ない "プライベート" インスタンス変数は、 Python にはありません。しかし、ほとんどの Python コードが従っている慣習があります。アンダースコアで始まる名前 (例えば _spam) は、 (関数であれメソッドであれデータメンバであれ) 非 public なAPIとして扱います。これらは、予告なく変更されるかもしれない実装の詳細として扱われるべきです。

これは結構衝撃だった。けどよく考えたら、あるメンバがpublicかprivateかの区別をすることって、オブジェクト指向の設計をより堅牢なものにしたいという人間のための違いであって、それをプログラミング言語の仕様ではなく命名規則の習慣にするに留めようというのは「プログラミングのためのプログラミング」を増やさない生産的な仕様という意味でスマートなのかもしれない?(コンパイルによる恩恵は知らない)

リスト内包表記

# Python
[i*2 for i in [1,2,3]]           #=> [2, 4, 6]
[i for i in [1,2,3] if i%2 != 0] #=> [1, 3]

この表記は結構戸惑った。私が実際にプログラミングするときは、まず[1,2,3]を書いてfor i in を書いてi*2を書いて、必要に応じてif i%2 != 0を書くという順番で書いていくので、結構書きにくい。

個人的にはRubyの書き方が好き。

# Ruby
[1,2,3].map{|i| i*2 }         #=> [2, 4, 6]
[1,2,3].select{|i| i%2 != 0 } #=> [1, 3]
作成者以外のコメントは許可されていません