اذهب إلى المحتوى

تعرّفنا في المقال السابق على كيفية الحصول على الوقت باستخدام الوحدتين time و datetime في لغة بايثون، وسنوضّح في هذا المقال كيفية كتابة البرامج التي تشغّل Launch برامجًا أخرى وفقًا لجدولٍ زمني محدّد باستخدام وحدتي subprocess و threading، فأسرع طريقة لكتابة البرامج في أغلب الأحيان هي الاستفادة من التطبيقات التي كتبها أشخاص آخرون مسبقًا.

تعدّد الخيوط Multithreading

لنفترض أنك تريد جدولة شيفرتك البرمجية لتشغيلها بعد تأخير محدّد أو في وقت معيَّن، حيث يمكنك إضافة شيفرة برمجية كما يلي في بداية برنامجك:

import time, datetime

startTime = datetime.datetime(2029, 10, 31, 0, 0, 0)
while datetime.datetime.now() < startTime:
    time.sleep(1)

print('Program now starting on Halloween 2029')
--snip--

تحدّد الشيفرة البرمجية السابقة وقت البدء في 31 من الشهر العاشر من عام 2029، وتستمر في استدعاء الدالة time.sleep(1)‎ حتى الوصول إلى وقت البدء، ولا يستطيع برنامجك فعل أيّ شيء أثناء انتظار انتهاء حلقة استدعاءات time.sleep()‎، ويبقى متوقفًا حتى يوم الهالوين من عام 2029، لأن برامج بايثون افتراضيًا لها خيط Thread تنفيذ واحد.

يمكنك فهم ما هو خيط التنفيذ من خلال تذكّر ما ناقشناه في مقالٍ سابق حول التحكم في التدفق عندما تخيّلت تنفيذ برنامجٍ ما على أنه وضع إصبعك على سطرٍ من الشيفرة البرمجية في برنامجك والانتقال إلى السطر التالي أو المكان التي ترسله تعليمة التحكم في التدفق، حيث يحتوي البرنامج ذو الخيط الواحد Single-threaded على إصبع واحد فقط، ولكن يحتوي البرنامج متعدد الخيوط Multithreaded على أصابع متعددة. يستمر كل إصبع في التحرك إلى السطر التالي من الشيفرة البرمجية كما تحدِّده تعليمات التحكم في التدفق، ولكن يمكن أن تكون الأصابع في أماكن مختلفة في البرنامج لتنفيذ أسطر مختلفة من الشيفرة البرمجية في الوقت ذاته. لاحظ أن جميع البرامج التي مرّت معنا حتى الآن ذات خيط واحد.

يمكنك تنفيذ الشيفرة البرمجية المؤجَّلة أو المجدولة في خيط منفصل باستخدام وحدة بايثون threading بدلًا من أن تنتظر شيفرتك البرمجية بأكملها انتهاء الدالة time.sleep()‎، حيث سيتوقف الخيط المنفصل مؤقتًا عند استدعاءات time.sleep، بينما يمكن لبرنامجك تنفيذ أشياء أخرى في الخيط الأصلي.

ننشئ خيطًا منفصلًا من خلال إنشاء كائن Thread أولًا باستخدام استدعاء الدالة threading.Thread()‎. إذًا لندخِل الشيفرة البرمجية التالية في ملفٍ جديد ونحفظه بالاسم threadDemo.py:

   import threading, time
   print('Start of program.')

 def takeANap():
       time.sleep(5)
       print('Wake up!')

 threadObj = threading.Thread(target=takeANap)
 threadObj.start()

   print('End of program.')

عرّفنا في الشيفرة البرمجية السابقة الدالة التي نريد استخدامها في خيطٍ جديد ➊، واستدعينا الدالة threading.Thread()‎ ومرّرنا لها وسيط الكلمات المفتاحية target=takeANap ➋ لإنشاء كائن Thread، وهذا يعني أن الدالة التي نريد استدعاءها في الخيط الجديد هي takeANap()‎. لاحظ أن وسيط الكلمات المفتاحية Keyword Argument الذي هو target=takeANap وليس target=takeANap()‎، لأنك تريد تمرير الدالة takeANap()‎ كوسيط، ولا تريد استدعاءها وتمرير قيمتها المُعادة.

خزّنا الكائن Thread الذي أنشأته الدالة threading.Thread()‎ في المتغير threadObj، ثم استدعينا الدالة threadObj.start()‎ ➌ لإنشاء الخيط الجديد والبدء في تنفيذ الدالة المستهدفة في الخيط الجديد. سيكون الناتج كما يلي عند تشغيل هذا البرنامج:

Start of program.
End of program.
Wake up!

