الآن كل مطور تقريبا على دراية بمفهوم "عدم التزامن" في البرمجة. في عصر تكون فيه المنتجات المعلوماتية مطلوبة للغاية بحيث يتعين عليها معالجة عدد كبير من الطلبات في وقت واحد والتفاعل أيضًا بالتوازي مع مجموعة كبيرة من الخدمات الأخرى - بدون برمجة غير متزامنة - في أي مكان. تبين أن الحاجة كانت كبيرة جدًا لدرجة أنه تم إنشاء لغة منفصلة ، والتي تتمثل السمة الرئيسية لها (بالإضافة إلى كونها أضيق الحدود) في العمل المحسن والمريح للغاية مع رمز متوازي / متزامن ، وهو Golang . على الرغم من حقيقة أن المقال لا يتعلق به على الإطلاق ، إلا أنني غالبًا ما أقوم بإجراء مقارنات وأشير إليه. لكن هنا في بايثون، والتي ستتم مناقشتها في هذه المقالة - هناك بعض المشاكل التي سأصفها وأعرض حلًا لأحدها. إذا كنت مهتمًا بهذا الموضوع - من فضلك ، تحت القط.
لقد حدث أن لغتي المفضلة ، التي أعمل بها ، وأنفذ مشاريع الحيوانات الأليفة ، وحتى الراحة والاسترخاء ، هي Python . أنا مفتون بجمالها وبساطتها إلى ما لا نهاية ، ووضوحها ، والتي خلفها ، بمساعدة أنواع مختلفة من السكر النحوي ، هناك فرص هائلة لوصف مقتضب لأي منطق تقريبًا يستطيع الخيال البشري القيام به. حتى أنني قرأت في مكان ما أن بايثون تسمى لغة فائقة المستوى ، حيث يمكن استخدامها لوصف التجريدات التي قد تكون مشكلة للغاية لوصفها بلغات أخرى.
لكن هناك فارق بسيط واحد خطير - بايثونمن الصعب جدًا ملاءمتها للمفاهيم الحديثة للغة مع إمكانية تنفيذ منطق متوازي / متزامن. اللغة ، التي نشأت فكرتها في الثمانينيات وهي نفس عمر Java ، حتى وقت معين لم تكن تعني تنفيذ أي رمز بشكل تنافسي. إذا تطلبت JavaScript في البداية التزامن للعمل غير المحظور في المتصفح ، وكانت Golang لغة جديدة تمامًا مع فهم حقيقي للاحتياجات الحديثة ، فإن Python لم يكن لديها مثل هذه المهام من قبل.
هذا ، بالطبع ، رأيي الشخصي ، لكن يبدو لي أن بايثون متأخرة جدًا في تنفيذ عدم التزامن ، منذ ظهور مكتبة غير متزامنة مضمنةكان ، بالأحرى ، رد فعل على ظهور تطبيقات أخرى لتنفيذ التعليمات البرمجية المتزامنة لبيثون. في الأساس ، تم تصميم asyncio لدعم التطبيقات الحالية ولا يحتوي فقط على تنفيذ حلقة الأحداث الخاصة به ، ولكن أيضًا غلاف للمكتبات غير المتزامنة الأخرى ، وبالتالي يقدم واجهة مشتركة لكتابة التعليمات البرمجية غير المتزامنة. و Python ، التي تم إنشاؤها في الأصل كلغة مقتضبة وقابلة للقراءة بسبب جميع العوامل المذكورة أعلاه ، عندما تصبح كتابة التعليمات البرمجية غير المتزامنة كومة من الزخارف والمولدات والوظائف. تم تصحيح الوضع قليلا بإضافة توجيهات خاصة المتزامن و ننتظر
لن أسردهم جميعًا وسأركز على واحد حاولت حله: هذا وصف للمنطق العام للتنفيذ غير المتزامن والمتزامن. على سبيل المثال ، إذا كنت أرغب في تشغيل دالة بالتوازي في Golang ، فأنا بحاجة فقط إلى استدعاء الوظيفة باستخدام الأمر go :
التنفيذ المتوازي للوظيفة في Golang
package main
import "fmt"
func function(index int) {
fmt.Println("function", index)
}
func main() {
for i := 0; i < 10; i++ {
go function(i)
}
fmt.Println("end")
}
ومع ذلك ، في Golang ، يمكنني تشغيل نفس الوظيفة بشكل متزامن:
تنفيذ تسلسلي للوظيفة في Golang
package main
import "fmt"
func function(index int) {
fmt.Println("function", index)
}
func main() {
for i := 0; i < 10; i++ {
function(i)
}
fmt.Println("end")
}
في Python ، تعتمد جميع coroutines (الوظائف غير المتزامنة) على المولدات ويحدث التبديل بينها أثناء استدعاء وظائف الحظر ، مما يعيد التحكم إلى حلقة الحدث باستخدام توجيه العائد . لأكون صريحًا ، لا أعرف كيف يعمل التزامن / التزامن في Golang ، لكنني لست مخطئًا إذا قلت إنه لا يعمل بالطريقة التي يعمل بها في Python . وعلى الرغم من الخلافات القائمة في الأجزاء الداخلية من تنفيذ Golang مترجم و سي بايثون مترجم وعدم جواز مقارنة التوازي / التزامن في نفوسهم، وأنا لا تزال تفعل هذا وتولي اهتماما عدم تنفيذ نفسها، ولكن لبناء الجملة. في بايثونلا يمكنني تولي وظيفة وتشغيلها بالتوازي / بالتزامن مع عامل واحد. لكي تعمل وظيفتي بشكل غير متزامن ، يجب أن أكتبها بوضوح غير متزامن قبل إعلانها ، وبعد ذلك لم تعد مجرد وظيفة ، إنها بالفعل coroutine. ولا يمكنني مزج مكالماتهم في نفس الكود بدون إجراءات إضافية ، لأن الوظيفة و coroutine في Python هما شيئان مختلفان تمامًا ، على الرغم من التشابه في الإعلان.
def func1(a, b):
func2(a + b)
await func3(a - b) # , await
كانت مشكلتي الرئيسية هي الحاجة إلى تطوير منطق يمكنه العمل بشكل متزامن وغير متزامن. مثال بسيط هو مكتبتي للتفاعل مع Instagram ، والتي تخليت عنها منذ فترة طويلة ، لكنني الآن تناولتها مرة أخرى (مما دفعني للبحث عن حل). أردت أن أنفذ فيه القدرة على العمل مع واجهة برمجة التطبيقات ليس فقط بشكل متزامن ، ولكن أيضًا بشكل غير متزامن ، ولم تكن هذه مجرد رغبة - عند جمع البيانات على الإنترنت ، يمكنك إرسال عدد كبير من الطلبات بشكل غير متزامن والحصول على إجابة لها جميعًا بشكل أسرع ، ولكن في نفس الوقت لا يتم جمع البيانات الضخمة دائما في حاجة. في الوقت الحالي ، تقوم المكتبة بتنفيذ ما يلي: للعمل مع Instagramهناك فئتان ، أحدهما للعمل المتزامن والآخر للعمل غير المتزامن. كل فئة لها نفس مجموعة الأساليب ، فقط في الطريقة الأولى تكون الطرق متزامنة ، وفي الثانية تكون غير متزامنة. كل طريقة تفعل الشيء نفسه - باستثناء كيفية إرسال الطلبات إلى الإنترنت. وفقط بسبب الاختلافات في إجراء حظر واحد ، اضطررت إلى تكرار المنطق بالكامل تقريبًا في كل طريقة. تبدو هكذا:
class WebAgent:
def update(self, obj=None, settings=None):
...
response = self.get_request(path=path, **settings)
...
class AsyncWebAgent:
async def update(self, obj=None, settings=None):
...
response = await self.get_request(path=path, **settings)
...
كل شيء آخر في طريقة التحديث وفي coroutine التحديث متطابق تمامًا. وكما يعلم الكثير من الناس ، فإن تكرار الكود يضيف الكثير من المشاكل ، خاصة عندما يتعلق الأمر بإصلاح الأخطاء والاختبار.
لقد كتبت مكتبة pySyncAsync الخاصة بي لحل هذه المشكلة . الفكرة هي كما يلي - بدلاً من الوظائف العادية و coroutines ، يتم تنفيذ المولد ، وسأطلق عليه في المستقبل قالبًا. من أجل تنفيذ قالب ، تحتاج إلى إنشائه كوظيفة عادية أو كوظيفة coroutine. عند تنفيذ القالب في اللحظة التي يحتاج فيها إلى تنفيذ كود غير متزامن أو متزامن داخل نفسه ، يقوم بإرجاع كائن استدعاء خاص باستخدام العائد، والتي تحدد ما يجب الاتصال به وما هي الحجج. اعتمادًا على كيفية إنشاء القالب - كدالة أو كوظيفة - بهذه الطريقة سيتم تنفيذ الطرق الموضحة في كائن الاستدعاء .
سأعرض مثالًا صغيرًا على نموذج يفترض القدرة على تقديم طلبات إلى google :
مثال على طلبات google باستخدام pySyncAsync
import aiohttp
import requests
import pysyncasync as psa
# google
# Call
@psa.register("google_request")
def sync_google_request(query, start):
response = requests.get(
url="https://google.com/search",
params={"q": query, "start": start},
)
return response.status_code, dict(response.headers), response.text
# google
# Call
@psa.register("google_request")
async def async_google_request(query, start):
params = {"q": query, "start": start}
async with aiohttps.ClientSession() as session:
async with session.get(url="https://google.com/search", params=params) as response:
return response.status, dict(response.headers), await response.text()
# 100
def google_search(query):
start = 0
while start < 100:
# Call , google_request
call = Call("google_request", query, start=start)
yield call
status, headers, text = call.result
print(status)
start += 10
if __name__ == "__main__":
#
sync_google_search = psa.generate(google_search, psa.SYNC)
sync_google_search("Python sync")
#
async_google_search = psa.generate(google_search, psa.ASYNC)
loop = asyncio.get_event_loop()
loop.run_until_complete(async_google_search("Python async"))
سأخبركم قليلاً عن الهيكل الداخلي للمكتبة. هناك فئة مدير يتم فيها تسجيل الوظائف و coroutines ليتم استدعاؤها باستخدام المكالمة . من الممكن أيضًا تسجيل القوالب ، لكن هذا اختياري. و مدير الطبقة ديها أساليب تسجل ، تولد، و القالب . تم استدعاء نفس الطرق في المثال أعلاه مباشرة من pysyncasync ، فقط استخدمت مثيلاً شاملاً لفئة Manager ، والذي تم إنشاؤه بالفعل في إحدى وحدات المكتبة النمطية. في الواقع، يمكنك إنشاء المثال الخاص بك واستدعاء السجل ، توليد و قالب أساليب منه.وبالتالي عزل المديرين عن بعضهم البعض ، على سبيل المثال ، إذا كان تعارض الأسماء ممكنًا.
تعمل طريقة التسجيل كديكور وتسمح لك بتسجيل وظيفة أو coroutine لمزيد من المكالمات من القالب. في السجل الديكور يقبل كحجة على الاسم الذي يتم تسجيل وظيفة أو coroutine في المدير. إذا لم يتم تحديد الاسم ، فسيتم تسجيل الوظيفة أو coroutine باسمها الخاص. تتيح لك
طريقة القالب تسجيل المولد كقالب في المدير. يعد هذا ضروريًا حتى تتمكن من الحصول على قالب بالاسم.
طريقة التوليديسمح لك بإنشاء وظيفة أو coroutine على أساس قالب. يتطلب الأمر وسيطين: الأول هو اسم القالب أو القالب نفسه ، والثاني هو "المزامنة" أو "غير المتزامن" - ما الذي يتم إنشاؤه للقالب - إلى دالة أو إلى coroutine. في الإخراج، و توليد طريقة يعطي وظيفة جاهزة أو coroutine.
سأقدم مثالاً على إنشاء قالب ، على سبيل المثال ، في coroutine:
def _async_generate(self, template):
async def wrapper(*args, **kwargs):
...
for call in template(*args, **kwargs):
callback = self._callbacks.get(f"{call.name}:{ASYNC}")
call.result = await callback(*call.args, **call.kwargs)
...
return wrapper
في الداخل ، يتم إنشاء coroutine ، والذي يتكرر ببساطة عبر المولد ويستقبل كائنات من فئة Call ، ثم يأخذ coroutine المسجل مسبقًا بالاسم (الاسم مأخوذ من المكالمة ) ، ويستدعيه بالحجج (التي يأخذها أيضًا من المكالمة ) ونتيجة تنفيذ هذا coroutine يخزن أيضًا في المكالمة .
كائنات فئة الاتصال هي مجرد حاويات لتخزين المعلومات حول ماذا وكيف تتصل وتسمح لك أيضًا بتخزين النتيجة في حد ذاتها. يمكن للغلاف أيضًا إرجاع نتيجة تنفيذ القالب ؛ لذلك ، يتم تغليف القالب في فئة مولد خاصة ، والتي لا تظهر هنا.
لقد حذفت بعض الفروق الدقيقة ، لكنني آمل أن أكون قد نقلت الجوهر بشكل عام.
لأكون صادقًا ، كتبت هذا المقال بدلاً من مشاركة أفكاري حول حل المشكلات باستخدام التعليمات البرمجية غير المتزامنة في Python.والأهم من ذلك ، الاستماع إلى آراء سكان خابراف. ربما سأصطدم بشخص ما في حل آخر ، ربما سيختلف شخص ما مع هذا التنفيذ المعين وسيخبرك كيف يمكنك تحسينه ، ربما سيخبرك شخص ما لماذا لا تكون هناك حاجة لمثل هذا الحل على الإطلاق ويجب ألا تخلط بين المتزامن و رمز غير متزامن ، رأي كل واحد منكم مهم جدًا بالنسبة لي. كما أنني لا أتظاهر بصحة كل تفكيري في بداية المقال. لقد فكرت على نطاق واسع في موضوع اللغات الأخرى وقد أكون مخطئًا ، بالإضافة إلى وجود احتمال أن أخلط بين المفاهيم ، من فضلك ، إذا لاحظت فجأة أي تناقضات - صفها في التعليقات. سأكون سعيدًا أيضًا إذا تم إجراء تعديلات على النحو وعلامات الترقيم.
وشكرا لكم على اهتمامكم بهذا الموضوع وعلى هذا المقال بالذات!