ما هي الاستثناءات؟ من الاسم الذي يتضح - تنشأ عند حدوث استثناء في البرنامج. قد تسأل لماذا الاستثناءات هي نمط مضاد ، وكيف تتعلق بالكتابة؟ حاولت
معرفة ذلك ، والآن أريد مناقشة هذا الأمر معك ، harazhiteli.
قضايا الاستثناء
من الصعب العثور على عيوب في ما تواجهه كل يوم. يحول العادة والعمى الخلل إلى ميزات ، ولكن دعونا نحاول أن ننظر إلى الاستثناءات بعقل مفتوح.
الاستثناءات يصعب اكتشافها
هناك نوعان من الاستثناءات: يتم إنشاء "صريح" عن طريق استدعاء
raise
مباشرة في التعليمات البرمجية التي تقرأها. يتم إخفاء "مخفي" في الوظائف المستخدمة ، والطبقات ، والأساليب.
المشكلة هي أن الاستثناءات "الخفية" يصعب بالفعل ملاحظتها. اسمحوا لي أن أريك مثالاً على وظيفة محض:
def divide(first: float, second: float) -> float: return first / second
تقسم الوظيفة ببساطة رقمًا واحدًا إلى آخر ، وتعيد
float
. يتم فحص الأنواع ويمكنك تشغيل شيء مثل هذا:
result = divide(1, 0) print('x / y = ', result)
هل لاحظت؟ في الواقع ، لن يصل تنفيذ البرنامج مطلقًا إلى
print
، لأن تقسيم 1 على 0 عملية مستحيلة ، سيؤدي إلى رفع
ZeroDivisionError
. نعم ، هذا الرمز آمن ، لكن لا يمكن استخدامه على أي حال.
لإشعار مشكلة محتملة حتى في مثل هذه التعليمات البرمجية البسيطة والقابلة للقراءة ، يحتاج المرء إلى تجربة. يمكن لأي شيء في Python التوقف عن العمل مع أنواع مختلفة من الاستثناءات: القسمة ، ومكالمات الوظائف ، و
int
، و
str
، والمولدات ، والتكرارات في الحلقات ، والوصول إلى السمات أو المفاتيح. حتى
raise something()
يمكن أن يسبب تعطل. علاوة على ذلك ، أنا لا أذكر عمليات الإدخال والإخراج.
ولن يتم دعم الاستثناءات المحددة في المستقبل القريب.
استعادة السلوك الطبيعي في المكان غير ممكن
ولكن على وجه التحديد لمثل هذه الحالة ، لدينا استثناءات. دعنا فقط نتعامل مع
ZeroDivisionError
الرمز آمنًا.
def divide(first: float, second: float) -> float: try: return first / second except ZeroDivisionError: return 0.0
الآن كل شيء على ما يرام. ولكن لماذا نعود 0؟ لماذا لا 1 أو
None
؟ بالطبع ، في معظم الحالات ، يكون الحصول على بلا أمرًا سيئًا تقريبًا (إن لم يكن أسوأ) كاستثناء ، ولكن لا يزال يتعين عليك الاعتماد على منطق العمل وخيارات استخدام الوظيفة.
ما الذي نتشاركه بالضبط؟ أرقام تعسفية ، أي وحدات محددة أو المال؟ ليس كل خيار يسهل التنبؤ به واستعادته. قد يتضح أن الاستخدام اللاحق لوظيفة واحدة سيكشف أن هناك حاجة إلى منطق استرداد مختلف.
الاستنتاج المحزن: حل كل مشكلة على حدة ، وفقًا لسياق الاستخدام المحدد.
لا توجد رصاصة فضية للتعامل مع
ZeroDivisionError
مرة واحدة وإلى الأبد. ونحن لا نتحدث عن إمكانية إدخال / إخراج معقدة مع الطلبات المتكررة والمهلة الزمنية.
ربما ليس من الضروري التعامل مع الاستثناءات بالضبط أين تنشأ؟ ربما مجرد إلقاءها في عملية تنفيذ التعليمات البرمجية - شخص ما سوف معرفة ذلك في وقت لاحق. ثم نحن مضطرون للعودة إلى الوضع الحالي.
عملية التنفيذ غير واضحة
حسنًا ، دعنا نأمل أن يقوم شخص آخر بالاستثناء وربما يتعامل معه. على سبيل المثال ، قد يطلب النظام من المستخدم تغيير القيمة المدخلة ، لأنه لا يمكن تقسيمها على 0. ويجب ألا تكون وظيفة
divide
مسؤولة بشكل صريح عن التعافي من الخطأ.
في هذه الحالة ، تحتاج إلى التحقق من المكان الذي اكتشفنا فيه الاستثناء. بالمناسبة ، كيفية تحديد بالضبط حيث سيتم معالجتها؟ هل من الممكن أن تذهب إلى المكان الصحيح في الكود؟ اتضح أنه لا ،
هذا مستحيل .
لا يمكن تحديد سطر التعليمات البرمجية الذي سيتم تنفيذه بعد طرح الاستثناء. يمكن التعامل مع أنواع الاستثناءات المختلفة بخيارات مختلفة
except
، ويمكن
تجاهل بعض الاستثناءات. ويمكنك رمي استثناءات إضافية في الوحدات النمطية الأخرى التي سيتم تنفيذها في وقت سابق ، وبشكل عام ، سيتم كسر كل المنطق.
افترض أن هناك اثنين من مؤشرات الترابط المستقلة في تطبيق: مؤشر ترابط عادي يتم تشغيله من أعلى إلى أسفل ، وخيط مؤشر ترابط استثناء يعمل كما يشاء. كيفية قراءة وفهم هذا الرمز؟
فقط مع تشغيل المصحح في وضع "التقاط كافة الاستثناءات".
استثناءات ، مثل
goto
سيئة السمعة ، المسيل للدموع هيكل البرنامج.
الاستثناءات ليست حصرية
دعونا نلقي نظرة على مثال آخر: رمز الوصول إلى واجهة برمجة تطبيقات HTTP المعتادة:
import requests def fetch_user_profile(user_id: int) -> 'UserProfile': """Fetches UserProfile dict from foreign API.""" response = requests.get('/api/users/{0}'.format(user_id)) response.raise_for_status() return response.json()
في هذا المثال ، حرفيًا ، يمكن أن يحدث كل شيء خطأ فيما يلي قائمة جزئية بالأخطاء المحتملة:
- قد لا تكون الشبكة متاحة ولن يتم تنفيذ الطلب على الإطلاق.
- قد لا يعمل الخادم.
- قد يكون الخادم مشغولًا جدًا ، ستحدث مهلة.
- قد يتطلب الخادم المصادقة.
- قد لا يحتوي API على عنوان URL هذا.
- قد يتم نقل مستخدم غير موجود.
- قد لا تكون حقوق كافية.
- قد يتعطل الخادم بسبب خطأ داخلي أثناء معالجة طلبك
- قد يعرض الخادم استجابة غير صالحة أو تالفة.
- قد يعرض الخادم JSON غير صالح ولا يمكن تحليله.
القائمة تطول وتطول ، تكمن العديد من المشاكل المحتملة في شفرة الأسطر الثلاثة المؤسفة. يمكننا القول أنه لا يعمل بشكل عام إلا من خلال فرصة محظوظة ، ومن المرجح أكثر أن يقع مع استثناء.
كيف تحمي نفسك؟
الآن وقد تأكدنا من أن الاستثناءات قد تلحق الضرر بالكود ، فلنتعرف على كيفية التخلص منها. لكتابة التعليمات البرمجية دون استثناء ، هناك أنماط مختلفة.
- الكتابة في كل مكان
except Exception: pass
. طريق مسدود. لا تفعل ذلك. - العودة
None
. شرير جدا نتيجةً لذلك ، سيتعين عليك إما البدء في كل سطر تقريبًا if something is not None:
وسيتم فقد كل المنطق وراء القمامة الخاصة بعمليات التطهير ، أو ستعاني من TypeError
طوال الوقت. ليس خيارا جيدا. - اكتب دروسًا لحالات الاستخدام الخاص. على سبيل المثال ، الفئة الأساسية
User
مع الفئات الفرعية للأخطاء مثل UserNotFound
و MissingUser
. يمكن استخدام هذا النهج في بعض المواقف المحددة ، مثل AnonymousUser
في Django ، ولكن التفاف جميع الأخطاء المحتملة في الفصول غير واقعي. سوف يتطلب الأمر الكثير من العمل وسيصبح نموذج المجال معقدًا بشكل لا يمكن تصوره. - استخدم الحاويات لف التفاف المتغير الناتج أو قيمة الخطأ في غلاف ومتابعة العمل مع قيمة الحاوية. هذا هو السبب في أننا أنشأنا مشروع
@dry-python/return
. بحيث تقوم هذه الوظائف بإرجاع شيء ذي معنى وكتابته وآمنة.
دعنا نرجع إلى مثال القسمة ، والذي يُرجع 0 عند حدوث خطأ ، هل يمكننا الإشارة صراحة إلى أن الوظيفة لم تنجح دون إرجاع قيمة رقمية محددة؟
from returns.result import Result, Success, Failure def divide(first: float, second: float) -> Result[float, ZeroDivisionError]: try: return Success(first / second) except ZeroDivisionError as exc: return Failure(exc)
نرفق القيم في واحدة من مغلفين:
Success
أو
Failure
. يتم توارث هذه الفئات من فئة
Result
الأساسية. يمكن تحديد أنواع القيم المحزومة في التعليق التوضيحي باستخدام الوظيفة التي تم إرجاعها ، على سبيل المثال ، تقوم
Result[float, ZeroDivisionError]
بإرجاع إما
Success[float]
أو
Failure[ZeroDivisionError]
.
ماذا يعطينا هذا؟ مزيد من
الاستثناءات ليست استثنائية ، ولكن المشاكل المتوقعة . أيضًا ، التفاف استثناء في
Failure
يحل مشكلة ثانية: تعقيد تحديد الاستثناءات المحتملة.
1 + divide(1, 0)
الآن من السهل اكتشافها. إذا رأيت
Result
في الكود ، فإن الوظيفة قد تطرح استثناءً. وأنت تعرف حتى نوعه مقدما.
علاوة على ذلك ، تمت كتابة المكتبة بالكامل
ومتوافقة مع PEP561 . وهذا يعني أن mypy سوف يحذرك إذا حاولت إرجاع شيء لا يتطابق مع النوع المعلن.
from returns.result import Result, Success, Failure def divide(first: float, second: float) -> Result[float, ZeroDivisionError]: try: return Success('Done')
كيفية العمل مع الحاويات؟
هناك
طريقتان :
map
للوظائف التي ترجع القيم العادية ؛bind
للوظائف التي ترجع الحاويات الأخرى.
Success(4).bind(lambda number: Success(number / 2))
الجمال هو أن هذا الرمز سوف يحميك من البرامج النصية غير الناجحة ، لأن
.bind
و
.map
لن يتم
.bind
بالنسبة للحاويات التي بها
Failure
:
Failure(4).bind(lambda number: Success(number / 2))
يمكنك الآن التركيز فقط على عملية التنفيذ الصحيحة والتأكد من أن الحالة الخاطئة لن تؤدي إلى كسر البرنامج في مكان غير متوقع. وهناك دائمًا فرصة
لتحديد الحالة الخاطئة وتصحيحها والعودة إلى المسار المتصور للعملية.
Failure(4).rescue(lambda number: Success(number + 1))
في نهجنا ، "يتم حل جميع المشاكل بشكل فردي" ، و "عملية التنفيذ أصبحت شفافة الآن." استمتع بالبرمجة التي تركب على القضبان!
ولكن كيف يمكن توسيع القيم من الحاويات؟
في الواقع ، إذا كنت تعمل مع وظائف لا تعرف شيئًا عن الحاويات ، فأنت بحاجة إلى القيم نفسها. ثم يمكنك استخدام
.unwrap()
أو
.value_or()
:
Success(1).unwrap()
انتظر ، كان علينا التخلص من الاستثناءات ، والآن اتضح أن جميع المكالمات
.unwrap()
يمكن أن تؤدي إلى استثناء آخر؟
كيف لا تفكر في UnwrapFailedErrors؟
حسنًا ، دعنا نرى كيف نتعايش مع الاستثناءات الجديدة. النظر في هذا المثال: تحتاج إلى التحقق من إدخال المستخدم وإنشاء نموذجين في قاعدة البيانات. قد تنتهي كل خطوة باستثناء ، وهذا هو السبب في أن يتم التفاف جميع الأساليب في
Result
:
from returns.result import Result, Success, Failure class CreateAccountAndUser(object): """Creates new Account-User pair."""
أولاً ، ليس عليك توسيع القيم في منطق عملك على الإطلاق:
class CreateAccountAndUser(object): """Creates new Account-User pair.""" def __call__(self, username: str, email: str) -> Result['User', str]: """Can return a Success(user) or Failure(str_reason).""" return self._validate_user( username, email, ).bind( self._create_account, ).bind( self._create_user, )
كل شيء سيعمل دون أي مشاكل ، لن يتم
.unwrap()
أي استثناءات ، لأنه لا
.unwrap()
استخدام
.unwrap()
. ولكن هل من السهل قراءة هذا الرمز؟ لا. وما البديل؟
@pipeline
:
from result.functions import pipeline class CreateAccountAndUser(object): """Creates new Account-User pair.""" @pipeline def __call__(self, username: str, email: str) -> Result['User', str]: """Can return a Success(user) or Failure(str_reason).""" user_schema = self._validate_user(username, email).unwrap() account = self._create_account(user_schema).unwrap() return self._create_user(account)
الآن هذا الرمز هو قراءة جيدة. إليك كيفية عمل كل من
.unwrap()
و
@pipeline
معًا: كلما فشلت طريقة
.unwrap()
وفشل
Failure[str]
، قام مصمم
@pipeline
وإرجاع
Failure[str]
كقيمة ناتجة. هكذا أقترح إزالة جميع الاستثناءات من الكود وجعلها آمنة ومكتوبة حقًا.
لف كل ذلك معا
حسنًا ، سنقوم الآن بتطبيق الأدوات الجديدة ، على سبيل المثال ، مع طلب على HTTP API. تذكر أن كل سطر يمكن أن يلقي استثناء؟ وليس هناك طريقة لحملهم على إرجاع الحاوية مع
Result
. ولكن يمكنك استخدام
ديكورsafe للالتفاف على الوظائف غير الآمنة وجعلها آمنة. فيما يلي خياران للرمز يقومان بنفس الشيء:
from returns.functions import safe @safe def divide(first: float, second: float) -> float: return first / second
الأول ، مع
@safe
، أسهل وأفضل للقراءة.
آخر ما يجب فعله في مثال طلب API هو إضافة
@safe
decorator. والنتيجة هي الكود التالي:
import requests from returns.functions import pipeline, safe from returns.result import Result class FetchUserProfile(object): """Single responsibility callable object that fetches user profile."""
لتلخيص كيفية التخلص من الاستثناءات وتأمين الكود :
- استخدم المجمّع
@safe
لجميع الطرق التي قد تؤدي إلى استثناء. سيغير نوع الإرجاع للدالة إلى Result[OldReturnType, Exception]
. - استخدم
Result
كحاوية لنقل القيم والأخطاء إلى تجريد بسيط. - استخدم
.unwrap()
لتوسيع القيمة من الحاوية. - استخدم
@pipeline
لتسهيل قراءة .unwrap
مكالمات .unwrap
.
من خلال مراعاة هذه القواعد ، يمكننا أن نفعل الشيء نفسه بالضبط - فقط آمنة وقابلة للقراءة بشكل جيد. تم حل جميع المشكلات التي كانت مع الاستثناءات:
- "من الصعب تحديد الاستثناءات . " الآن يتم لفهم في حاوية
Result
مكتوبة ، مما يجعلها شفافة تمامًا. - "استعادة السلوك الطبيعي في المكان أمر مستحيل . " الآن يمكنك تفويض عملية الاسترداد بأمان إلى المتصل. لمثل هذه الحالة ، هناك
.fix()
و. .rescue()
. - "تسلسل الإعدام غير واضح . " الآن هم واحد مع تدفق العمل المعتاد. من البداية إلى النهاية.
- "الاستثناءات ليست استثنائية" . نحن نعرف! ونتوقع حدوث خطأ ما وجاهزًا لأي شيء.
استخدام الحالات والقيود
من الواضح ، لا يمكنك استخدام هذا النهج في جميع التعليمات البرمجية الخاصة بك. ستكون
آمنة للغاية بالنسبة لمعظم المواقف اليومية ولا تتوافق مع المكتبات أو الأُطُر الأخرى. لكن يجب عليك كتابة أهم أجزاء منطق عملك تمامًا كما أظهرت ، وذلك لضمان التشغيل الصحيح لنظامك وتسهيل الدعم في المستقبل.
هل الموضوع يجعلك تفكر أو حتى تبدو متوترة؟ تعال إلى موسكو بيثون كونف ++ في 5 أبريل ، سنناقش! إلى جانبي ، سيكون هناك Artyom Malyshev ، مؤسس مشروع الثعبان الجاف والمطور الأساسي لقنوات Django ، هناك. سيتحدث أكثر عن الثعبان الجاف والمنطق التجاري.