قد يكون الخرج السابق مربكًا بعض الشيء، حيث إذا كانت التعليمة print('End of program.')‎ هي السطر الأخير من البرنامج، فقد تعتقد أنه آخر شيء سيُطبَع، ولكن تُشغَّل الدالة المستهدفة للمتغير threadObj في خيط تنفيذ جديد عند استدعاء الدالة threadObj.start()‎، لذا تظهر العبارة Wake up!‎ في النهاية. فكّر في الأمر كإصبعٍ ثانٍ يظهر في بداية الدالة takeANap()‎، حيث يستمر الخيط الرئيسي في تنفيذ التعليمة print('End of program.')‎، بينما يتوقف الخيط الجديد الذي كان ينفّذ استدعاء time.sleep(5)‎ مؤقتًا لمدة 5 ثوانٍ، ويطبع عبارة ‎'Wake up!'‎ بعد أن يستيقظ من غفوته لمدة 5 ثوان، ثم يعود من الدالة takeANap()‎، وبالتالي فإن عبارة ‎'Wake up!'‎ هي آخر ما يطبعه البرنامج زمنيًا.

ينتهي البرنامج عادةً عند تشغيل السطر الأخير من الشيفرة البرمجية في الملف أو عند استدعاء الدالة sys.exit()‎، ولكن يحتوي البرنامج threadDemo.py على خيطين هما: الأول هو الخيط الأصلي الذي بدأ في بداية البرنامج وينتهي بعد التعليمة print('End of program.')‎، والخيط الثاني الذي ينشأ عند استدعاء الدالة threadObj.start()‎ ويبدأ عند بداية الدالة takeANap()‎ وينتهي بعد العودة من الدالة takeANap()‎.

لن ينتهي برنامج بايثون حتى تنتهي جميع خيوطه. لاحظ أن الخيط الثاني لا يزال ينفّذ الاستدعاء time.sleep(5)‎ عند تشغيل البرنامج threadDemo.py بالرغم من انتهاء الخيط الأصلي.

تمرير الوسطاء إلى الدالة المستهدفة للخيط

إذا أخذت الدالة المستهدفة التي تريد تشغيلها في الخيط الجديد وسطاءً، فيمكنك تمرير وسطائها إلى الدالة threading.Thread()‎. لنفترض مثلًا أنك تريد تشغيل استدعاء الدالة print()‎ التالية في خيطها الخاص:

>>> print('Cats', 'Dogs', 'Frogs', sep=' & ')
Cats & Dogs & Frogs

يحتوي استدعاء الدالة print()‎ السابق على ثلاث وسطاء عادية هي: 'Cats' و 'Dogs' و 'Frogs' ووسيط كلمات مفتاحية واحد هو sep=' & '‎، حيث يمكن تمرير الوسطاء العادية كقائمة إلى وسيط الكلمات المفتاحية args في الدالة threading.Thread()‎، ويمكن تحديد وسيط الكلمات المفتاحية كقاموس لوسيط الكلمات المفتاحية kwargs في الدالة threading.Thread()‎.

لندخِل الآن ما يلي في الصدفة التفاعلية:

>>> import threading
>>> threadObj = threading.Thread(target=print, args=['Cats', 'Dogs', 'Frogs'],
kwargs={'sep': ' & '})
>>> threadObj.start()
Cats & Dogs & Frogs

نتأكد من تمرير الوسطاء 'Cats' و 'Dogs' و 'Frogs' إلى الدالة print()‎ في الخيط الجديد من خلال تمرير args=['Cats', 'Dogs', 'Frogs']‎ إلى الدالة threading.Thread()‎، ونتأكد من تمرير وسيط الكلمات المفتاحية sep=' & '‎ إلى الدالة print()‎ في الخيط الجديد من خلال تمرير kwargs={'sep': '& '}‎ إلى الدالة threading.Thread()‎. يؤدي استدعاء الدالة threadObj.start()‎ إلى إنشاء خيط جديد لاستدعاء الدالة print()‎، وستمرّر إليها 'Cats' و 'Dogs' و 'Frogs' كوسطاء والقيمة ' & ' لوسيط الكلمات المفتاحية sep.

تُعَد الطريقة التالية غير صحيحة لإنشاء الخيط الجديد الذي يستدعي الدالة print()‎:

threadObj = threading.Thread(target=print('Cats', 'Dogs', 'Frogs', sep=' & '))

تؤدي الطريقة السابقة إلى استدعاء الدالة print()‎ وتمرير القيمة المُعادة الخاصة بها كوسيط الكلمات المفتاحية target، حيث تكون القيمة المُعادة الخاصة بالدالة print()‎ دائمًا None، ولا تؤدي إلى تمرير الدالة print()‎ نفسها، لذا استخدم وسطاء الكلمات المفتاحية args و kwargs الخاصة بالدالة threading.Thread()‎ عند تمرير الوسطاء إلى دالة في خيطٍ جديد.

مشاكل التزامن Concurrency

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

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

تطبيق عملي: برنامج متعدد الخيوط لتنزيل قصص XKCD الهزلية Comics

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

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

