読み飛ばし推奨
こんなCSVに遭遇しました。
列 ご と に 行 数 が 違 う。 おまけに途中に空欄がある。
取り合えずPython標準のcsvモジュールで列ごとにリストを作成してみると、
import csv
col_x = []
col_y = []
values = []
with open("mapdata.csv", encoding="utf-8") as f:
reader = csv.reader(f)
for row in reader:
col_x.append(row[0])
col_y.append(row[1])
values.append(row[2])
print(col_x)
# ['X軸', '0', '10', '', '30', '40', '50', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
print(col_y)
# ['Y軸', '100', '150', '200', '300', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
print(values)
# ['値', '235', '393', '471', '501', '607', '736', '900', '975', '1022', '1180', '1247', '1394', '1436', '1506', '1675', '1721', '1855', '1942', '2096', '2177', '2285', '2331', '2420', '2519']
うーん、X軸Y軸の後ろが空文字列で埋められちゃってますね……
シンプルに解答
def remove_trailing(__l: Sequence, empty=("", None, [])):
if __l[-1] not in empty:
return __l
index = next((i for i, c in enumerate(reversed(__l)) if c not in empty), default=0)
return __l[:-index]
解説
まずは引数から
def remove_trailing(__l: Sequence, empty=("", None, [])):
__l
が対象のリスト。関数の仕様上、リストでなくても動作するので型ヒントでは Sequence
としている。
empty=("", None, [])
は空と見なす値のタプル。例えば、 Nan
という文字列も空の値と判定したいときはこのタプルを ("", None, [], "Nan")
とすればよい。
if分岐
if __l[-1] not in empty:
return __l
ここが今回の肝。最初にif文でチェックを入れること。これを省くと、最後の要素が空欄でなかったときに返り値が空のリストになってしまう。
indexの取得
index = next((i for i, c in enumerate(reversed(__l)) if c not in empty), default=0)
reversed(__l)
とすることで、リストを逆順にしたイテレータが取得できる。イテレータってなんだっけ? という人向けに補足すると、要はループ処理時などで順に取得できるようになっているオブジェクトである。こういう適当なことを書くとコメント欄で詳しい人が論陣を張ってくれるそうなのでそちらを見てほしい。(おい)
逆順にしたうえで enumerate
に放り込むと、リストの要素 c
とそのインデックス i
(厳密には逆順でのインデックス)が同時に得られる。そして、後ろの if c not in empty
によって値が空でないときの i
のみが残される。
next()
は競技プログラミングをやる人には見慣れた物だろうが、馴染みのない人もいるであろうから簡単に説明しておくと、先に述べたイテレータの次の要素を取得する関数である*。
今回は、next()
の内部がジェネレータ式となっており、 reversed
されたリストと繰り返し回数を順に取得している。取得された値は if
以下のチェックにかけられ、True
となる値のみが、括弧内先頭の i
として残される。
で、先頭の i
として残された最初の一つ、すなわち、元のリストで言うと空の要素が何個続いたかの数値が、next()
関数によって取得される。
最後の default=0
は、 next()
関数が何も取得するものがなかったときに使われる値である。今回だと、リストの中身が頭から尾っぽまで空っぽだった時に 0
を返す、ということである。これをサボると StopIteration
という例外発生してあらびっくり、となるので注意されたい。
Return
return __l[:-index]
特に説明は不要だろう。Pythonの名物スライスを用いて、後ろから数えて index
番目の要素より前の要素だけを返している。
なぜリスト内包表記でやらないのか?
def remove_trailing(__l: Iterable, empty=("", None, [])):
return [c for c in __l if c not in empty]
1行で終わるじゃん。
と、思ったそこのあなた、賢い。これでいいならこうしてました。
今回はリストの途中にも空の要素があって、そこは残しておきたかったのでこういう少し回りくどい方法が必要でした。
とはいえ、他にもいろんな方法があるかと思いますので、こういうやり方もあるよ、という方は是非コメント欄かご自身の記事でご紹介ください。別記事立てる場合は、この記事へリンクを貼っていただけると探しやすくて助かります。