متى تستخدم فهم القائمة في بايثون

يوفر فهم القوائم في بايثون طريقةً موجزةً لإنشاء القوائم من خلال تضمين حلقة ومنطق شرطي اختياري في سطر واحد. يمكنك استخدام فهم القوائم لتحويل عناصر كائن قابل للتكرار وتصفيتها بكفاءة. كما يتيح لك استبدال الحلقات المعقدة ودوال ()map بتعبيرات أسهل قراءةً وأسرع في كثير من الأحيان. بفهم فهم القوائم، يمكنك تحسين شفرتك البرمجية لتحقيق أداء ووضوح أفضل.

في هذا الدرس، ستستكشف كيفية الاستفادة من فهم القوائم لتبسيط برمجتك. ستكتسب أيضًا فهمًا للتنازلات التي تصاحب استخدامها، مما يُمكّنك من تحديد متى تُفضّل أساليب أخرى.

تحويل القوائم في بايثون

هناك عدة طرق مختلفة لإنشاء عناصر وإضافة عناصر إلى قوائم في بايثون. في هذا القسم، ستستكشف حلقات for ودالة ()map لتنفيذ هذه المهام. بعد ذلك، ستتعلم كيفية استخدام فهم القوائم، ومتى يمكن أن يفيد فهم القوائم برنامج بايثون الخاص بك.

استخدام حلقات for

أكثر أنواع الحلقات شيوعًا هو حلقة for. يمكنك استخدامها لإنشاء قائمة عناصر في ثلاث خطوات:

  • إنشاء قائمة فارغة.
  • تكرار على عنصر قابل للتكرار أو مجموعة من العناصر.
  • أضف كل عنصر إلى نهاية القائمة.

إذا كنت تريد إنشاء قائمة تحتوي على أول عشرة مربعات مثالية، فيمكنك إكمال هذه الخطوات في ثلاثة أسطر من التعليمات البرمجية:

>>> squares = []
>>> for number in range(10):
...     squares.append(number * number)
...
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

هنا، تُنشئ قائمة فارغة، squares. ثم تستخدم حلقة for للتكرار على range(10). وأخيرًا، تضرب كل رقم في نفسه وتضيف النتيجة إلى نهاية القائمة.

العمل مع كائنات map

كنهج بديل قائم على البرمجة الوظيفية، يمكنك استخدام دالة ()map. تُمرر دالة وعنصرًا قابلًا للتكرار، وستُنشئ دالة ()map كائنًا. يحتوي هذا الكائن على النتيجة التي ستحصل عليها من تشغيل كل عنصر قابل للتكرار من خلال الدالة المُدخلة.

على سبيل المثال، ضع في اعتبارك موقفًا تحتاج فيه إلى حساب السعر بعد الضريبة لقائمة المعاملات:

>>> prices = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08
>>> def get_price_with_tax(price):
...     return price * (1 + TAX_RATE)
...

>>> final_prices = map(get_price_with_tax, prices)
>>> final_prices
<map object at 0x7f34da341f90>

>>> list(final_prices)
[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]

هنا، لديك كائن قابل للتكرار، prices، ودالة ()get_price_with_tax. مرر كلا الوسيطتين إلى ()map، وخزّن كائن map الناتج في final_prices. وأخيرًا، حوّل final_prices إلى قائمة باستخدام ()list.

الاستفادة من فهم القائمة

فهم القوائم هو طريقة ثالثة لإنشاء القوائم أو تحويلها. باستخدام هذا النهج الأنيق، يمكنك إعادة كتابة حلقة for من المثال الأول بسطر واحد فقط من التعليمات البرمجية:

>>> squares = [number * number for number in range(10)]
>>> squares
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

بدلاً من إنشاء قائمة فارغة وإضافة كل عنصر إلى النهاية، يمكنك ببساطة تعريف القائمة ومحتوياتها في نفس الوقت باتباع التنسيق التالي:

