درس ۲۱: شی گرایی (OOP) در پایتون: Context Manager ،Descriptors ،Decorator¶

Photo by Mathyas Kurmann¶
این درس نیز در ادامه مجموعه دروس آموزش شی گرایی در زبان برنامهنویسی پایتون میباشد که به شرح و جمعبندی برخی موارد مرتبط با مفاهیم کلاس و شی موجود در پایتون میپردازد. مواردی که ممکن است قابل گذر باشند ولی هریک نکاتی دارند که در توسعه برنامه شی گرای پایتونی به شما کمک خواهند کرد. مواردی همچون صفت ویژه __slots__ در کلاسها، ایجاد Decorator با استفاده از کلاس در پایتون و همچنین ایجاد قابلیت getter و setter در پایتون با استفاده از مفاهیم Descriptors و دکوراتور property که در ادامه تا حد کافی شرح داده خواهند شد.
✔ سطح: متوسط
__slots__¶
پیش از هر توضیحی به نمونه کد زیر توجه نمایید:
1class Sample:
2 def __init__(self, a, b):
3 self.a = a
4 self.b = b
5
6
7objet = Sample(1, 2)
8print(objet.__dict__)
9
10print('-' * 30)
11
12objet.c = 3
13print(objet.__dict__)
14
15print('-' * 30)
16
17objet.__dict__['d'] = 4
18print(objet.__dict__)
19print(objet.d)
{'a': 1, 'b': 2}
------------------------------
{'a': 1, 'b': 2, 'c': 3}
------------------------------
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
4
پیشتر نیز صحبت کرده بودیم، میتوان حتی پس از ایجاد یک شی نیز به آن Attribute جدید اضافه کنیم (به دو سطر ۱۲ و ۱۷ توجه نمایید). دادههای مربوط به تمام Attributeهای یک شی توسط یک شی دیکشنری که از طریق __dict__
در دسترس میباشد، نگهداری میشود. در پس زمینه پایتون این __dict__
میباشد که امکان افزودن Attribute به شی را به صورت پویا (Dynamic) فراهم آورده است.
__slots__
[اسناد پایتون] یک Attribute ویژه در پایتون میباشد که با مقداردهی آن میتوان از ایجاد __dict__
جلوگیری و در نتیجه قابلیت افزودن Attribute جدید به شی را غیرفعال و تعداد Attributeهای آن را از همان نقطه ایجاد، ثابت نگهداشت:
1class Sample:
2
3 __slots__ = ('a', 'b')
4
5 def __init__(self, a, b):
6 self.a = a
7 self.b = b
8
9
10obj = Sample(1, 2)
11print(obj.__dict__)
Traceback (most recent call last):
File "sample.py", line 11, in <module>
print(obj.__dict__)
AttributeError: 'Sample' object has no attribute '__dict__'
از مزایای __slots__
میتوان به کاهش مصرف حافطه (RAM) به خصوص در مورد کلاسهایی که قرار است اشیایی خیلی زیادی از آنها ایجاد گردد، اشاره نمود.
از طریق __slots__
همچنین میتوان اجازه داد که کدام Attribute در آینده برای شی ایجاد گردد:
1class Sample:
2
3 __slots__ = ('a', 'b', 'c')
4
5 def __init__(self, a, b):
6 self.a = a
7 self.b = b
8
9objet = Sample(1, 2)
10
11objet.c = 3
12
13print('a: ', objet.a)
14print('b: ', objet.b)
15print('c: ', objet.c)
16
17objet.d = 4
a: 1
b: 2
c: 3
Traceback (most recent call last):
File "sample.py", line 17, in <module>
objet.d = 4
AttributeError: 'Sample' object has no attribute 'd'
اکنون نمونه کد زیر را در وضعیت وراثت در نظر بگیرید:
1class Parent:
2 def __init__(self, a, b):
3 self.a = a
4 self.b = b
5
6
7class Child(Parent):
8 def __init__(self, a, b):
9 super().__init__(a, b)
10
11
12child = Child(1, 2)
13print(child.__dict__)
14
15child.c = 3
16print(child.__dict__)
17
18print('a: ', child.a)
19print('b: ', child.b)
20print('c: ', child.c)
{'a': 1, 'b': 2}
{'a': 1, 'b': 2, 'c': 3}
a: 1
b: 2
c: 3
اگر کلاس Parent شامل __slots__
بوده و در نتیجه فاقد __dict__
باشد:
1class Parent:
2 __slots__ = ('a', 'b')
3
4 def __init__(self, a, b):
5 self.a = a
6 self.b = b
7
8
9class Child(Parent):
10
11 def __init__(self, a, b):
12 super().__init__(a, b)
13
14
15child = Child(1, 2)
16print(child.__dict__)
17
18child.c = 3
19print(child.__dict__)
20
21print('a: ', child.a)
22print('b: ', child.b)
23print('c: ', child.c)
{}
{'c': 3}
a: 1
b: 2
c: 3
اگر هر دو کلاس شامل __slots__
باشند:
1class Parent:
2 __slots__ = ('a', 'b')
3
4 def __init__(self, a, b):
5 self.a = a
6 self.b = b
7
8
9class Child(Parent):
10 __slots__ = ('c')
11
12 def __init__(self, a, b):
13 super().__init__(a, b)
14
15
16child = Child(1, 2)
17
18child.c = 3
19print('a: ', child.a)
20print('b: ', child.b)
21print('c: ', child.c)
a: 1
b: 2
c: 3
در وراثت چندگانه، چنانچه __slots__
مربوط به superclassها حاوی مقدار تکراری باشد، آنگاه باعث بروز خطا میگردد:
1class ParentOne:
2 __slots__ = ('a', 'b')
3
4class ParentTwo:
5 __slots__ = ('z', 'b')
6
7
8class Child(ParentOne, ParentTwo):
9 __slots__ = ('c')
10
11
12child = Child()
Traceback (most recent call last):
File "sample.py", line 8, in <module>
class Child(ParentOne, ParentTwo):
TypeError: multiple bases have instance lay-out conflict
بهتر است superclassها حاوی یک __slots__
خالی (شی توپِل خالی) باشند و هر subclass خود محتوای __slots__
خود را تعریف نماید:
1class ParentOne:
2 __slots__ = ()
3
4class ParentTwo:
5 __slots__ = ()
6
7
8class Child(ParentOne, ParentTwo):
9 __slots__ = ('a', 'b', 'z', 'c')
10
11
12child = Child()
در مواقع خاص که میخواهید هم Attributeها را محدود کنید و هم قابلیت __dict__
را حفظ کنید، میتوانید __dict__
را هم به مقدار __slots__
اضافه نمایید.
Decorators¶
از درس سیزدهم با مف��وم Decoratorها و نیز کاربرد آنها به همراه تابع در زبان برنامهنویسی پایتون آشنا شدهایم، در این بخش به بررسی Decoratorها به همراه کلاسها و متدها میپردازیم.
علاوه بر اینکه با استفاده از کلاس میتوان یک Decorator ایجاد کرد، از Decoratorها نیز میتوان بر روی کلاس یا متدهای داخل یک کلاس بهره گرفت. در ادامه به بررسی این موارد میپردازیم.
قراردادن Decorator بر روی متد¶
این کار همانند قراردادن Decorator بر روی تابع میباشد (درس سیزدهم) و تفاوتی ندارد. پیشتر نیز از Decoratorهایی همچون classmethod@
یا staticmethod@
بر روی متدها استفاده میکردیم. به مثالی در همین زمینه توجه نمایید:
1import functools
2
3def debug(func):
4 """Print the function signature and return value
5 Source: https://realpython.com/primer-on-python-decorators/#debugging-code"""
6
7 @functools.wraps(func)
8 def wrapper_debug(*args, **kwargs):
9 args_repr = [repr(a) for a in args]
10 kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
11 signature = ", ".join(args_repr + kwargs_repr)
12 print(f"Calling {func.__name__}({signature})")
13 value = func(*args, **kwargs)
14 print(f"{func.__name__!r} returned {value!r}")
15 return value
16 return wrapper_debug
17
18
19
20class Sample:
21
22 @debug
23 def __init__(self, x=0, y=0):
24 self.x = x
25 self.y = y
26
27
28sample = Sample(5, y=6)
Calling __init__(<__main__.Sample object at 0x7fd96ddec8d0>, 5, y=6)
'__init__' returned None
در نمونه کد بالا یک Decorator با نام debug
ایجاد گردیده است (Decorator درس سیزدهم و f-string درس هفتم)، با قراردادن این Decorator بر روی یک تابع یا متد: نام تابع، آرگومانهای ارسال شده و همچنین مقدار خروجی تابع را بر روی خروجی نمایش میدهد.
قراردادن Decorator بر روی کلاس¶
در زبان برنامهنویسی پایتون میتوان یک Decorator را به کل یک کلاس اعمال کرد، در این صورت نیز تفاوتی با آنچه در توابع دیدیم، نمیکند. تنها در این حالت، این کلاس است که به Decorator ارسال میگردد. دو نمونه کد زیر معادل یکدیگر هستند:
def decorator_name(a_class):
def wrapper():
# Do Something!
print('Class name:', a_class.__name__)
return a_class()
return wrapper
# 1
@decorator_name
class Sample():
pass
sample = Sample()
# 2
class Sample():
pass
SampleWrapper = decorator_name(Sample)
sample = SampleWrapper()
# Output
Class name: Sample
کلاس به عنوان Decorator¶
در زبان برنامهنویسی پایتون میتوان از کلاسها همچون توابع برای ایجاد Decorator استفاده کرد. در این صورت شیای که Decorator به آن اعمال شده است از طریق متد __init__
دریافت میگردد. همچنین میبایست متد __call__
را پیادهسازی کرده باشیم تا اشیای کلاس قابلیت callable را داشته باشند (درس هفدهم)، عملیات اصلی Decorator میبایست داخل این متد پیادهسازی گردد:
class CountCalls:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__!r}")
return self.func()
# 1
@CountCalls
def func():
''' a function'''
print(func.__doc__)
func()
func()
# 2
def func():
''' a function'''
obj = CountCalls(func)
print(obj.__doc__)
obj()
obj()
# Output
None
Call 1 of 'func'
Call 2 of 'func'
functools.update_wrapper
همانند کاربرد تابع wraps
از ماژول functools
در هنگام ساخت Decorator از توابع، در اینجا نیز میتوانیم جهت حفظ اطلاعات مربوط به تابع اصلی، اینبار از تابع update_wrapper
این ماژول استقاده کنیم [اسناد پایتون] - اگر کلاس CountCalls را به صورت زیر تغییر دهیم، آنگاه خروجی هر دو حالت نیز به شرح زیر تغییر خواهد کرد، چرا که اکنون __doc__
در دسترس باقی مانده است:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0
def __call__(self):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__!r}")
return self.func()
a function
Call 1 of 'func'
Call 2 of 'func'
Descriptors¶
توصیفگر (Descriptor) کلاسی است که کنترل عملیاتهای دریافت (get)، تنظیم (set) و حذف (delete) را بر روی یک attribute از شیای دیگر را فراهم میکند. Descriptor یک راهکار پایتونی (Pythonic) برای ایجاد مکانیزم get & set رایج در دیگر زبانهای برنامهنویسی میباشد.
چگونه میتوان یک Descriptor در پایتون ایجاد کرد؟ [اسناد پایتون]
۱- یک کلاس ایجاد کنیم که در آن حداقل یکی از متدهای خاص __set__
،__get__
و __delete__
بازپیادهسازی (یا بهتر است بگوییم Override) شود.
۲- از شی این کلاس به عنوان مقدار attribute مناسب از کلاس مورد نظر استفاده کنیم.
کاربرد Descriptor پایتون چیست؟
هر زمان بخواهیم رویدادهایی همچون دریافت (get)، تنظیم (set) و حذف (delete) را بر روی یک attribute کنترل کنیم. برای مثال کلاسی شامل یک attribute با نام ایمیل (email) است، میخواهیم پیش از تنظیم مقدار بر روی این فیلد، مقدار جدید به صورت خودکار اعتبارسنجی (Validation) شود و در صورت صحت عملیات انجام شود:
1import re
2
3class EmailField:
4
5 def __init__(self, email=None):
6 self.email = email
7
8 def __get__(self, instance, owner=None):
9 print('-' * 10, 'CALLED[__get__]')
10 print('instance:', instance)
11 print('owner:', owner)
12 print('-' * 30)
13 print()
14
15 return self.email
16
17 def __set__(self, instance, value):
18 print('-' * 10, 'CALLED[__set__]')
19 print('instance:', instance)
20 print('value:', value)
21 print('-' * 30)
22
23 if re.match('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$', value):
24 self.email = value
25 print('Successful!\n')
26 else:
27 print(f'{value} is not a valid email!\n')
28
29
30class Student:
31 email = EmailField()
32
33
34obj = Student()
35
36email = obj.email # CALLED[__get__]
37
38obj.email = 'python$$1400' # CALLED[__set__]
39
40obj.email = '[email protected]' # CALLED[__set__]
41
42print(obj.email) # CALLED[__get__]
---------- CALLED[__get__]
instance: <__main__.Student object at 0x7f828bb9f4e0>
owner: <class '__main__.Student'>
------------------------------
---------- CALLED[__set__]
instance: <__main__.Student object at 0x7f828bb9f4e0>
value: python$$1400
------------------------------
python$$1400 is not a valid email!
---------- CALLED[__set__]
instance: <__main__.Student object at 0x7f828bb9f4e0>
value: [email protected]
------------------------------
Successful!
---------- CALLED[__get__]
instance: <__main__.Student object at 0x7f62e42c64e0>
owner: <class '__main__.Student'>
------------------------------
[email protected]
در نمونه کد، بالا کلاس EmailField
یک Descriptor برای اتریبیوت email
از کلاس Student
میباشد. همانطور که مشاهده میشود، هرگاه مقداری به email
انتساب داده میشود (سطرهای ۳۸ و ۴۰)، به صورت خودکار متد __set__
از کلاس Descriptor آن فراخوانی میگردد و به همین ترتیب هرگاه مقدار آن درخواست میگردد (سطرهای ۳۶ و ۴۲)، متد __get__
فراخوانی میگردد.
پیشنهاد میشود در صورت امکان مقدار attribute را توسط Descriptor نگهداری نکنید و از Descriptor تنها برای انجام عملیات مربوطه استفاده نمایید. بنابراین مثال قبل را میتوانیم به صورت زیر بازنویسی نماییم:
1import re
2
3class EmailField:
4
5 def __init__(self, attr_name):
6 self.attr_name = attr_name
7
8 def __get__(self, instance, owner=None):
9 return instance.__dict__.get(self.attr_name)
10
11 def __set__(self, instance, value):
12 if re.match('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$', value):
13 instance.__dict__[self.attr_name] = value
14
15
16class Student:
17 email = EmailField('email')
18
19
20obj = Student()
21obj.email = '[email protected]'
22
23print(obj.email)
python@coderz.ir
در این روش تنها نام attribute نگهداری و از آن برای دستیابی به مقدار آن attribute، از طریق خود شی اقدام کردیم.
اگر از نسخه 3.6 به بعد پایتون بهرهمند هستید، با استفاده از متد __set_name__
[اسناد پایتون] در کلاس Descriptor، دیگر حتی نیازی به پیادهسازی متد __init__
و ارسال دستی نام attribute هم نخواهد بود:
1import re
2
3class EmailField:
4
5 def __set_name__(self, owner, name):
6 self.attr_name = name
7
8 def __get__(self, instance, owner=None):
9 return instance.__dict__.get(self.attr_name)
10
11 def __set__(self, instance, value):
12 if re.match('^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$', value):
13 instance.__dict__[self.attr_name] = value
14
15
16class Student:
17 email = EmailField()
نکته
از Descriptor تنها برای Class Attributeها میتوان استفاده کرد.
property@¶
خیلی ساده، این دکوراتور (property@
) را میتوان یک Descriptor سطح بالا دانست که توسط کتابخانه استاندارد پایتون برای Instance Attributeها فراهم آورده شده است. به نمونه کد زیر توجه نمایید:
1import re
2
3class Contact:
4
5 def __init__(self, name, phone):
6 self._name = name
7 self._phone = phone
8
9 @property
10 def name(self):
11 return self._name
12
13 @name.setter
14 def name(self, new_name):
15 if new_name and len(new_name) > 0:
16 self._name = new_name
17 else:
18 print("Please enter a valid name")
19
20 @name.deleter
21 def name(self):
22 del self._name
23
24 @property
25 def phone(self):
26 return self._phone
27
28
29 @phone.setter
30 def phone(self, new_phone):
31 if re.match(r'^09\d{9}$', new_phone):
32 self._phone = new_phone
33 else:
34 print("Please enter a valid phone")
35
36 @phone.deleter
37 def phone(self):
38 del self._phone
39
40
41obj = Contact(name='Saeid', phone='09999999999')
42
43obj.phone = '09123456'
44print('-' * 30)
45print(obj.name)
46print(obj.phone)
Please enter a valid phone
------------------------------
Saeid
09999999999
در این مثال، کلاس Contact
حاوی دو Instance Attribute با نامهای name
و phone
میباشد. برای اینکه بتوانیم رویدادهایی همچون دریافت (get)، تنظیم (set) و حذف (delete) را بر روی آنها کنترل کنیم، از دکوراتور property@
استفاده کردیم. به این صورت که:
۱- نخست باید توجه داشت که نام Attributeها با یک کاراکتر _
شروع کردیم. با این کار به دیگر برنامهنویسان خواهیم گفت که این Attribute با سطح دسترسی protected میباشد (درس بیستم):
def __init__(self, name, phone):
self._name = name
self._phone = phone
۲- برای هر کدام یک متد getter ساختیم و به آن دکوراتور property@
انتساب دادیم. نام این متد را همنام با Attributeها ولی بدون _
انتخاب کردیم:
@property
def name(self):
return self._name
@property
def phone(self):
return self._phone
نام این متد هر چیزی انتخاب شود، در زمان درخواست مقدار Attribute باید از این نام (به جای نام اصلی Attribute) استفاده گردد (سطرهای ۴۵ و ۴۶).
۳- اکنون میتوانیم دو متد دیگر برای عملیات set و delete پیادهسازی کنیم و به آنها دکوراتورهای زیر را انتساب دهیم:
@<property_getter_method_name>.setter
@<property_getter_method_name>.deleter
بخش نخست از نام دکوراتور (property_getter_method_name) میبایست همان نام متد getter باشد.
در این مثال ما از همان نام متد getter برای نامگذاری این دو متد استفاده کردیم. ولی باید توجه داشته باشید که نام این دو متد هر چیزی انتخاب شود، در زمان تنظیم مقدار (سطر ۴۳) یا حذف Attribute باید از این نام (به جای نام اصلی Attribute) استفاده گردد.
نکته
از property@
تنها برای Instance Attributeها میتوان استفاده کرد.
یک کاربرد پنهان در استفاده از property@
، امکان ایجاد Attributeهای read-only و غیرقابل تغییر پس از نمونهسازی شی خواهد بود. برای این منظور تنها کافی است از پیادهسازی متد setter صرفنظر کنیم! به نمونه کد پایین توجه نمایید:
1class StaticNumber:
2
3 def __init__(self, number):
4 self._number = number
5
6 @property
7 def number(self):
8 return self._number
9
10
11
12obj = StaticNumber(number='000111')
13
14obj.number = '000222'
Traceback (most recent call last):
File "sample.py", line 14, in <module>
obj.number = '000222'
AttributeError: can't set attribute
Context Manager و دستور with/as
¶
یکی دیگر از قابلیتهای کمتر شناخته شده در زبان برنامهنویسی پایتون، Context Manager میباشد [اسناد پایتون]. با این حال اکثر برنامهنویسان پایتون به صورت مداوم از آن بهره میگیرند. اگر درس دهم را به یاد داشته باشیم، از دستور with/as
برای کار با فایلها در پایتون استفاده میکردیم و شاهد راحتی و زیبایی کارها نسبت به قبل بودیم. در آن زمان تنها اشاره شد که شی فایل پایتون را میتوان با دستور with/as
استفاده کرد چون این شی از قابلیت Context Manager پشتیبانی میکند.
به صورت کلی Context Manager در زبان برنامهنویسی پایتون قابلیتی برای مدیرت منابع (فایلها، دیتابیس، ارتباط و سایر منابع) میباشد، منابعی که کار کردن با آنها همواره نیازمند عملیات ثابتی همچون باز (Open) و بسته (Close) - Start/Stop, Lock/Release, Change/Reset - کردن هستند.
در این بخش میخواهیم به بررسی چگونگی ایجاد یک کلاس به همراه قابلیت Context Manager بپردازیم که در نهایت از اشیای آن بتوانیم در کنار دستور with/as
استفاده نماییم. ابتدا اجازه دهید بار دیگر ساختار دستور with/as
را بررسی نماییم:
with context_expression [as target]:
with_statement_body
در این ساختار بخش as
اختیاری بوده و تنها زمانی که در داخل بدنه دستور with
به شی تولید شده توسط context_expression
نیاز داشته باشیم، استفاده میگردد؛ در این صورت یک ارجاع از شی مورد نیاز به نام دلخواه target
ایجاد و در دسترس قرار میگیرد. context_expression
نیز معرف یک شیای است که توانایی مدیریت یا handle کردن دو وضعیت «ورود به» (entry into) و «خروج از» (exit from) را داشته باشد. برای ایجاد همچین شیای میبایست دو متد خاص __enter__
[اسناد پایتون] و __exit__
[اسناد پایتون] را در کلاس مورد نظر خود پیادهسازی کنیم:
1class SampleContextManager:
2 def __enter__(self):
3 print('---> Entered into context manager!')
4
5 def __exit__(self, *args):
6 print('<--- Exiting from context manager!')
7
8
9with SampleContextManager():
10 print('Inside context manager!')
---> Entered into context manager!
Inside context manager!
<--- Exiting from context manager!
همانطوری که از خروجی نمونه کد بالا قابل مشاهده میباشد، در هنگام اجرای دستور with
، ابتدا متد __enter__
از شی Context Manager و سپس دستورات داخل بدنه دستور with
و در نهایت نیز متد __exit__
از شی Context Manager اجرا میگردد.
اگر بخواهیم کمی عمیقتر به ماجرا نگاه کنیم:
اجرای متد
__enter__
زمانی است که خط اجرای برنامه میخواهد وارد اجرای دستورات داخلwith
یا به اصطلاح وارد runtime context شود و خروجی این متد میبایست شیای باشد که میخواهیم در طول اجرای دستورwith
یا به اصطلاح context، با آن کار کنیم. البته خروجی میتواندNone
باشد ولی باید توجه داشت که خروجی این متد است که توسط دستورas
به نامtarget
ارجاع میخورد!اجرای متد
__exit__
زمانی است که انجام کار دستوراتwith
یا اجرای context به پایان رسیده است. این متد در واقع فرصتی برای تمیزکاری یا به اصطلاح clean up کردن آثار اجرای context میباشد. به مانند پاک کردن فایلهایی که موقت ایجاد شدهاند، حذف اشیای اضافی باقیمانده یا انجام عمل بستن یک فایل یا پایان دادن یک ارتباط (Connection) یا...
برای آشنایی بیشتر در نمونه کد زیر یک Wrapper برای شی فایل ایجاد کردهایم:
1class FileWritterWrapper:
2 def __init__(self, filename):
3 self.filename = filename
4
5 def __enter__(self):
6 self.opened_file = open(self.filename, 'a')
7 self.opened_file.write('====== OPEN FILE ======\n')
8 return self.opened_file
9
10 def __exit__(self, *args):
11 self.opened_file.write('\n====== CLOSE FILE ======\n')
12 self.opened_file.close()
13
14
15with FileWritterWrapper('test_log.txt') as managed_file:
16 managed_file.write('Inside context manager!')
محتویات فایل test_log.txt، پس از اجرای کد بالا:
====== OPEN FILE ======
Inside context manager!
====== CLOSE FILE ======
به متد __exit__
برگردیم، براساس مستندات پایتون تعریف کامل این متد به شکل زیر است:
__exit__(self, exc_type, exc_value, traceback)
سه پارامتر انتهایی در صورت بروز Exception هنگام اجرای context (دستورات داخل بدنه with
) دارای مقدار غیر None
و در غیر این صورت برابر با مقدار None
خواهند بود. وجود این مقادیر به معنی عدم پایان صحیح context میباشد که ممکن است بتواند در گرفتن تصمیم شما در زمان خروج از context تاثیر داشته باشد.
1class SampleContextManager:
2 def __enter__(self):
3 print('---> Entered into context manager!')
4
5 def __exit__(self, exc_type, exc_value, traceback):
6 print('exc_type:', exc_type)
7 print('exc_value:', exc_value)
8 print('traceback:', traceback)
9 print('<--- Exiting from context manager!')
10
11
12with SampleContextManager():
13 print('|||||||||Inside context manager! - Top')
14 a = 8 / 0
15 print('|||||||||Inside context manager! - Bottom')
16
17
18print('***FINISH***')
---> Entered into context manager!
|||||||||Inside context manager! - Top
exc_type: <class 'ZeroDivisionError'>
exc_value: division by zero
traceback: <traceback object at 0x7f1c8aebd0c8>
<--- Exiting from context manager!
Traceback (most recent call last):
File "sample.py", line 14, in <module>
a = 8 / 0
ZeroDivisionError: division by zero
همانطور که از نمونه کد بالا قابل مشاهده است، در زمان اجرای دستورات context یک خطای (تقسیم بر صفر) ZeroDivisionError
رخ داده است. نکته قابل توجه این است که حتی با وجود بروز خطا و ناتمام ماندن اجرای context، ولی بدنه متد __exit__
به صورت کامل اجرا شده است. در واقع مفسر پایتون اعلام Exception را که میتواند منجر به توقف کل برنامه شود را به صورت موقت تا پایان اجرا __exit__
معلق نگه میدارد.
در چنین حالتی اگر متد __exit__
مقدار True
را برگرداند، مفسر پایتون از بروز Exception خودداری خواهد کرد:
1class SampleContextManager:
2 def __enter__(self):
3 print('---> Entered into context manager!')
4
5 def __exit__(self, exc_type, exc_value, traceback):
6 print('exc_type:', exc_type)
7 print('exc_value:', exc_value)
8 print('traceback:', traceback)
9 print('<--- Exiting from context manager!')
10 return True
11
12
13with SampleContextManager():
14 print('|||||||||Inside context manager! - Top')
15 a = 8 / 0
16 print('|||||||||Inside context manager! - Bottom')
17
18
19print('***FINISH***')
---> Entered into context manager!
|||||||||Inside context manager! - Top
exc_type: <class 'ZeroDivisionError'>
exc_value: division by zero
traceback: <traceback object at 0x7f4b3d520048>
<--- Exiting from context manager!
***FINISH***
یادآوری: میدانیم که خروجی هر تابع یا متد به صورت پیشفرض برای None
میباشد و این مقدار در مقام ارزشسنجی بولین، ارزشی برابر با مقدار False
دارد.
در طی دروس آینده به مبحث Exception و مدیریت آن خواهیم پرداخت.
😊 امیدوارم مفید بوده باشه