splitの仕様確認
aaaa#bbbb#cccc
のように、文字列を任意の区切り文字(例の場合#
が区切り文字)で分割し、分割した文字列に対して何かしらの処理を行いたい場合に、str.split
の利用がまず思いつきます。
s = "aaaa#bbbb#cccc"
s_list = s.split("#")
print(s_list)
# ['aaaa', 'bbbb', 'cccc']
str.split
のリファレンスを読むと、maxsplit
を使うことで、分割数を指定できるらしいです。戻り値であるリストの長さは最大でmaxsplit+1
になります。
str.split(sep=None, maxsplit=- 1)
文字列を sep をデリミタ文字列として区切った単語のリストを返します。maxsplit が与えられていれば、最大で maxsplit 回分割されます (つまり、リストは最大 maxsplit+1 要素になります)。 maxsplit が与えられないか -1 なら、分割の回数に制限はありません (可能なだけ分割されます)。
つまり、maxsplit=1
の場合、例で挙げた文字列はaaaa
とbbbb#cccc
に分割されることになります。maxsplit=2
の場合aaaa
とbbbb
とcccc
に分割されます。
では、maxplit
を使いたくなる場面はどんな場面でしょうか?私がまず思いつくのは、splitするデータが後からどんどん追加されていく状況です。例えば文字列の方が別スレッドで外部から受信したデータになっていて、処理スレッド側で逐次セパレータで分割して1件ずつ処理するようなプログラムを実装する必要がある場合です。そんなときは、次のようなプログラムを書きたくなります。
while True:
# データを受信してbuffの末尾に追加
buff += recv_data()
# 処理対象データdataとそれ以外buffに分割
data, buff = buff.split(sep, maxsplit=1)
# 何かしらの処理
process(data)
ただし、文字列比較などの文字列操作はアルゴリズムによりますが一般に重たい処理です。buff
が長くなればなるほどsplit
の処理に時間がかかってくることが想定されるので、処理するデータの内容によっては意図せず時間のかかる処理を何度も実行してしまう可能性があります。こんな実装をしてしまって処理時間は大丈夫なのでしょうか。
というわけで、実験してみました。
実験
import time
class Counter:
cnt = 0
def process(self, split_str):
self.cnt += 1
def generate(length, sep):
buff = "0123456789"
for i in range(length - 1):
buff += sep
buff += "0123456789"
return buff
if __name__ == "__main__":
length = 100000
sep = "#"
print("--- 手法1 ------------")
counter = Counter()
buff = generate(length, sep)
t_start = time.time()
for _ in range(length - 1):
split_str, buff = buff.split(sep, maxsplit=1)
counter.process(split_str)
counter.count(buff)
t_end = time.time()
print(f"processが呼ばれた回数: {counter.cnt} 回")
print(f"実行時間: {t_end - t_start} 秒")
print("--- 手法2 ------------")
counter = Counter()
buff = generate(length, sep)
t_start = time.time()
split_str_list = buff.split(sep)
for split_str in split_str_list:
counter.process(split_str)
t_end = time.time()
print(f"processが呼ばれた回数: {counter.cnt} 回")
print(f"実行時間: {t_end - t_start} 秒")
Counter
はsplit_str
にないかしらの処理をさせる想定のクラスです。実験ではとりあえず関数が呼ばれた回数をカウントしています。
また、手法1はmaxsplit
を利用して1回の分割をセパレータの個数回実行する方法、手法2はmaxplist
を利用せずセパレータ個+1の分割を1回だけ実行する方法です。
実験結果
--- 手法1 ------------
processが呼ばれた回数: 100000 回
実行時間: 3.0889523029327393 秒
--- 手法2 ------------
processが呼ばれた回数: 100000 回
実行時間: 0.022452116012573242 秒
見ての通りで、splitをセパレータの個数回呼んだ場合、セパレータで一括分割した場合に比べて100倍以上の時間がかかることがわかりました。予想よりも時間の差が大きいことがわかりました。
おわりに
実際には1ループの処理時間のオーバヘッドは大したことがありませんが、ループが短い時間にたくさん回る場合で、性能要件が厳しい場合は、面倒くさがらずにsplit
の回数を減らすよう注意したほうがよさそうです。
maxsplit
は使うべきではない!とまではいいませんが、個人的にはどういう場面でmaxsplit
を使うのかよくわからなくなりました。。。