tuneoの日記: Pythonでこういう書き方はしていいんだっけ? 24
日記 by
tuneo
使用する処理系は諸般の事情でPython2.6。久々のPythonで頭がボケてる……。
やりたいのはリストの操作なんだけど、ループでリストの要素を頭からなめて、特定の条件を満たす場合はあれこれ処理した後でin placeで削除したい。
たとえば文字列を空白で分割して、部分文字列が"f"で始まっていた場合はリストから削除する、という処理だ。
l = " ".split("foo bar baz")
for i, e in enumerate(l):
if e.startswith("f"):
あれこれ
del l[i]
print l
というコードは正当で、常に["bar", "baz"]を返すことを期待していいんだったっけ?
……もちろん上記の例のごときはリスト内包表記なりfilter関数を使って書けばいいのだが、本チャンのコードはちゃんとややこしい。
駄目っです。リファレンス参照どぞ (スコア:1)
[:]でコピー作らないと駄目よー
詳しくはリファレンス参照:2.6 [python.org] 3だけど日本語化されてる。中身は同じ [python.org]
Re:駄目っです。リファレンス参照どぞ (スコア:1)
shallow copyをなめるループを回しながらオリジナルを削除すればよいということですかね。
Re:駄目っです。リファレンス参照どぞ (スコア:1)
その場合リストの"i個目"がオリジナルとズレるので「del l[i]」がバグります。
eがユニークで有る事を保証できるならeをキーにremoveするのが手っ取り早いです。(#3899890 [srad.jp])
i個目に重要な意味が有る/データ数が多い等でdelを使うなら、末尾からループさせるか、iに削除した分のマイナス補正して下さい。(del l[i-削除済数]とか)
もしくは、forループを諦めてwhileにし、削除時はiをカウントアップしないようなアルゴリズムにして下さい。
どれを選ぶかは、お好みで。
Re:駄目っです。リファレンス参照どぞ (スコア:1)
あーそりゃそうか要素数変わるからl[i]もズレますやね。
in placeで、という縛りがなければ新しいリストを作って置き換えることができるわけですが。
Re: (スコア:0)
リファレンス参照って馬から落馬チックだな
Re: (スコア:0)
そのとおり。リファレンス参照は関係ない話で、原因は 「iがズレますよ。」ですね
"foo bar baz"で[:]を使うと動くように見えるのはタダの勘違いです
"foo bar baz"ならもとのコードでも[:]でもどちらでも bar baz と
正しくうごいているようにみえます
別コメントにあるように"foo foo foo"で試せば[:]でもエラーがでます
Re:駄目っです。リファレンス参照どぞ (スコア:1)
"foo bar baz"ならもとのコードでも[:]でもどちらでも bar baz と
正しくうごいているようにみえます
今回のテストデータでは正しくうごいているようにみえるだけです。
"foo foo bar"というテストデータでテストすれば解りますが、
for中に要素数が変わると"foo bar"と2個目のfで始まる奴が残ります。
私がリンクしたリファレンスのNote(注釈)を読まないから「厄介なバグ」を見逃したり、作りこみます。
iがズレるのは、
del l[i]
を
l.remove(e)
みたいにiに依存しない形で変更する事で回避可能です。
もしくはオフセットを持たすか、whileで手動でカウントアップするか。
removeの場合は意図的に残したみたいなパターンで重複が有り得る場合は破綻しますけど。
例えば1個目のfooは残す場合、foo,foo,barな場合、1個目が消えちゃいます。
例:
l = "foo foo foo".split(" ")
for i, e in enumerate(l[:]):
if e.startswith("f"):
いろいろ
l.remove(e)
print(l)
実行結果:
['bar']
これがメモリ喰ったり遅いけど、元のコードからあまり変わらず解りやすい方法。
データ数が多い等でdelで頑張りたいなら、#3899763 [srad.jp]さんのように逆順で回すのが楽です。
どれがベストかは削除の条件やらユニーク有無やらどの程度のデータ量が有るかとか解らないのでなんとも。
# そもそも私もPythonは滅多に触らないのでもっと良い書き方が有るかもです。
Re:駄目っです。リファレンス参照どぞ (スコア:1)
テストデータが間違ってるじゃねーか。
l = "foo foo foo".split(" ")
を
l = "foo foo bar".split(" ")
に置換してお読みください。
Re: (スコア:0)
要素を一意にしてからチェックするのは処理時間的に無駄な気がする
Re:駄目っです。リファレンス参照どぞ (スコア:1)
要素を一意にしてからチェックするのは処理時間的に無駄な気がする
他の要因、例えば別のリストが居てそっちの内容も影響するとか等、
条件判定でeを残すパターンが有るとかなければ一意でなくても良いです。
remove(e)ですが、処理的にリスト内の探索の時間が無駄になります。
ですが、それが問題にならないくらい途中を削除する事がNGです。
削除する度に内部で削除した以降の要素を手前に持ってくる移動処理をして、リストが再構築されてると思わしき挙動なのでめちゃ重です。
リストが一万件越えて大半を削除する事になると体感できるぐらいになってくるかと。
# ちゃんと計測したことないけど、新規リスト作って、最後に入れ替えた方が早いと思う。
# でも、今回は「in placeで削除」らしいので対象外。
でも、せいぜい数千件程度とか、削除の頻度はそんなにないとか、使い捨てでたまにしか走らないプログラムなら必要十分。
後は処理時間の削減やコードの見通し悪化やどの程度コードを書く時間かけるかの費用対効果です。
使い捨てスクリプトやバッチに数分悩んだら、遅いアルゴリズムでも処理終わって次の作業出来てるかもしれないし。
実測してみた。 (スコア:1)
要素を一意にしてからチェックするのは処理時間的に無駄な気がする
# ちゃんと計測したことないけど、新規リスト作って、最後に入れ替えた方が早いと思う。
気になったのでちょろっと書いた糞コードと実測結果晒しておきます。
ちゃんとデバッグしてないからバグってたり、動かなかったらごめんなさい。
Python使いが見たら罵倒されると思う。
# coding: utf-8
import timeit
def make_list_foo_only(size):
"""
foo+初期インデックスを生成:foo0, foo1, ..., fooX
"""
l = list("foo{:97d}".format(x) for x in range(size))
return l
def make_list_bar_only(size):
"""
bar+初期インデックスを生成: bar0, bar1, ..., barX
"""
l = list("bar{:97d}".format(x) for x in range(size))
return l
def make_list_alternate_foo_bar(size):
"""
foo,bar+初期インデックスが交互を生成: foo0, bar1, ..., fooX or barX
"""
l = list("bar{:97d}".format(x) if x % 2 else "foo{:97d}".format(x) for x in range(size))
return l
def make_list_bar50_foo50(size):
"""
前半をbar+初期インデックス、後半foo+初期インデックスを生成: bar0, bar1, ..., fooX
"""
l = list("bar{:97d}".format(x) if x < size / 2 else "foo{:97d}".format(x) for x in range(size))
return l
def make_list_foo50_bar50(size):
"""
前半をfoo+初期インデックス、後半bar+初期インデックスを生成: foo0, foo1, ..., barX
"""
l = list("foo{:97d}".format(x) if x < size / 2 else "bar{:97d}".format(x) for x in range(size))
return l
def list_remove(l):
"""
公式ドキュメントの方法で、スライスによる一時コピー有り。
e.startswith("f")以外の条件で削除しないケースが有る場合はNG
シンプルだけど項目数が増えると糞遅くなる。
"""
for i, e in enumerate(l[:]):
if e.startswith("f"):
# あれこれ
l.remove(e)
return l
def list_del_reversed(l):
"""
末尾から削除、スライスによる一時コピーは不要。
あれこれ処理するのが後ろからになる事が許容できるならこれが良好。
"""
for i in reversed(range(len(l))): # xrangeは3でrangeに要置換なので使用せず
e = l[i]
if e.startswith("f"):
# あれこれ
del l[i]
return l
def list_del_offset(l):
"""
リスト先頭から処理が必要な場合の妥協案。
スライスによる一時コピー有り。
"""
足すときは先頭から減らすときは後ろから (スコア:0)
ループでこういうことやるときは基本消すときは後ろから足すときは前からやればいい感じ。
基本なので細かいところは全部省略。
Re: (スコア:0)
tuneoはしったか君なんでそういった常識を教えてやっても意味ないような…
駄目 (スコア:0)
間違いがふたつ有りますね
1つ目は一行目。正しくは l = "foo bar baz".split(" ") ですね
2つ目は本題で l = "foo foo foo".split(" ") とすれば直ぐに駄目なことが解ります
Re:駄目 (スコア:1)
・「これはやってもいいんだっけ?」という疑問を持つこと自体が「間違い」
・「いろんな要素があるリストの特定の要素だけを削除するコード」をお題にするときは「全部の要素が削除対象」というケースを例に挙げる必要がある
貴重なご意見ありがとうございました。
Re:駄目 (スコア:1)
#3899575 [srad.jp]さんの言いたいことが曲解されてるような気がするのでツッコミます。
・「これはやってもいいんだっけ?」という疑問を持つこと自体が「間違い」
ロジック的にバグってるという「間違い」ですよ。
・「いろんな要素があるリストの特定の要素だけを削除するコード」をお題にするときは「全部の要素が削除対象」というケースを例に挙げる必要がある
貴重なご意見ありがとうございました。
重要なのは全部fooな事でなく、fooが2連続しているという事です。
削除漏れが起きるテストパターンの例示ですね。
原理:foo1個目削除→foo元2個目が1個目になる→次のループで、foo元3個目がfoo2個目として渡ってくる。→foo元2個目が削除されずに残る。
って事です。
Re: (スコア:0)
むしろ全てに疑問を持つべきですよね。
エクセルじゃだめなんですかとかメモ帳じゃだめなのかそもそもやらなきゃいけないのかレベルから始まり本当にそれでできるのかメモリは足りるか時間は足りるかエトセトラ。
Re: (スコア:0)
kei100さんは教えるの下手くそなのでツッコみますね
L = [1, 2]
del L[0]
del L[1]
この話はこれだけで説明できます。リファレンス参照とか関係無い話はやめましょう
Re:駄目 (スコア:1)
ですよねー
教えるの下手くそですよね。
リファレンス参照って、Pythonのドキュメント(Forの注釈)を読めって意味だったんですけどね。
ところで、foreach中にコレクションの要素を削除という、.NETなら例外で落ちる所業 [microsoft.com]してるけど、
Pythonは例外で死なずに内部カウンタが狂うだけで中途半端に動く&回避策が書いて有るけど関係無いって話なんです?
Re: (スコア:0)
あなたの最初のコメントは
> [:]でコピー作らないと駄目よー
> 詳しくはリファレンス参照:2.6 [python.org] 3だけど日本語化されてる。中身は同じ [python.org]
です
「コピー作らないと駄目よー」って勘違いしてた訳で、それを誤魔化すのは止めましょうよ
Re:駄目 (スコア:1)
そちらこそ、ちゃんとリンク先の注釈読んでます?
シーケンス全体のスライスを使って一時的なコピーを作ることで避けられます。
ディープコピーしてもよいけど、丸ごとスライスする事で、一時コピーをforに渡してるんですよ?
コピーを渡さないと内部カウンタがずれるので、その事込みでロジックを組まないと削除漏れが起きます。
# しかも明文化されてない内部仕様に依存するので将来動くかさえ不確実。
Re: (スコア:0)
その説明で理解できるなら件のコードを書いた時点で気づくと思う。
自然言語の説明はやっぱ必要。
Re: (スコア:0)
目的を達成できるかという点ではすでに出ているようにだめ。
Pythonでこういうやり方が良いのかという点でもだめ。
Pythonなら判定を
"foo bar baz".startswith('f') or ' f' in "foo bar baz"
とかにしたほうがPythonぽい