その関数、空のシーケンス渡しても大丈夫?
max()は、普通は2変数以上の引数を渡します。
print(max(1,2,3)) # 3
しかし、ここにiterableを渡せます。listはiterableなので次のようなことができるわけですね。
L=[1,2,3]
print(max(L)) # 3
ところが、Lが空のシーケンスの時、これはエラーになってしまいます。
L=[]
print(max(L)) # ValueError : max() arg is an empty sequence
このように、iterableが引数に取れるけど、空のシーケンスを渡すとエラーになってしまう関数があります。このエラーをどうやって回避するのかというのが今回のお題です。
それ具体的にはどの関数?
このような関数はPythonの組み込み関数だと、私はmin()とmax()ぐらいしか思いつきません。他にもあればコメント欄で教えてください。
max()は引数が1つの時はiterableとみなされる
いま、シーケンスが空の時は、0を出力したいとします。またLの要素はすべて1以上であるとします。
次のように書けばどうかと言われるかも知れませんね。
L=[]
print(max(*L,0)) # TypeError'int' object is not iterable
ところがこの場合、Lが空だとmax(0)の意味になり、max関数を1変数の引数で呼び出していることになります。
max関数は1変数の時は引数をiterableとみなすので(この仕様自体がどうかという気はしなくはないですが、Python 3.11(現時点での最新版)ではこうなっているということで…)、max(0)の0をiterableとみなすことになりますが、0はintであり、iterableではないのでエラーとなります。
minとmaxならデフォルト値を指定できるよ
このminとmaxでは、デフォルト値を指定できます。
L=[]
print(max(L,default=0)) # 0
これで正解なのですが、もう少し汎用的に使える技を知っておかないと、minとmax以外の関数で同じようなことに遭遇した場合に困ります。
またショートコーディング的に見ると、この方法は文字数が多いので少し避けたいです。
三項演算子を使う方法
L=[]
print(max(L)if L else 0)) # 0
こちらは、min(),max()以外で同じような仕様の関数に対しても用いることができる、汎用性の高い方法ではありますが、さきほどより1文字増えています。ショートコーディング的にはちょっと嫌です。
ショートカット演算子を使う方法
L=[]
print(L and max(L)or 0) # 0
これは上の三項演算子を用いる場合より1文字短く書けていますが、それでもdefaultを指定する場合と同じ文字数です。
また、この書き方の場合、max(L)がFalsy(この場合、0)を返した場合、orが評価されてしまいますので、それで本当に良いのかを考える必要が出てきます。
今回の条件ではorのところが0が書いてあるので問題ないのですが、次のような場合、意図しない(?)動作になります。
L=[-2,-1,0]
print(L and max(L)or-1) # -1 ← 0が出力されて欲しかった
この問題については前回記事に詳しく書いてありますのでそちらもご覧ください。
【ショートコーディング】Pythonの三項演算子の周辺
https://qiita.com/yaneurao/items/41b250a4eeb152d4c190
Lに連結できる場合
Lに要素を加えてからmax()を呼び出して良いならば、Lに連結してしまう方法が考えられます。
L=[]
print(max(L+[0])) # 0
この方法は短く書けていてショートコーディング的には悪くはないのですが、この場合、Lが +[0] で連結できるようなものでないといけません。例えば、次のようにmapオブジェクトだと連結できずにエラーになります。
L=map(int,"1 2".split())
print(max(L+[0])) # TypeError : unsupported operand type(s) for +: 'map' and 'list'
Lに連結できない場合
Lがmapオブジェクトのように連結できない場合、ここで二つ方法があります。
一つ目の方法は、listにunpackしてしまう方法です。
L=map(int,"1 2".split())
print(max([*L,0])) # 2
L=map(int,"".split())
print(max([*L,0])) # 0
これは説明不要ですね。
二つ目の方法は、0を二回書くという方法です。
L=map(int,"1 2".split())
print(max(*L,0,0))
これは何なのかと言いますと、Lが空の時に、max(0)と等価になって1変数関数としてmax()を呼び出すのが良くないというのを説明しました。ですから、ここに2変数用意してやろうというのがこのコードの意図です。
これは、大事なことなので2度書きました構文(やねうらお命名)です。
文字数的には、2つ目の方法も上の例では1つ目の方法と変わらないのですが、0,0 ではなく -1,-1 のような場合、1つ目の方法のほうが短く書けるのでショートコーディング的にはあまり使い所がないかも知れません。
まあ、2つ目の方法のほうが、「unpackどうだったかな…?」とか考える必要はなく、わかりやすさはあります。
あと、ショートコーディングで後者の書き方をしている人が時々いるので、このテクニックも知っておくとそういうコード見た時に「これなんだ?」とならずに済みます。
print(max([*L,-1])) # ← 1つ目の方法
print(max(*L,-1,-1)) # ← 2つ目の方法
連結しては困る場合
連結するということは、その値をシーケンスに含めてmax()を呼び出すということで、シーケンスが空だったら0とするのとは微妙に違います。
連結して困る場合、普通は前述の三項演算子かショートカット演算子で書くのがショートコーディング的には最短かと思います。
しかし次のように後ろに何らかの式が続く場合、ショートカット演算子で書くと丸括弧が必要になってdefaultで書くより文字数が増えてしまいます。
return(min(L)if L else 0)+X # 三項演算子で書いた場合
return(L and min(L)or 0)+X # ショートカット演算子で書いた場合
return min(L,default=0)+X # defaultを指定した場合
このように
- 丸括弧が必要になる場合
- 丸括弧は必要だけど、丸括弧をつけたら直前のスペースが省略できる場合
- Lが空の時に0でない値を返したい場合
- min(),max()のようにdefaultが指定できない場合
など、細かい条件によりどれがショートコーディング的にベストかは微妙に変化します。
その時々でベストなもの選択してください。
mapオブジェクトに連結
本記事のテーマとしては以上で話は終わりなのですが、さきほどmapオブジェクトへの連結の話題が出てきたのでこの問題をあと少しだけ掘り下げておきます。
例えば、次のようにmapオブジェクトを扱っているとします。
A=map(int,"1 2".split())
for i in A:print(i)
ここに 3 を連結したいとします。さきほど出てきたテクニックを用いると次のように書けます。
A=map(int,"1 2".split())
for i in[*A,3]:print(i)
この場合、inの直後なのでinと [ との間にスペースは必要なく、(スペースを一つ減らせて5文字増えたので結果として)4文字増えただけですが、状況によっては5文字増えることになります。
では連結できるのがこのタイミングしかないのかと言うと、それは違ってて、上のケースであればsplit()の直後でも連結できます。
A=map(int,"1 2".split()+[3])
for i in A:print(i)
この場合も同様に4文字増えますが、上の例とは違い、5文字になる可能性はありません。
このようにmapのようなiterableに連結したい場合、連結するタイミングは一箇所とは限らないので、ショートコーディングを考える上ではその時々のベストを探っていく必要があります。[*A,N]みたいな形で連結はできるけど、それがベストとは限らないってことですね。
まとめ
今回は、空のシーケンスを渡すとエラーになる関数とその周辺の問題についてまとめてみました。
関数の引数としてiterableを渡す時は、その関数が空のシーケンスを渡しても大丈夫かな?というのを意識するようにしましょう。これは割り算をする時に0除算の可能性がないかを考えるのと同じことです。
引き続きこの連載ではPythonでのショートコーディングのテクニックを網羅的かつ体系的に書いていこうと思いますので、良かったらフォローしてくださいね。
また、ショートコーディングを自分ではしないにせよ、知っておくと便利になるPythonのテクニックが色々出てきますので、読んで損はないと思います!!