new_list = [expression for member in iterable]

يتضمن كل فهم قائمة في بايثون ثلاثة عناصر:

  • التعبير هو العنصر نفسه، أو استدعاء دالة، أو أي تعبير صحيح آخر يُرجع قيمة. في المثال أعلاه، التعبير number * number هو مربع قيمة العنصر.
  • العضو هو الكائن أو القيمة في القائمة أو العنصر القابل للتكرار. في المثال أعلاه، قيمة العضو هي رقم.
  • الكائن القابل للتكرار هو قائمة، أو مجموعة، أو تسلسل، أو مُولِّد، أو أي كائن آخر يُعيد عناصره واحدًا تلو الآخر. في المثال السابق، الكائن القابل للتكرار هو range(10).

نظرًا لمرونة متطلبات التعبير، فإن فهم القائمة في بايثون يعمل بكفاءة في العديد من الأماكن التي تستخدم فيها دالة ()map. يمكنك إعادة كتابة مثال التسعير باستخدام فهم القائمة الخاص به:

>>> prices = [1.09, 23.56, 57.84, 4.56, 6.78]
>>> TAX_RATE = .08
>>> def get_price_with_tax(price):
...     return price * (1 + TAX_RATE)
...

>>> final_prices = [get_price_with_tax(price) for price in prices]
>>> final_prices
[1.1772000000000002, 25.4448, 62.467200000000005, 4.9248, 7.322400000000001]

الفرق الوحيد بين هذا التنفيذ و()map هو أن فهم القائمة في Python يعيد قائمة، وليس كائن map.

تعزيز فهم قائمة بايثون

من فوائد استخدام فهم القوائم في بايثون أنه أداة واحدة يُمكن استخدامها في مواقف مُختلفة. بالإضافة إلى إنشاء القوائم القياسية، يُمكن استخدام فهم القوائم أيضًا للتعيين والتصفية. ستجد في هذا القسم تقنيات مُتقدمة للتعامل مع فهم القوائم في بايثون.

تصفية القيم من قائمة

الطريقة الأكثر شيوعًا لإضافة المنطق الشرطي إلى فهم القائمة هي إضافة شرط إلى نهاية التعبير.

في وقت سابق، رأيت هذه الصيغة لكيفية إنشاء فهم القائمة:

new_list = [expression for member in iterable]

مع أن هذه الصيغة دقيقة، إلا أنها غير مكتملة بعض الشيء. يُضيف وصفٌ أكثر شمولاً لصيغة الفهم دعماً للشروط الاختيارية.

هنا، تأتي عبارتك الشرطية قبل قوس الإغلاق مباشرةً:

new_list = [expression for member in iterable if conditional]

تعتبر الشروط مهمة لأنها تسمح لفهم القائمة بتصفية القيم غير المرغوب فيها، وهو ما يتطلب عادةً استدعاء ()filter:

>>> sentence = "the rocket came back from mars"
>>> [char for char in sentence if char in "aeiou"]
['e', 'o', 'e', 'a', 'e', 'a', 'o', 'a']

في كتلة التعليمات البرمجية هذه، تقوم العبارة الشرطية بتصفية أي أحرف في sentence ليست حروف العلة.

يمكن للشرط اختبار أي تعبير صحيح. إذا كنت بحاجة إلى مرشح أكثر تعقيدًا، فيمكنك نقل منطق الشرط إلى دالة منفصلة:

>>> sentence = (
...     "The rocket, who was named Ted, came back "
...     "from Mars because he missed his friends."
... )
>>> def is_consonant(letter):
...     vowels = "aeiou"
...     return letter.isalpha() and letter.lower() not in vowels
...

