LoginSignup
0
0

More than 1 year has passed since last update.

Pythonのstr.splitは短い時間に大量に呼んではならない

Posted at

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の場合、例で挙げた文字列はaaaabbbb#ccccに分割されることになります。maxsplit=2の場合aaaabbbbccccに分割されます。

では、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}")

Countersplit_strにないかしらの処理をさせる想定のクラスです。実験ではとりあえず関数が呼ばれた回数をカウントしています。

また、手法1はmaxsplitを利用して1回の分割をセパレータの個数回実行する方法、手法2はmaxplistを利用せずセパレータ個+1の分割を1回だけ実行する方法です。

実験結果

--- 手法1 ------------
processが呼ばれた回数: 100000 回
実行時間: 3.0889523029327393 秒
--- 手法2 ------------
processが呼ばれた回数: 100000 回
実行時間: 0.022452116012573242 秒

見ての通りで、splitをセパレータの個数回呼んだ場合、セパレータで一括分割した場合に比べて100倍以上の時間がかかることがわかりました。予想よりも時間の差が大きいことがわかりました。

おわりに

実際には1ループの処理時間のオーバヘッドは大したことがありませんが、ループが短い時間にたくさん回る場合で、性能要件が厳しい場合は、面倒くさがらずにsplitの回数を減らすよう注意したほうがよさそうです。

maxsplitは使うべきではない!とまではいいませんが、個人的にはどういう場面でmaxsplitを使うのかよくわからなくなりました。。。

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0