المستوى: مبتدئ — هذا المقال مكتوب لمن يعرف أساسيات بايثون (المتغيرات والحلقات) ولا يحتاج خبرة سابقة. كل مفهوم مشروح بمثال أولًا ثم بشكل علمي دقيق.
ليه بناء نص حرف-حرف ممكن يبطّئ كودك 42 مرة؟
لو بتبني نص كبير جوّه حلقة وبتضيف عليه حرف أو سطر في كل لفة، انت غالبًا بتنسخ النص كله من الأول في كل خطوة من غير ما تحس. في تجربة قِستها بنفسي على بايثون، بناء نص فيه 100 ألف حرف بالطريقة دي أخد 122 مللي ثانية، والطريقة الصح أخدت 2.9 مللي ثانية بس — أسرع حوالي 42 مرة. السبب مفهوم واحد اسمه عدم قابلية النص للتغيير (Immutability).
المشكلة باختصار
في بايثون، النص (str) كائن غير قابل للتعديل. يعني لما تكتب s = s + "x"، بايثون مش بيضيف حرف على النص القديم. هو بيعمل نص جديد تمامًا فيه المحتوى القديم + الحرف الجديد، وبيرمي القديم. لو ده حصل مرة، مش مشكلة. لو حصل 100 ألف مرة جوّه حلقة، انت بتعمل 100 ألف نسخة، وكل نسخة أكبر من اللي قبلها.
المفهوم بمثال بسيط الأول
تخيّل إنك بتكتب في دفتر بقلم حبر مايتمسحش. عايز تزوّد كلمة في سطر مكتوب؟ مفيش "تعديل". لازم تجيب صفحة جديدة وتنسخ السطر كله من الأول وتضيف الكلمة في الآخر. لو السطر كلمتين، النسخ سريع. لكن لو بقى صفحة كاملة، كل "تعديل" بسيط بيكلّفك إعادة نسخ الصفحة بالكامل.
النص في بايثون زي الدفتر بالحبر بالظبط. كل "تعديل" = صفحة جديدة + نسخ كامل. ده اللي بيخلي الشغل يتراكم.
المفهوم بشكل علمي ودقيق
أي كائن في بايثون له عنوان في الذاكرة تقدر تشوفه بدالة id(). جرّب الكود ده:
s = "hi"
print(id(s)) # مثال: 140278... (عنوان النص الأصلي)
s = s + "!" # ظاهريًا "تعديل"، لكنه إنشاء كائن جديد
print(id(s)) # عنوان مختلف تمامًا — ده نص تاني خالص
العنوان بيتغير لأن s + "!" أنتج كائنًا جديدًا. النص القديم "hi" فضل زي ما هو في مكانه لحد ما يتنضّف. ده معنى Immutability علميًا: حالة الكائن مابتتغيرش بعد إنشائه؛ أي عملية "تعديل" بترجّع كائنًا جديدًا.
دلوقتي طبّق ده على حلقة. لو بنيت نص طوله n بإضافة حرف في كل خطوة، الخطوة رقم k بتنسخ نص طوله k. مجموع النسخ = 1 + 2 + 3 + ... + n، واللي بيساوي تقريبًا n²/2. ده اللي بنسمّيه التعقيد التربيعي O(n²): ضِعف حجم البيانات يخلي الزمن يتضاعف أربع مرات، مش مرتين.
الفخ والحل بكود شغّال
ده الفخ (بناء النص من بدايته في كل لفة، وهو أوضح حالة O(n²)):
# الفخ: O(n^2) — كل لفة تنسخ النص كله
s = ""
for i in range(100_000):
s = "x" + s
والحل: اجمع القطع في قائمة (list) — وهي قابلة للتعديل بكفاءة — وبعدين ادمجها مرة واحدة بـ "".join():
# الحل: O(n) — تجميع رخيص ثم دمج واحد
parts = []
for i in range(100_000):
parts.append("x")
s = "".join(parts)
الفرق المقيس على نفس الجهاز (CPython 3) لبناء 100 ألف حرف:
الطريقة الأولى أخدت 122.1 مللي ثانية، والـ join أخدت 2.9 مللي ثانية. الفرق بيكبر مع كبر n لأن المنحنى تربيعي.
سيناريو واقعي
تخيّل خدمة بتبني تقرير CSV فيه 200 ألف سطر، وبتجمّع كل سطر في نص واحد بـ report += line داخل حلقة، ثم نمط البناء عندك بيمنع تحسين بايثون الداخلي (مثلًا بتدمج في البداية، أو النص ليه أكتر من مرجع). الطلب اللي المفروض يخلص في أجزاء من الثانية ممكن يقعد ثواني ويرفع زمن الاستجابة (latency) للمستخدم. التحويل لـ "".join(lines) بيحوّل المنحنى من تربيعي لخطي ويرجّع الزمن لمكانه.
الـ trade-off اللي لازم تعرفه
هنا نقطة مهمة وصريحة. CPython (أشهر نسخة من بايثون) فيه تحسين خاص: لو عملت s += "x" (إضافة في الآخر) وs ليه مرجع واحد بس، بيقدر يوسّع النص في مكانه بدل النسخ الكامل. في قياسي، النمط ده أخد 4.3 مللي ثانية مقابل 3.4 للـ join — يعني سريع.
بتكسب: كود أبسط أحيانًا. بتخسر: ضمانة الأداء. التحسين ده مش جزء من لغة بايثون نفسها، وبيقع في الحالات دي:
- تنفيذات تانية زي PyPy أو Jython مش بالضرورة بتعمله.
- لو النص ليه أكتر من مرجع (متغير تاني بيشاور عليه)، بيرجع نسخ كامل.
- لو بتبني من البداية (
s = "x" + s) أو بتعملreplaceمتكرر، التحسين مابيشتغلش أصلًا.
عشان كده القاعدة الآمنة: لما تبني نص من قطع كتيرة، استخدم join. الافتراض هنا إن عدد القطع كبير (آلاف فأكتر). في لغات تانية الموضوع أصرح: في Java استخدم StringBuilder، وفي JavaScript اجمع في مصفوفة ثم arr.join("").
متى لا تستخدم هذه الطريقة
لو بتجمّع كام قطعة بس (أقل من بضع آلاف)، أو بتركّب نص ثابت معروف، متعقّدش الكود بـ list و join. الفرق وقتها مهمل، والوضوح أهم. واستخدم f-string للدمج البسيط: f"{name}: {value}" أوضح وأسرع من تجميع يدوي.
الخطوة التالية
افتح أكبر حلقة في مشروعك بتبني نص، ودوّر على += أو + على نص جوّه الحلقة. حوّلها لتجميع في قائمة ثم "".join(parts) في الآخر، وقِس الفرق بـ time.perf_counter() قبل وبعد. لو شغّال على بيانات كبيرة، الفرق هيبان فورًا.
المصادر
- توثيق بايثون الرسمي — أنواع التسلسل النصية و str (النص غير قابل للتغيير): docs.python.org/3/library/stdtypes.html
- الأسئلة الشائعة في توثيق بايثون — أكفأ طريقة لدمج نصوص كثيرة (التوصية بـ join): docs.python.org/3/faq/programming.html
- توثيق بايثون — التابع str.join: docs.python.org/3/library/stdtypes.html#str.join
- ويكي بايثون — التعقيد الزمني للعمليات (TimeComplexity): wiki.python.org/moin/TimeComplexity
- توثيق Oracle لـ Java — StringBuilder للبناء الكفء للنصوص: docs.oracle.com/.../StringBuilder.html