>>> [char for char in sentence if is_consonant(char)]
['T', 'h', 'r', 'c', 'k', 't', 'w', 'h', 'w', 's', 'n', 'm', 'd',
 'T', 'd', 'c', 'm', 'b', 'c', 'k', 'f', 'r', 'm', 'M', 'r', 's', 'b',
 'c', 's', 'h', 'm', 's', 's', 'd', 'h', 's', 'f', 'r', 'n', 'd', 's']

هنا، أنشئ مرشحًا معقدًا، ()is_consonant، ومرّر هذه الدالة كعبارة شرطية لفهم القائمة. لاحظ أنك تمرر أيضًا قيمة العضو char كمعامل إلى دالتك.

يمكنك وضع الشرط في نهاية العبارة للتصفية الأساسية، ولكن ماذا لو أردت تغيير قيمة عنصر بدلاً من تصفيتها؟ في هذه الحالة، من المفيد وضع الشرط قرب بداية العبارة. يمكنك القيام بذلك بالاستفادة من العبارة الشرطية:

new_list = [true_expr if conditional else false_expr for member in iterable]

بوضع المنطق الشرطي في بداية فهم القائمة، يمكنك استخدام المنطق الشرطي للاختيار من بين عدة خيارات إخراج ممكنة. على سبيل المثال، إذا كانت لديك قائمة أسعار، فقد ترغب في استبدال الأسعار السالبة بالقيمة 0 وترك القيم الموجبة دون تغيير:

>>> original_prices = [1.25, -9.45, 10.22, 3.78, -5.92, 1.16]
>>> [price if price > 0 else 0 for price in original_prices]
[1.25, 0, 10.22, 3.78, 0, 1.16]

هنا، تعبيرك هو تعبير شرطي، price if price > 0 else 0. هذا يُخبر بايثون بإخراج قيمة  price إذا كان الرقم موجبًا، واستخدام 0 إذا كان الرقم سالبًا. إذا بدا هذا مُربكًا، فقد يكون من المفيد اعتبار المنطق الشرطي دالة مستقلة:

>>> def get_price(price):
...     return price if price > 0 else 0
...

>>> [get_price(price) for price in original_prices]
[1.25, 0, 10.22, 3.78, 0, 1.16]

الآن، تم تضمين تعبيرك الشرطي داخل ()get_price، ويمكنك استخدامه كجزء من فهم القائمة لديك.

إزالة التكرارات باستخدام فهم المجموعة والقاموس

بينما يُعد فهم القائمة في بايثون أداة شائعة، يمكنك أيضًا إنشاء فهم المجموعات والقواميس. فهم المجموعة يُشبه فهم القائمة في بايثون تقريبًا. الفرق هو أن فهم المجموعات يضمن عدم احتواء المُخرجات على أي تكرارات. يمكنك إنشاء فهم المجموعات باستخدام الأقواس المعقوفة بدلًا من الأقواس المربعة:

>>> quote = "life, uh, finds a way"
>>> {char for char in quote if char in "aeiou"}
{'a', 'e', 'u', 'i'}

يُخرِج فهم المجموعة جميع حروف العلة الفريدة التي وجدها في علامتي الاقتباس. على عكس القوائم، لا تضمن المجموعات حفظ العناصر بترتيب مُحدد. لهذا السبب، يكون الحرف a هو أول حرف علة في المجموعة، على الرغم من أن الحرف i هو أول حرف علة في علامتي الاقتباس.

فهم القاموس متشابه، مع المتطلب الإضافي المتمثل في تحديد المفتاح:

>>> {number: number * number for number in range(10)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}

لإنشاء القاموس، يمكنك استخدام الأقواس المتعرجة ({}) بالإضافة إلى زوج القيمة الأساسية (number: number * number) في تعبيرك.

تعيين القيم باستخدام عامل Walrus

قدّم بايثون 3.8 تعبير الإسناد، المعروف أيضًا باسم مُشغّل الفظ. لفهم كيفية استخدامه، انظر المثال التالي.

