نموذج كتابة البرمجة في بايثون – استخدام فحص الأنواع بفعالية

منذ إصدار Python 3.5، تحاول وحدة الكتابة بالأنواع في Python، التي تسمى “typing”، توفير طريقة لتوجيه أنواع البيانات لمساعدة المفحّصين الثابتين وأدوات التحليل في تنبؤ الأخطاء بدقة.

نظرًا لضرورة عمل Python على تحديد نوع الكائنات أثناء وقت التشغيل، يصبح أحيانًا من الصعب جدًا على المطورين معرفة ما يحدث بالضبط في الشيفرة.

حتى المفحّصون الخارجيون للأنواع مثل بيئة تطوير PyCharm لا تنتج أفضل النتائج؛ حيث تتوقع الأخطاء بشكل صحيح فقط في حوالي 50٪ من الأحيان، وفقًا لهذه الإجابة على StackOverflow.

يحاول Python التخفيف من هذه المشكلة من خلال إدخال ما يُعرف بـ “توجيه الأنواع” (التعليق بالنوع) لمساعدة المفحصين الخارجيين في تحديد أي أخطاء. هذه طريقة جيدة لتلميح نوع الكائن(ات) المستخدمة، أثناء وقت التجميع نفسه للتأكد من عمل المفحصين بشكل صحيح.

هذا يجعل شيفرات Python أكثر قراءة وقوة أيضًا للقراء الآخرين!

ملاحظة: هذا لا يقوم بالتحقق الفعلي من الأنواع أثناء وقت التجميع. إذا كان الكائن الفعلي المُرجَع لم يكن من نفس النوع المُلمَح، فلن يحدث أي خطأ في التجميع. هذا هو السبب في استخدام مفحّصي الأنواع الخارجيين، مثل mypy، لتحديد أي أخطاء في الأنواع.


لاستخدام وحدة الكتابة (typing) بشكل فعّال، يُفضل استخدام فاحص/محلل نوع خارجي للتحقق من تطابق الأنواع الثابتة. أحد أشهر فواحص الأنواع المستخدمة بشكل واسع في Python هو mypy، لذا أوصي بتثبيته قبل قراءة بقية المقال.

لقد قمنا بالفعل بشرح أساسيات فحص الأنواع في Python. يمكنك الاطلاع على هذا المقال أولاً.

سنستخدم mypy كفاحص للأنواع الثابتة في هذا المقال، ويمكن تثبيته عبر:

pip3 install mypy

يمكنك تشغيل mypy على أي ملف Python للتحقق مما إذا كانت الأنواع متطابقة. هذا كما لو كنت تقوم بـ ‘ترجمة’ كود Python.

mypy program.py

بعد تصحيح الأخطاء، يمكنك تشغيل البرنامج بشكل طبيعي باستخدام:

python program.py

الآن بعد توفيرنا للمتطلبات الأساسية، دعونا نحاول استخدام بعض ميزات الوحدة.


تلميحات الأنواع / تعليقات الأنواع

على الوظائف

يمكننا توضيح الوظيفة لتحديد نوع العائد وأنواع معاملاتها.

def print_list(a: list) -> None:
    print(a)

يُبلغ هذا النوع المدقق (mypy في حالتي) أن لدينا وظيفة print_list()، التي ستأخذ قائمة (list) كمعامل وتعيد None.

def print_list(a: list) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

لنقم بتشغيل هذا على مدقق الأنواع mypy أولاً:

vijay@JournalDev:~ $ mypy printlist.py 
printlist.py:5: error: Argument 1 to "print_list" has incompatible type "int"; expected "List[Any]"
Found 1 error in 1 file (checked 1 source file)

كما هو متوقع، نحصل على خطأ؛ لأن السطر رقم ٥ يحتوي على معامل من نوع int، بدلاً من list.

على المتغيرات

منذ Python 3.6، يمكننا أيضًا توضيح أنواع المتغيرات، مذكرين النوع. ولكن هذا ليس إلزاميًا إذا أردت تغيير نوع المتغير قبل عودة الوظيفة.

# يوضح 'radius' ليكون عائدًا من نوع float
radius: float = 1.5

# يمكننا توضيح متغير بدون تعيين قيمة!
sample: int

# يوضح 'area' ليعيد float
def area(r: float) -> float:
    return 3.1415 * r * r


print(area(radius))

# طباعة جميع التوضيحات للوظيفة باستخدام
# القاموس '__annotations__'
print('Dictionary of Annotations for area():', area.__annotations__)

ناتج mypy:

vijay@JournalDev: ~ $ mypy find_area.py && python find_area.py
Success: no issues found in 1 source file
7.068375
Dictionary of Annotations for area(): {'r': <class 'float'>, 'return': <class 'float'>}

هذه الطريقة الموصى بها لاستخدام mypy، بتقديم التعليقات النوعية أولاً، قبل استخدام فاحص الأنواع.


أسماء الأنواع

يوفر لنا وحدة typing أسماء الأنواع، والتي تُعرف عن طريق تعيين نوع للاسم المستعار.

from typing import List

