طريقة أنيقة لإزالة العناصر من التسلسل في بيثون؟[ينسخ]
-
09-06-2019 - |
سؤال
هذا السؤال لديه بالفعل إجابة هنا:
عندما أكتب تعليمات برمجية في بايثون، غالبًا ما أحتاج إلى إزالة العناصر من قائمة أو نوع تسلسل آخر بناءً على بعض المعايير.لم أجد حلاً أنيقًا وفعالاً، حيث أن إزالة العناصر من القائمة التي تقوم بتكرارها حاليًا أمر سيء.على سبيل المثال، لا يمكنك القيام بذلك:
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" المتعددة؟).هل لدى أي شخص حل أكثر أناقة، أو على الأقل أكثر كفاءة؟
ماذا عن واحد يعمل مع القواميس؟
المحلول
هناك طريقتان سهلتان لإنجاز التصفية فقط:
استخدام
filter
:names = filter(lambda name: name[-5:] != "Smith", names)
استخدام فهم القائمة:
names = [name for name in names if name[-5:] != "Smith"]
لاحظ أن كلتا الحالتين تحتفظان بالقيم التي يتم تقييم الدالة الأصلية لها True
, ، لذلك عليك عكس المنطق (أي.أنت تقول "الاحتفاظ بالأشخاص الذين ليس لديهم الاسم الأخير سميث" بدلاً من "إزالة الأشخاص الذين لديهم الاسم الأخير سميث").
يحرر مضحك...قام شخصان بنشر كل من الإجابات التي اقترحتها بشكل فردي أثناء قيامي بنشر إجابتي.
نصائح أخرى
يمكنك أيضًا التكرار للخلف عبر القائمة:
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
التعيين إلى "الأسماء [:]" يعني في الأساس "استبدال محتويات قائمة الأسماء بالقيمة التالية".إنه يختلف عن مجرد تعيين الأسماء، حيث أنه لا يؤدي إلى إنشاء كائن قائمة جديد.الجانب الأيمن من المهمة عبارة عن تعبير منشئ (لاحظ استخدام الأقواس بدلاً من الأقواس المربعة).سيؤدي هذا إلى تكرار بايثون عبر القائمة.
تشير بعض ملفات التعريف السريعة إلى أن هذا أسرع بنسبة 30% تقريبًا من أسلوب فهم القائمة، وأسرع بحوالي 40% من أسلوب التصفية.
تنبيه قضائي:في حين أن هذا الحل أسرع من الحل الواضح، إلا أنه أكثر غموضًا، ويعتمد على تقنيات بايثون الأكثر تقدمًا.إذا كنت تستخدمه، أوصي بإرفاقه بتعليق.ربما يكون من المفيد استخدامه فقط في الحالات التي تهتم فيها حقًا بأداء هذه العملية المحددة (والتي تكون سريعة جدًا بغض النظر عن الأمر).(في الحالة التي استخدمت فيها هذا، كنت أقوم بإجراء بحث في شعاع A*، واستخدمت هذا لإزالة نقاط البحث من شعاع البحث.)
استخدام فهم القائمة
list = [x for x in list if x[-5:] != "smith"]
هناك أوقات لا تعمل فيها التصفية (إما باستخدام عامل التصفية أو فهم القائمة).يحدث هذا عندما يحتفظ كائن آخر بمرجع إلى القائمة التي تقوم بتعديلها وتحتاج إلى تعديل القائمة في مكانها.
for name in names[:]:
if name[-5:] == 'Smith':
names.remove(name)
والفرق الوحيد عن الكود الأصلي هو استخدام names[:]
بدلاً من names
في الحلقة.وبهذه الطريقة يتكرر الكود عبر نسخة (سطحية) من القائمة وتعمل عمليات الإزالة كما هو متوقع.نظرًا لأن نسخ القائمة سطحي، فهو سريع إلى حد ما.
سيكون الفلتر رائعًا لهذا الغرض.مثال بسيط:
names = ['mike', 'dave', 'jim']
filter(lambda x: x != 'mike', names)
['dave', 'jim']
يحرر: إن فهم قائمة كوري رائع أيضًا.
names = filter(lambda x: x[-5:] != "Smith", names);
كلا الحلين، منقي و فهم يتطلب بناء قائمة جديدة.لا أعرف ما يكفي عن الأجزاء الداخلية من لغة بايثون للتأكد من ذلك، لكنني يفكر أن النهج الأكثر تقليدية (ولكنه أقل أناقة) يمكن أن يكون أكثر كفاءة:
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
على أية حال، بالنسبة للقوائم القصيرة، سألتزم بأي من الحلين المقترحين سابقًا.
للإجابة على سؤالك حول العمل مع القواميس، يجب ملاحظة أن 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) .في هذه الحالة، يمكنك استخدام الدالة no-so pythonic التالية:
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 هو متفوق على الحل الألغام.إنه أكثر بيثونية ويعمل بشكل أسرع.إذن، إليك تطبيق 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') أكثر تعقيدًا، أو لها عدة شروط.
الحل الأصلي الخاص بك هو في الواقع أكثر كفاءة للقوائم الكبيرة جدًا، حتى لو اتفقنا على أنه أقبح.ولكن إذا كنت قلقًا من احتمال وجود عدة "جون سميث"، فيمكن إصلاح ذلك عن طريق الحذف بناءً على الموضع وليس على القيمة:
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
لا يمكننا اختيار حل دون الأخذ في الاعتبار حجم القائمة، ولكن بالنسبة للقوائم الكبيرة، أفضّل الحل ثنائي التمرير بدلاً من المرشح أو استيعاب القوائم
في حالة مجموعة.
toRemove = set([])
for item in mySet:
if item is unwelcome:
toRemove.add(item)
mySets = mySet - toRemove
هنا أنا filter_inplace
التنفيذ الذي يمكن استخدامه لتصفية العناصر من قائمة في مكانها، لقد توصلت إلى هذا بنفسي بشكل مستقل قبل العثور على هذه الصفحة.إنها نفس الخوارزمية التي نشرها PabloG، ولكنها أصبحت أكثر عمومية حتى تتمكن من استخدامها لتصفية القوائم في مكانها، كما أنها قادرة على الإزالة من القائمة بناءً على 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
حسنًا، من الواضح أن هذه مشكلة تتعلق ببنية البيانات التي تستخدمها.استخدم جدول التجزئة على سبيل المثال.تدعم بعض التطبيقات إدخالات متعددة لكل مفتاح، بحيث يمكن للمرء إما إخراج العنصر الأحدث أو إزالته جميعًا.
لكن هذا، وما ستجده هو الحل، الأناقة من خلال بنية بيانات مختلفة، وليس الخوارزمية.ربما يمكنك القيام بعمل أفضل إذا تم فرزها، أو شيء من هذا القبيل، ولكن التكرار في القائمة هو طريقتك الوحيدة هنا.
يحرر: يدرك المرء أنه طلب "الكفاءة" ...كل هذه الطرق المقترحة تتكرر فقط على القائمة، وهو نفس ما اقترحه.