لنفترض أنك بحاجة إلى إرسال عشرة طلبات إلى واجهة برمجة تطبيقات (API) لإرجاع بيانات درجة الحرارة. أنت تريد فقط إرجاع نتائج أعلى من 100 درجة فهرنهايت. افترض أن كل طلب سيُرجع بيانات مختلفة. في هذه الحالة، لا يوفر تعبير الصيغة  expression for member in iterable if conditional  أي طريقة للشرط لتعيين البيانات إلى متغير يمكن للتعبير الوصول إليه.

تحتاج إلى درجة الحرارة في كلٍّ من التعبير والشرط، لذا يُمثل هذا تحديًا. يحل مُعامل الفظ (:=) هذه المشكلة، إذ يسمح لك بتشغيل تعبير مع تعيين قيمة المخرجات لمتغير في الوقت نفسه. يوضح المثال التالي كيفية تحقيق ذلك باستخدام دالة ()get_weather_data لتوليد بيانات طقس وهمية:

>>> import random
>>> def get_weather_data():
...     return random.randrange(90, 110)
...

>>> [temp for _ in range(20) if (temp := get_weather_data()) >= 100]
[107, 102, 109, 104, 107, 109, 108, 101, 104]

لاحظ أن مُعامل الفظ يجب أن يكون في الجزء الشرطي من فهمك. لن تحتاج غالبًا إلى استخدام تعبير التعيين داخل فهم القائمة في بايثون، ولكنه أداة مفيدة يمكنك استخدامها عند الحاجة.

تحديد متى لا تستخدم فهم القائمة

فهم القوائم مفيد، ويمكن أن يساعدك في كتابة شيفرة برمجية أنيقة وسهلة القراءة ولكنه ليس الخيار الأمثل في جميع الحالات. فقد يُبطئ تشغيل شيفرتك أو يستهلك ذاكرة أكبر. إذا كان أداؤك أقل أو أصعب في الفهم، فمن الأفضل اختيار بديل.

احذر من الفهم المتداخل

يمكنك دمج الفهم لإنشاء مجموعات من القوائم والقواميس والمجموعات ضمن مجموعة. على سبيل المثال، لنفترض أن مختبرًا للمناخ يتتبع درجة الحرارة العظمى في خمس مدن مختلفة خلال الأسبوع الأول من شهر يونيو. قد تكون بنية البيانات المثالية لتخزين هذه البيانات قائمة بايثون مدمجة ضمن قاموس. يمكنك إنشاء البيانات باستخدام الفهم المتداخل:

>>> cities = ["Austin", "Tacoma", "Topeka", "Sacramento", "Charlotte"]
>>> {city: [0 for _ in range(7)] for city in cities}
{
    'Austin': [0, 0, 0, 0, 0, 0, 0],
    'Tacoma': [0, 0, 0, 0, 0, 0, 0],
    'Topeka': [0, 0, 0, 0, 0, 0, 0],
    'Sacramento': [0, 0, 0, 0, 0, 0, 0],
    'Charlotte': [0, 0, 0, 0, 0, 0, 0]
}

أنشئ القاموس الخارجي باستخدام فهم القاموس. التعبير هو زوج مفتاح-قيمة يحتوي على فهم آخر. سيُنشئ هذا الكود بسرعة قائمة بيانات لكل مدينة من المدن.

القوائم المتداخلة طريقة شائعة لإنشاء المصفوفات، والتي ستستخدمها غالبًا لأغراض رياضية. ألقِ نظرة على الكود أدناه:

>>> [[number for number in range(5)] for _ in range(6)]
[
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4],
    [0, 1, 2, 3, 4]
]

إن فهم القائمة الخارجية [… for _ in range(6)] ينشئ ستة صفوف، بينما يملأ فهم القائمة الداخلية [number for number in range(5)] كل من هذه الصفوف بالقيم.

