حتى المطورون ذوو الخبرة يرتكبون أخطاء في Python تبدو بسيطة لكنها تكسر الإنتاجية وتجمد السيرفرات. إليك أخطر الأخطاء التي رأيتها في الشركات الكبرى، وكيف تتجنبها قبل أن تكلفك وظيفتك.
في أحد أيام الجمعة، تلقيت مكالمة طوارئ من فريق backend في شركة ناشئة تقدر قيمتها بمليارات الدولارات. السيرفرات كلها متوقفة، الـ CPU عند ١٠٠٪، والـ logs تظهر نفس الخطأ الغريب: «MemoryError» يظهر فجأة بعد ساعات من التشغيل السليم. بعد ست ساعات من الـ debugging، اكتشفنا أن المطور أضاف سطراً واحداً بسيطاً داخل حلقة تكرارية: `data.append(item)` بدلاً من `data.extend(item)`. الفرق بين الكلمتين؟ الأولى تضيف مرجعاً للقائمة، والثانية تضيف العناصر نفسها. النتيجة؟ قائمة داخل قائمة داخل قائمة، تنمو بشكل أسي حتى تأكل كل الـ RAM المتاحة. هذا ليس خطأ مبتدئين — هذا خطأ يرتكبه محترفون تحت ضغط المواعيد النهائية.
Python لغة جميلة ومرنة، وهذا بالضبط ما يجعلها خطيرة. المرونة تأتي بثمن: أخطاء لا تظهر إلا في الإنتاج، سلوكيات غير متوقعة تحت الضغط، وعمليات حسابية تبدو صحيحة لكنها تنتج نتائج خاطئة تماماً. في هذا المقال، سأفكك أخطر الأخطاء التي رأيتها في بيئات عمل حقيقية — من شركات fintech إلى منصات SaaS — وأشرح لك ليس فقط كيف تتجنبها، بل لماذا تحدث أصلاً خلف الكواليس في الذاكرة والمعالج.
الكود التالي يبدو بريئاً جداً، أليس كذلك؟ دالة تأخذ قائمة وتضيف إليها عنصراً جديداً. لكن عندما تستدعيها مرتين، يحدث شيء غريب: القائمة تحتفظ بالعناصر من الاستدعاء السابق. لماذا؟ لأن Python تقوم بتقييم الـ default arguments مرة واحدة فقط عند تعريف الدالة، وليس عند كل استدعاء. هذا يعني أن القائمة `items` هي نفس الكائن في الذاكرة لكل استدعاء للدالة — أي تعديل عليها سيؤثر على كل الاستدعاءات المستقبلية.
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — المفاجأة هنا!
print(add_item(3, [])) # [3] — هذا ما توقعتَه أصلاًالحل؟ استخدم `None` كقيمة افتراضية، ثم تحقق داخل الدالة. هكذا تضمن أن القائمة تُنشأ من جديد عند كل استدعاء. هذا الخطأ شائع جداً لدرجة أن فريق Python نفسه ناقش إزالته في الإصدارات الجديدة، لكنهم قرروا الاحتفاظ به لأسباب التوافقية. لكن في بيئات الإنتاج، هذا الخطأ يسبب تسريبات ذاكرة صامتة قد لا تكتشفها إلا بعد أيام من التشغيل. رأيته في شركة تداول عملات رقمية تسبب في تجميد الـ API لمدة ٤ ساعات لأن الدالة كانت تُستخدم لتخزين الـ session tokens — وكل مستخدم جديد كان يرث الـ tokens من المستخدمين السابقين.
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsالدرس هنا ليس فقط في كتابة الكود الصحيح، بل في فهم كيف تدير Python الذاكرة. الـ default arguments تُخزن في سمة `__defaults__` الخاصة بالدالة، ويمكنك فحصها في أي وقت باستخدام `add_item.__defaults__`. جرب هذا في الـ REPL وشاهد كيف تتغير القيم مع كل استدعاء — ستفهم لماذا هذا السلوك ليس مجرد «feature غريب»، بل تصميم متعمد له استخدامات مفيدة في حالات معينة مثل الـ memoization.
في عام ٢٠٢١، قررت شركة ناشئة في مجال تحليل البيانات ترحيل نظامها من Python إلى Go لأن «Python بطيء في الـ multi-threading». بعد شهرين من العمل، اكتشفوا أن المشكلة لم تكن في Python نفسها، بل في فهمهم الخاطئ للـ GIL. الـ GIL هو قفل يمنع أكثر من thread من تنفيذ كود Python في نفس الوقت — حتى على معالجات متعددة الأنوية. هذا يعني أن الـ threads في Python لا توفر توازي حقيقي في العمليات الـ CPU-bound، بل فقط في العمليات الـ I/O-bound مثل قراءة الملفات أو طلبات الشبكة.
المشكلة الأكبر أن الكثير من المطورين لا يدركون الفرق بين التوازي (parallelism) والتزامن (concurrency). الـ threads جيدة للتزامن، لكنها لا تجعل برنامجك أسرع في العمليات الحسابية. انظر إلى هذا المثال:
import threading
import time
def count_up(n):
for i in range(n):
pass
start = time.time()
thread1 = threading.Thread(target=count_up, args=(10**7,))
thread2 = threading.Thread(target=count_up, args=(10**7,))
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f"Time with threads: {time.time() - start:.2f}s")
# قارن مع التنفيذ التسلسلي
start = time.time()
count_up(10**7)
count_up(10**7)
print(f"Time without threads: {time.time() - start:.2f}s")في معظم الحالات، ستجد أن التنفيذ التسلسلي أسرع من التنفيذ بـ threads بسبب الـ GIL. لكن جرب استبدال `count_up` بدالة تقوم بطلب HTTP، وستجد أن الـ threads توفر تحسناً كبيراً. هذا هو المفتاح: استخدم الـ threads للعمليات التي تنتظر الـ I/O، واستخدم الـ multiprocessing للعمليات التي تحتاج قوة المعالج. في شركة fintech عملت معها، كان فريق الـ backend يستخدم الـ threads لمعالجة حسابات العملاء، ثم تفاجأوا بأن النظام يتباطأ عند معالجة ١٠٠٠ حساب في نفس الوقت. الحل؟ استبدلوا الـ threads بـ `concurrent.futures.ProcessPoolExecutor`، وحصلوا على تسريع بمقدار ٤ مرات لأن كل process له GIL خاص به.
الكود التالي يبدو منطقياً: تريد نسخ قائمة وتعديل النسخة دون التأثير على الأصلية. لكن عندما تغير عنصراً داخلياً في القائمة، تجد أن الأصلية تغيرت أيضاً. لماذا؟ لأن `copy()` تقوم بعمل shallow copy فقط — أي أنها تنسخ المراجع إلى الكائنات الداخلية، وليس الكائنات نفسها. هذا يعني أنك إذا عدلت كائناً داخلياً، فالتغيير سيظهر في كل النسخ التي تشير إلى نفس الكائن في الذاكرة.
original = [[1, 2], [3, 4]]
copied = original.copy()
copied[0][0] = 99
print(original) # [[99, 2], [3, 4]] — المفاجأة!
print(copied) # [[99, 2], [3, 4]]الحل؟ استخدم `deepcopy` من وحدة `copy` إذا كنت تريد نسخة كاملة ومستقلة. لكن احذر: الـ deep copy بطيئة وقد تستهلك ذاكرة كبيرة إذا كانت البيانات معقدة. في مشروع كبير عملت عليه، كان فريق الـ data science يستخدم `copy()` لنسخ الـ dataframes قبل المعالجة، ثم تفاجأوا بأن النتائج تتغير بشكل عشوائي. السبب؟ الـ dataframes تحتوي على مراجع إلى نفس البيانات في الذاكرة، وأي تعديل على نسخة واحدة يؤثر على الأخرى. الحل كان استخدام `df.copy(deep=True)`، لكن هذا زاد وقت المعالجة من ٣٠ ثانية إلى ٣ دقائق بسبب حجم البيانات الكبير. في النهاية، قرروا إعادة هيكلة الكود لتجنب النسخ تماماً واستخدام الـ views بدلاً من الـ copies حيثما أمكن.
الدرس هنا ليس فقط في استخدام `deepcopy`، بل في فهم كيفية إدارة Python للذاكرة. عندما تقوم بعمل `a = b`، فأنت لا تنشئ كائناً جديداً، بل فقط مرجعاً جديداً لنفس الكائن. يمكنك التحقق من ذلك باستخدام `a is b`، الذي سيرجع `True` إذا كانا يشيران إلى نفس الكائن في الذاكرة. هذا السلوك فعال جداً من حيث الذاكرة، لكنه قد يسبب أخطاء صعبة الاكتشاف إذا لم تكن مدركاً له.
الـ async/await في Python هو سلاح ذو حدين. من ناحية، يسمح لك بكتابة كود غير متزامن بطريقة تبدو تسلسلية وسهلة القراءة. من ناحية أخرى، إذا استخدمت دالة blocking داخل سياق غير متزامن، فستجمد الـ event loop بأكمله، مما يجعل كل الـ coroutines الأخرى تنتظر. المشكلة أن الكثير من المكتبات لا تدعم الـ async، والمطورون ينسون هذا التفصيل الحيوي.
import asyncio
import time
async def blocking_task():
print("Blocking task started")
time.sleep(2) # هذه دالة blocking!
print("Blocking task finished")
async def main():
await asyncio.gather(blocking_task(), blocking_task())
asyncio.run(main())الكود السابق سيستغرق ٤ ثوانٍ لإكماله، وليس ثانيتين كما قد تتوقع. لماذا؟ لأن `time.sleep()` هي دالة blocking، وهي تجمد الـ event loop بأكمله. الحل؟ استخدم `asyncio.sleep()` بدلاً منها. لكن المشكلة الأكبر هي عندما تستخدم مكتبات خارجية لا تدعم الـ async. مثلاً، إذا استخدمت `requests.get()` داخل دالة غير متزامنة، فستجمد الـ event loop. في شركة SaaS عملت معها، كان فريق الـ backend يستخدم `requests` داخل دوال `async`، ثم تفاجأوا بأن الـ API يتجمد عند معالجة أكثر من ١٠ طلبات في نفس الوقت. الحل كان استبدال `requests` بـ `aiohttp`، لكن هذا تطلب إعادة كتابة جزء كبير من الكود.
الدرس هنا هو أن الـ async/await ليس سحراً — إنه مجرد طريقة لتنظيم الـ coroutines. إذا استخدمت دالة blocking، فأنت تخبر Python: «توقف كل شيء وانتظر هذه الدالة». هذا ليس خطأ في Python، بل خطأ في فهم المطور لكيفية عمل الـ event loop. يمكنك تجنب هذه المشكلة باستخدام أدوات مثل `anyio` أو `trio` التي توفر واجهات موحدة للـ async، أو باستخدام مكتبات تدعم الـ async بشكل أصلي مثل `httpx` بدلاً من `requests`.
Python تسمح لك بتخصيص سلوك الكائنات باستخدام الـ magic methods مثل `__add__` و`__eq__`. هذه ميزة قوية، لكنها قد تصبح كابوساً إذا أسأت استخدامها. المثال الكلاسيكي هو عندما تكتب كوداً مثل `a + b` ويتوقع المطور أن يجمع بين قائمتين، لكن بدلاً من ذلك، يقوم الكود بشيء غير متوقع تماماً — مثل دمج البيانات أو إجراء عملية حسابية معقدة. المشكلة أن الـ magic methods لا تظهر في توقيع الدالة، مما يجعل الكود صعب الفهم والصيانة.
class Data:
def __init__(self, value):
self.value = value
def __add__(self, other):
return Data(self.value * other.value) # عملية ضرب بدلاً من الجمع!
a = Data(3)
b = Data(4)
result = a + b # ما المتوقع؟ 7، لكن النتيجة هي 12في شركة تطوير ألعاب، استخدم فريق الـ backend الـ magic methods لتنفيذ نظام معادلات معقدة داخل الكلاس. المشكلة أن المطورين الجدد لم يفهموا لماذا `player1 + player2` لا يقوم بجمع الـ scores، بل يحسب احتمالية الفوز. بعد أسابيع من الـ debugging، قرروا إزالة كل الـ magic methods واستبدالها بدوال عادية مثل `player1.merge(player2)`. الدرس؟ الـ magic methods قوية، لكنها تجعل الكود غامضاً. استخدمها فقط عندما يكون السلوك واضحاً وبديهياً — مثل تنفيذ `__str__` لعرض الكائن بشكل مقروء، أو `__eq__` لمقارنة الكائنات. إذا كنت بحاجة إلى سلوك معقد، فاكتب دالة عادية بدلاً من الاعتماد على الـ operator overloading.
الـ magic methods مفيدة أيضاً في تحسين أداء الكود. مثلاً، يمكنك استخدام `__slots__` لتقليل استخدام الذاكرة في الكائنات التي تحتوي على الكثير من السمات. لكن حتى هذه الميزة قد تسبب مشاكل إذا لم تفهم كيف تعمل. `__slots__` تمنع إنشاء `__dict__` للكائن، مما يوفر ذاكرة لكنه يمنع إضافة سمات جديدة ديناميكياً. في مشروع كبير، استخدم فريق الـ data engineering `__slots__` لتحسين أداء الـ data pipelines، ثم تفاجأوا بأن بعض المكتبات الخارجية تفشل لأن تلك المكتبات تعتمد على إضافة سمات ديناميكياً للكائنات. الحل كان إزالة `__slots__` من بعض الكلاسات والاعتماد على تحسينات أخرى مثل الـ `__weakref__`.
في عام ٢٠١٩، قررت شركة ناشئة إعادة كتابة نظام الـ payments الخاص بها من Python إلى TypeScript لأن «الكود أصبح غير قابل للصيانة». بعد عام من العمل، اكتشفوا أن المشكلة لم تكن في Python، بل في عدم استخدام الـ type hints. الـ type hints في Python ليست إلزامية، لكنها تحول الكود من مجموعة من الدوال الغامضة إلى وثيقة حية تشرح نفسها بنفسها. بدونها، يصبح من الصعب جداً فهم ما تتوقع الدالة كمدخلات وما ترجعه كخرج، خاصة في المشاريع الكبيرة التي يعمل عليها أكثر من مطور.
# بدون type hints
def process_data(data, threshold):
results = []
for item in data:
if item > threshold:
results.append(item * 2)
return results
# مع type hints
def process_data(data: list[float], threshold: float) -> list[float]:
results: list[float] = []
for item in data:
if item > threshold:
results.append(item * 2)
return resultsالـ type hints لا تجعل الكود أسرع، لكنها تجعل عملية الـ debugging أسهل بكثير. في شركة fintech، كان فريق الـ backend يواجه مشكلة غريبة: بعض الـ transactions تفشل بدون سبب واضح. بعد أيام من الـ debugging، اكتشفوا أن الدالة المسؤولة عن معالجة الـ transactions كانت تتوقع `decimal.Decimal` لكن كانت تستقبل `float` في بعض الحالات، مما يسبب أخطاء في التقريب. لو استخدموا الـ type hints، لكان الـ IDE أو الـ type checker مثل `mypy` قد حذرهم من المشكلة قبل أن تصل إلى الإنتاج. اليوم، معظم الشركات الكبيرة التي تستخدم Python تفرض استخدام الـ type hints في الكود الجديد، وبعضها يستخدم أدوات مثل `pydantic` للتحقق من صحة البيانات في وقت التشغيل.
الـ type hints مفيدة أيضاً في تحسين تجربة المطور. عندما تكتب `def get_user(id: int) -> User | None:`، فأنت تخبر المطورين الآخرين: هذه الدالة تتوقع عدداً صحيحاً وترجع إما كائن `User` أو `None`. هذا يجعل الكود أكثر قابلية للقراءة والصيانة. لكن احذر: الـ type hints ليست حلاً سحرياً. إذا كتبت `def process(data: Any) -> Any:`، فأنت لم تضف أي قيمة. الهدف هو استخدام الـ type hints لتوضيح النوايا، وليس فقط لإرضاء الـ linter. في مشروع كبير عملت عليه، كان فريق الـ backend يستخدم `Any` في كل مكان، ثم تفاجأوا بأن الـ type hints لم تساعدهم في اكتشاف الأخطاء. الحل كان استبدال `Any` بأنواع محددة مثل `dict[str, list[int]]` أو استخدام الـ generics مثل `TypeVar` و`Generic` لكتابة كود مرن وآمن في نفس الوقت.
بعد عشر سنوات من كتابة Python في بيئات عمل حقيقية، تعلمت درساً واحداً مهماً: اللغة نفسها ليست المشكلة، بل فهمك لكيفية عملها خلف الكواليس هو ما يصنع الفرق. الأخطاء التي ناقشناها ليست مجرد «gotchas» — إنها مؤشرات على فجوات في فهمك للأساسيات. عندما تفهم كيف تدير Python الذاكرة، وكيف يعمل الـ GIL، وكيف تتفاعل الكائنات مع بعضها البعض، ستكتب كوداً لا يكسر الإنتاجية ولا يجمد السيرفرات.
نصيحة عملية واحدة أخيرة: استخدم الأدوات التي تجبرك على كتابة كود أفضل. قم بتشغيل `mypy` في الـ CI pipeline، واستخدم `pylint` مع قواعد صارمة، واكتب اختبارات وحدة تغطي ليس فقط الكود السعيد، بل أيضاً الحالات الحدية والأخطاء المتوقعة. في شركة ناشئة عملت معها، أضفنا `mypy` إلى الـ CI pipeline واكتشفنا ٤٧ خطأ في الـ type hints في أول تشغيل — أخطاء كانت ستكلفنا ساعات من الـ debugging لو وصلت إلى الإنتاج. اليوم، لا نكتب سطر كود جديد بدون الـ type hints والاختبارات، وهذا وفر علينا مئات الساعات من العمل.
Python لغة قوية ومرنة، لكنها تتطلب منك أن تكون متيقظاً. لا تعتمد على «العمل بشكل صحيح» فقط — افهم لماذا يعمل الكود بالطريقة التي يعمل بها. عندما تفعل ذلك، ستكتب كوداً لا يعمل فقط، بل يعمل بشكل جيد تحت الضغط، وسهل الصيانة، وقابل للتوسع. وهذا هو الفرق بين المطور الجيد والمطور المحترف.