الخطوة الأولى: تعديل البرنامج لاستخدام دالة

سيكون هذا البرنامج في أغلبه مشابهًا لشيفرة التنزيل البرمجية التي كتبناها في مقالٍ سابق، لذا سنتخطّى شرح requests وشيفرة المكتبة Beautiful Soup. التغييرات الرئيسية التي يجب أن تجريها هي استيراد الوحدة threading وإنشاء الدالة downloadXkcd()‎ التي تأخذ أرقام البداية والنهاية للقصة الهزلية كمعاملاتٍ لها.

سيؤدّي استدعاء الدالة downloadXkcd(140, 280)‎ مثلًا إلى تكرار شيفرة التنزيل لتنزيل القصص الهزلية 140 و 141 و 142 وحتى القصة الهزلية 279. سيستدعي كلُّ خيط تنشئه الدالةَ downloadXkcd()‎ ويمرّر مجالًا مختلفًا من القصص الهزلية لتنزيلها.

أضِف الشيفرة البرمجية التالية إلى برنامج threadedDownloadXkcd.py الخاص بك:

   #! python3
   # threadedDownloadXkcd.py - تنزيل قصص‫ XKCD الهزلية باستخدام خيوط متعددة

   import requests, os, bs4, threading
 os.makedirs('xkcd', exist_ok=True)    # ‫تخزين القصص الهزلية في المجلد ‎./xkcd

 def downloadXkcd(startComic, endComic):
      for urlNumber in range(startComic, endComic):
           # تنزيل الصفحة
           print('Downloading page https://xkcd.com/%s...' % (urlNumber))
         res = requests.get('https://xkcd.com/%s' % (urlNumber))
           res.raise_for_status()

         soup = bs4.BeautifulSoup(res.text, 'html.parser')

           # البحث عن عنوان‫ URL للصورة الهزلية
         comicElem = soup.select('#comic img')
           if comicElem == []:
               print('Could not find comic image.')
           else:
             comicUrl = comicElem[0].get('src')
               # تنزيل الصورة
               print('Downloading image %s...' % (comicUrl))
             res = requests.get('https:' + comicUrl)
               res.raise_for_status()

               # حفظ الصورة في المجلد‫ ‎./xkcd
               imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)),
'wb')
               for chunk in res.iter_content(100000):
                   imageFile.write(chunk)
               imageFile.close()

# إنشاء وبدء كائنات الخيط‫ Thread
# انتظار انتهاء جميع الخيوط

نستورد الوحدات التي نحتاجها، ثم ننشئ مجلدًا لتخزين القصص الهزلية ➊، ونبدأ بتعريف الدالة downloadxkcd()‎ ➋، ثم نمر ضمن حلقة على جميع الأرقام الموجودة في المجال المحدَّد ➌ وننزّل كل صفحة ➍. نستخدم المكتبة Beautiful Soup للبحث في شيفرة HTML لكل صفحة ➎ والعثور على الصورة الهزلية ➏، حيث إذا لم نعثر على صورة هزلية في الصفحة، فإننا نطبع رسالة، وإلّا سنحصل على عنوان URL للصورة ➐ وننزّلها ➑. أخيرًا، نحفظ الصورة في المجلد الذي أنشأناه.

الخطوة الثانية: إنشاء وبدء الخيوط

عرّفنا الدالة downloadxkcd()‎ وسننشئ الآن الخيوط المتعددة التي يستدعي كل منها الدالة downloadxkcd()‎ لتنزيل مجالات مختلفة من القصص الهزلية من موقع XKCD. أضِف الشيفرة البرمجية التالية إلى البرنامج threadedDownloadXkcd.py بعد تعريف الدالة downloadxkcd()‎:

#! python3
# threadedDownloadXkcd.py - تنزيل قصص‫ XKCD الهزلية باستخدام خيوط متعددة

--snip--

# ‫إنشاء وبدء كائنات الخيط Thread
downloadThreads = []             # قائمة بجميع كائنات الخيط‫ Thread
for i in range(0, 140, 10):    # التكرار 14 مرة وإنشاء 14 خيطًا
    start = i
    end = i + 9
    if start == 0:
        start = 1 # لا توجد قصة هزلية رقمها 0، لذا اضبط المتغير على القيمة 1
    downloadThread = threading.Thread(target=downloadXkcd, args=(start, end))
    downloadThreads.append(downloadThread)
    downloadThread.start()

