إذا كنتَ كمعظم مستخدمي بايثون، فربما بدأتَ رحلتكَ مع بايثون بتعلم دالة ()print. ساعدتكَ هذه الدالة في كتابة جملة “أهلًا بالعالم!” الخاصة بك، وأضفت الحيوية على شيفرتك البرمجية. علاوةً على ذلك، يمكنكَ استخدامها لتنسيق الرسائل، بل وحتى اكتشاف بعض الأخطاء البرمجية. ولكن إذا كنتَ تعتقد أن هذا كل ما تحتاج معرفته عن دالة ()print في بايثون، فأنتَ تفوّت الكثير!
استمر بالقراءة للاستفادة القصوى من هذه الدالة الصغيرة التي تبدو مملة وغير مُقدّرة. سيساعدك هذا الدرس على استخدام دالة ()print في بايثون بفعالية. مع ذلك، استعد للتعمق أكثر أثناء قراءتك للأقسام. قد تُفاجأ بكمية ما تقدمه دالة ()print!
إذا كنتَ مبتدئًا في بايثون، فستستفيد كثيرًا من قراءة الجزء الأول من هذا الدرس، الذي يوضح أساسيات الطباعة في بايثون. أما إذا لم تكن كذلك، فلا تتردد في الانتقال إلى الأقسام التي تهمك.
أجرِ الاختبار: اختبر معلوماتك مع اختبارنا التفاعلي “دالة الطباعة في بايثون” (()print). ستحصل على درجة عند إكماله لمساعدتك على تتبع تقدمك في التعلم.
الطباعة في جوزة
حان الوقت للبدء بإلقاء نظرة على بعض الأمثلة العملية للطباعة في بايثون. بنهاية هذا القسم، ستعرف كل الطرق الممكنة لاستدعاء دالة ()print.
إنتاج خطوط فارغة
أبسط مثال لاستخدام ()print يتطلب بضع ضغطات مفاتيح فقط:
print()
يؤدي هذا إلى ظهور سطر جديد غير مرئي، مما يؤدي بدوره إلى ظهور سطر فارغ على الشاشة. لإضافة مسافة رأسية، يمكنك استدعاء دالة ()print عدة مرات متتالية كما يلي:
print()
print()
print()
إنه تمامًا كما لو كنت تضغط على مفتاح الإدخال على لوحة المفاتيح في برنامج معالجة النصوص أو محرر النصوص.
مع أنك لا تُمرّر أي وسيطات إلى دالة ()print، إلا أنك لا تزال بحاجة إلى وضع أقواس فارغة في نهاية السطر لإعلام بايثون بتنفيذ تلك الدالة فعليًا بدلًا من مجرد الإشارة إليها باسمها. بدون أقواس، ستحصل على مرجع لكائن الدالة الأساسي:
>>> print()
>>> print
<built-in function print>
يعمل مقتطف الكود أعلاه داخل REPL في بايثون، كما هو موضح بالموجه (>>>). ولأن REPL تُنفذ كل سطر من شيفرة بايثون فورًا، فسترى سطرًا فارغًا بعد استدعاء دالة ()print. من ناحية أخرى، عند تخطي الأقواس الأخيرة، سترى تمثيلًا نصيًا لدالة ()print نفسها.
كما رأيتَ للتو، يؤدي استدعاء دالة ()print بدون مُعاملات إلى سطر فارغ، وهو سطر يحتوي فقط على حرف السطر الجديد. لا تخلط بين هذا وبين سلسلة نصية فارغة، لا تحتوي على أي أحرف على الإطلاق، ولا حتى حرف السطر الجديد!
يمكنك استخدام سلسلة الأحرف الخاصة بـ Python لتصور هذين:
- سطر فارغ: “n\”
- سلسلة فارغة: “”
السلسلة النصية الأولى تتكون من حرف واحد فقط، في حين أن السلسلة النصية الثانية لا تحتوي على أي محتوى، فهي فارغة.
على الرغم من أن Python عادةً ما يتولى أمر حرف السطر الجديد نيابةً عنك، فمن المفيد أن تفهم كيفية التعامل معه بنفسك.
التعامل مع الأسطر الجديدة
حرف السطر الجديد هو حرف تحكم خاص يُستخدم للإشارة إلى نهاية السطر. عادةً لا يظهر على الشاشة، ولكن بعض برامج تحرير النصوص تعرض هذه الأحرف غير القابلة للطباعة برسومات بسيطة.
كلمة “حرف” تُعتبر تسمية خاطئة في هذه الحالة، إذ يمكن أن يتجاوز طول السطر الجديد حرفًا واحدًا. على سبيل المثال، يُمثل نظام التشغيل ويندوز، وكذلك بروتوكول HTTP، الأسطر الجديدة بزوج من الأحرف. أحيانًا، يجب مراعاة هذه الاختلافات لتصميم برامج محمولة حقًا.
للتعرف على ما يشكل سطرًا جديدًا في نظام التشغيل الخاص بك، يمكنك استخدام وحدة os من مكتبة Python القياسية.
إذا كنت تستخدم نظام ويندوز، فستجد أن نظام التشغيل لديك يُمثِّل سطرًا جديدًا باستخدام تسلسل من حرفي تحكم. يتكون هذا التسلسل من إرجاع السطر (r\) متبوعًا بتغذية السطر (n\):
>>> import os
>>> os.linesep
'\r\n'
في أنظمة Unix وLinux والإصدارات الحديثة من macOS، يكون الحرف n\ واحدًا:
>>> import os
>>> os.linesep
'\n'
تحتوي معظم لغات البرمجة على مجموعة محددة مسبقًا من تسلسلات الهروب للأحرف الخاصة مثل هذه:
| تسلسل الهروب | Character |
|---|---|
\\ | الشرطة المائلة للخلف |
\b | مسافة للخلف |
\t | علامة التبويب |
\r | Carriage Return (CR) |
\n | Line Feed (LF) |
يُذكرنا الأخيران بالآلات الكاتبة الميكانيكية، التي تتطلب أمرين منفصلين لإدراج سطر جديد. الأمر الأول يُعيد العربة إلى بداية السطر الحالي، بينما يُحرك الأمر الثاني العربة إلى السطر التالي.
بالنظر إلى رموز أحرف ASCII المقابلة، ستجد أن وضع الشرطة المائلة العكسية قبل حرف في سلسلة نصية يغير معناه. مع ذلك، لا تسمح جميع الأحرف بذلك، فقط بعض الأحرف الخاصة.
للتحقق من رمز ASCII لحرف ما، قد ترغب في استخدام الدالة ()ord المضمنة:
>>> ord("r")
114
>>> ord("\r")
13
هذه الدالة تأخذ سلسلة نصية من حرف واحد، لذا يُحتسب التسلسل r\ كحرف واحد. يُرجى العلم أنه لإنشاء تسلسل إفلات صحيح، يجب ألا تكون هناك مسافة بين حرف الشرطة المائلة العكسية والحرف.
سبق أن رأيتَ كيفية طباعة سطر فارغ باستخدام دالة ()print. مع ذلك، في الحالات الأكثر شيوعًا، قد ترغب في إيصال رسالة إلى المستخدم النهائي. هناك عدة طرق لتحقيق ذلك، ستستكشفها الآن.
تمرير الحجج
أولاً، يمكنك تمرير سلسلة حرفية مباشرةً إلى ()print كحجة:
>>> print("Please wait while the program is loading...")
Please wait while the program is loading...
سيؤدي هذا إلى طباعة الرسالة تمامًا كما هي مكتوبة على الشاشة.
بعد ذلك، يمكنك استخراج هذه الرسالة إلى متغير خاص بها باسم ذي معنى لتحسين قابلية القراءة وتشجيع إعادة استخدام الكود:
>>> message = "Please wait while the program is loading..."
>>> print(message)
Please wait while the program is loading...
أخيرًا، يمكنك تمرير تعبير، مثل ربط السلسلة إلى ()print، والذي سيتم تقييمه ديناميكيًا قبل عرض النتيجة:
>>> import os
>>> print("Hello, " + os.getlogin() + "! How are you?")
Hello, jdoe! How are you?
بينما يعمل هذا كما هو متوقع، هناك عشرات أدوات تنسيق السلاسل في بايثون تُقدم قواعد نحوية أكثر إيجازًا وسهولة في القراءة. إحدى هذه الأدوات هي سلاسل نصية منسقة (أو سلاسل f)، والتي يمكنك استخدامها مع دالة ()print:
>>> import os
>>> print(f"Hello, {os.getlogin()}! How are you?")
Hello, jdoe! How are you?
النتيجة هي نفسها السابقة، لكن f-strings ستمنعك من الوقوع في خطأ شائع، وهو نسيان تحويل نوع المتعاملات المتسلسلة. بايثون لغة ذات نوع قوي، مما يعني أنها لن تسمح لك بذلك:
>>> "My age is " + 42
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
"My age is " + 42
~~~~~~~~~~~~~^~~~
TypeError: can only concatenate str (not "int") to str
هذا خطأ، لأن إضافة أرقام إلى سلاسل نصية غير منطقية. يجب تحويل الرقم إلى سلسلة نصية أولًا بشكل صريح لربطهما معًا:
>>> "My age is " + str(42)
'My age is 42'
إذا لم تقم بمعالجة مثل هذه الأخطاء بنفسك، فسوف يخبرك مفسر Python بوجود مشكلة من خلال إظهار تتبع.
كما هو الحال مع أي دالة، لا يهم إن كنتَ تُمرّر حرفًا أو متغيرًا أو تعبيرًا. لكن، على عكس العديد من الدوال الأخرى، تقبل دالة ()print أي شيء بغض النظر عن نوعه.
حتى الآن، لم تتطرق إلا إلى السلسلة النصية، ولكن ماذا عن أنواع البيانات الأخرى؟ جرّب قيمًا حرفية لأنواع بيانات مُدمجة مختلفة وانظر النتيجة:
>>> print(42) # <class 'int'>
42
>>> print(3.14) # <class 'float'>
3.14
>>> print(1 + 2j) # <class 'complex'>
(1+2j)
>>> print(True) # <class 'bool'>
True
>>> print([1, 2, 3]) # <class 'list'>
[1, 2, 3]
>>> print((1, 2, 3)) # <class 'tuple'>
(1, 2, 3)
>>> print({"red", "green", "blue"}) # <class 'set'>
{'red', 'green', 'blue'}
>>> print({"name": "Alice", "age": 42}) # <class 'dict'>
{'name': 'Alice', 'age': 42}
>>> print("hello") # <class 'str'>
hello
انتبه لثابت “None“. على الرغم من استخدامه للإشارة إلى عدم وجود قيمة، سيظهر كـ “None” بدلًا من سلسلة نصية فارغة:
>>> print(None)
None
كيف تعرف دالة ()print كيفية التعامل مع كل هذه الأنواع المختلفة؟ الإجابة المختصرة هي أنها لا تعرف ذلك. فهي تستدعي ضمنيًا دالة ()str لتحويل أي كائن إلى سلسلة نصية. بعد ذلك، تُعامل السلاسل النصية بطريقة موحدة.
في وقت لاحق من هذا البرنامج التعليمي، ستتعلم كيفية استخدام هذه الآلية لطباعة أنواع البيانات المخصصة مثل فئاتك.
حسنًا، يمكنك الآن استدعاء دالة ()print بدون أي وسيطات أو باستخدام وسيطة واحدة. لقد تعلمت كيفية عرض الرسائل الثابتة والمنسقة على الشاشة. في القسم الفرعي التالي، ستستكشف تنسيق الرسائل بمزيد من التفصيل.
فصل الحجج المتعددة
لقد رأيت استدعاء ()print بدون أي وسيطات لإنتاج سطر فارغ، وبواسطة وسيطة واحدة لعرض رسالة ثابتة أو منسقة.
اتضح أن دالة ()print دالة متغيرة، أي أنها تقبل أي عدد من الوسائط الموضعية، بما في ذلك صفر، أو واحد، أو أكثر. وهذا مفيد جدًا في الحالات الشائعة لتنسيق الرسائل، حيث ترغب في ربط بعض العناصر معًا.
ألقي نظرة على هذا المثال:
>>> import os
>>> print("My name is", os.getlogin(), "and I am", 42)
My name is jdoe and I am 42
قامت دالة ()print بدمج جميع الوسائط الأربعة التي تم تمريرها إليها، مع إدراج مسافة واحدة بينها حتى لا ينتهي بك الأمر برسالة مضغوطة مثل “اسمي jdoe وأنا 42”.
لاحظ أنه تولى أيضًا عملية تحويل النوع بشكل صحيح من خلال استدعاء ()str ضمنيًا لكل وسيطة قبل ربطها معًا. إذا تذكرت من القسم الفرعي السابق، فقد يؤدي التجميع غير المباشر بسهولة إلى خطأ بسبب عدم توافق الأنواع:
>>> print("My age is: " + 42)
Traceback (most recent call last):
File "<python-input-0>", line 1, in <module>
print("My age is: " + 42)
~~~~~~~~~~~~~~^~~~
TypeError: can only concatenate str (not "int") to str
بالإضافة إلى قبول عدد متغير من الوسائط الموضعية، تقوم ()print بتعريف أربعة وسائط مسماة أو كلمات رئيسية، وهي اختيارية لأنها جميعًا لها قيم افتراضية:
| حجة الكلمة الرئيسية | القيمة الافتراضية | الوصف |
|---|---|---|
sep | " " | سلسلة مُدرجة بين القيم |
end | "\n" | سلسلة مضافة بعد القيمة الأخيرة |
file | None | كائن يشبه الملف لكتابة الإخراج إليه (الافتراضي هو stdout) |
flush | False | هل يجب إجبار التيار على التدفق؟ |
يمكنك عرض وثائقهم المختصرة عن طريق استدعاء help(print) من المترجم التفاعلي.
في الوقت الحالي، ستركز على sep، وهو اختصار لـ separator (فاصل)، ويُخصص له مسافة واحدة (” “) افتراضيًا. يُحدد هذا الفاصل القيمة التي تُربط بها العناصر. يجب أن تكون إما سلسلة نصية أو None (لا شيء)، ولكن الأخير له نفس تأثير المسافة الافتراضية:
>>> print("hello", "world", sep=None)
hello world
>>> print("hello", "world", sep=" ")
hello world
>>> print("hello", "world")
hello world
إذا كنت تريد إلغاء الفاصل تمامًا، فيجب عليك تمرير سلسلة فارغة (“”) بدلاً من ذلك:
>>> print("hello", "world", sep="")
helloworld
قد ترغب في ربط دالة ()print بحججها كسطرين منفصلين. في هذه الحالة، ما عليك سوى تمرير حرف السطر الجديد الموضح سابقًا:
>>> print("hello", "world", sep="\n")
hello
world
سيكون المثال الأكثر فائدة لمعلمة sep هو طباعة شيء مثل مسارات الملفات:
>>> print("home", "user", "documents", sep="/")
home/user/documents
تذكر أن الفاصل يأتي بين العناصر، وليس حولها، لذا عليك أن تأخذ ذلك في الاعتبار بطريقة أو بأخرى:
>>> print("/home", "user", "documents", sep="/")
/home/user/documents
>>> print("", "home", "user", "documents", sep="/")
/home/user/documents
على وجه التحديد، يمكنك إدراج حرف الشرطة المائلة للأمام (/) في الحجة الموضعية الأولى، أو استخدام سلسلة فارغة كحجة أولى لفرض الشرطة المائلة الأمامية.
تتضمن حالة استخدام أخرى مثيرة للاهتمام تصدير البيانات إلى تنسيق القيم المنفصلة بفاصلة (CSV):
>>> print(1, "Python Tricks", "Dan Bader", sep=",")
1,Python Tricks,Dan Bader
لن يُعالج هذا الأمر الحالات الشاذة، مثل إفلات الفواصل، بشكل صحيح، ولكن في حالات الاستخدام البسيطة، يُفترض أن يُفي بالغرض. سيظهر السطر المعروض أعلاه في نافذة الطرفية. لحفظه في ملف، يجب إعادة توجيه المخرجات. لاحقًا في هذا القسم، ستتعرف على كيفية استخدام ()print لكتابة نص إلى ملفات مباشرةً من بايثون.
أخيرًا، لا يقتصر استخدام معامل sep على حرف واحد فقط. يمكنك ربط العناصر بسلاسل نصية بأي طول:
>>> print("node", "child", "child", sep=" -> ")
node -> child -> child
فيما يلي، ستستكشف حجج الكلمات الأساسية المتبقية لدالة ()print.
الطباعة إلى ملف
صدق أو لا تصدق، دالة ()print لا تعرف كيفية تحويل الرسائل إلى نص على شاشتك، وبصراحة، لا تحتاج إلى ذلك. هذه مهمة الطبقات البرمجية ذات المستوى الأدنى، التي تفهم البايتات وتعرف كيفية توزيعها.
دالة ()print هي تجريدٌ لهذه الطبقات، تُوفر واجهةً سهلةً تُفوِّض الطباعة الفعلية إلى سلسلةٍ من الأحرف. يمكن أن تكون هذه السلسلة أي ملفٍّ على القرص، أو منفذ شبكة، أو حتى مخزن بياناتٍ مؤقتٍ في الذاكرة.
بالإضافة إلى التدفقات المخصصة، هناك ثلاثة تدفقات قياسية يوفرها نظام التشغيل:
- الإدخال القياسي (stdin)
- الإخراج القياسي (stdout)
- الخطأ المعياري (stderr)
الإخراج القياسي هو ما تراه في المحطة الطرفية عندما تقوم بتشغيل برامج سطر الأوامر المختلفة، بما في ذلك نصوص Python الخاصة بك:
$ cat hello.py
print("This will appear on stdout")
$ python hello.py
This will appear on stdout
ما لم يُنص على خلاف ذلك، ستكتب دالة ()print افتراضيًا إلى المخرجات القياسية. مع ذلك، يمكنك توجيه نظام التشغيل لاستبدال المخرجات القياسية مؤقتًا بتدفق ملف، بحيث يُرسل أي مخرج إلى ذلك الملف بدلًا من الشاشة:
$ python hello.py > file.txt
$ cat file.txt
This will appear on stdout
يقوم قوس الزاوية اليمنى (>) بإعادة توجيه مجرى الإخراج القياسي لبرنامج Python الخاص بك إلى ملف محلي.
يُشبه الخطأ المعياري الخطأ القياسي من حيث ظهوره على الشاشة. ومع ذلك، فهو مسار منفصل، والغرض منه تسجيل رسائل الخطأ للتشخيص. بإعادة توجيه أحدهما أو كليهما، يُمكنك الحفاظ على النظام نظيفًا.
تستخدم بعض البرامج ألوانًا مختلفة للتمييز بين الرسائل المطبوعة في stdout وstderr:

بينما يُعدّ كلٌّ من stdout وstderr للكتابة فقط، فإن stdin – المدخل القياسي – للقراءة فقط. يمكنك تشبيه المدخل القياسي بلوحة المفاتيح. ولكن كما هو الحال مع المدخلين الآخرين، يمكنك استبدال stdin بملف لقراءة البيانات منه.
في Python، يمكنك الوصول إلى جميع التدفقات القياسية من خلال وحدة sys المضمنة:
>>> import sys
>>> sys.stdin
<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>
>>> sys.stdin.fileno()
0
>>> sys.stdout
<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
>>> sys.stdout.fileno()
1
>>> sys.stderr
<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>
>>> sys.stderr.fileno()
2
كما ترى، تشبه هذه القيم المحددة مسبقًا كائنات تشبه الملفات مع سمات الوضع والترميز، بالإضافة إلى توابع .read() و.write()، من بين العديد من التوابع الأخرى.
افتراضيًا، تُرسل دالة ()print المخرجات إلى sys.stdout عبر وسيطة الملف، ولكن يمكنك تغيير ذلك. بتمرير كائن ملف مفتوح في وضع الكتابة أو الإضافة إلى وسيطة الملف، يمكنك توجيه المخرجات مباشرةً إلى ذلك الملف بدلاً من الشاشة:
>>> with open("file.txt", mode="w", encoding="utf-8") as output_file:
... print("hello world", file=output_file)
...
سيؤدي هذا إلى جعل الكود الخاص بك محصنًا ضد إعادة توجيه البث على مستوى نظام التشغيل، وهو ما قد يكون مرغوبًا فيه أو لا يكون.
لاحظ أن دالة print() لا تتحكم في ترميز الأحرف. بل تقع مسؤولية ترميز سلاسل Unicode المُستقبَلة إلى بايتات بشكل صحيح على عاتق التدفق الأساسي. كأفضل ممارسة، يُنصح بضبط ترميز الأحرف صراحةً إلى UTF-8، وهو خيار جيد في معظم الحالات. فهو يغطي نطاقًا واسعًا من أحرف Unicode مع الحفاظ على التوافق مع ASCII.
بالنسبة للأنظمة القديمة، قد ترغب في تحديد ترميز مختلف وفقًا لذلك:
>>> with open("file.txt", mode="w", encoding="iso-8859-1") as file:
... print("über naïve café", file=file)
...
بدلاً من ملف حقيقي موجود في نظام الملفات لديك، يمكنك إنشاء ملف وهمي، يُحفظ في ذاكرة جهاز الكمبيوتر. ستستخدم هذه التقنية لاحقًا لمحاكاة دالة ()print في اختبارات الوحدات:
>>> import io
>>> fake_file = io.StringIO()
>>> print("hello world", file=fake_file)
>>> fake_file.getvalue()
'hello world\n'
باستخدام هذا الكود، يمكنك حفظ المخرجات المطبوعة في مخزن مؤقت للسلاسل النصية في الذاكرة بدلاً من عرضها على الشاشة. تكتب دالة ()print إلى المخزن المؤقت، بينما تسترجع دالة ()getvalue المحتوى المتراكم كسلسلة نصية.
إذا وصلت إلى هذه النقطة، فلن يتبقى لك سوى وسيطة كلمة رئيسية واحدة في دالة ()print، والتي ستُلقي نظرة عليها لاحقًا. مع أنها ربما تكون الأقل استخدامًا، إلا أن هناك أوقاتًا يكون فيها استخدامها ضروريًا للغاية.
تخزين مؤقت لإستدعاءات ()print
في القسم السابق، تعلمت أن دالة ()print تُفوّض الطباعة إلى كائن يشبه الملف مثل sys.stdout. مع ذلك، تُخزّن بعض التدفقات مؤقتًا عمليات إدخال/إخراج مُعينة لتحسين الأداء، مما قد يُعيق العملية. ألقِ نظرة على مثال.
تخيل أنك تكتب مؤقتًا للعد التنازلي، والذي يجب أن يضيف الوقت المتبقي إلى نفس السطر كل ثانية:
3...2...1...Go!
قد تبدو محاولتك الأولى كالتالي:
import time
num_seconds = 3
for countdown in reversed(range(num_seconds + 1)):
if countdown > 0:
print(countdown, end="...")
time.sleep(1)
else:
print("Go!")
طالما أن متغير العد التنازلي أكبر من الصفر، يستمر الكود في إضافة نص دون سطر جديد، ثم ينتقل إلى وضع السكون لثانية واحدة. أخيرًا، عند انتهاء العد التنازلي، يطبع الأمر “Go!” وينهي السطر.
بشكل غير متوقع، بدلاً من العد التنازلي لكل ثانية، يتوقف البرنامج عن العمل بشكل هدر لمدة ثلاث ثوانٍ، ثم يقوم فجأة بطباعة السطر بأكمله مرة واحدة:
ذلك لأن نظام التشغيل في هذه الحالة يُخزّن عمليات الكتابة اللاحقة مؤقتًا في المخرجات القياسية. يجب أن تعلم أن هناك ثلاثة أنواع من التدفقات فيما يتعلق بالتخزين المؤقت:
- غير مُخزَّن مؤقتًا
- مُخزَّن مؤقتًا في السطر
- مُخزَّن مؤقتًا في الكتلة
لا يحتاج التخزين المؤقت إلى شرح، إذ لا يتم التخزين المؤقت، وتُفعّل جميع عمليات الكتابة فورًا. ينتظر التدفق المُخزّن مؤقتًا في السطر قبل إطلاق أي استدعاءات إدخال/إخراج حتى يظهر فاصل سطر في مكان ما في المخزن المؤقت، بينما يسمح التدفق المُخزّن مؤقتًا في الكتل للمخزن المؤقت بالامتلاء إلى حجم معين بغض النظر عن محتواه.
يُخزَّن الإخراج القياسي مؤقتًا على السطر والكتلة، حسب الحدث الذي يسبقه. يُساعد التخزين المؤقت على تقليل عدد مكالمات الإدخال/الإخراج المُكلفة. تخيَّل، على سبيل المثال، إرسال الرسائل عبر شبكة ذات زمن انتقال مرتفع.
عند الاتصال بخادم بعيد لتنفيذ أوامر عبر بروتوكول SSH، قد تُنتج كل ضغطة مفتاح حزمة بيانات فردية، وهي أكبر بكثير من حمولتها. يا لها من تكلفة إضافية! من المنطقي الانتظار حتى كتابة بضعة أحرف على الأقل ثم إرسالها معًا. وهنا يأتي دور التخزين المؤقت.
من ناحية أخرى، قد يُسبب التخزين المؤقت أحيانًا آثارًا غير مرغوب فيها، كما رأيتَ في مثال العد التنازلي. لإصلاح ذلك، يمكنك ببساطة توجيه دالة ()print لتفريغ الدفق قسرًا دون انتظار حرف سطر جديد في المخزن المؤقت باستخدام علامة التفريغ الخاصة بها:
import time
num_seconds = 3
for countdown in reversed(range(num_seconds + 1)):
if countdown > 0:
print(countdown, end="...", flush=True)
time.sleep(1)
else:
print("Go!")
هذا كل شيء – يجب أن يعمل العد التنازلي الآن كما هو متوقع. جربه وشاهد الفرق بنفسك!
تهانينا. لقد شاهدتَ الآن أمثلةً على استدعاء دالة )print تغطي جميع مُعاملاتها. أنت تعرف غرضها ومتى تستخدمها. مع ذلك، فهمُ توقيع الدالة هو مجرد البداية. في الأقسام القادمة، ستفهم السبب.
طباعة أنواع البيانات المخصصة
حتى الآن، كنت تتعامل فقط مع أنواع البيانات المُدمجة، مثل السلاسل والأرقام، ولكنك غالبًا ما ترغب في طباعة كائنات من أنواع بياناتك الخاصة. عادةً، يمكنك تعريف أنواع بيانات مخصصة في بايثون باستخدام إحدى الأدوات التالية:
- الصفوف المسماة
- فئات البيانات
- الفئات البسيطة
تُعدّ الثنائيات المُسمّاة مثاليةً للكائنات البسيطة التي لا تعتمد على منطق، والغرض منها هو حمل سلسلة من القيم. عند طباعتها، تُعرض تلقائيًا تمثيلًا نصيًا واضحًا وسهل القراءة:
>>> from collections import namedtuple
>>> Person = namedtuple("Person", "name age")
>>> jdoe = Person("John Doe", 42)
>>> print(jdoe)
Person(name='John Doe', age=42)
هذا رائع طالما أن حفظ البيانات هو كل ما تحتاجه. ولكن لإضافة سلوكيات إلى نوع الشخص، يُفضّل تنفيذه كفئة فرعية من typeing.NamedTuple، وهو نوع مُكتوب من namedtuple:
>>> from typing import NamedTuple
>>> class Person(NamedTuple):
... name: str
... age: int
...
... def greet(self):
... print(f"My name's {self.name}, and I'm {self.age} years old.")
...
>>> jdoe = Person("John Doe", 42)
>>> jdoe.greet()
My name's John Doe, and I'm 42 years old.
>>> print(jdoe)
Person(name='John Doe', age=42)
هنا، يمكنك استخدام تلميحات النوع لتحديد حقول الفئة، .name و.age. باستخدام الكلمة المفتاحية class، ستحصل الآن على نص فئة يمكنك من خلاله تحديد أساليب مخصصة لتنفيذ إجراءات مثل طباعة رسالة ترحيب. في الوقت نفسه، ستحصل على نفس تمثيل السلسلة النصية كما كان من قبل مجانًا.
هذا أفضل من استخدام مجموعة مُسمّاة عادية، ليس فقط لأنك تحصل على طباعة مجانية، بل يمكنك أيضًا إضافة أساليب وخصائص مخصصة إلى الفئة. مع ذلك، فهو يحل مشكلة ويضيف أخرى. تذكر أن الصفوف، بما فيها الصفوف المُسمّاة، غير قابلة للتغيير في بايثون، لذا لا يمكن تغيير قيمها بعد إنشائها:
>>> jdoe.age += 1
Traceback (most recent call last):
File "<python-input-5>", line 1, in <module>
jdoe.age += 1
^^^^^^^^
AttributeError: can't set attribute
صحيح أن تصميم أنواع بيانات ثابتة أمر مرغوب فيه عمومًا، ولكن في كثير من الحالات، ستحتاج إلى أن تسمح بالتغيير. لحسن الحظ، تحتوي بايثون على فئات بيانات، والتي يمكنك اعتبارها مجموعات قابلة للتغيير. بهذه الطريقة، تحصل على أفضل ما في العالمين.
إليك كيفية تنفيذ نوع Person الخاص بك كفئة بيانات قابلة للتغيير:
>>> from dataclasses import dataclass
>>> @dataclass
... class Person:
... name: str
... age: int
...
... def celebrate_birthday(self):
... self.age += 1
...
>>> jdoe = Person("John Doe", 42)
>>> jdoe.celebrate_birthday()
>>> print(jdoe)
Person(name='John Doe', age=43)
مع أن هذا الكود يبدو مطابقًا تقريبًا لمثال NamedTuple الذي شاهدته سابقًا، إلا أنه يسمح لك بتعديل حالة الشخص. لم يكن ذلك ممكنًا باستخدام tuple مُسمّى، لأنه لا يسمح بالتعديل المكاني لعناصره.
تُعدّ كلٌّ من الصفوف المُسمّاة وفئات البيانات إضافاتٍ حديثةً نسبيًا إلى بايثون. لطالما كانت الطريقة التقليدية والأكثر شيوعًا لتحديد أنواع البيانات المُخصّصة هي استخدام فئاتٍ بسيطة، مما يمنحك تحكّمًا دقيقًا. وحتى اليوم، ستجدها كثيرًا في العديد من قواعد البيانات.
إليك نفس نوع Person الذي تم تنفيذه كفئة Python قياسية:
>>> class Person:
... def __init__(self, name, age):
... self.name = name
... self.age = age
...
للأسف، لا تحتوي الفئات البسيطة كهذه على تمثيل نصي جذاب افتراضيًا. إذا أنشأتَ الآن مثيلًا لفئة Person وحاولتَ طباعته، فستحصل على هذا الناتج الغريب:للأسف، لا تحتوي الفئات البسيطة كهذه على تمثيل نصي جذاب افتراضيًا. إذا أنشأتَ الآن مثيلًا لفئة Person وحاولتَ طباعته، فستحصل على هذا الناتج الغريب:
>>> jdoe = Person("John Doe", 42)
>>> print(jdoe)
<__main__.Person object at 0x78ec55f50ec0>
كما تعلمت سابقًا، تستدعي دالة ()print ضمنيًا دالة ()str المُدمجة لتحويل وسيطاتها الموضعية إلى سلاسل نصية. في الواقع، يؤدي استدعاء ()str يدويًا على نسخة من فئة Person الاعتيادية إلى نفس نتيجة طباعتها:
>>> str(jdoe)
'<__main__.Person object at 0x78ec55f50ec0>'
تبحث دالة ()str بدورها عن إحدى طريقتين خاصتين – تُعرفان أيضًا بالطريقتين السحريتين – داخل نص الفئة، واللتين عادةً ما تُطبّقهما بنفسك. إذا لم تعثر على إحداهما، فستعود إلى التمثيل الافتراضي غير المألوف الذي رأيته للتو. هذه الطرق الخاصة، حسب ترتيب البحث، هي:
من المتوقع أن يُرجع الأول نصًا قصيرًا وسهل القراءة، يتضمن معلومات مُخزّنة في السمات الأكثر صلة. ليس من الضروري دائمًا تضمين جميع السمات عند طباعة الكائنات لتجنب كشف البيانات الحساسة، مثل كلمات مرور المستخدم.
يجب أن توفر الطريقة الخاصة الأخرى معلومات كاملة عن الكائن، ويفضل أن تسمح باستعادة حالته من سلسلة نصية. في هذه الحالة، يجب أن تُرجع شيفرة بايثون صحيحة، بحيث يمكنك تمريرها مباشرةً إلى ()eval، كما يلي:
>>> repr(jdoe)
"Person(name='John Doe', age=42)"
>>> type(eval(repr(jdoe)))
<class '__main__.Person'>
لاحظ استخدام دالة مضمنة أخرى، ()repr، والتي تحاول دائمًا استدعاء .repr() في كائن، لكنها تعود إلى التمثيل الافتراضي إذا لم تتمكن من العثور على هذه الطريقة.
فيما يلي كيفية تنفيذ إحدى هذه الطرق الخاصة في فئة Person الخاصة بك لتوفير تمثيل سلسلة مفيد للكائن:
>>> class Person:
... def __init__(self, name, age):
... self.name = name
... self.age = age
...
... def __str__(self):
... class_name = type(self).__name__
... return f"{class_name}(name={self.name!r}, age={self.age!r})"
...
>>> jdoe = Person("John Doe", 42)
>>> print(jdoe)
Person(name='John Doe', age=42)
بعد الحصول على اسم الفصل بشكل ديناميكي، يمكنك استخدام سلسلة f لتنسيق سلسلة الإخراج، مع التأكد من عرض سمات name. وage. بوضوح.
يمنحك بايثون حرية كبيرة في تعريف أنواع بياناتك الخاصة إذا لم تُلبِّ أيٌّ من الأنواع المُدمجة احتياجاتك. بعضها، مثل الثنائيات المُسمّاة وفئات البيانات، يُقدّم تمثيلات نصية جيدة المظهر دون الحاجة إلى أي جهد منك. مع ذلك، لتحقيق أقصى قدر من المرونة، ستحتاج إلى تعريف فئة وتجاوز الطرق الخاصة الموضحة أعلاه.
فهم print() في بايثون
أنت تعرف كيفية استخدام دالة ()print جيدًا في هذه المرحلة، ولكن فهمها بشكل أعمق سيُمكّنك من استخدامها بفعالية ووعي أكبر. بعد قراءة هذا القسم، ستفهم كيف تحسّنت الطباعة في بايثون على مر السنين.
من العبارة إلى الدالة
في إصدارات بايثون القديمة، كانت دالة الطباعة (print) عبارة وليس دالة. على الرغم من أن دالة ()print أصبحت الآن دالة، إلا أن البعض لا يزالون يستخدمونها عادةً باسم “عبارة الطباعة”، وهو مصطلح عفا عليه الزمن تقنيًا. يتماشى هذا التغيير النحوي مع فلسفة بايثون في التعامل مع معظم العمليات كدوال، وذلك بفضل فوائدها العديدة، والتي ستتعرف عليها في القسم التالي.
حسنًا، ()print هي دالة في بايثون. وبشكل أكثر تحديدًا، هي دالة مدمجة، مما يعني أنك لست بحاجة لاستيرادها من أي مكان:
>>> print
<built-in function print>
إنه متاح دائمًا في مساحة الاسم العالمية حتى تتمكن من استدعائه مباشرةً، ولكن يمكنك أيضًا الوصول إليه من خلال وحدة المضمنة من المكتبة القياسية:
>>> import builtins
>>> builtins.print
<built-in function print>
>>> builtins.print("Hello, World!")
Hello, World!
بهذه الطريقة، يمكنك تجنب تضارب الأسماء مع الدوال المخصصة. لنفترض أنك تريد إعادة تعريف دالة ()print بحيث لا تُضيف سطرًا جديدًا، مع الحفاظ على الدالة الأصلية باسم جديد مثل ()println:
>>> import builtins
>>> println = builtins.print
>>> def print(*args, **kwargs):
... builtins.print(*args, **kwargs, end="")
...
>>> println("Hello, World!")
Hello, World!
>>> print("Hello, World!\n")
Hello, World!
لديك الآن دالتان منفصلتان للطباعة، تمامًا كما هو الحال في لغة برمجة جافا. ستُعرّف دالات ()print مخصصة في قسم المحاكاة لاحقًا أيضًا. تذكر أيضًا أنه لن تتمكن من استبدال ()print من الأساس إذا لم تكن دالة.
من ناحية أخرى، فإن ()print ليست دالة بالمعنى الرياضي، لأنها لا ترجع أي قيمة ذات معنى بخلاف القيمة الضمنية None:
>>> value = print("Hello, World!")
Hello, World!
>>> print(value)
None
هذه الدوال، في الواقع، هي إجراءات أو برامج فرعية تُستدعى لتحقيق تأثير جانبي، وهو في النهاية تغيير في الحالة العامة. في حالة ()print، يتمثل هذا التأثير الجانبي في عرض رسالة على المخرجات القياسية أو الكتابة إلى ملف.
فوائد الدوال
لأن ()print دالة، فهي تحمل توقيعًا واضحًا وخصائص معروفة. يمكنك العثور على وثائقها بسرعة باستخدام محرر النصوص الذي تختاره، دون الحاجة إلى تذكر قواعد لغوية معقدة لتنفيذ مهمة معينة.
علاوة على ذلك، يسهل توسيع الدوال. إضافة ميزة جديدة إلى دالة ما سهلة كإضافة وسيطة مفتاحية أخرى، بينما تغيير اللغة لدعم هذه الميزة الجديدة أكثر تعقيدًا. على سبيل المثال، إعادة توجيه التدفق أو مسح المخزن المؤقت.
من فوائد دالة ()print إمكانية التركيب. تُسمى الدوال في بايثون كائنات من الدرجة الأولى، وهي طريقة مُبتكرة للقول إنها قيم، تمامًا مثل السلاسل النصية أو الأرقام. بهذه الطريقة، يمكنك تعيين دالة لمتغير، أو تمريرها إلى دالة أخرى، أو حتى إرجاع دالة من أخرى. ولا تختلف دالة ()print في هذا الصدد. على سبيل المثال، يمكنك الاستفادة منها لحقن التبعيات:
def download(url, log=print):
log(f"Downloading {url}")
# ...
def suppress_print(*args):
pass # Do not print anything
download("/js/app.js", log=suppress_print)
هنا، يتيح لك معامل السجل حقن دالة استدعاء، والتي تكون افتراضيًا ()print، ويمكن استدعاؤها بأي دالة. في هذا المثال، تُعطّل الطباعة تمامًا عن طريق استبدال ()print بدالة وهمية لا تفعل شيئًا.
يتيح لك التكوين دمج عدة دوال في دالة جديدة من نفس النوع. يمكنك ملاحظة ذلك عمليًا بتحديد دالة ()error مخصصة أدناه، تُطبع في مسار الخطأ القياسي وتُضيف بادئة لجميع الرسائل بمستوى سجل مُحدد:
>>> import sys
>>> from functools import partial
>>> redirect = lambda function, stream: partial(function, file=stream)
>>> prefix = lambda function, prefix: partial(function, prefix)
>>> error = prefix(redirect(print, sys.stderr), "[ERROR]")
>>> error("Something went wrong")
[ERROR] Something went wrong
دالة ()error المخصصة مبنية باستخدام دوال جزئية لتحقيق التأثير المطلوب. إنها مفهوم متقدم مستوحى من نموذج البرمجة الوظيفية، لذا لا داعي للتعمق فيه الآن. مع ذلك، إذا كنت مهتمًا بهذا الموضوع، فألقِ نظرة على وحدة functools.
بخلاف العبارات، الدوال قيم. هذا يعني أنه يمكنك دمجها مع التعبيرات، وتحديدًا تعبيرات لامدا. فبدلًا من تعريف دالة كاملة لاستبدال دالة ()print، يمكنك استخدام تعبير لامدا مجهول الهوية يستدعيها مباشرةً:
>>> download("/js/app.js", lambda message: print("[INFO]", message))
[INFO] Downloading /js/app.js
مع ذلك، بما أن تعبير لامدا مُعرّف في مكانه، فلا سبيل للإشارة إليه في أي مكان آخر من الكود. هذا منطقي فقط عندما لا تنوي إعادة استخدام دالتك المجهولة.
كما ترى، تُتيح الدوال حلاً أنيقًا وقابلًا للتوسع، وهو متوافق مع بقية اللغة. ولأن ()print دالة، فهي توفر مرونة كبيرة في كيفية تكييفها مع احتياجات الطباعة لديك.
الطباعة الجميلة
إذا كنت تعتقد أن الطباعة تقتصر على إضاءة البكسلات على الشاشة، فأنت محق من الناحية الفنية. ومع ذلك، هناك طرق لجعلها تبدو رائعة. في هذا القسم، ستتعلم كيفية تنسيق هياكل البيانات المعقدة، وإضافة الألوان والزخارف الأخرى، وبناء واجهات مستخدم نصية (TUIs)، واستخدام الرسوم المتحركة، وحتى تشغيل الأصوات مع النص!
هياكل البيانات المتداخلة ذات الطباعة الجميلة
تتيح لك لغات الحاسوب عرض البيانات، بالإضافة إلى الشيفرة البرمجية القابلة للتنفيذ، بطريقة منظمة. على عكس بايثون، تمنحك معظم لغات البرمجة حرية كبيرة في استخدام المسافات والتنسيق. قد يكون هذا مفيدًا، على سبيل المثال، في ضغط البيانات، ولكنه قد يؤدي أحيانًا إلى شيفرة برمجية أقل قابلية للقراءة.
الطباعة المتقنة تعني جعل البيانات أو الشيفرة البرمجية تبدو أكثر جاذبية للعين البشرية، مما يسهل فهمها. يتم ذلك عن طريق إضافة مسافات بادئة لبعض الأسطر، وإضافة أسطر جديدة، وإعادة ترتيب العناصر، وما إلى ذلك.
يأتي بايثون مزودًا بوحدة pprint ضمن مكتبته القياسية، مما يساعدك على تحسين هياكل البيانات الكبيرة التي لا تتسع لسطر واحد. ولأنها تطبع بطريقة أسهل على المستخدم، فإن العديد من أدوات REPL الشائعة، بما في ذلك JupyterLab وIPython، تستخدمها افتراضيًا بدلاً من دالة ()print الاعتيادية.
إذا كنت لا تمانع في فقدان الوصول إلى دالة )print الأصلية عالميًا، فيمكنك استبدالها بـ ()pprint في الكود الخاص بك باستخدام إعادة تسمية الاستيراد:
>>> from pprint import pprint as print
>>> print
<function pprint at 0x7f7a775a3510>
الآن، سيؤدي استدعاء ()print إلى تفويض ()pprint فعليًا. لاستخدام النسخة المُحسّنة فقط من حين لآخر، يمكنك إعادة تعيين ()print إلى ()pprint ضمن نطاق دالة مخصصة:
>>> from pprint import pprint
>>> def function():
... print = pprint
... print(print)
...
>>> function()
<function pprint at 0x730ff4de4040>
>>> print(print)
<built-in function print>
اعتمادًا على المكان في الكود الخاص بك الذي تستدعي فيه دالة ()print، فسوف تستدعي إما دالة ()print الأصلية المرفقة مع Python أو الدالة الموجودة في وحدة pprint.
لكي يكون لديك دائمًا كلتا الوظيفتين في متناول يدك، قد ترغب في استيراد ()pprint أو اسمها المختصر ()pp صراحةً:
>>> from pprint import pp
للوهلة الأولى، لا يوجد فرق كبير بين ()print و()pprint – أو اسمها المستعار ()pp – وفي بعض الحالات، لا يوجد فرق تقريبًا:
>>> print(42)
42
>>> pp(42)
42
>>> print("hello")
hello
>>> pp("hello")
'hello'
طباعة رقم باستخدام دالة ()print و ()pp تبدو متطابقة، لكن السلسلة النصية تبدو مختلفة. ذلك لأن دالة ()pp تستدعي دالة ()repr بدلاً من دالة ()str المعتادة لتحويل النوع. هذا يضمن تمثيلًا متسقًا لهياكل البيانات المتداخلة.
تصبح الاختلافات أكثر وضوحًا عندما تبدأ في تغذية ()pp بهياكل بيانات أكثر تعقيدًا:
>>> data = {"powers": [x**10 for x in range(10)]}
>>> pp(data)
{'powers': [0,
1,
1024,
59049,
1048576,
9765625,
60466176,
282475249,
1073741824,
3486784401]}
تُطبّق هذه الدالة تنسيقًا مناسبًا لتحسين قابلية القراءة، ولكن يُمكنك تخصيصها بشكل أكبر باستخدام بعض المعلمات. على سبيل المثال، يُمكنك تقييد تسلسل هرمي مُتداخل بعرض علامة حذف أسفل مستوى مُحدد:
>>> cities = {"USA": {"Texas": {"Dallas": ["Irving"]}}}
>>> pp(cities, depth=3)
{"USA": {"Texas": {"Dallas": [...]}}}
تستخدم دالة ()print العادية أيضًا علامات الحذف ولكن لعرض هياكل البيانات المتكررة، والتي تشكل دورة، لتجنب خطأ تجاوز سعة المكدس:
>>> items = [1, 2, 3]
>>> items.append(items)
>>> print(items)
[1, 2, 3, [...]]
ومع ذلك، فإن ()pp أكثر وضوحًا بشأن هذا الأمر من خلال تضمين الهوية الفريدة لكائن مرجعي ذاتيًا:
>>> pp(items)
[1, 2, 3, <Recursion on list with id=140635757287688>]
>>> id(items)
140635757287688
العنصر الأخير في القائمة هو نفس الكائن الموجود في القائمة بأكملها.
تقوم دالة ()pp بفرز مفاتيح القاموس تلقائيًا قبل الطباعة، مما يسمح بمقارنة متسقة. عند مقارنة السلاسل النصية، غالبًا ما لا يُهمك ترتيب السمات التسلسلية. مع ذلك، يُفضل دائمًا مقارنة القواميس الفعلية قبل تسلسلها.
غالبًا ما تُمثِّل القواميس بيانات JSON، وهي شائعة الاستخدام على الإنترنت. لتحويل قاموس إلى سلسلة نصية بتنسيق JSON صحيح، يمكنك الاستفادة من وحدة json، فهي تتميز أيضًا بإمكانيات طباعة مُحسَّنة.
>>> import json
>>> data = {"username": "jdoe", "password": "s3cret"}
>>> ugly = json.dumps(data)
>>> pretty = json.dumps(data, indent=4, sort_keys=True)
>>> print(ugly)
{"username": "jdoe", "password": "s3cret"}
>>> print(pretty)
{
"password": "s3cret",
"username": "jdoe"
}
لاحظ، مع ذلك، أنك بحاجة إلى طباعة النص بنفسك، لأن دالة ()json.dumps تُرجع سلسلة نصية من بايثون فقط. وبالمثل، تحتوي وحدة pprint على دالة ()pformat إضافية تُرجع أيضًا سلسلة نصية، في حال احتجت إلى شيء آخر غير الطباعة.
من المثير للدهشة أن توقيع ()pp يختلف تمامًا عن توقيع دالة ()print:
pp(
object,
stream=None,
indent=1,
width=80,
depth=None,
*,
compact=False,
sort_dicts=False,
underscore_numbers=False
)
باستثناء الوسيطة الموضعية الأولى، الكائن، لا يُمكن تمرير أكثر من قيمة واحدة إلى ()pp للطباعة. يُظهر هذا مدى تركيز الدالة على طباعة هياكل البيانات.
لفتح طرق أكثر قوة للطباعة بأسلوب في Python، قد ترغب في البحث في تقنيات أخرى.
إضافة الألوان باستخدام تسلسلات الهروب ANSI
مع تطور الحواسيب الشخصية، أصبحت رسوماتها أفضل وعرضها ألوانًا أكثر. ومع ذلك، كان لكلٍّ من المُصنِّعين أفكاره الخاصة حول تصميم واجهة برمجة التطبيقات (API) للتحكم بها. تغير هذا قبل بضعة عقود عندما أدخل المعهد الوطني الأمريكي للمعايير رموز ANSI لتوحيد تنسيق النصوص والألوان في جميع المحطات الطرفية.
تدعم معظم محاكيات المحطات الطرفية الحالية هذا المعيار إلى حد ما. حتى وقت قريب، كان نظام التشغيل ويندوز استثناءً ملحوظًا. لذلك، إذا كنت ترغب في أفضل قابلية للنقل، فاستخدم مكتبة بايثون خارجية مثل colorama. فهي تترجم أكواد ANSI إلى نظائرها المناسبة في ويندوز مع الحفاظ عليها سليمة في أنظمة التشغيل الأخرى.
للتحقق مما إذا كان جهازك يفهم مجموعة فرعية من تسلسلات الهروب ANSI، مثل تلك المتعلقة بالألوان، يمكنك محاولة استخدام الأمر التالي:
$ tput colors
256
يُشير الطرفية الافتراضية في بعض توزيعات لينكس (GNOME Terminal) إلى إمكانية عرض 256 لونًا مختلفًا، بينما يدعم xterm ثمانية ألوان فقط. سيُرجع الأمر السابق رقمًا سالبًا إذا كانت الألوان غير مدعومة.
تسلسلات الهروب ANSI تُشبه لغة ترميز للطرفية. في HTML، تعمل مع وسوم، مثل <b> أو <i>، لتغيير مظهر العناصر في المستند. تُدمج هذه الوسوم مع محتواك، لكنها غير مرئية بحد ذاتها. وبالمثل، لن تظهر رموز الهروب في الطرفية طالما أنها تتعرف عليها. وإلا، ستظهر حرفيًا كما لو كنت تتصفح مصدر موقع إلكتروني.
كما يوحي اسمه، يجب أن يبدأ التسلسل بحرف Esc غير القابل للطباعة، وقيمته في نظام ASCII هي 27، ويُرمز لها أحيانًا بـ 0x1b في النظام السداسي عشر أو 033 في النظام الثماني. يمكنك استخدام أحرف الأرقام في بايثون للتحقق بسرعة من أنها نفس الرقم:
>>> 27 == 0x1b == 0o33
True
بالإضافة إلى ذلك، يمكنك الحصول عليه باستخدام تسلسل الهروب e\ في shell:
$ echo -e "\e"
تتخذ تسلسلات الهروب ANSI الأكثر شيوعًا الشكل التالي:
| العنصر | الوصف | مثال |
|---|---|---|
| Esc | حرف الهروب غير القابل للطباعة | \033 |
[ | قوس مربع مفتوح | [ |
| الرمز الرقمي | رقم واحد أو أكثر مفصول بـ؛ | 0 |
| رمز الحرف | حرف كبير أو صغير | m |
يمكن أن يكون الرمز الرقمي رقمًا واحدًا أو أكثر مفصولًا بفاصلة منقوطة، بينما يكون رمز الحرف حرفًا واحدًا فقط. يُحدد معيار ANSI معناهما الدقيق. على سبيل المثال، لإعادة ضبط جميع التنسيقات، اكتب أحد الأوامر المكافئة أدناه، والتي تستخدم الرمز صفر والحرف m:
$ echo -e "\e[0m"
$ echo -e "\x1b[0m"
$ echo -e "\033[0m"
في الطرف الآخر من الطيف، لديك قيم ترميز مركبة. لتعيين المقدمة والخلفية بقنوات RGB، بما أن جهازك يدعم عمق 24 بت، يمكنك توفير أرقام متعددة:
$ echo -e "\e[38;2;0;0;0m\e[48;2;255;255;255mBlack on white\e[0m"
لا يقتصر الأمر على ضبط لون النص باستخدام رموز ANSI. يمكنك، على سبيل المثال، مسح نافذة الطرفية وتمريرها، وتغيير خلفيتها، وتحريك المؤشر، وجعل النص يومض، أو تزيينه بتسطير.
في Python، من المحتمل أن تكتب دالة مساعدة للسماح بتغليف أكواد عشوائية في تسلسل:
>>> def esc(*codes):
... return f"\033[{';'.join(str(code) for code in codes)}m"
...
>>> print(esc(31, 1, 4) + "really" + esc(0) + " important")
سيؤدي هذا إلى ظهور الكلمة بالفعل بالخط الأحمر (31)، والغامق (1)، والمسطر (4):

ومع ذلك، هناك تجريدات ذات مستوى أعلى حول أكواد الهروب ANSI، مثل مكتبة colorama المذكورة، بالإضافة إلى أدوات لبناء واجهات مستخدم كاملة في وحدة التحكم.
بناء واجهات مستخدم وحدة التحكم
مع أن استخدام أكواد الهروب من ANSI قد يكون ممتعًا للغاية، إلا أنك في الواقع ستحتاج في النهاية إلى وحدات بناء أكثر تجريدًا لإنشاء واجهة مستخدم. إذا كنت تُنشئ تطبيقًا تجاريًا، فجرب Rich وTextual، اللذين يوفران أدوات رسومية نموذجية. ومع ذلك، للحصول على تحكم كامل في الطرفية، يبدو أن curses هو الخيار الأكثر شيوعًا.
بالأساس، تتيح لك هذه المكتبة التفكير من خلال عناصر واجهة مستخدم رسومية مستقلة بدلًا من مجرد كتلة نصية. كما تمنحك حرية كبيرة في التعبير عن موهبتك الفنية، إذ يشبه الأمر رسم لوحة بيضاء. تُخفي المكتبة تعقيدات التعامل مع محطات طرفية مختلفة. بالإضافة إلى ذلك، تدعم المكتبة أحداث لوحة المفاتيح بشكل ممتاز، مما قد يكون مفيدًا لكتابة ألعاب الفيديو.
ماذا عن إنشاء لعبة ثعبان كلاسيكية؟ أنت على وشك إنشاء محاكي ثعبان باستخدام بايثون:
أولاً، عليك استيراد وحدة curses. بما أنها تُعدِّل حالة الطرفية قيد التشغيل، فمن المهم معالجة الأخطاء واستعادة الحالة السابقة بسلاسة. يمكنك القيام بذلك يدويًا، ولكن المكتبة تأتي مع غلاف مناسب لوظيفتك الرئيسية:
import curses
def main(screen):
pass
if __name__ == "__main__":
curses.wrapper(main)
لاحظ أن الدالة ()main الخاصة بك تقبل مرجعًا إلى كائن النافذة الرئيسي، المعروف أيضًا باسم stdscr، والذي ستستخدمه لاحقًا للإعداد الإضافي.
إذا شغّلت هذا البرنامج الآن، فلن تلاحظ أي تأثيرات لأنه سينتهي فورًا. مع ذلك، يمكنك إضافة مهلة قصيرة لإلقاء نظرة سريعة:
import curses
import time
def main(screen):
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
هذه المرة، أصبحت الشاشة فارغة تمامًا لثانية واحدة، لكن المؤشر ظل يومض. لإخفائه، ما عليك سوى استدعاء إحدى دوال التكوين المُعرّفة في الوحدة:
import curses
import time
def main(screen):
curses.curs_set(0) # Hide the cursor
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
قم بتعريف الثعبان كقائمة من النقاط في إحداثيات الشاشة، والتي هي صف-عمود (y، x) وتبدأ في الزاوية العلوية اليسرى:
import curses
import time
def main(screen):
curses.curs_set(0) # Hide the cursor
snake = [(0, i) for i in reversed(range(20))]
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
رأس الثعبان هو دائمًا العنصر الأول في القائمة، بينما الذيل هو الأخير. الشكل الأولي للثعبان أفقي، يبدأ من أعلى يسار الشاشة ويتجه نحو اليمين. مع بقاء إحداثياته الصادية عند الصفر، يتناقص إحداثياته السينية من الرأس إلى الذيل.
[(0, 19), (0, 18), (0, 17), ..., (0, 0)]
لرسم الثعبان، ابدأ بالرأس (@) ثم بالأجزاء المتبقية (*). يحمل كل جزء إحداثيات (y, x)، لذا يمكنك فك رموزها:
import curses
import time
def main(screen):
curses.curs_set(0) # Hide the cursor
snake = [(0, i) for i in reversed(range(20))]
# Draw the snake
screen.addstr(*snake[0], "@")
for segment in snake[1:]:
screen.addstr(*segment, "*")
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
مرة أخرى، إذا قمت بتشغيل هذا الكود الآن، فلن يعرض أي شيء، لأنك بحاجة إلى تحديث الشاشة صراحةً بعد ذلك:
import curses
import time
def main(screen):
curses.curs_set(0) # Hide the cursor
snake = [(0, i) for i in reversed(range(20))]
# Draw the snake
screen.addstr(*snake[0], "@")
for segment in snake[1:]:
screen.addstr(*segment, "*")
screen.refresh()
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
أنت تريد تحريك الثعبان في أحد الاتجاهات الأربعة، والتي يمكن تعريفها كمتجهات. في النهاية، سيتغير الاتجاه استجابةً لضغطة مفتاح السهم، لذا يمكنك ربطه برموز مفاتيح المكتبة:
import curses
import time
def main(screen):
curses.curs_set(0) # Hide the cursor
directions = {
curses.KEY_UP: (-1, 0),
curses.KEY_DOWN: (1, 0),
curses.KEY_LEFT: (0, -1),
curses.KEY_RIGHT: (0, 1),
}
direction = directions[curses.KEY_RIGHT]
snake = [(0, i) for i in reversed(range(20))]
# Draw the snake
screen.addstr(*snake[0], "@")
for segment in snake[1:]:
screen.addstr(*segment, "*")
screen.refresh()
time.sleep(1)
if __name__ == "__main__":
curses.wrapper(main)
كيف يتحرك الثعبان؟ اتضح أن رأسه فقط هو الذي ينتقل إلى موقع جديد، بينما تتحرك جميع الأجزاء الأخرى نحوه. في كل خطوة، تبقى جميع الأجزاء تقريبًا كما هي، باستثناء الرأس والذيل. بافتراض أن الثعبان لا ينمو، يمكنك إزالة الذيل وإضافة رأس جديد في بداية القائمة:
# Move the snake
snake.pop()
snake.insert(0, tuple(map(sum, zip(snake[0], direction))))
للحصول على إحداثيات الرأس الجديدة، عليك إضافة متجه الاتجاه إليه. مع ذلك، فإن إضافة عناصر ثنائية في بايثون ينتج عنها عنصر ثنائي أكبر بدلًا من المجموع الجبري لمكونات المتجه المقابلة. إحدى طرق حل هذه المشكلة هي استخدام الدوال المدمجة ()zip و()sum و()map.
سيتغير الاتجاه عند ضغطة زر، لذا عليك استدعاء دالة ()getch للحصول على رمز المفتاح المضغوط. مع ذلك، إذا لم يتوافق المفتاح المضغوط مع مفاتيح الأسهم المحددة سابقًا كمفاتيح قاموس، فلن يتغير الاتجاه:
# Change direction on arrow keystroke
direction = directions.get(screen.getch(), direction)
افتراضيًا، .getch() هي استدعاء حجب يمنع الثعبان من الحركة إلا عند الضغط على مفتاح. لذلك، يجب جعل الاستدعاء غير حجب بإضافة تكوين آخر:
import curses
import time
def main(screen):
curses.curs_set(0) # Hide the cursor
screen.nodelay(True) # Don't block I/O calls
# ...
# ...
لقد أوشكت على الانتهاء، لكن بقي شيء واحد أخير. إذا كررت هذا الكود الآن، سيبدو الثعبان وكأنه ينمو بدلًا من أن يتحرك. ذلك لأنه يجب عليك مسح الشاشة تمامًا قبل كل تكرار.
وأخيرًا، هذا كل ما تحتاجه للعب لعبة الثعبان في بايثون:
import curses
import time
def main(screen):
curses.curs_set(0) # Hide the cursor
screen.nodelay(True) # Don't block I/O calls
directions = {
curses.KEY_UP: (-1, 0),
curses.KEY_DOWN: (1, 0),
curses.KEY_LEFT: (0, -1),
curses.KEY_RIGHT: (0, 1),
}
direction = directions[curses.KEY_RIGHT]
snake = [(0, i) for i in reversed(range(20))]
while True:
screen.erase()
# Draw the snake
screen.addstr(*snake[0], "@")
for segment in snake[1:]:
screen.addstr(*segment, "*")
# Move the snake
snake.pop()
snake.insert(0, tuple(map(sum, zip(snake[0], direction))))
# Change direction on arrow keystroke
direction = directions.get(screen.getch(), direction)
screen.refresh()
time.sleep(0.1)
if __name__ == "__main__":
curses.wrapper(main)
هذه ليست سوى لمحة عن الإمكانيات التي تتيحها وحدة curses. يمكنك استخدامها لتطوير ألعاب كهذه أو تطبيقات أكثر توجهًا للأعمال.
عيشها مع الرسوم المتحركة الرائعة
لا تقتصر فائدة الرسوم المتحركة على جعل واجهة المستخدم أكثر جاذبية فحسب، بل تُحسّن أيضًا تجربة المستخدم بشكل عام. فعندما تُقدّم ملاحظات مُبكرة للمستخدم، على سبيل المثال، سيعرف ما إذا كان برنامجك لا يزال يعمل أم أنه حان وقت إيقافه.
لتحريك النص في الطرفية، يجب أن تكون قادرًا على تحريك المؤشر بحرية. يمكنك القيام بذلك باستخدام إحدى الأدوات المذكورة سابقًا، مثل رموز الهروب ANSI أو مكتبة curses. ومع ذلك، هناك طريقة أبسط.
إذا كان من الممكن تقييد الرسوم المتحركة بسطر واحد من النص، فقد تكون مهتمًا بتسلسلين خاصين من أحرف الهروب:
- إرجاع العربة (r\): يحرك المؤشر إلى بداية السطر
- Backspace (\b): يحرك المؤشر حرفًا واحدًا إلى اليسار ويحذف الحرف الموجود في ذلك المكان
يعمل زر الرجوع (CR) بطريقة غير مُدمرة، دون الكتابة فوق نص مكتوب مسبقًا حتى تستدعي دالة ()print مرة أخرى. في المقابل، يحذف زر المسافة الخلفية الحرف الموجود على يسار المؤشر، مما يُزيله فعليًا من النص.
ستُلقي الآن نظرة على بعض الأمثلة. على سبيل المثال، قد ترغب في عرض عجلة دوارة للإشارة إلى أن شيئًا ما قيد التنفيذ، حتى لو لم تكن تعرف الوقت المتبقي لإنهائه بالضبط:

تستخدم العديد من أدوات سطر الأوامر هذه الحيلة أثناء تنزيل البيانات عبر الشبكة. يمكنك إنشاء رسوم متحركة بسيطة بتقنية إيقاف الحركة من سلسلة من الشخصيات التي تتناوب في دورة واحدة:
from itertools import cycle
from time import sleep
for frame in cycle(r"-\|/-\|/"):
print("\r", frame, sep="", end="", flush=True)
sleep(0.2)
تُطبع الحلقة الحرف التالي، ثم تُنقل المؤشر إلى بداية السطر وتُحل محل ما كان قبله دون إضافة سطر جديد. لا تُريد مساحة إضافية بين وسيطات الموضع، لذا يجب أن يكون الفاصل فارغًا. لاحظ أيضًا استخدام سلاسل بايثون الخام نظرًا لوجود أحرف الشرطة المائلة العكسية في النص الحرفي.
عندما تعرف الوقت المتبقي أو نسبة إكمال المهمة، ستتمكن من عرض شريط تقدم متحرك:

أولاً، عليك حساب عدد الوسوم المراد عرضها وعدد المساحات الفارغة المراد إضافتها. بعد ذلك، امسح السطر وأنشئ الشريط من الصفر:
from time import sleep
def progress(percent=0, width=30):
left = width * percent // 100
right = width - left
print("\r[", "#" * left, " " * right, "]",
f" {percent:.0f}%",
sep="", end="", flush=True)
for i in range(101):
progress(i)
sleep(0.1)
كما كان من قبل، يقوم كل طلب تحديث بإعادة طلاء السطر بأكمله ليعكس التقدم الحالي.
لاحظ وجود مكتبات خارجية فعّالة تتيح لك عرض أشرطة التقدم هذه في الطرفية. تُعد مكتبة tqdm خيارًا شائعًا لتتبع التقدم بسهولة. إذا كنت بحاجة إلى أدوات نصية أكثر تطورًا، فإن مكتبات مثل Rich وTextual خيارات رائعة.
صنع الأصوات باستخدام ()print
إذا كنتَ كبيرًا بما يكفي لتتذكر أجهزة الكمبيوتر المزودة بمكبر صوت، فلا بد أنك تتذكر أيضًا صوت الصفير المميز، والذي يُستخدم غالبًا للإشارة إلى وجود مشاكل في الأجهزة. لم تكن هذه الأجهزة تُصدر أي أصوات إضافية، ومع ذلك بدت ألعاب الفيديو أفضل بكثير بفضله.
لا يزال بإمكانك اليوم الاستفادة من هذا مكبر الصوت الصغير، ولكن من المرجح أن حاسوبك المحمول لم يكن مزودًا به. في هذه الحالة، يمكنك تفعيل محاكاة جرس الطرفية في واجهة المستخدم، بحيث يُصدر صوت تحذير النظام بدلاً من ذلك.
اذهب واكتب هذا الأمر لترى ما إذا كان الجهاز الطرفي الخاص بك قادرًا على تشغيل الصوت:
$ echo -e "\a"
عادةً ما يطبع هذا نصًا، لكن علامة -e تُمكّن من تفسير رموز الإفلات العكسية. كما ترى، هناك تسلسل إفلات مخصص \a، وهو اختصار لكلمة “تنبيه”، يُخرج رمز جرس خاصًا. تُصدر بعض المحطات صوتًا عند رؤيته.
وبالمثل، يمكنك طباعة هذا الحرف في بايثون. ربما في حلقة لتكوين لحن. مع أنها نغمة واحدة فقط، يمكنك تغيير مدة التوقفات بين التكرارات المتتالية. يبدو هذا حلاً مثاليًا لتشغيل شفرة مورس!
القواعد هي التالية:
- يتم ترميز الحروف باستخدام تسلسل من رموز النقطة (·) والشرطة (-).
- النقطة هي وحدة واحدة من الزمن.
- الشرطة هي ثلاث وحدات زمنية.
- يتم فصل الرموز الفردية في الرسالة بوحدة زمنية واحدة.
- يتم فصل رموز الحرفين المتجاورين بمسافة ثلاث وحدات زمنية.
- يتم فصل رموز كلمتين متجاورتين بمسافة سبع وحدات زمنية.
وفقًا لهذه القواعد، يمكنك “طباعة” إشارة SOS إلى أجل غير مسمى بالطريقة التالية:
# ...
while True:
dot(); symbol_space()
dot(); symbol_space()
dot(); letter_space()
dash(); symbol_space()
dash(); symbol_space()
dash(); letter_space()
dot(); symbol_space()
dot(); symbol_space()
dot(); word_space()
باستخدام الفاصلة المنقوطة (;)، يمكنك وضع عدة عبارات بايثون في سطر واحد. مع أن هذا يُحسّن سهولة القراءة في هذا المثال تحديدًا، إلا أنه من الأفضل عادةً اتباع أساليب التنسيق القياسية للوضوح.
يمكنك تنفيذ الوظائف الفردية التي تظهر في المثال أعلاه في عشرة أسطر فقط من التعليمات البرمجية:
from time import sleep
speed = 0.1
def signal(duration, symbol):
sleep(duration)
print(symbol, end="", flush=True)
dot = lambda: signal(speed, "·\a")
dash = lambda: signal(3*speed, "−\a")
symbol_space = lambda: signal(speed, "")
letter_space = lambda: signal(3*speed, "")
word_space = lambda: signal(7*speed, " ")
# ...
ربما يمكنكِ تطوير أداة سطر أوامر لترجمة النصوص إلى شفرة مورس؟ نأمل أن يكون تحديًا ممتعًا ومفيدًا!
طرق تصحيح الأخطاء في بايثون
في هذا القسم، ستُلقي نظرة على الأدوات المتاحة لتصحيح الأخطاء في بايثون، بدءًا من دالة ()print البسيطة، مرورًا بوحدة logging، ووصولًا إلى مُصحِّح أخطاء متكامل. بعد قراءته، ستتمكن من تحديد الأنسب لكل حالة.
ملاحظة: تصحيح الأخطاء هو عملية البحث عن الأسباب الجذرية للأخطاء أو العيوب في البرامج بعد اكتشافها، واتخاذ خطوات لإصلاحها.
التتبع
يُعرف التتبع أيضًا باسم تصحيح أخطاء الطباعة أو تصحيح أخطاء الإنسان البدائي، وهو أبسط أشكال استكشاف أخطاء البرامج وإصلاحها. ورغم أنه قديم الطراز بعض الشيء، إلا أنه لا يزال فعالًا وله استخداماته.
الفكرة هي متابعة مسار تنفيذ البرنامج حتى يتوقف فجأةً، أو يُعطي نتائج غير صحيحة، لتحديد التعليمات الدقيقة المتعلقة بالمشكلة. يمكنك تحقيق ذلك عن طريق إدراج أوامر طباعة بعلامات خاصة لجذب انتباهك في أماكن مختارة بعناية.
ألق نظرة على هذا المثال، الذي يظهر خطأ التقريب:
>>> def average(numbers):
... if len(numbers) > 0:
... return sum(numbers) / len(numbers)
...
>>> average([0.2, 0.1])
0.15000000000000002
بالنسبة لقيمتي الإدخال، 0.2 و0.1، تُرجع الدالة نتيجة قريبة من القيمة المتوقعة 0.15، ولكن مع إزاحة طفيفة. للتحقق من مصدر هذا الاختلاف، يمكنك إدخال استدعاءات ()print في مواقع رئيسية داخل الدالة، لتتبع الحسابات الوسيطة:
>>> def average(numbers):
... print("debug1:", numbers)
... if len(numbers) > 0:
... print("debug2:", sum(numbers))
... return sum(numbers) / len(numbers)
...
>>> average([0.2, 0.1])
debug1: [0.2, 0.1]
debug2: 0.30000000000000004
0.15000000000000002
الآن، يمكنك أن ترى بوضوح أن الدالة تحسب مجموع الأرقام المدخلة بشكل غير صحيح. ويعود ذلك إلى عدم دقة حسابات الفاصلة العائمة في الحواسيب. تذكر أن الأرقام تُخزَّن في صيغة ثنائية. القيمة العشرية 0.1 تُمثَّل في صيغة ثنائية لا نهائية، ويتم تقريبها.
يُمكن لتتبع حالة المتغيرات في مراحل مختلفة من الخوارزمية أن يُعطيك فكرة عن مكان المشكلة. هذه الطريقة بسيطة وسهلة الاستخدام، وتناسب معظم لغات البرمجة. كما أنها تُمثل تمرينًا رائعًا في عملية التعلم.
من ناحية أخرى، بمجرد إتقان تقنيات أكثر تقدمًا، يصعب التراجع عنها، لأنها تتيح لك اكتشاف الأخطاء بشكل أسرع. التتبع عملية يدوية شاقة، وقد يسمح بتسرب المزيد من الأخطاء. تستغرق دورة البناء والنشر وقتًا. بعد ذلك، عليك أن تتذكر حذف جميع استدعاءات ()print التي أجريتها بدقة دون المساس بالاستدعاءات الأصلية عن طريق الخطأ.
علاوة على ذلك، يتطلب منك إجراء تغييرات في الكود، وهو أمر ليس ممكنًا دائمًا. ربما تقوم بتصحيح أخطاء تطبيق يعمل على خادم ويب بعيد أو تحاول تشخيص مشكلة ما بعد المعالجة. أحيانًا، لا يمكنك الوصول إلى النتائج القياسية.
وهنا تحديدًا تبرز أهمية التسجيل، إذ يتيح لك التقاط معلومات مفصلة حول تنفيذ التطبيق دون الحاجة إلى تعديل الكود مباشرةً. ستستكشف هذه التقنية لاحقًا.
التسجيل
تخيل للحظة أنك تدير موقعًا للتجارة الإلكترونية. في أحد الأيام، اتصل بك عميل غاضب يشكو من فشل معاملة، قائلاً إنه فقد أمواله. يدّعي أنه حاول شراء بعض المنتجات، لكن في النهاية، حدث خطأ غامض منعه من إتمام الطلب. ومع ذلك، عندما راجع حسابه البنكي، وجد المال مفقودًا.
أنت تعتذر بصدق وتعيد المبلغ، ولكنك لا تريد تكرار هذا الأمر مستقبلًا. كيف يمكنك تصحيح هذا الخطأ؟ لو كان لديك أثر لما حدث، ويفضل أن يكون ذلك في شكل قائمة زمنية للأحداث مع سياقها.
عند قيامك بتصحيح أخطاء الطباعة، فكّر في تحويلها إلى رسائل سجل دائمة. قد يفيدك هذا في مثل هذه الحالات، عندما تحتاج إلى تحليل مشكلة بعد حدوثها، في بيئة لا يمكنك الوصول إليها.
تتوفر أدوات متطورة لتجميع السجلات والبحث فيها، ولكن في أبسط صورها، يمكنك اعتبار السجلات ملفات نصية. ينقل كل سطر معلومات مفصلة حول حدث ما في نظامك. عادةً، لا تحتوي هذه السجلات على معلومات تعريف شخصية، مع أنه في بعض الحالات، قد يكون ذلك إلزاميًا بموجب القانون.
فيما يلي تفصيل لسجل السجل النموذجي:
[2019-06-14 15:18:34,517][DEBUG][root][MainThread] Customer(id=123) logged out
كما ترى، يتميز بهيكلية منظمة. بالإضافة إلى رسالة وصفية، هناك بعض الحقول القابلة للتخصيص التي توضح سياق الحدث. هنا، لديك التاريخ والوقت الدقيقين، ومستوى السجل، واسم المُسجل، واسم السلسلة.
تتيح لك مستويات السجل تصفية الرسائل بسرعة لتقليل التشويش. على سبيل المثال، إذا كنت تبحث عن خطأ، فلن ترغب في رؤية جميع التحذيرات أو رسائل التصحيح. من السهل تعطيل الرسائل أو تفعيلها في مستويات سجل معينة من خلال التهيئة، دون حتى لمس الكود.
باستخدام السجل، يمكنك فصل رسائل التصحيح عن المخرجات القياسية. تنتقل جميع رسائل السجل افتراضيًا إلى مسار الخطأ القياسي، والذي يمكن عرضه بألوان مختلفة. مع ذلك، يمكنك إعادة توجيه رسائل السجل إلى ملفات منفصلة، حتى للوحدات النمطية الفردية!
في كثير من الأحيان، قد يؤدي التسجيل الخاطئ إلى نفاد مساحة قرص الخادم. لتجنب ذلك، يمكنك إعداد خاصية تدوير السجلات، والتي ستحتفظ بملفات السجلات لمدة محددة، مثل أسبوع واحد، أو حتى تصل إلى حجم معين. مع ذلك، يُنصح دائمًا بأرشفة السجلات القديمة. تُلزم بعض اللوائح بالاحتفاظ ببيانات العملاء لعدة سنوات!
مقارنةً بلغات البرمجة الأخرى، يُعدّ تسجيل الدخول في بايثون أمرًا سهلاً، لأن وحدة تسجيل الدخول مُدمجة مع المكتبة القياسية. ما عليك سوى استيرادها وتكوينها بسطرين فقط من التعليمات البرمجية.
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
يمكنك استدعاء الدوال المحددة على مستوى الوحدة النمطية، والتي تكون مرتبطة بمسجل الجذر، ولكن الممارسة الشائعة هي الحصول على مسجل مخصص لكل ملف مصدر Python الخاص بك:
>>> logging.debug("This is a debug message from the root logger")
DEBUG:root:This is a debug message from the root logger
>>> logger = logging.getLogger(__name__)
>>> logger.debug("This is a debug message from the dedicated logger")
DEBUG:__main__:This is a debug message from the dedicated logger
ميزة استخدام مُسجِّلات مُخصَّصة هي أنها تُتيح لك تحكمًا أدق. عادةً ما تُسمَّى هذه المُسجِّلات باسم الوحدة التي عُرِّفت فيها من خلال المتغير __name__.
سبب أخير للانتقال من دالة ()print إلى التسجيل هو سلامة خيوط التنفيذ. في قسم قادم، ستلاحظ أن الأولى لا تعمل بشكل جيد مع خيوط التنفيذ المتعددة.
تصحيح الأخطاء
الحقيقة هي أن التتبع أو التسجيل لا يُعتبران تصحيحًا حقيقيًا للأخطاء. لإجراء تصحيح فعلي، تحتاج إلى مُصحِّح أخطاء، وهو أداة تُتيح لك القيام بما يلي:
- انتقل عبر الكود بطريقة تفاعلية.
- تعيين نقاط التوقف، بما في ذلك نقاط التوقف الشرطية.
- استبطان المتغيرات في الذاكرة.
- تقييم التعبيرات المخصصة في وقت التشغيل.
يُوزَّع مُصحِّح أخطاء بدائي يعمل في الطرفية، ويُسمَّى pdb اختصارًا لـ “مُصحِّح أخطاء بايثون”، كجزء من المكتبة القياسية. هذا يجعله متاحًا دائمًا، لذا قد يكون خيارك الوحيد لإجراء تصحيح الأخطاء عن بُعد. ولعل هذا سبب وجيه للتعرّف عليه.
مع ذلك، لا يأتي البرنامج مع واجهة رسومية، لذا قد يكون استخدام pdb صعبًا بعض الشيء. إذا لم تتمكن من تعديل الكود، فعليك تشغيل pdb كوحدة بايثون باستخدام الخيار -m وتمرير موقع البرنامج النصي كمعامل سطر أوامر:
$ python -m pdb my_script.py
يمكنك أيضًا إعداد نقطة توقف مباشرةً في الكود، مما سيوقف تنفيذ البرنامج النصي مؤقتًا وينقلك إلى مصحح الأخطاء. كانت الطريقة القديمة تتطلب خطوتين:
>>> import pdb
>>> pdb.set_trace()
> <python-input-1>(1)<module>()
(Pdb)
يُظهر هذا موجهًا تفاعليًا، قد يبدو مُخيفًا للوهلة الأولى. مع ذلك، لا يزال بإمكانك كتابة بايثون أصلي في هذه المرحلة لفحص حالة المتغيرات المحلية أو تعديلها. بالإضافة إلى ذلك، لا يوجد سوى عدد قليل من الأوامر الخاصة بمُصحح الأخطاء التي يُمكنك استخدامها لشرح الكود.
يمكنك أيضًا استدعاء دالة ()breakpoint المضمنة، والتي تقوم بنفس الشيء، ولكن بطريقة أكثر إحكاما ومع بعض الميزات الإضافية:
>>> def average(numbers):
... if len(numbers) > 0:
... breakpoint()
... return sum(numbers) / len(numbers)
...
>>> average([0.2, 0.1])
> <python-input-0>(3)average()
(Pdb)
من المرجح أنك ستستخدم مصحح أخطاء بصريًا مدمجًا مع محرر أكواد في أغلب الأحيان. يتميز PyCharm بمصحح أخطاء ممتاز يتميز بأداء عالٍ، ولكن ستجد العديد من بيئات التطوير المتكاملة (IDEs) البديلة المزودة بمصححات أخطاء، مدفوعة ومجانية.
تصحيح الأخطاء ليس الحل الأمثل. أحيانًا، يكون التسجيل أو التتبع حلاً أفضل. على سبيل المثال، غالبًا ما تنتج العيوب التي يصعب إعادة إنتاجها، مثل حالات التسابق، عن الاقتران الزمني. عند التوقف عند نقطة توقف، قد يُخفي هذا التوقف الطفيف في تنفيذ البرنامج المشكلة. يشبه الأمر مبدأ هايزنبرغ: لا يمكنك قياس وملاحظة خطأ في الوقت نفسه.
والخلاصة هي أن هذه الأساليب ليست متعارضة، بل إنها تكمل بعضها البعض.
استكشاف نظيرات الطباعة
الآن، أنت تعرف الكثير عن دالة ()print. لكن لن يكتمل الموضوع دون التطرق إلى نظائرها. بينما تُعنى دالة ()print بالمخرجات، هناك دوال ومكتبات للمدخلات.
جمع مدخلات المستخدم
يأتي بايثون مزودًا بدالة مدمجة لقبول مُدخلات المستخدم، تُسمى كما هو متوقع ()input. تقبل هذه الدالة البيانات من مسار الإدخال القياسي، والذي عادةً ما يكون لوحة المفاتيح:
>>> name = input("Enter your name: ")
Enter your name: John Doe
>>> print(name)
John Doe
تعيد هذه الدالة دائمًا سلسلة، لذا قد تحتاج إلى تحليلها وفقًا لذلك:
>>> try:
... age = int(input("How old are you? "))
... except ValueError:
... print(f"That's not a correct number!")
...
How old are you? eighteen
That's not a correct number!
معلمة المطالبة اختيارية تمامًا، لذا لن يظهر أي شيء إذا تخطيتها، ولكن ستظل الدالة تعمل:
>>> user_input = input()
Typing some text...
>>> print(user_input)
Typing some text...
ومع ذلك، فإن إضافة دعوة وصفية للعمل يجعل تجربة المستخدم أفضل بكثير.
التعامل مع المدخلات بشكل آمن
إن طلب كلمة مرور من المستخدم باستخدام دالة ()input فكرة سيئة، لأنها ستظهر كنص عادي أثناء كتابتها. في هذه الحالة، يُنصح باستخدام دالة ()getpass، التي تُخفي الأحرف المكتوبة. تُعرّف هذه الدالة في وحدة تحمل الاسم نفسه، وهي متوفرة أيضًا في المكتبة القياسية:
>>> from getpass import getpass
>>> password = getpass()
Password:
>>> print(password)
s3cret
تحتوي وحدة getpass على دالة أخرى للحصول على اسم المستخدم من متغير بيئة. قد تجدها مفيدة لملء استبيان مسبقًا:
>>> from getpass import getuser
>>> default_user = getuser()
>>> input(f"Enter your name ({default_user!r}): ") or default_user
Enter your name ('jdoe'):
'jdoe'
عندما لا يقدم المستخدم أي إدخال، فإنك تقوم بإجراء تقييم مختصر للتعبير المنطقي، والعودة إلى القيمة الافتراضية.
إضافة إمكانيات Readline
الآن، افترض أنك كتبت واجهة سطر أوامر تفهم ثلاثة تعليمات، بما في ذلك تعليمات لإضافة الأرقام:
print('Type "help", "exit", "add a [b [c ...]]"')
while True:
command, *arguments = input("~ ").split(" ")
if len(command) > 0:
if command.lower() == "exit":
break
elif command.lower() == "help":
print("This is help.")
elif command.lower() == "add":
print(sum(map(int, arguments)))
else:
print("Unknown command")
للوهلة الأولى، يبدو الأمر وكأنه مطالبة نموذجية عند تشغيله:
$ python calculator.py
Type "help", "exit", "add a [b [c ...]]"
~ add 1 2 3 4
10
~ aad 2 3
Unknown command
~ exit
$
لكن بمجرد ارتكاب خطأ ما ورغبتك في إصلاحه، ستجد أن أيًا من مفاتيح الوظائف لا يعمل كما هو متوقع. على سبيل المثال، يؤدي الضغط على مفتاح السهم الأيسر إلى ظهور ^[[D على الشاشة بدلاً من تحريك المؤشر للخلف:
$ python calculator.py
Type "help", "exit", "add a [b [c ...]]"
~ aad^[[D
لإصلاح هذه المشكلة، يمكنك استخدام وحدة readline من مكتبة بايثون القياسية. يكفي استيراد هذه الوحدة فقط من أعلى النص البرمجي:
import readline
# ...
هذا كل شيء! بفضل ذلك، لن تعمل مفاتيح الأسهم فحسب، بل ستتمكن أيضًا من البحث في السجل الدائم لأوامرك المخصصة، واستخدام الإكمال التلقائي، وتعديل السطر باستخدام الاختصارات:
$ python calculator.py
Type "help", "exit", "add a [b [c ...]]"
(reverse-i-search)`a': add 1 2 3 4
مع ذلك، قد يكون التعامل مع المدخلات القياسية باستخدام الدوال الأصلية في بايثون محدودًا في بعض الأحيان. لحسن الحظ، تتوفر العديد من الحزم الخارجية التي توفر أدوات أكثر تطورًا.
تقييم مكتبات الطرف الثالث
تتوفر العديد من حزم بايثون الخارجية التي تتيح بناء واجهات رسومية معقدة لجمع البيانات من المستخدم. من بين ميزاتها:
- التنسيق والتصميم المتقدم
- التحليل الآلي والتحقق من صحة بيانات المستخدم وتطهيرها
- أسلوب إعلاني لتحديد التخطيطات
- الإكمال التلقائي التفاعلي
- أدوات محددة مسبقًا مثل قوائم المراجعة أو القوائم
- دعم الماوس
- سجل قابل للبحث للأوامر المكتوبة
- تمييز بناء الجملة
شرح هذه الأدوات خارج نطاق هذا الدرس، ولكن قد ترغب بتجربتها بنفسك. بالإضافة إلى الأدوات الغنية والنصية المذكورة، يمكنك الاطلاع على ما يلي:
أنت الآن مُسلَّحٌ بمعرفةٍ وافيةٍ حول دالة ()print في بايثون والعديد من المواضيع ذات الصلة. لقد رأيتَ كيف تعمل هذه الدالة من الداخل، واستكشفتَ جميع عناصرها الرئيسية من خلال أمثلةٍ عملية.
الآن وقد عرفت كل هذا، يمكنك إنشاء برامج تفاعلية تتواصل مع المستخدمين أو تُنتج بيانات بتنسيقات ملفات شائعة. ستتمكن من تشخيص مشاكل برمجتك بسرعة وحماية نفسك منها. وأخيرًا، ستعرف كيفية تطبيق لعبة الثعبان الكلاسيكية.
إذا كنت لا تزال متعطشًا لمزيد من المعلومات، أو لديك أسئلة، أو تريد فقط مشاركة أفكارك، فلا تتردد في التواصل معنا في قسم التعليقات أدناه.
يمكنك طباعة سطر فارغ في بايثون باستدعاء دالة ()print دون أي وسيطات. يؤدي هذا إلى إنشاء سطر جديد، يظهر كسطر فارغ على الشاشة.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.
يُحدد مُعامل sep في دالة ()print الفاصل المُدرج بين عدة وسيطات. افتراضيًا، يكون الفاصل مسافة، ولكن يُمكنك تغييره إلى أي سلسلة نصية، بما في ذلك السلسلة الفارغة أو الأحرف الخاصة مثل أسطر الإضافة.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.
لمنع إضافة سطر جديد في نهاية استدعاء ()print، اضبط معلمة النهاية على سلسلة فارغة (“”). يسمح هذا لاستدعاءات ()print اللاحقة بالاستمرار على نفس السطر.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.
يمكنك إعادة توجيه مُخرَج دالة ()print إلى ملف باستخدام مُعامل file. افتح الملف في وضع الكتابة أو الإضافة، ثم مرره إلى دالة ()print باستخدام مُعامل الكلمة المفتاحية file.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.