Python の不便なところ
◆ しばらく Python 使ってきたので感じた不満点や不便な点のまとめです
インデント
インデントベースなのは書きやすく見やすい利点はありますが 壊れやすいですちゃんとしたエディタのみでの編集ならそこまで問題ではありませんが ソースコードをいくつかのツールにコピペすることもあります
ホワイトスペースの扱いが雑なツールもあるので そこで壊れることも少なくないです
HTML として保存する系に多いです
ウェブページで公開されてるコードでもインデントが消えているのをたまに見かけます
HTML ソースを見てインデントがわかるならまだいいですが 公開までの過程のどこかで消えてしまったのか HTML でもインデントが残ってない場合もあります
他にもチャットツールなどでも 最初のホワイトスペースが消えることがあります
他の多くの言語で見る {} を使ったブロックならホワイトスペースに意味はないので壊れてもフォーマッタを使えば元通りです
しかし Python の場合は開始は 「:」 があるものの終わりはインデントでしか判断できないので復元できません
また それは別のファイルからコードの一部をコピペしてきたときも起こります
他の言語ならコピペしてフォーマッタにかけるだけで良いのですが Python の場合は手動でやらないといけません
フォーマットした結果意図しないインデントになってしまったものを判断するのは辛いです
シンタックスエラー
エラーの表示があまり親切じゃないですわかりやすいときはわかやすいのですが ときどきすごくわかりづらくどこが間違ってるのかを見つけるのにすごく時間がかかりました
あと 行のどこが悪いかを教えてほしいです
ある程度長いとどこかわかりません
ただ 出てるときもあるみたいで実行方法やバージョンによる違いなのかもしれません
バックスラッシュ
C 言語や shellscript で見かける行末バックスラッシュを使わないといけないことがあります改行を改行とみなさないためのものです
例えば メソッドチェーンが長くなって改行したいとき
result = longlongfunction().longlongmethod1().longlongmethod2()
.longlongmethod3().longlongmethod4().longlongmethod5()
これは構文エラーです
こうしないといけません
result = longlongfunction().longlongmethod1().longlongmethod2() \
.longlongmethod3().longlongmethod4().longlongmethod5()
これがすごく嫌いです
他言語でも行末バックスラッシュの機能はあるものが多いですが ほぼ使ったことはないです
というか使いたくないです
JavaScript のように 積極的に次の行と結合しない以上 次の行にも式が続いてるとみなしてくれないんですよね
JavaScript の場合 それが文を切る場所の判断の問題になってセミコロン必要になったりするわけですけど
最後に 「.」 を置けばどうだろうと試してみましたがこれもエラーでした
React の JSX で見かけるカッコで括ることで式が終わってないと伝える方法もあるので 基本はこっちを使ってます
result = (
longlongfunction().longlongmethod1().longlongmethod2()
.longlongmethod3().longlongmethod4().longlongmethod5()
)
行が多くなるのが欠点ですがバックスラッシュよりはマシです
条件演算子
? と : の条件演算子が Python にはありませんif と else を使うのですが if 式というわけではなく 後ろに if という特殊な形です
他言語の ? : と比べると
true_value \
if condition \
else false_value
condition
? true_value
: false_value
? : ではまず条件があって その条件の true/false に応じて次のインデントの上か下を見ればいいのですが 先に true 時の値が来るので読みづらいです
ですが if と else だから変に思うのであって こういう風に書くとそういう演算子としてありそうに見えます
true_value <<? condition ?>> false_value
真ん中に応じて左か右のどちらかの値です
実際は
true_value if condition else false_value
ですが 脳内で if/else を条件を囲む記号として見れば ありなのかもしれません
インクリメント・デクリメント
++ や -- 演算子がないですないのは別に良いのですが前置の場合はシンタックスエラーになりません
つい書いてしまったときにバグになります
++a は (+(+a)) なので構文として正しいです
++ を使うのであれば数値なので型のエラーもなく a の数値そのままになります
1 増えてないだけなのでバグに気づきにくいのが問題です
f"" のエスケープ
f"" を使うことで テキスト中に値を埋め込めるのですが {} の中で "" が使えませんf"aaa{"x"}aaa"
これは構文エラーです
{} の中は独立してパースされて f 付き文字列の "" は関係ないかと思ったらそんなことはなかったです
JavaScript の埋め込みだと
`${`${1}`}`
のようにネストしても問題ないのでこの感覚で書いてるとエラーになりました
{} の中が文字列だけというのはあまりないですが dict のキーとして文字列をつかうことはけっこうあります
f"x is {items["x"]}"
バックスラッシュでエスケープすればいいかと思いましたが {} の中ではバックスラッシュが書けないとエラーになりました
これをするには別のクオートが必要です
f"x is {items['x']}"
あまりないですが f 付き文字列のネストをすると両方のクオートを使ってしまいます
一応 Python には """ や ''' があるので 3 つ並ぶことがなければ
f"""a{"x"}a"""
という書き方もできます
" と """ と ' と ''' の 4 つとも使ってしまうようなコードはさすがにないのでしょうけど この制限はなくしてほしかったです
else
Python では if 以外に while, for, try でも else が書けますif はわかるのですが他の else の使いみちがないです
try の場合 エラーがないときに except 句の代わりに実行されます
ですが それなら try の最後に書いておけば十分です
途中 return するときでも正常に抜ければ共通で実行できるのなら便利かなとは思いました
finally でもいいですが それだとエラー時にも実行してしまいますし
しかし やってみると途中 return だと実行されません
def foo(a):
try:
print(f"foo({a})")
if a:
return 1
exception: pass
else:
print("ELSE")
finally:
print("FINALLY")
foo(0)
foo(2)
foo(0)
ELSE
FINALLY
foo(2)
FINALLY
一番最後まで実行したときだけです
それなら try の最後に書いておくのと全く変わりません
for の場合は break しなかった場合です
def foo(c):
print(f"foo({c})")
for i in range(5):
print(i)
if c == i:
break
else:
print("ELSE")
foo(2)
foo(10)
foo(2)
0
1
2
foo(10)
0
1
2
3
4
ELSE
break した場合なら break の直前に書けばいいので break せずに終了した場合のみの処理が書けるのは便利かもしれません
しかし for の else ならイテレーションが 1 度も実行されない データなしの場合に実行されるものであってほしかったです
テンプレートエンジンほどではないですが データが空の場合の特殊な処理がしたいケースはあります
break の有無よりもこっちのほうが有益というか else の処理として直感的だと思います
空かどうかは len(elements) == 0 のチェックで十分かもしれません
ですが それを言えば break したかどうかもフラグで十分です
この辺を考えると else はやっぱり if のみにあるのが良いのだと思います
他の言語で採用されないわけです
Python ってああもできるこうもできるではなく 一つのものは一つのやり方になってコードが揃うようにするという考えだと聞いていたのに この else はそれとは逆いってるように思います
Python3 で消しておけばよかったのにと思うのですが そんなに需要あるのでしょうか……
これまでいろいろライブラリのコードを見ていても if 以外の else を見た覚えが特にないです
except
try-catch の書き方が他の言語とは違っていて catch 句の代わりに except 句ですJavaScript などの
catch (error) {}
をするのに
except Exception as e: pass
と書きます
書く量が多いです
いちいち型が必要です
基本全部受け取るので省略したいです
例外起きるときに型までわかってるケースなんて少ないので except の中で type(e) で判断すれば十分だと思うのですよね
falthy
これは JavaScript に慣れてるせいもありますが bool 型に変換されるときの基準がわかりづらいですJavaScript だとオブジェクトなら全部 true でしたが Python は int や str もオブジェクトです
なので 値次第です
例えば [] は空なので False です
__len__ で長さを取得できるオブジェクトなら 長さが 0 なら False になります
リストも文字列も同じ扱いで長さが 0 なら False です
他には __bool__ で bool 型変換時の処理を定義できるので True か False は自由にできます
https://docs.python.org/3/library/stdtypes.html#truth
override
上で書いたような特定の処理時に呼び出される __XXX__ 形式のメソッドが bool 型変換以外にも多くあります便利ではあるものの多すぎて覚えれませんし 標準から変更するなら完璧に実装されていないと整合性がとれないことになったりもしそうです
工夫すればパイプライン演算子を作ったり 独自の使い方もできます
class FnPipe:
def __init__(self, value):
self.fns = []
self.value = value
def __or__(self, fn):
self.fns.append(fn)
return self
def __invert__(self):
v = self.value
for fn in self.fns:
v = fn(v)
return v
value = ~(
FnPipe(10)
| (lambda x: x + 2)
| (lambda x: x ** 2)
| (lambda x: x // 2)
)
print(value)
# 72
わかってる人には便利ですがはじめて見た人には全然わからないものだと思います
演算子のオーバーライドを禁止している言語なら 初めて見るコードあっても 「|」 はビット演算で OR であることが保証されているので読みやすいというのはあります
便利過ぎる分 見ただけじゃ何してるかわからないのですよね
コレクションメソッド
リストに map や join などのメソッドがないので関数がネストして見づらいですnumbers = [1, 2, 3, 4, 5]
" ".join(map(str, map(lambda x: x * 2, numbers)))
# '2 4 6 8 10'
JavaScript だとこう書けます
const numbers = [1, 2, 3, 4, 5]
numbers.map(e => e * 2).join(" ")
// "2 4 6 8 10"
Python だと join の引数のリストなどは中身が文字列じゃないとダメなので数値だと変換が必要です
ディクショナリとオブジェクト
ディクショナリとオブジェクトは別なので 「item.foo」 と 「item["foo"]」 を使い分けないといけません文字列をキーにしたディクショナリならオブジェクト風に 「.」 でアクセスしたいです
Python でもオブジェクトに attribute は自由に追加できます
その中身はディクショナリで管理されています
class A: pass
a = A()
a.foo = "bar"
a.foo
# bar
a.__dict__
# {'foo': 'bar'}
a.__dict__ = {"x": 10}
a.x
# 10
ただし これは A のようなユーザ作成のクラスが必要です
標準の object では追加できません
o = object()
o.foo = "bar"
# AttributeError: 'object' object has no attribute 'foo'
一応 types.SimpleNamespace という A の代わりになるようなクラスは用意されています
import types
s = types.SimpleNamespace()
s.foo = 10
s.foo
# 10
ですが types のインポートが必要だったり __dict__ の置き換えができなかったり使いやすいとは言えません
import types
s = types.SimpleNamespace()
s.__dict__ = {"a": 100}
# AttributeError: readonly attribute
ディクショナリを使わずキーワード引数で作るか 既存ディクショナリを使うなら空で作って update を使えば可能です
import types
s = types.SimpleNamespace(a=1000)
s.a
# 1000
import types
s = types.SimpleNamespace()
s.__dict__.update({"a": 1000})
print(s.a)
# 1000
dict と set と tuple
リテラルで値を作るときにパット見のわかりやすさが足りないですprint({"a", "b"})
print({"a": 1, "b": 2})
print({})
print(())
print((1))
print(((2)))
print(1,)
print((1,))
{'b', 'a'}
{'a': 1, 'b': 2}
{}
()
1
2
1
(1,)
dict と set がどっちも {} なので空の場合は区別できないです
空の場合 dict 型になって 空の set 型はリテラルで作れません
タプルは () じゃなくて 「,」 が重要なので () をどれだけ囲んでも意味がないです
ただし 引数など 「,」 が別の意味になるときは () が必要です
このわかりづらさを見てると [] の前に記号つけて見分けるのほうが良かったんじゃないかと思うほどです
例えばこんなの⇩
list1 = [1, "a", True]
tuple1 = @[1, "a", True]
set1 = #[1, "a", True]
分割代入
Python にも分割代入機能がありますただ JavaScript よりもちょっと弱めです
リストは普通につかえます
a = [1, 2]
[x, y] = a
print(x, y)
# 1 2
(p, q) = a
print(p, q)
# 1 2
s, t = a
print(s, t)
# 1 2
問題はディクショナリです
d = {"a": "b", "c": "d"}
x, y = d
print(x, y)
# a c
キーになるんですよね
for でディクショナリをループしても取得できるのはキーです
ディクショナリらしく {} を使って受け取ればできるのかとやってみましたが
{ x, y } = d
# SyntaxError: cannot assign to set display
エラーです
一応こういうことをすれば
x, y = [d[k] for k in ["a", "c"]]
できなくはないですが
x = d["a"]
y = d["c"]
のほうが見やすいです
リストの範囲外アクセス
範囲外アクセスは None などの値は返ってこずにエラー扱いです"key=value".split("=", 1)[1]
# 'value'
"key".split("=", 1)[1]
# IndexError: list index out of range
無いならデフォルト値でいいことは多いのに None を返すことはできないです
こういう書き方をしたいのですけど
"key".split("=", 1)[1] or "default"
リストの部分リストを取得するのは範囲外でも問題ないのですけどね
"key".split("=", 1)[10:20]
# []
dict 型なら get や pop メソッドがあって 第二引数でデフォルト値を指定できます
しかし list にはそういうメソッドがないので自分で関数を作るしか無いです
関数なしで 1 行でやるなら工夫が必要です
[1:] ならエラーにならないことを利用して ほしい要素から始まるリストか要素なしのリストにして それに None を追加します
その [0] にアクセスするとほしい要素か None が取れます
[*"key".split("=", 1)[1:], None][0]
# None
[*"key=value".split("=", 1)[1:], None][0]
# 'value'
コレクションのイテレーション
for 文で list を key, value でループしたいと enumerate を使いますが dict だと dict.items() ですvalues = [10, 20]
for k, v in enumerate(values):
print(k, v)
# 0 10
# 1 20
values = {"a": 1, "b": 2}
for k, v in values.items():
print(k, v)
# a 1
# b 2
まあ JavaScript でも [].entries() と Object.entries({}) なので同じといえばそうなのですが 揃えてほしい感がすごくあります
一応 メソッドは関数としても使えるので
values = {"a": 1, "b": 2}
for k, v in dict.items(values):
print(k, v)
# a 1
# b 2
にはなりますが なんか違うんですよね
変数名の _ のありなし
標準ライブラリを使っていて思うのが 命名規則です区切りの 「_」 があったりなかったりします
例を上げると
_ なし
- startswith
- getattr
- isinstance
- urllib
- unittest
- argparse
_ あり
- is_file
- is_dir
- get_exec_path
- add_argument
モジュールごとに方針が違うならまだわかります
しかし 1 モジュール内でもバラバラです
例えば urllib でも
- geturl
- urlunparse
- parse_qs
- quote_from_bytes
という感じです
_ に限らず mkdir と makedirs があったりだし 名前については覚えるのは諦めたほうがいいかもしれません
この辺は PHP に似た適当さですね