ننشئ أولًا قائمة فارغة downloadThreads، والتي ستساعدنا على تعقّب العديد من كائنات Thread التي سننشئها، ثم نبدأ حلقة for، حيث ننشئ في كل تكرار من هذه الحلقة كائن Thread باستخدام الدالة threading.Thread()‎، ونلحِق هذا الكائن بالقائمة، ونستدعي التابع start()‎ لبدء تشغيل الدالة downloadXkcd()‎ في الخيط الجديد. تضبط حلقة for المتغير i على القيم من 0 إلى 140 بخطوة مقدارها 10، لذا يجب ضبط المتغير i على القيمة 0 في التكرار الأول، وعلى القيمة 10 في التكرار الثاني، وعلى القيمة 20 في التكرار الثالث، وإلخ. نمرّر الوسيط args=(start, end)‎ إلى الدالة threading.Thread()‎، لذا سيكون الوسيطان المُمرَّران إلى الدالة downloadXkcd()‎ هما 1 و9 في التكرار الأول، و10 و19 في التكرار الثاني، و20 و29 في التكرار الثالث، وإلخ.

سينتقل الخيط الرئيسي إلى التكرار التالي من حلقة for وينشئ الخيط التالي عند استدعاء التابع start()‎ الخاص بالكائن Thread ويبدأ الخيط الجديد بتشغيل الشيفرة البرمجية الموجودة ضمن الدالة downloadXkcd()‎.

الخطوة الثالثة: انتظار انتهاء جميع الخيوط

يتحرك الخيط الرئيسي كالمعتاد بينما تنزّل الخيوطُ الأخرى التي أنشأناها القصصَ الهزلية، ولكن لنفترض أن هناك بعض التعليمات البرمجية التي لا تريد تشغيلها في الخيط الرئيسي حتى يكتمل تنفيذ جميع الخيوط الأخرى، حيث سيتوقّف استدعاء التابع join()‎ الخاص بالكائن Thread حتى انتهاء هذا الخيط. يمكن للخيط الرئيسي استدعاء التابع join()‎ على كلٍّ من الخيوط الأخرى باستخدام حلقة for للتكرار على كافة كائنات Thread الموجودة في القائمة downloadThreads. أضِف الآن ما يلي إلى نهاية برنامجك:

#! python3
# threadedDownloadXkcd.py - تنزيل قصص‫ XKCD الهزلية باستخدام خيوط متعددة

--snip--

# الانتظار حتى انتهاء جميع الخيوط
for downloadThread in downloadThreads:
    downloadThread.join()
print('Done.')

لن تُطبَع السلسلة النصية ‎'Done.'‎ حتى إعادة جميع استدعاءات التابع join()‎، حيث إذا اكتمل كائن Thread عند استدعاء التابع join()‎ الخاص به، فسيعود التابع مباشرةً ببساطة. إذا أردتَ توسيع هذا البرنامج باستخدام شيفرة برمجية تُشغَّل فقط بعد تنزيل كافة القصص الهزلية، فيمكنك وضع شيفرتك البرمجية الجديدة مكان السطر print('Done.')‎.

تشغيل Launching برامج أخرى من برنامج بايثون

يمكن لبرنامج بايثون الخاص بك بدء برامج أخرى على حاسوبك باستخدام الدالة Popen()‎ الموجودة في الوحدة المُدمَجة subprocess، حيث يرمز الحرف P في اسم هذه الدالة إلى العملية Process. إذا كان لديك نسخ Instances متعددة من تطبيق مفتوح، فستُعَد كلّ نسخة من هذه النسخ عمليةً منفصلة للبرنامج نفسه، فمثلًا إذا فتحتَ نوافذ متعددة لمتصفح الويب في الوقت نفسه، فإن كلّ نافذة من تلك النوافذ هي عملية مختلفة لبرنامج متصفح الويب. يوضّح الشكل التالي مثالًا لعمليات آلة حاسبة متعددة مفتوحة في وقتٍ واحد:

01 000087

ست عمليات مُشغَّلة لبرنامج الآلة الحاسبة نفسه

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

إذا أردتَ بدء برنامج خارجي من سكربت بايثون الخاص بك، فمرّر اسم ملف البرنامج إلى الدالة subprocess.Popen()‎. انقر بزر الفأرة الأيمن على عنصر القائمة "ابدأ Start" الخاص بالتطبيق وحدّد الخيار "خصائص Properties" لعرض اسم ملف التطبيق في نظام ويندوز، وانقر مع الضغط على مفتاح CTRL على التطبيق وحدّد الخيار "إظهار محتويات الحزمة Show Package Contents" للعثور على مسار الملف القابل للتنفيذ في نظام ماك macOS. ستعيد بعد ذلك الدالة Popen()‎ مباشرةً، وضع في بالك أن البرنامج الذي شغّلناه لا يعمل في الخيط نفسه لبرنامج بايثون الخاص بك.

أدخِل ما يلي في الصدفة التفاعلية على حاسوبٍ يعمل بنظام ويندوز:

>>> import subprocess
>>> subprocess.Popen('C:\\Windows\\System32\\calc.exe')
<subprocess.Popen object at 0x0000000003055A58>

أدخِل ما يلي على نظام يعمل بنظام أوبنتو لينكس Ubuntu Linux:

>>> import subprocess
>>> subprocess.Popen('/snap/bin/gnome-calculator')
<subprocess.Popen object at 0x7f2bcf93b20>

تختلف العملية قليلًا على نظام ماك macOS، لذا اطّلع على قسم "فتح الملفات باستخدام التطبيقات الافتراضية" من هذا المقال لمزيد من التفاصيل.

تكون القيمة المُعادة هي كائن Popen الذي له تابعان مفيدان هما: poll()‎ و wait()‎، حيث يمكنك التفكير في التابع poll()‎ بأنك تسأل سائقك "هل وصلنا؟" مرارًا وتكرارًا حتى الوصول إلى وِجهتك، ويعيد هذا التابع القيمة None إذا كانت العملية لا تزال قيد التشغيل في وقت استدعاء هذا التابع. إذا أُنهي البرنامج، فسيعيد العدد الصحيح لرمز الخروج exit code الخاص بالعملية، حيث يُستخدَم رمز الخروج للإشارة إلى أن العملية انتهت بدون أخطاء (رمز الخروج 0) أو ما إذا قد تسبّب خطأٌ ما في إنهاء العملية (رمز خروج غير صفري قيمته 1 عادةً، ولكنه قد يختلف اعتمادًا على البرنامج).

يشبه التابع wait()‎ الانتظار حتى يصل السائق إلى وِجهتك، حيث يوقِف هذا التابع التنفيذ حتى تنتهي العملية التي شغّلناها، ويُعَد ذلك مفيدًا إذا أردتَ أن يتوقف برنامجك مؤقتًا حتى ينتهي المستخدم من البرنامج الآخر. القيمة المُعادة من التابع wait()‎ هي العدد الصحيح لرمز الخروج الخاص بالعملية.

أدخِل ما يلي في الصدفة التفاعلية على نظام ويندوز، ولاحظ أن استدعاء التابع wait()‎ سيوقِف التنفيذ حتى إنهاء برنامج الرسام MS Paint الذي شغّلناه:

   >>> import subprocess
 >>> paintProc = subprocess.Popen('c:\\Windows\\System32\\mspaint.exe')
 >>> paintProc.poll() == None
   True
 >>> paintProc.wait() # ‫لن يعود حتى إغلاق برنامج الرسام MS Paint
   0
   >>> paintProc.poll()
   0

فتحنا في المثال السابق عملية برنامج الرسام MS Paint ➊، وتحقّقنا مما إذا كان التابع poll()‎ يعيد القيمة None ➋ بينما لا تزال العملية قيد التشغيل، حيث ينبغي أن يكون ذلك صحيحًا لأنها لا تزال قيد التشغيل. أغلقنا بعد ذلك برنامج الرسام MS Paint واستدعينا التابع wait()‎ للعملية المنتهية ➌. سيعيد الآن التابعان wait()‎ و poll()‎ القيمة 0، مما يشير إلى أن العملية قد انتهت بدون أخطاء.

ملاحظة: إذا شغّلت برنامج الآلة الحاسبة calc.exe على نظام ويندوز 10 باستخدام الدالة subprocess.Popen()‎، فستلاحظ أن التابع wait()‎ يعيد مباشرةً على عكس برنامج الرسام mspaint.exe بالرغم من أن تطبيق الآلة الحاسبة لا يزال قيد التشغيل، والسبب أن برنامج الآلة الحاسبة calc.exe يطلِق تطبيق الآلة الحاسبة ثم يغلق نفسه مباشرةً. يُعَد برنامج الآلة الحاسبة الخاص بنظام ويندوز "تطبيق متجر مايكروسوفت موثوق به"، ولن ندخل في تفاصيله في هذا المقال، ولكن يكفي أن نقول أنه يمكن تشغيل البرامج بعدة طرقٍ خاصة بالتطبيق وبنظام التشغيل.

تمرير وسطاء سطر الأوامر إلى الدالة Popen()‎

يمكنك تمرير وسطاء سطر الأوامر إلى العمليات التي تنشئها باستخدام الدالة Popen()‎ من خلال تمرير قائمة تمثّل الوسيط الوحيد لهذه الدالة. ستكون السلسلة النصية الأولى في هذه القائمة هي اسم الملف التنفيذي للبرنامج الذي تريد تشغيله، وتكون جميع السلاسل النصية اللاحقة وسطاء سطر الأوامر التي تمرّرها إلى البرنامج عندما يبدأ. تمثّل هذه القائمة قيمة sys.argv للبرنامج الذي شغّلناه.