حتى الآن، الغرض من كل فهم متداخل بديهي إلى حد كبير. ومع ذلك، هناك حالات أخرى، مثل تسطيح القوائم، حيث يُقال إن المنطق يجعل شيفرتك أكثر إرباكًا. خذ هذا المثال، الذي يستخدم فهم قائمة متداخلة لتسطيح مصفوفة:

matrix = [
...     [0, 0, 0],
...     [1, 1, 1],
...     [2, 2, 2],
... ]

>>> [number for row in matrix for number in row]
[0, 0, 0, 1, 1, 1, 2, 2, 2]

كود تسطيح المصفوفة مُختصر، لكن قد لا يكون من السهل فهم آلية عمله. من ناحية أخرى، إذا استخدمت حلقات for لتسطيح المصفوفة نفسها، فسيكون فهم كودك أسهل بكثير:

>>> matrix = [
...     [0, 0, 0],
...     [1, 1, 1],
...     [2, 2, 2],
... ]
>>> flat = []
>>> for row in matrix:
...     for number in row:
...         flat.append(number)
...
>>> flat
[0, 0, 0, 1, 1, 1, 2, 2, 2]

الآن يمكنك أن ترى أن الكود يتنقل عبر صف واحد من المصفوفة في كل مرة، ويسحب جميع العناصر الموجودة في هذا الصف قبل الانتقال إلى الصف التالي.

اختر المولدات لمجموعات البيانات الكبيرة

يعمل فهم القائمة في بايثون عن طريق تحميل قائمة الإخراج كاملةً في الذاكرة. يُعد هذا مناسبًا بشكل عام للقوائم الصغيرة أو حتى المتوسطة. إذا كنت ترغب في جمع مربعات أول ألف عدد صحيح، فسيحل فهم القائمة هذه المشكلة بشكل رائع:

>>> sum([number * number for number in range(1000)])
332833500

لكن ماذا لو أردتَ جمع مربعات أول مليار عدد صحيح؟ إذا جرّبتَ ذلك على جهازك، فقد يتوقف عن الاستجابة. ذلك لأن بايثون يحاول إنشاء قائمة بمليار عدد صحيح، مما يستهلك ذاكرة أكبر مما يحتاجه جهازك. إذا حاولتَ القيام بذلك على أي حال، فقد يتباطأ جهازك أو حتى يتعطل.

عندما يُصبح حجم القائمة مُشكلةً، غالبًا ما يكون من المُفيد استخدام مُولّد بدلاً من فهم القائمة في بايثون. لا يُنشئ المُولّد بنية بيانات واحدة كبيرة في الذاكرة، بل يُعيد كائنًا قابلًا للتكرار. يُمكن لشفرتك البرمجية طلب القيمة التالية من الكائن القابل للتكرار عدة مرات حسب الحاجة أو حتى الوصول إلى نهاية التسلسل، مع تخزين قيمة واحدة فقط في كل مرة.

إذا جمعتَ أول مليار مربع باستخدام مُولِّد، فمن المُرجَّح أن يعمل برنامجك لفترة، ولكن من غير المُفترض أن يُؤدِّي ذلك إلى تجمُّد جهاز الكمبيوتر. في المثال أدناه، ستستخدم مُولِّدًا:

>>> sum(number * number for number in range(1_000_000_000))
333333332833333333500000000

يمكنك معرفة أن هذا مُولِّد لأن التعبير ليس بين قوسين أو أقواس متعرجة. اختياريًا، يمكن وضع المُولِّدات بين قوسين.

لا يزال المثال أعلاه يتطلب الكثير من العمل، ولكنه يُجري العمليات ببطء. بسبب التقييم البطيء، لا يحسب الكود القيم إلا عند طلبها صراحةً. بعد أن يُنتج المولد قيمة، يُمكنه إضافتها إلى المجموع التشغيلي، ثم تجاهلها وتوليد القيمة التالية. عندما تطلب دالة ()sum القيمة التالية، تبدأ الدورة من جديد. تُبقي هذه العملية مساحة الذاكرة صغيرة.

