الدالة filter: فن اختيار البيانات الصحيحة واستبعاد الضجيج
في عالم البيانات الضخم، نادراً ما نحتاج إلى كل شيء. غالباً ما نبحث عن "الإبرة في كومة القش"؛ مثل استخراج المستخدمين النشطين فقط، أو الأرقام الموجبة، أو الكلمات التي تبدأ بحرف معين. بدلاً من كتابة حلقات for معقدة وجمل if متداخلة، توفر بايثون دالة filter(). هذه الدالة تعمل كـ "مصفاة" ذكية تمرر فقط العناصر التي توافق معاييرك، مما يجعل كودك أكثر نظافة، سرعة، واحترافية.
1. تعريف: المفهوم والآلية - كيف تعمل filter؟
دالة filter() هي دالة ذات ترتيب أعلى (Higher-Order Function) تعتمد على مبدأ "الاختبار" (Testing). هي تأخذ دالة اختبار (تسمى Predicate في علوم الحاسوب) ومجموعة بيانات، وتقوم بتطبيق الاختبار على كل عنصر واحداً تلو الآخر.
إذا أعادت دالة الاختبار القيمة True للعنصر، يتم الاحتفاظ به في النتيجة النهائية. أما إذا أعادت False، يتم استبعاده فوراً ولا يظهر في النتيجة. إنها مثل حارس أمن يسمح فقط للأشخاص الذين يحملون تصريحاً صحيحاً بالدخول.
الفرق بين filter و map: بينما map() تُحوّل كل عنصر، filter() تختار بعض العناصر وتتجاهل الباقي. map() تُرجع نفس عدد العناصر (محولة)، بينما filter() قد تُرجع عدداً أقل (أو حتى صفر عناصر).
الصيغة العامة (Syntax):
filter(function, iterable)
- function: دالة يجب أن تُرجع قيمة منطقية (Boolean: True أو False). هذه الدالة تُطبق على كل عنصر لتحديد ما إذا كان سيُحتفظ به أم لا. إذا مررت
Noneبدلاً من دالة، ستقومfilter()بتصفية كل العناصر التي تُعتبر "خاطئة" (Falsy) في بايثون (مثل 0، النصوص الفارغة، القوائم الفارغة، None، False). - iterable: المجموعة التي تريد تصفيتها (قائمة، tuple، set، range، أو أي كائن قابل للتكرار).
map، تُرجع filter كائناً مكرراً (Iterator) وليس قائمة. هذا يعني أنها لا تحسب النتائج فوراً (Lazy Evaluation)، مما يوفر الذاكرة. يجب تحويله إلى قائمة باستخدام list() أو tuple() لرؤية النتائج.
2. لماذا نستخدم filter بدلاً من الحلقات؟
قبل أن ندخل في الأمثلة، دعنا نفهم لماذا filter() أفضل من الحلقة التقليدية:
- الوضوح (Declarative Style):
filter()تعبّر عن النية بوضوح - "احتفظ بالعناصر التي تحقق هذا الشرط". الحلقة تتطلب قراءة عدة أسطر لفهم نفس الفكرة. - الأداء (Performance):
filter()مُحسّنة داخلياً في C، مما يجعلها أسرع من الحلقات في بايثون النقي. - توفير الذاكرة (Memory Efficiency): بفضل الـ Iterator، لا تُنشئ
filter()قائمة كاملة في الذاكرة إلا عند الحاجة. - قلة الأخطاء: لا حاجة لإنشاء قائمة فارغة أو استخدام
append()، مما يقلل احتمالية الأخطاء. - البرمجة الوظيفية (Functional Programming):
filter()تشجع على كتابة كود بدون تأثيرات جانبية، مما يسهل الاختبار والصيانة.
3. أمثلة عملية متنوعة
أ) تصفية الأرقام (الأعداد الزوجية):
لنبدأ بمثال كلاسيكي: استخراج الأرقام الزوجية فقط من قائمة.
def is_even(n):
"""تفحص إذا كان الرقم زوجياً"""
return n % 2 == 0
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# الطريقة التقليدية بالحلقة
evens_loop = []
for num in numbers:
if is_even(num):
evens_loop.append(num)
print(f"بالحلقة: {evens_loop}")
# الطريقة الذكية بـ filter
evens_filter = filter(is_even, numbers)
print(f"نوع النتيجة: {type(evens_filter)}") #
# تحويل إلى قائمة
print(f"بـ filter: {list(evens_filter)}") # [2, 4, 6, 8, 10]
شرح الكود سطراً بسطر:
- الأسطر 1-3: نعرّف دالة
is_evenتستقبل رقماً وتعيدTrueإذا كان زوجياً (باقي القسمة على 2 يساوي 0). - السطر 5: قائمة أرقام للتجربة.
- الأسطر 8-12: الطريقة التقليدية: نُنشئ قائمة فارغة، نمر على كل رقم، نفحصه، وإذا كان زوجياً نضيفه.
- السطر 15: الطريقة الذكية: نستخدم
filter(is_even, numbers). نمرر اسم الدالة (بدون أقواس) والقائمة. - السطر 16: نطبع نوع النتيجة لنرى أنها
filter object. - السطر 19: نحول الـ Iterator إلى قائمة لرؤية النتائج.
ب) استخدام filter مع Lambda (السرعة والأناقة):
في الحياة الواقعية، نادراً ما نعرف دالة مستقلة للتصفية، بل نستخدم Lambda مباشرة. هذا يجعل الكود أكثر إيجازاً.
scores = [45, 88, 32, 95, 60, 50, 22, 75]
# تصفية الطلاب الناجحين (درجة >= 50)
passed = list(filter(lambda s: s >= 50, scores))
print(f"درجات الناجحين: {passed}")
# تصفية الطلاب المتفوقين (درجة >= 80)
excellent = list(filter(lambda s: s >= 80, scores))
print(f"درجات المتفوقين: {excellent}")
# مثال آخر: تصفية الأرقام الموجبة
numbers = [-5, 3, -2, 8, 0, -1, 10]
positives = list(filter(lambda n: n > 0, numbers))
print(f"\nالأرقام الموجبة: {positives}")
شرح الكود سطراً بسطر:
- السطر 1: قائمة درجات الطلاب.
- السطر 4: نستخدم
filterمع lambda. Lambda تستقبل كل درجة (s) وتفحص إذا كانت >= 50. إذا كان الشرط صحيحاً، تُحتفظ بالدرجة. - السطر 8: نفس الفكرة لكن مع شرط مختلف (>= 80).
- الأسطر 12-14: مثال على تصفية الأرقام الموجبة من قائمة تحتوي على أرقام سالبة وموجبة.
ج) تصفية القيم الفارغة (None Filtering):
ميزة خاصة في filter(): إذا مررت None بدلاً من دالة، ستزيل تلقائياً كل القيم "الخاطئة" (Falsy Values).
data = ["أحمد", "", "سارة", None, "خالد", 0, False, "ليلى", []]
# تمرير None كدالة يزيل كل القيم التي تعتبر False في بايثون
clean_data = list(filter(None, data))
print(f"البيانات الأصلية: {data}")
print(f"البيانات النظيفة: {clean_data}")
# ما هي القيم الـ Falsy في بايثون؟
falsy_values = [0, "", None, False, [], {}, ()]
print(f"\nالقيم الخاطئة: {falsy_values}")
print(f"بعد التصفية: {list(filter(None, falsy_values))}")
شرح الكود سطراً بسطر:
- السطر 1: قائمة تحتوي على قيم مختلطة: نصوص، نصوص فارغة، None، أرقام، قوائم فارغة.
- السطر 4: نستخدم
filter(None, data). عندما تكون الدالةNone، تفحصfilter()كل عنصر: إذا كان "صحيحاً" (Truthy) تحتفظ به، وإلا تستبعده. - السطر 10: قائمة بكل القيم الـ Falsy في بايثون.
- السطر 11: عند تصفيتها بـ
None، تُستبعد كلها لأنها كلها Falsy!
د) تصفية النصوص بشروط معقدة:
words = ["Python", "Java", "C++", "JavaScript", "Ruby", "PHP", "Go"]
# الكلمات التي تبدأ بحرف كبير
capitalized = list(filter(lambda w: w[0].isupper(), words))
print(f"تبدأ بحرف كبير: {capitalized}")
# الكلمات الطويلة (أكثر من 4 أحرف)
long_words = list(filter(lambda w: len(w) > 4, words))
print(f"الكلمات الطويلة: {long_words}")
# الكلمات التي تحتوي على حرف 'a'
with_a = list(filter(lambda w: 'a' in w.lower(), words))
print(f"تحتوي على 'a': {with_a}")
4. مقارنة شاملة: filter vs List Comprehension vs for loop
بايثون توفر عدة طرق للتصفية. أيهما تختار؟ دعنا نقارن:
| الميزة | الدالة filter() | List Comprehension | حلقة for |
|---|---|---|---|
| الذاكرة | أفضل بكثير لأنها لا تحسب العناصر إلا عند الحاجة (Lazy). | تستهلك ذاكرة أكبر لأنها تنشئ القائمة فوراً. | تستهلك ذاكرة أكبر. |
| المرونة | محدودة بالتصفية فقط. | تسمح بالتصفية والتحويل في نفس الوقت. | الأكثر مرونة - أي منطق معقد. |
| الوضوح | أوضح عندما يكون لديك دالة اختبار جاهزة. | أوضح عندما يكون الشرط بسيطاً ومكتوباً في مكانه. | أطول وأقل وضوحاً. |
| السرعة | سريعة جداً (مُحسّنة في C). | سريعة أيضاً. | الأبطأ عموماً. |
قاعدة عامة: إذا كنت تريد فقط "تصفية" البيانات دون تغييرها، فـ filter ممتازة. إذا كنت تريد تصفيتها وتغيير شكلها في نفس الوقت (مثلاً: تربيع الأرقام الزوجية فقط)، فـ List Comprehension هي الأفضل.
مثال مقارنة عملي:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 1. تصفية فقط - filter أفضل
evens_filter = list(filter(lambda n: n % 2 == 0, numbers))
# 2. تصفية فقط - List Comprehension
evens_comp = [n for n in numbers if n % 2 == 0]
# 3. تصفية + تحويل - List Comprehension أفضل
evens_squared = [n**2 for n in numbers if n % 2 == 0]
print(f"filter: {evens_filter}")
print(f"comprehension: {evens_comp}")
print(f"تصفية + تحويل: {evens_squared}")
5. تطبيق عملي متقدم: نظام إدارة المحتوى (CMS)
لنقم بتصفية قائمة من المقالات بناءً على حالتها (منشورة أم مسودة) وعدد المشاهدات. هذا مثال واقعي من تطبيقات الويب.
articles = [
{"title": "تعلم بايثون", "status": "published", "views": 1500, "author": "أحمد"},
{"title": "مستقبل الذكاء الاصطناعي", "status": "draft", "views": 0, "author": "سارة"},
{"title": "أساسيات الويب", "status": "published", "views": 800, "author": "خالد"},
{"title": "دليل المبتدئين", "status": "published", "views": 50, "author": "ليلى"},
{"title": "البرمجة الوظيفية", "status": "published", "views": 1200, "author": "أحمد"}
]
# 1. تصفية المقالات المنشورة فقط
published = list(filter(lambda a: a["status"] == "published", articles))
print(f"عدد المقالات المنشورة: {len(published)}")
# 2. تصفية المقالات الرائجة (منشورة + أكثر من 100 مشاهدة)
trending = list(filter(
lambda a: a["status"] == "published" and a["views"] > 100,
articles
))
print("\n--- المقالات الرائجة الآن ---")
for art in trending:
print(f"📰 {art['title']} | 👁 {art['views']} | ✍️ {art['author']}")
# 3. مقالات كاتب معين
ahmed_articles = list(filter(lambda a: a["author"] == "أحمد", articles))
print(f"\nعدد مقالات أحمد: {len(ahmed_articles)}")
شرح الكود سطراً بسطر:
- الأسطر 1-7: قائمة من القواميس، كل قاموس يمثل مقالة بخصائصها.
- السطر 10: نصفي المقالات المنشورة فقط بفحص
status == "published". - الأسطر 14-17: تصفية متقدمة بشرطين: المقالة يجب أن تكون منشورة AND لديها أكثر من 100 مشاهدة. نستخدم
andفي Lambda. - الأسطر 19-21: نطبع المقالات الرائجة بشكل منسق.
- السطر 24: نصفي المقالات حسب الكاتب.
6. تقنيات متقدمة: دمج filter مع map
القوة الحقيقية تظهر عند دمج filter() مع map() لمعالجة البيانات بشكل متسلسل.
prices = [10, 25, 50, 75, 100, 150, 200]
# الخطوة 1: تصفية الأسعار الأكبر من 50
expensive = filter(lambda p: p > 50, prices)
# الخطوة 2: تطبيق خصم 20% على الأسعار المصفاة
discounted = map(lambda p: p * 0.8, expensive)
# النتيجة النهائية
result = list(discounted)
print(f"الأسعار بعد التصفية والخصم: {result}")
# يمكن كتابتها في سطر واحد
result_oneliner = list(map(lambda p: p * 0.8, filter(lambda p: p > 50, prices)))
print(f"نفس النتيجة: {result_oneliner}")
7. نصائح للمحترفين
-
استخدام filter مع المجموعات الكبيرة: إذا كنت تتعامل مع ملفات ضخمة، لا تحول نتيجة
filterإلى قائمة فوراً. استخدمها في حلقةforمباشرة للحفاظ على الذاكرة.# جيد للملفات الكبيرة for item in filter(lambda x: x > 100, huge_list): process(item) # معالجة واحد تلو الآخر - الجمع بين map و filter: يمكنك تصفية البيانات أولاً باستخدام
filterثم تحويل النتائج باستخدامmap. هذا هو جوهر معالجة البيانات الوظيفية (Functional Data Processing). - تجنب التعقيد في Lambda: إذا كان شرط التصفية طويلاً جداً أو معقداً، عرّف دالة مستقلة بـ
defليكون كودك قابلاً للقراءة والصيانة. - استخدم filter(None, ...) للتنظيف السريع: طريقة سريعة لإزالة كل القيم الفارغة أو الخاطئة من قائمة.
ملخص الدرس
- دالة filter() هي الأداة المثالية لاستخراج عناصر محددة من مجموعة بيانات.
- تعتمد على دالة اختبار (Predicate) تُرجع True للقيم التي نريد الاحتفاظ بها.
- تتميز بالكفاءة العالية في استهلاك الذاكرة بفضل أسلوب التقييم الكسول (Lazy Evaluation).
- يمكن استخدامها مع Lambda للشروط البسيطة أو مع دوال معرّفة للشروط المعقدة.
- تُرجع Iterator يجب تحويله لقائمة باستخدام
list(). - يمكن دمجها مع
map()لمعالجة بيانات متقدمة. - تعتبر جزءاً أساسياً من ترسانة أي مبرمج بايثون محترف يتعامل مع البيانات.
الخطوة التالية
بعد أن تعلمنا التحويل والتصفية، حان الوقت لنتعلم كيف ندمج كل هذه العناصر في قيمة واحدة نهائية ومفيدة.
الدرس التالي: الدالة reduce (تجميع وتقليص البيانات)