# الفيكتور هو قائمة من القيم العائمة
Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

a = scale(scalar=2.0, vector=[1.0, 2.0, 3.0])
print(a)

الناتج

vijay@JournalDev: ~ $ mypy vector_scale.py && python vector_scale.py
Success: no issues found in 1 source file
[2.0, 4.0, 6.0]

في المقتطف أعلاه، Vector هو اسم مستعار، الذي يمثل قائمة من القيم العائمة. يمكننا تلميح النوع في اسم مستعار، وهو ما يقوم به البرنامج أعلاه.

القائمة الكاملة للأسماء المستعارة المقبولة معروضة هنا.

لنلقي نظرة على مثال آخر، الذي يتحقق فيه كل زوج مفتاح:قيمة في قاموس ويتحقق مما إذا كانوا يتطابقون مع التنسيق اسم:بريد إلكتروني.

from typing import Dict
import re

# إنشاء اسم مستعار يُسمى 'ContactDict'
ContactDict = Dict[str, str]

def check_if_valid(contacts: ContactDict) -> bool:
    for name, email in contacts.items():
        # التحقق مما إذا كانت الاسماء والبريد الإلكتروني سلاسل نصية
        if (not isinstance(name, str)) or (not isinstance(email, str)):
            return False
        # التحقق من وجود بريد إلكتروني [email protected]
        if not re.match(r"[a-zA-Z0-9\._\+-]+@[a-zA-Z0-9\._-]+\.[a-zA-Z]+$", email):
            return False
    return True


print(check_if_valid({'vijay': '[email protected]'}))
print(check_if_valid({'vijay': '[email protected]', 123: '[email protected]'}))

الناتج من mypy

vijay@JournalDev:~ $ mypy validcontacts.py 
validcontacts.py:19: error: Dict entry 1 has incompatible type "int": "str"; expected "str": "str"
Found 1 error in 1 file (checked 1 source file)

هنا، نحصل على خطأ تركيبي ثابت في وقت الترجمة في mypy، لأن معلمة name في قاموسنا الثاني هي عدد صحيح (123). وبالتالي، الأسماء المستعارة هي طريقة أخرى لفرض فحص دقيق للأنواع من mypy.


أنشئ أنواع البيانات المحددة بواسطة NewType()

يمكننا استخدام وظيفة NewType() لإنشاء أنواع جديدة محددة من قبل المستخدم.

from typing import NewType

# أنشئ نوع مستخدم جديد يسمى 'StudentID' ويتكون من
# عدد صحيح
StudentID = NewType('StudentID', int)
sample_id = StudentID(100)

سيعامل فحص النوع الثابت النوع الجديد كما لو كان فئة فرعية من النوع الأصلي. وهذا مفيد للمساعدة في اكتشاف الأخطاء المنطقية.

from typing import NewType

# أنشئ نوع مستخدم جديد يسمى 'StudentID'
StudentID = NewType('StudentID', int)

def get_student_name(stud_id: StudentID) -> str:
    return str(input(f'Enter username for ID #{stud_id}:\n'))

stud_a = get_student_name(StudentID(100))
print(stud_a)

# هذا غير صحيح!!
stud_b = get_student_name(-1)
print(stud_b)

الناتج من mypy

vijay@JournalDev:~ $ mypy studentnames.py  
studentnames.py:13: error: Argument 1 to "get_student_name" has incompatible type "int"; expected "StudentID"
Found 1 error in 1 file (checked 1 source file)

النوع أي

هذا نوع خاص، يُبلغ فحص النوع الثابت (mypy في حالتي) أن كل نوع متوافق مع هذا الكلمة الرئيسية.

لنأخذ في اعتبارنا وظيفتنا القديمة print_list()، مقبولة الآن مع وسائط من أي نوع.

from typing import Any

def print_list(a: Any) -> None:
    print(a)

print_list([1, 2, 3])
print_list(1)

الآن، لن يظهر أي أخطاء عند تشغيل mypy.

vijay@JournalDev:~ $ mypy printlist.py && python printlist.py
Success: no issues found in 1 source file
[1, 2, 3]
1

سيتم استخدام Any تلقائيًا لجميع الوظائف بدون نوع إرجاع أو نوع معلمة.

def foo(bar):
    return bar

# سيعامل فاحص النوع الثابت المذكور أعلاه
# على أنه يحمل نفس التوقيع كما في:
def foo(bar: Any) -> Any:
    return bar

بالتالي يمكنك استخدام أي لمزج الشفرة المكتوبة بشكل ثابت وديناميكي.


الختام

في هذا المقال، تعلمنا عن وحدة الـ typing في Python، والتي تكون مفيدة جدًا في سياق فحص الأنواع، مما يتيح لفاحصي الأنواع الخارجيين مثل mypy الإبلاغ بدقة عن أي أخطاء.

هذا يوفر لنا وسيلة لكتابة شفرة بشكل ثابت في Python، والذي هو لغة ديناميكية الطبيعة بتصميمها!


المراجع


Source:
https://www.digitalocean.com/community/tutorials/python-typing-module