تعمل دالة ()map أيضًا بشكل كسول، مما يعني أن الذاكرة لن تكون مشكلة إذا اخترت استخدامها في هذه الحالة:

>>> sum(map(lambda number: number * number, range(1_000_000_000)))
333333332833333333500000000

يعتمد الأمر عليك فيما إذا كنت تفضل تعبير المولد أو ()map.

ملف تعريف لتحسين الأداء

إذًا، أيُّ نهجٍ أسرع؟ هل ينبغي استخدام فهم القوائم أم أحد بدائلها؟ بدلًا من الالتزام بقاعدة واحدة تنطبق على جميع الحالات، من الأفضل أن تسأل نفسك ما إذا كان الأداء مهمًا في حالتك الخاصة أم لا. إذا لم يكن كذلك، فمن الأفضل عادةً اختيار النهج الذي يُؤدي إلى أكواد أكثر وضوحًا!

إذا كنتَ في وضعٍ يكون فيه الأداء مهمًا، فمن الأفضل عادةً تحديد أنماط مختلفة والاستماع إلى البيانات. مكتبة timeit مفيدةٌ لتوقيت مدة تشغيل أجزاء من الشيفرة البرمجية. يمكنك استخدام timeit لمقارنة وقت تشغيل دالة ()map، وحلقات for، وفهم القوائم:

>>> import random
>>> import timeit
>>> TAX_RATE = .08
>>> PRICES = [random.randrange(100) for _ in range(100_000)]
>>> def get_price(price):
...     return price * (1 + TAX_RATE)
...
>>> def get_prices_with_map():
...     return list(map(get_price, PRICES))
...
>>> def get_prices_with_comprehension():
...     return [get_price(price) for price in PRICES]
...
>>> def get_prices_with_loop():
...     prices = []
...     for price in PRICES:
...         prices.append(get_price(price))
...     return prices
...

>>> timeit.timeit(get_prices_with_map, number=100)
2.0554370979998566

>>> timeit.timeit(get_prices_with_comprehension, number=100)
2.3982384680002724

>>> timeit.timeit(get_prices_with_loop, number=100)
3.0531821520007725

هنا، تُعرّف ثلاث طرق، كلٌّ منها يستخدم أسلوبًا مختلفًا لإنشاء قائمة. ثم تُخبر timeit بتشغيل كلٍّ من هذه الدوال ١٠٠ مرة، ويُرجع timeit الوقت الإجمالي الذي استغرقه تشغيل هذه العمليات المئة.

كما يوضح الكود الخاص بك، يكمن الفرق الأكبر بين النهج القائم على الحلقة و()map، حيث يستغرق تنفيذ الحلقة وقتًا أطول بنسبة 50%. يعتمد مدى أهمية هذا على احتياجات تطبيقك.

في هذا البرنامج التعليمي، تعلمت كيفية استخدام فهم القائمة في Python لإنجاز مهام معقدة دون جعل الكود الخاص بك معقدًا للغاية.

عند اختيار طريقة إنشاء قائمة، جرّب عدة تطبيقات، وفكّر في الأنسب للقراءة والفهم في حالتك الخاصة. إذا كان الأداء مهمًا، فيمكنك استخدام أدوات تحليل البيانات لتزويدك ببيانات عملية بدلًا من الاعتماد على الحدس أو التخمينات حول الطريقة الأنسب.

تذكر أنه على الرغم من أن فهم قوائم بايثون يحظى باهتمام كبير، إلا أن حدسك وقدرتك على استخدام البيانات عند الحاجة سيساعدانك على كتابة شيفرة برمجية واضحة تُلبي المهمة المطروحة. هذا، في النهاية، هو مفتاح جعل شيفرتك متوافقة مع بايثون!


اكتشاف المزيد من بايثون العربي

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading