Python でシーケンスから項目を削除するエレガントな方法はありますか?[重複]
-
09-06-2019 - |
質問
この質問にはすでに答えがあります:
- 反復中にリストから項目を削除するにはどうすればよいですか? 26 件の回答
Python でコードを作成しているとき、いくつかの基準に基づいてリストまたはその他のシーケンス タイプから項目を削除する必要があることがよくあります。現在反復処理しているリストから項目を削除するのは良くないため、エレガントで効率的な解決策は見つかりませんでした。たとえば、次のようなことはできません。
for name in names:
if name[-5:] == 'Smith':
names.remove(name)
私は通常、次のようなことをすることになります。
toremove = []
for name in names:
if name[-5:] == 'Smith':
toremove.append(name)
for name in toremove:
names.remove(name)
del toremove
これは非効率的で、かなり醜く、バグが多い可能性があります (複数の「John Smith」エントリをどのように処理するのでしょうか?)。もっと洗練された解決策、少なくともより効率的な解決策を持っている人はいますか?
辞書と連携できるものはどうでしょうか?
解決
フィルタリングだけを行う簡単な方法は次の 2 つです。
使用する
filter
:names = filter(lambda name: name[-5:] != "Smith", names)
リスト内包表記の使用:
names = [name for name in names if name[-5:] != "Smith"]
どちらの場合も、述語関数が次のように評価する値が保持されることに注意してください。 True
, したがって、ロジックを逆にする必要があります(つまり、「スミスという姓を持つ人々を削除する」ではなく、「スミスという姓を持たない人々を残す」と言う)。
編集 面白い...私が提案した回答の両方を、私が投稿したときに 2 人が個別に投稿しました。
他のヒント
リストを逆方向に反復処理することもできます。
for name in reversed(names):
if name[-5:] == 'Smith':
names.remove(name)
これには、新しいリストを作成しないという利点があります(たとえば、 filter
またはリスト内包表記)、リストのコピーの代わりに反復子を使用します(たとえば、 [:]
).
逆方向の反復中に要素を削除するのは安全ですが、要素を挿入するのはやや難しいことに注意してください。
明白な答えは、ジョンと他の数人が出したものです。
>>> names = [name for name in names if name[-5:] != "Smith"] # <-- slower
ただし、これには、元のオブジェクトを再利用するのではなく、新しいリスト オブジェクトが作成されるという欠点があります。いくつかのプロファイリングと実験を行った結果、私が思いついた最も効率的な方法は次のとおりです。
>>> names[:] = (name for name in names if name[-5:] != "Smith") # <-- faster
「names[:]」への代入とは、基本的には「namesリストの内容を次の値に置き換える」ことを意味します。新しいリスト オブジェクトを作成しないという点で、単に名前を割り当てるのとは異なります。代入の右側はジェネレーター式です (角括弧ではなく括弧を使用していることに注意してください)。これにより、Python はリスト全体を反復処理します。
簡単なプロファイリングによると、これはリスト理解アプローチよりも約 30% 高速で、フィルター アプローチよりも約 40% 高速です。
警告:このソリューションは明白なソリューションよりも高速ですが、より曖昧で、より高度な Python テクニックに依存しています。使用する場合は、コメントを添えることをお勧めします。おそらく、この特定の操作のパフォーマンス (何があってもかなり高速です) を本当に気にする場合にのみ使用する価値があります。(これを使用した場合、A* ビーム サーチを実行しており、これをサーチ ビームからサーチ ポイントを削除するために使用しました。)
使用する リスト内包表記
list = [x for x in list if x[-5:] != "smith"]
フィルタリング (フィルタまたはリスト内包表記のいずれかを使用) が機能しない場合があります。これは、変更中のリストへの参照が他のオブジェクトに保持されており、そのリストを適切に変更する必要がある場合に発生します。
for name in names[:]:
if name[-5:] == 'Smith':
names.remove(name)
元のコードとの唯一の違いは、次の使用方法です。 names[:]
の代わりに names
for ループ内で。こうすることで、コードはリストの (浅い) コピーを反復処理し、削除は期待どおりに機能します。リストコピーは浅いのでかなり早いです。
フィルターはこれに最適です。簡単な例:
names = ['mike', 'dave', 'jim']
filter(lambda x: x != 'mike', names)
['dave', 'jim']
編集: Corey のリストの理解力も素晴らしいです。
names = filter(lambda x: x[-5:] != "Smith", names);
どちらのソリューションも、 フィルター そして 理解 新しいリストを作成する必要があります。Python の内部についてはよくわかりませんが、 考える より伝統的な (ただしエレガントさは劣る) アプローチの方が効率的である可能性があると考えられます。
names = ['Jones', 'Vai', 'Smith', 'Perez']
item = 0
while item <> len(names):
name = names [item]
if name=='Smith':
names.remove(name)
else:
item += 1
print names
とにかく、短いリストについては、以前に提案した 2 つの解決策のいずれかを使用します。
辞書の操作に関する質問に答えるには、Python 3.0 には次のものが含まれることに注意してください。 辞書内包表記:
>>> {i : chr(65+i) for i in range(4)}
それまでの間、次の方法で準辞書内包含を行うことができます。
>>> dict([(i, chr(65+i)) for i in range(4)])
または、より直接的な答えとして:
dict([(key, name) for key, name in some_dictionary.iteritems if name[-5:] != 'Smith'])
リストをその場でフィルタリングする必要があり、リストのサイズが非常に大きい場合、前の回答で述べた list.remove() に基づくアルゴリズムは、計算量が O(n^2) であるため、不適切である可能性があります。 。この場合、次のような不要な Python 関数を使用できます。
def filter_inplace(func, original_list):
""" Filters the original_list in-place.
Removes elements from the original_list for which func() returns False.
Algrithm's computational complexity is O(N), where N is the size
of the original_list.
"""
# Compact the list in-place.
new_list_size = 0
for item in original_list:
if func(item):
original_list[new_list_size] = item
new_list_size += 1
# Remove trailing items from the list.
tail_size = len(original_list) - new_list_size
while tail_size:
original_list.pop()
tail_size -= 1
a = [1, 2, 3, 4, 5, 6, 7]
# Remove even numbers from a in-place.
filter_inplace(lambda x: x & 1, a)
# Prints [1, 3, 5, 7]
print a
編集:実際、解決策は https://stackoverflow.com/a/4639748/274937 私のソリューションよりも優れています。よりPython的で、より高速に動作します。したがって、ここに新しい filter_inplace() 実装があります。
def filter_inplace(func, original_list):
""" Filters the original_list inplace.
Removes elements from the original_list for which function returns False.
Algrithm's computational complexity is O(N), where N is the size
of the original_list.
"""
original_list[:] = [item for item in original_list if func(item)]
この例ではフィルターとリストの内包表記は問題ありませんが、いくつかの問題があります。
- リストのコピーを作成して新しいリストを返しますが、元のリストが非常に大きい場合には非効率的になります。
- 項目を選択する基準 (あなたの場合、name[-5:] == 'Smith' の場合) がより複雑である場合、または複数の条件がある場合、それらは非常に面倒になる可能性があります。
たとえそれが醜いことに同意するとしても、元のソリューションは実際には非常に大きなリストに対してより効率的です。ただし、「John Smith」が複数存在する可能性があることが心配な場合は、値ではなく位置に基づいて削除することで修正できます。
names = ['Jones', 'Vai', 'Smith', 'Perez', 'Smith']
toremove = []
for pos, name in enumerate(names):
if name[-5:] == 'Smith':
toremove.append(pos)
for pos in sorted(toremove, reverse=True):
del(names[pos])
print names
リストのサイズを考慮せずにソリューションを選択することはできませんが、大きなリストの場合は、フィルターやリストの内包表記の代わりに 2 パス ソリューションを使用することをお勧めします。
セットの場合。
toRemove = set([])
for item in mySet:
if item is unwelcome:
toRemove.add(item)
mySets = mySet - toRemove
これが私のものです filter_inplace
リストから項目をその場でフィルタリングするために使用できる実装ですが、このページを見つける前に、私は独自にこれを思いつきました。これは PablogG が投稿したものと同じアルゴリズムですが、より汎用的なものになっているため、リストを適切にフィルタリングするために使用できます。また、以下に基づいてリストから削除することもできます。 comparisonFunc
逆が設定されている場合 True
;言ってみれば、一種の逆フィルターです。
def filter_inplace(conditionFunc, list, reversed=False):
index = 0
while index < len(list):
item = list[index]
shouldRemove = not conditionFunc(item)
if reversed: shouldRemove = not shouldRemove
if shouldRemove:
list.remove(item)
else:
index += 1
これは明らかに、使用しているデータ構造に問題があります。たとえば、ハッシュテーブルを使用します。一部の実装ではキーごとに複数のエントリをサポートしているため、最新の要素をポップオフすることも、すべての要素を削除することもできます。
しかし、これは、アルゴリズムではなく、異なるデータ構造による優雅さであり、あなたが見つけようとしている解決策です。ソートなどすればもっとうまくできるかもしれませんが、ここではリストを反復することが唯一の方法です。
編集: 彼が「効率」を求めていたことはわかります...これらの提案されたメソッドはすべて、リストを反復処理するだけであり、これは彼が提案したものと同じです。