لا تستخدم معظم التطبيقات ذات واجهة المستخدم الرسومية Graphical User Interface -أو GUI اختصارًا- وسطاء سطر الأوامر على نطاق واسع كما تفعل البرامج المستندة إلى سطر الأوامر أو البرامج المستندة إلى الطرفية Terminal، ولكن تقبل معظم تطبيقات واجهة المستخدم الرسومية وسيطًا واحدًا للملف الذي ستفتحه التطبيقات مباشرةً عندما تبدأ. إذا استخدمتَ نظام ويندوز، فأنشئ مثلًا ملفًا نصيًا بسيطًا بالاسم C:\Users\Al\hello.txt، ثم أدخِل ما يلي في الصدفة التفاعلية:

>>> subprocess.Popen(['C:\\Windows\\notepad.exe', 'C:\\Users\Al\\hello.txt'])
<subprocess.Popen object at 0x00000000032DCEB8>

لن يؤدي ذلك إلى تشغيل تطبيق المفكرة Notepad فحسب، بل سيؤدي أيضًا إلى فتح الملف C:\Users\Al\hello.txt مباشرةً.

أدوات مجدول المهام Task Scheduler و Launchd و cron

إذا كنت خبيرًا في استخدام الحاسوب، فقد تكون على دراية بأداة مجدول المهام Task Scheduler على ويندوز أو أداة launchd على نظام ماك macOS أو أداة الجدولة cron على نظام لينكس، حيث تتيح لك هذه الأدوات المُوثَّقة جيدًا والموثوقة جدولةَ تشغيل التطبيقات في أوقات محددة.

يوفّر عليك استخدام المجدول المُدمَج مع نظام تشغيلك كتابةَ الشيفرة البرمجية الخاصة بالتحقق من ساعتك لجدولة برامجك، ولكن يمكنك استخدام الدالة time.sleep()‎ إذا أردتَ فقط إيقاف البرنامج مؤقتًا لفترة وجيزة، أو يمكنك تكرار شيفرتك البرمجية حتى تاريخ ووقت محدَّدين واستدعاء time.sleep(1)‎ في كل مرة خلال الحلقة بدلًا من استخدام المجدول الخاص بنظام التشغيل.

فتح المواقع باستخدام شيفرة بايثون

يمكن للدالة webbrowser.open()‎ تشغيل متصفح ويب من برنامجك إلى موقع ويب محدّد بدلًا من فتح تطبيق المتصفح باستخدام الدالة subprocess.Popen()‎.

تشغيل سكربتات بايثون الأخرى

يمكنك تشغيل سكربت بايثون من شيفرة بايثون أخرى مثل أيّ تطبيق آخر، فما عليك فعله سوى تمرير ملف بايثون python.exe القابل للتنفيذ إلى الدالة Popen()‎ واسم ملف سكربت ‎.py الذي تريد تشغيله بوصفه وسيطًا لهذه الدالة، فمثلًا سيشغّل ما يلي السكربت hello.py الذي كتبناه في مقالٍ سابق:

>>> subprocess.Popen(['C:\\Users\\<YOUR USERNAME>\\AppData\\Local\\Programs\\
Python\\Python38\\python.exe', 'hello.py'])
<subprocess.Popen object at 0x000000000331CF28>

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

 C:\Users\<YOUR USERNAME>\AppData\Local\Programs\Python\Python38\python.exe، وعلى نظام ماك macOS هو ‎/Library/Frameworks/Python.framework/Versions/3.8/bin/python3،

وعلى نظام لينكس هو:

 ‎/usr/bin/python3.8

إذا شغّل برنامج بايثون الخاص بك برنامجَ بايثون آخر، فسيُشغَّل كلا البرنامجين ضمن عمليات منفصلة ولن يتمكن كلّ منهما من مشاركة متغيرات الآخر على عكس استيراد برنامج بايثون بوصفه وحدة Module.

فتح الملفات باستخدام التطبيقات الافتراضية

سيؤدّي النقر المزدوج على ملف ‎.txt على حاسوبك إلى تشغيل التطبيق المرتبط بلاحقة الملف ‎.txt تلقائيًا، وسيكون لحاسوبك ارتباطات متعددة بامتدادات الملفات المُعَدّة مسبقًا، ويمكن لبايثون أيضًا فتح الملفات بهذه الطريقة باستخدام الدالة Popen()‎.

يحتوي كل نظام تشغيل على برنامج يطبّق شيئًا يعادل النقر المزدوج على ملف مستند لفتحه، وهو البرنامج start على نظام ويندوز، والبرنامج open على نظام ماك macOS، والبرنامج see على نظام أوبنتو لينكس. أدخِل مثلًا ما يلي في الصدفة التفاعلية مع تمرير 'start' أو 'open' أو 'see' إلى الدالة Popen()‎ اعتمادًا على نظامك:

>>> fileObj = open('hello.txt', 'w')
>>> fileObj.write('Hello, world!')
12
>>> fileObj.close()
>>> import subprocess
>>> subprocess.Popen(['start', 'hello.txt'], shell=True)

كتبنا في مثالنا السابق عبارة Hello, world!‎ في ملف hello.txt جديد، ثم استدعينا الدالة Popen()‎، ومرّرنا إليها قائمة تحتوي على اسم البرنامج (وهو 'start' في مثالنا لنظام ويندوز) واسم الملف. مرّرنا أيضًا وسيط الكلمات المفتاحية shell=True، ويُعَد هذا الوسيط مطلوبًا فقط على نظام ويندوز. يعرِف نظام التشغيل جميع ارتباطات الملفات ويمكنه معرفة أنه يجب عليه تشغيل برنامج المفكرة Notepad.exe مثلًا للتعامل مع الملف hello.txt.

يُستخدَم البرنامج open لفتح ملفات المستندات والبرامج على نظام ماك macOS. إذًا لندخِل ما يلي في الصدفة التفاعلية إذا كان حاسوبك عليه نظام ماك، ويجب أن يفتح تطبيق الآلة الحاسبة:

>>> subprocess.Popen(['open', '/Applications/Calculator.app/'])
<subprocess.Popen object at 0x10202ff98>

تطبيق عملي: برنامج بسيط للعد التنازلي

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

إليك الخطوات العامة التي سيفعلها برنامجك:

  1. العد التنازلي من العدد 60.
  2. تشغيل ملف صوتي alarm.wav عندما يصل العد التنازلي إلى الصفر.

وبالتالي يجب أن تطبّق شيفرتك البرمجية الخطوات التالية:

  1. التوقف مؤقتًا لمدة ثانية واحدة بين عرض كل عدد في العد التنازلي من خلال استدعاء الدالة time.sleep()‎.
  2. استدعاء الدالة subprocess.Popen()‎ لفتح الملف الصوتي باستخدام التطبيق الافتراضي.

افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد واحفظه بالاسم countdown.py.

الخطوة الأولى: العد التنازلي

يتطلب هذا البرنامج الوحدة time لاستخدام الدالة time.sleep()‎ ووحدة subprocess لاستخدام الدالة subprocess.Popen()‎. أدخِل الآن الشيفرة البرمجية التالية واحفظ الملف بالاسم countdown.py:

   #! python3
   # countdown.py - سكربت بسيط للعد التنازلي

   import time, subprocess

 timeLeft = 60
   while timeLeft > 0:
     print(timeLeft, end='')
     time.sleep(1)
     timeLeft = timeLeft - 1

  # تشغيل الملف الصوتي في نهاية العد التنازلي

استوردنا الوحدتين time و subprocess، ثم أنشأنا متغيرًا بالاسم timeLeft ليحتفظ بعدد الثواني المتبقية في العد التنازلي ➊. يمكن أن تبدأ عند القيمة 60، أو يمكنك تغيير القيمة إلى ما تريده، أو يمكنك ضبطها من وسيط سطر الأوامر.

يمكنك في حلقة while عرض العدد المتبقي ➋، والتوقف مؤقتًا لمدة ثانية واحدة ➌، ثم إنقاص المتغير timeLeft بمقدار 1 ➍ قبل بدء الحلقة مرة أخرى، وستستمر الحلقة في التكرار طالما أن المتغير timeLeft أكبر من 0، ثم سينتهي العد التنازلي.

الخطوة الثانية: تشغيل الملف الصوتي

توجد وحدات خارجية لتشغيل الملفات الصوتية بتنسيقات مختلفة، ولكن الطريقة السريعة والسهلة لذلك هي تشغيل أيّ تطبيق يستخدمه المستخدم لتشغيل الملفات الصوتية. سيكتشف نظام التشغيل من امتداد الملف ‎.wav‎ التطبيقَ الذي يجب تشغيله لتشغيل الملف، ويمكن أن يكون ملف ‎.wav‎ له أحد تنسيقات الملفات الصوتية الأخرى مثل ‎.mp3 أو ‎.ogg.

يمكنك استخدام أيّ ملف صوتي موجود على حاسوبك لتشغيله في نهاية العد التنازلي، أو يمكنك تنزيل alarm.wav.

أضِف ما يلي إلى شيفرتك البرمجية:

#! python3
# countdown.py - سكربت بسيط للعد التنازلي

import time, subprocess

--snip--

# تشغيل الملف الصوتي في نهاية العد التنازلي
subprocess.Popen(['start', 'alarm.wav'], shell=True)

سيعمل الملف alarm.wav (أو الملف الصوتي الذي تختاره) بعد انتهاء الحلقة لإعلام المستخدم بانتهاء العد التنازلي. تأكّد من تضمين 'start' في القائمة التي تمرّرها إلى الدالة Popen()‎ وتمرير وسيط الكلمات المفتاحية shell=True على نظام ويندوز، وتأكّد من تمرير 'open' بدلًا من 'start' وإزالة shell=True على نظام ماك macOS.

يمكنك حفظ ملف نصي في مكانٍ ما مع رسالة مثل الرسالة "انتهى وقت الاستراحة!" بدلًا من تشغيل ملف صوتي، حيث يمكنك استخدام الدالة Popen()‎ لفتحه في نهاية العد التنازلي، مما يؤدي إلى إنشاء نافذة منبثقة تحتوي على رسالة بفعالية، أو يمكنك استخدام الدالة webbrowser.open()‎ لفتح موقع ويب محدَّد في نهاية العد التنازلي. يمكن أن يكون منبه برنامج العد التنازلي الخاص بك أيّ شيء تريده على عكس بعض تطبيقات العد التنازلي المجانية التي تجدها على الإنترنت.

أفكار لبرامج مماثلة

يمثّل العد التنازلي تأخيرًا بسيطًا قبل مواصلة تنفيذ البرنامج، ويمكنك استخدامه أيضًا لتطبيقات وميزات أخرى كما يلي:

  • استخدام الدالة time.sleep()‎ لمنح المستخدم فرصة الضغط على الاختصار CTRL-C لإلغاء إجراءٍ ما مثل حذف الملفات. يمكن لبرنامجك طباعة رسالة "اضغط على CTRL-C للإلغاء Press CTRL-C to cancel"، ثم معالجة أيّ استثناءات KeyboardInterrupt باستخدام تعليمات try و except.
  • يمكنك استخدام كائنات timedelta مع العد التنازلي طويل الأجل لقياس عدد الأيام والساعات والدقائق والثواني حتى نقطة ما (مثل عيد ميلاد أو ذكرى سنوية) في المستقبل.

مشاريع للتدريب

حاول كتابة البرامج التي تؤدي المهام التي سنوضّحها فيما يلي لكسب خبرة عملية أكبر من خلال استخدام المعلومات التي حصلتَ عليها من المقال السابق وهذا المقال.

برنامج المؤقت الزمني ولكن بمظهر أجمل

وسّع مشروع المؤقت الزمني Stopwatch الذي ناقشناه في المقال السابق من خلال استخدام توابع السلاسل النصية rjust()‎ و ljust()‎ "لتجميل" الخرج، فبدلًا من الخرج التالي:

Lap #1: 3.56 (3.56)
Lap #2: 8.63 (5.07)
Lap #3: 17.68 (9.05)
Lap #4: 19.11 (1.43)

سيبدو الخرج كما يلي:

Lap # 1:   3.56 (  3.56)
Lap # 2:   8.63 (  5.07)
Lap # 3:  17.68 (  9.05)
Lap # 4:  19.11 (  1.43)

لاحظ أنك ستحتاج إلى نسخٍ نوعها سلاسل نصية من المتغيرات lapNum و lapTime و totalTime التي نوعها أعداد صحيحة وأعداد عشرية لاستدعاء توابع السلاسل النصية عليها. استخدم بعد ذلك وحدة pyperclip التي وضّحناها في مقالٍ سابق لنسخ الخرج النصي إلى الحافظة حتى يتمكّن المستخدم من لصق الخرج بسرعة في ملف نصي أو بريد إلكتروني.

برنامج لتنزيل القصص الهزلية المجدول على الويب

اكتب برنامجًا يفحص مواقع الويب الخاصة بالعديد من القصص الهزلية على الويب وينزّل الصور تلقائيًا عند تحديث القصص الهزلية عن آخر زيارة للبرنامج. يمكن لمجدول نظام تشغيلك -مثل Scheduled Tasks على ويندوز وlaunchd على نظام ماكmacOS، وcron على نظام لينكس- تشغيل برنامج بايثون الخاص بك مرة واحدة يوميًا، ويمكن لبرنامج بايثون نفسه تنزيل القصة الهزلية ثم نسخها إلى سطح المكتب بحيث يسهل العثور عليها، مما يحرّرك من الاضطرار إلى التحقق من موقع الويب بنفسك للتأكد من تحديثه.

الخلاصة

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

يمكن لبرامج بايثون الخاصة بك تشغيل تطبيقات أخرى باستخدام الدالة subprocess.Popen()‎، حيث يمكن تمرير وسطاء سطر الأوامر إلى استدعاء هذه الدالة لفتح مستندات محددة باستخدام التطبيق. يمكنك أيضًا استخدام برنامج start أو open أو see مع الدالة Popen()‎ لاستخدام ارتباطات الملفات الخاصة بحاسوبك لمعرفة التطبيق الذي سيُستخدَم لفتح مستند تلقائيًا، ويمكن لبرامج بايثون الخاصة بك الاستفادة من إمكاناتها لتلبية احتياجات الأتمتة الخاصة بك باستخدام التطبيقات الأخرى الموجودة على حاسوبك.

ترجمة -وبتصرُّف- للقسم Scheduling Tasks, and Launching Programs من مقال Keeping Time, Scheduling Tasks, and Launching Programs لصاحبه Al Sweigart.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...