Python Coding Academy
135 subscribers
7 photos
3 files
4 links
🐍 Learn Python. Build Skills. Create the Future.

📚 Python lessons from beginner to advanced
💻 Clear explanations & real-world examples
🚀 Learn to code. Learn to build.
Download Telegram
🐍 آموزش جامع Python — پست ۱۱۳
تغییر عناصر شیء با __setitem__
در پست قبل یاد گرفتیم با __getitem__ کاری کنیم که بتوانیم از [] برای خواندن عناصر یک شیء استفاده کنیم.
حالا می‌خواهیم با همان [] بتوانیم مقدار یک عنصر را تغییر دهیم.
برای این کار از __setitem__ استفاده می‌کنیم.
🔹 __setitem__
وقتی Python چنین کدی می‌بیند:
object[index] = value

می‌تواند متد زیر را اجرا کند:
object.__setitem__(index, value)

🔹 مثال ساده
class Playlist:
def __init__(self, songs):
self.songs = songs

def __getitem__(self, index):
return self.songs[index]

def __setitem__(self, index, value):
self.songs[index] = value


playlist = Playlist([
"Song A",
"Song B",
"Song C"
])

playlist[1] = "New Song"

print(playlist[1])

خروجی:
New Song

در اینجا:
playlist[1] = "New Song"

باعث می‌شود Python از __setitem__ استفاده کند.
🔹 چه اتفاقی می‌افتد؟
این کد:
playlist[1] = "New Song"

تقریباً به این شکل عمل می‌کند:
playlist.__setitem__(1, "New Song")

و داخل متد:
self.songs[index] = value

مقدار داخل لیست تغییر می‌کند.
🔹 ترکیب __getitem__ و __setitem__
با داشتن هر دو متد، شیء ما می‌تواند هم خوانده و هم تغییر داده شود:
playlist = Playlist([
"Song A",
"Song B",
"Song C"
])

print(playlist[0])

playlist[0] = "Updated Song"

print(playlist[0])

خروجی:
Song A
Updated Song

⚠️ نکته مهم
__getitem__ برای دریافت مقدار است:
playlist[0]

اما __setitem__ برای قرار دادن یا تغییر مقدار است:
playlist[0] = "New Song"

پس:
__getitem__  → obj[index]
__setitem__ → obj[index] = value

🎯 تمرین
کلاس Inventory را کامل کن:
class Inventory:
def __init__(self, items):
self.items = items

def __getitem__(self, index):
# code

def __setitem__(self, index, value):
# code


inventory = Inventory([
"Keyboard",
"Mouse",
"Monitor"
])

print(inventory[1])

inventory[1] = "Gaming Mouse"

print(inventory[1])

خروجی مورد انتظار:
Mouse
Gaming Mouse

✅ پاسخ کامل تمرین
class Inventory:
def __init__(self, items):
self.items = items

def __getitem__(self, index):
return self.items[index]

def __setitem__(self, index, value):
self.items[index] = value


inventory = Inventory([
"Keyboard",
"Mouse",
"Monitor"
])

print(inventory[1])

inventory[1] = "Gaming Mouse"

print(inventory[1])

خروجی:
Mouse
Gaming Mouse

پست ۱۱۳ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۱۴
پیمایش روی شیء با __iter__
تا اینجا یاد گرفتیم با __getitem__ به عناصر یک شیء دسترسی داشته باشیم.
حالا می‌خواهیم کاری کنیم که بتوانیم مستقیماً روی شیء با for پیمایش کنیم.
برای این کار از __iter__ استفاده می‌کنیم.
🔹 __iter__
وقتی می‌نویسیم:
for item in object:
print(item)

Python باید بداند چگونه روی object پیمایش کند.
یکی از روش‌های اصلی این کار استفاده از:
__iter__()

است.
🔹 مثال ساده
class Playlist:
def __init__(self, songs):
self.songs = songs

def __iter__(self):
return iter(self.songs)


playlist = Playlist([
"Song A",
"Song B",
"Song C"
])

for song in playlist:
print(song)

خروجی:
Song A
Song B
Song C

🔹 چرا iter()؟
در اینجا:
return iter(self.songs)

تابع iter() از لیست یک iterator می‌سازد.
سپس for از آن برای گرفتن عناصر یکی‌یکی استفاده می‌کند.
🔹 استفاده با list()
یک شیء قابل پیمایش را می‌توان به لیست هم تبدیل کرد:
print(list(playlist))

خروجی:
['Song A', 'Song B', 'Song C']

🔹 یک مثال دیگر
class Team:
def __init__(self, members):
self.members = members

def __iter__(self):
return iter(self.members)


team = Team([
"Ali",
"Sara",
"Reza"
])

for member in team:
print(member)

خروجی:
Ali
Sara
Reza

⚠️ نکته مهم
__iter__ باید یک iterator برگرداند.
در مثال ما:
return iter(self.members)

این کار را برای لیست داخلی انجام می‌دهد.
بنابراین:
__iter__ → امکان پیمایش شیء با for

🧠 مقایسه تا اینجا
__contains__ → in
__getitem__ → obj[index]
__setitem__ → obj[index] = value
__iter__ → for item in obj

هرکدام رفتار متفاوتی از شیء را کنترل می‌کنند.
🎯 تمرین
کلاس Library را طوری کامل کن که بتوانیم روی کتاب‌های آن با for پیمایش کنیم:
class Library:
def __init__(self, books):
self.books = books

def __iter__(self):
# code


library = Library([
"Python",
"Django",
"Flask"
])

for book in library:
print(book)

خروجی مورد انتظار:
Python
Django
Flask

✅ پاسخ کامل تمرین
class Library:
def __init__(self, books):
self.books = books

def __iter__(self):
return iter(self.books)


library = Library([
"Python",
"Django",
"Flask"
])

for book in library:
print(book)

خروجی:
Python
Django
Flask

پست ۱۱۴ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۱۵
ساخت Iterator با __next__
در پست قبل با __iter__ آشنا شدیم.
حالا می‌خواهیم ببینیم یک Iterator چگونه می‌تواند عناصر را یکی‌یکی در اختیار Python قرار دهد.
برای این کار از __next__ استفاده می‌کنیم.
🔹 __next__
هر بار که next() را روی یک Iterator اجرا کنیم، عنصر بعدی را برمی‌گرداند:
numbers = iter([10, 20, 30])

print(next(numbers))
print(next(numbers))
print(next(numbers))

خروجی:
10
20
30

بعد از تمام شدن عناصر، next() با خطای StopIteration مواجه می‌شود.
🔹 ساخت Iterator خودمان
می‌توانیم کلاسی بسازیم که خودش Iterator باشد:
class Counter:
def __init__(self, start, end):
self.current = start
self.end = end

def __iter__(self):
return self

def __next__(self):
if self.current > self.end:
raise StopIteration

value = self.current
self.current += 1
return value

حالا:
counter = Counter(1, 3)

print(next(counter))
print(next(counter))
print(next(counter))

خروجی:
1
2
3

🔹 استفاده با for
چون کلاس ما __iter__ و __next__ دارد، می‌توانیم از آن با for هم استفاده کنیم:
counter = Counter(1, 3)

for number in counter:
print(number)

خروجی:
1
2
3

🔹 چرا return self؟
در این مثال:
def __iter__(self):
return self

خود شیء Counter همان Iterator است.
پس Python همان شیء را برای دریافت مقادیر بعدی استفاده می‌کند.
🔹 نقش StopIteration
وقتی دیگر مقداری باقی نمانده باشد:
if self.current > self.end:
raise StopIteration

به Python می‌گوییم:
دیگر عنصری برای پیمایش وجود ندارد.

for این وضعیت را مدیریت می‌کند و حلقه را متوقف می‌کند.
🧠 تفاوت __iter__ و __next__
__iter__ → Iterator را برمی‌گرداند
__next__ → عنصر بعدی را برمی‌گرداند

برای ساخت یک Iterator معمولاً هر دو متد را داریم:
__iter__()
__next__()

🎯 تمرین
کلاس Countdown را کامل کن تا از عدد 3 تا 1 را یکی‌یکی برگرداند:
class Countdown:
def __init__(self, start):
self.current = start

def __iter__(self):
# code

def __next__(self):
# code


countdown = Countdown(3)

for number in countdown:
print(number)

خروجی مورد انتظار:
3
2
1

✅ پاسخ کامل تمرین
class Countdown:
def __init__(self, start):
self.current = start

def __iter__(self):
return self

def __next__(self):
if self.current < 1:
raise StopIteration

value = self.current
self.current -= 1
return value


countdown = Countdown(3)

for number in countdown:
print(number)

خروجی:
3
2
1

در اینجا:
self.current -= 1

در هر مرحله عدد را یک واحد کاهش می‌دهد.
وقتی مقدار به 0 برسد:
raise StopIteration

اجرا می‌شود و حلقه for متوقف می‌شود.
پست ۱۱۵ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۱۶
ساخت Generator با yield
در پست‌های قبل با Iterator و __next__ آشنا شدیم.
Python راه ساده‌تری برای ساخت Iteratorها دارد: Generator.
برای ساخت Generator معمولاً از yield استفاده می‌کنیم.
🔹 yield چیست؟
yield شبیه return است، اما یک تفاوت مهم دارد:
return اجرای تابع را تمام می‌کند.
yield مقدار را برمی‌گرداند و وضعیت اجرای تابع را حفظ می‌کند.
مثلاً:
def numbers():
yield 1
yield 2
yield 3

حالا:
for number in numbers():
print(number)

خروجی:
1
2
3

🔹 Generator Object
تابعی که yield دارد، هنگام فراخوانی معمولی، نتیجه را یک‌جا اجرا نمی‌کند؛ یک Generator Object برمی‌گرداند:
def numbers():
yield 1
yield 2
yield 3


result = numbers()

print(result)

result یک Generator است.
🔹 اجرای مرحله‌ای
Generator مقدارها را یکی‌یکی تولید می‌کند:
def numbers():
yield 1
yield 2
yield 3


numbers_generator = numbers()

print(next(numbers_generator))
print(next(numbers_generator))
print(next(numbers_generator))

خروجی:
1
2
3

بعد از تمام شدن Generator، next() باعث StopIteration می‌شود.
🔹 تفاوت return و yield
با return:
def get_number():
return 1
return 2

فقط اولین return قابل اجراست.
اما با yield:
def get_numbers():
yield 1
yield 2

مقادیر می‌توانند یکی‌یکی تولید شوند.
🔹 چرا Generator مهم است؟
فرض کن می‌خواهیم اعداد 1 تا 1,000,000 را تولید کنیم.
ساختن یک لیست کامل:
numbers = list(range(1, 1_000_001))

همه مقادیر را در حافظه نگه می‌دارد.
اما Generator می‌تواند مقدارها را در زمان نیاز تولید کند:
def numbers():
for i in range(1, 1_000_001):
yield i

در این حالت لازم نیست همه مقادیر از ابتدا در یک لیست ذخیره شوند.
این ویژگی برای کار با داده‌های بزرگ بسیار مهم است.
🧠 خلاصه
yield     → تولید مقدار به‌صورت مرحله‌ای
Generator → تولیدکننده مقدارها
next() → دریافت مقدار بعدی
StopIteration → پایان Generator

و یک نکته مهم:
هر تابعی که

yield
داشته باشد، یک Generator Function است.

🎯 تمرین
یک Generator به نام count_up بنویس که اعداد 1 تا 5 را یکی‌یکی تولید کند:
def count_up():
# code


for number in count_up():
print(number)

خروجی مورد انتظار:
1
2
3
4
5

پست ۱۱۶ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۱۷
Generator با yield و ورودی پویا
در پست قبل دیدیم که yield می‌تواند مقدارها را یکی‌یکی تولید کند.
حالا یک مرحله جلوتر می‌رویم و یاد می‌گیریم Generator چگونه می‌تواند با حلقه و داده‌های پویا کار کند.
🔹 تولید مقدار با حلقه
به‌جای اینکه چند yield پشت سر هم بنویسیم، می‌توانیم از حلقه استفاده کنیم:
def count_up(limit):
for number in range(1, limit + 1):
yield number

حالا:
for number in count_up(5):
print(number)

خروجی:
1
2
3
4
5

🔹 چرا limit + 1؟
در range() مقدار انتهایی شامل نمی‌شود:
range(1, 5)

این اعداد را تولید می‌کند:
1
2
3
4

پس برای اینکه 5 هم تولید شود، می‌نویسیم:
range(1, limit + 1)

🔹 Generator فقط تا زمانی که لازم است
می‌توانیم فقط چند مقدار اول را دریافت کنیم:
numbers = count_up(100)

print(next(numbers))
print(next(numbers))
print(next(numbers))

خروجی:
1
2
3

در اینجا Generator لازم نیست همه اعداد را از ابتدا تولید کند.
هر بار که next() اجرا می‌شود، Generator تا yield بعدی ادامه پیدا می‌کند.
🔹 یک مثال کاربردی‌تر
فرض کنیم می‌خواهیم فقط اعداد زوج را تولید کنیم:
def even_numbers(limit):
for number in range(2, limit + 1, 2):
yield number

حالا:
for number in even_numbers(10):
print(number)

خروجی:
2
4
6
8
10

⚠️ نکته مهم
Generator با yield مقدارها را یکی‌یکی تولید می‌کند، نه اینکه همه را از ابتدا در یک لیست قرار دهد.
به همین دلیل برای داده‌های زیاد می‌تواند از نظر مصرف حافظه مناسب‌تر باشد.
🧠 خلاصه
yield → تولید مرحله‌ای مقدار
range → تولید دنباله عددی
next() → دریافت مقدار بعدی
Generator → تولید مقدار در زمان نیاز

🎯 تمرین
یک Generator به نام odd_numbers بنویس که اعداد فرد از 1 تا 9 را تولید کند:
def odd_numbers():
# code


for number in odd_numbers():
print(number)

خروجی مورد انتظار:
1
3
5
7
9

پست ۱۱۷ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۱۸
ارسال مقدار به Generator با send()
در پست‌های قبل یاد گرفتیم Generator با yield مقدار تولید می‌کند.
اما yield فقط برای خروجی گرفتن نیست؛ می‌توانیم با send() یک مقدار را به Generator برگردانیم.
🔹 send()
مثال ساده:
def calculator():
number = yield
print(number)

برای شروع Generator ابتدا باید آن را اجرا کنیم:
calc = calculator()

next(calc)

حالا می‌توانیم مقدار ارسال کنیم:
calc.send(10)

خروجی:
10

🔹 yield می‌تواند مقدار دریافت کند
در این کد:
def calculator():
number = yield
result = number * 2
print(result)

حالا:
calc = calculator()

next(calc)
calc.send(5)

خروجی:
10

🔹 yield هم می‌تواند مقدار برگرداند
می‌توانیم هم مقدار دریافت کنیم و هم مقدار تولید کنیم:
def calculator():
number = yield
result = number * 2
yield result

حالا:
calc = calculator()

next(calc)

result = calc.send(5)

print(result)

خروجی:
10

در اینجا:
calc.send(5)

مقدار 5 را به Generator می‌فرستد و Generator به yield result می‌رسد.
⚠️ نکته مهم
قبل از اولین send() معمولاً باید Generator را با:
next(generator)

تا اولین yield جلو ببریم.
این کار Generator را آماده دریافت مقدار می‌کند.
🧠 خلاصه
yield      → تولید مقدار و توقف موقت
next() → ادامه Generator تا yield بعدی
send(value) → ارسال مقدار به yield

Generator فقط یک تولیدکننده ساده نیست؛ می‌تواند بین کد بیرونی و خودش داده رد و بدل کند.
🎯 تمرین
Generator زیر را کامل کن تا عدد دریافت‌شده را دو برابر کند:
def double():
# code


generator = double()

next(generator)

result = generator.send(7)

print(result)

خروجی مورد انتظار:
14

✅ پاسخ کامل تمرین
def double():
number = yield
result = number * 2
yield result


generator = double()

next(generator)

result = generator.send(7)

print(result)

خروجی:
14

در اینجا ابتدا:
next(generator)

Generator را تا اولین yield جلو می‌برد.
سپس:
generator.send(7)

عدد 7 را به این قسمت می‌رساند:
number = yield

بنابراین:
number = 7
7 * 2 = 14

و در نهایت:
yield result

مقدار 14 را تولید می‌کند.
پست ۱۱۸ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۱۹
مدیریت پایان Generator با return
در پست‌های قبل با yield، next() و send() آشنا شدیم.
حالا می‌خواهیم ببینیم اگر یک Generator با return تمام شود، چه اتفاقی می‌افتد.
🔹 استفاده از return در Generator
یک Generator می‌تواند هم yield داشته باشد و هم return:
def numbers():
yield 1
yield 2
return

حالا:
for number in numbers():
print(number)

خروجی:
1
2

وقتی return اجرا شود، Generator تمام می‌شود.
🔹 return می‌تواند یک مقدار هم داشته باشد
در Generator می‌توانیم بنویسیم:
def numbers():
yield 1
yield 2
return "Done"

اما این مقدار مثل yield مستقیماً توسط for چاپ نمی‌شود.
اگر با next() ادامه دهیم، مقدار بازگشتی داخل StopIteration قرار می‌گیرد:
generator = numbers()

print(next(generator))
print(next(generator))

try:
next(generator)
except StopIteration as error:
print(error.value)

خروجی:
1
2
Done

🔹 چرا for مقدار return را نشان نمی‌دهد؟
حلقه for وقتی به StopIteration برسد، آن را برای پایان پیمایش مدیریت می‌کند.
بنابراین:
for number in numbers():
print(number)

فقط مقادیری را که با yield تولید شده‌اند دریافت می‌کند.
🔹 تفاوت yield و return
yield  → یک مقدار تولید می‌کند و Generator را موقتاً متوقف می‌کند
return → Generator را کاملاً تمام می‌کند

مثلاً:
def example():
yield 10
yield 20
return "Finished"

ترتیب اتفاق‌ها:
yield 10
↓
yield 20
↓
return "Finished"
↓
StopIteration

⚠️ نکته مهم
در Generator:
return value

در نهایت باعث ایجاد StopIteration می‌شود و مقدار value در:
StopIteration.value

قرار می‌گیرد.
این موضوع بیشتر در پیاده‌سازی‌ها و کار با Generatorها به شکل مستقیم اهمیت دارد.
🧠 خلاصه
yield          → تولید مقدار
return → پایان Generator
StopIteration → اعلام پایان

🎯 تمرین
Generator زیر را طوری کامل کن که:
عددهای 1 و 2 را تولید کند.
سپس با return پیام "Finished" را برگرداند.
مقدار return را با StopIteration.value دریافت کن.
def process():
# code


generator = process()

print(next(generator))
print(next(generator))

try:
next(generator)
except StopIteration as error:
print(error.value)

خروجی مورد انتظار:
1
2
Finished

✅ پاسخ کامل تمرین
def process():
yield 1
yield 2
return "Finished"


generator = process()

print(next(generator))
print(next(generator))

try:
next(generator)
except StopIteration as error:
print(error.value)

خروجی:
1
2
Finished

در اینجا:
yield 1

اولین مقدار را تولید می‌کند.
سپس:
yield 2

دومین مقدار را تولید می‌کند.
در نهایت:
return "Finished"

Generator را تمام می‌کند و "Finished" داخل StopIteration.value قرار می‌گیرد.
پست ۱۱۹ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۲۰
مدیریت منابع با __enter__ و __exit__
تا اینجا با چند Magic Method برای کنترل رفتار اشیاء آشنا شدیم.
حالا به یکی از کاربردی‌ترین Protocolهای Python می‌رسیم:
Context Manager
Context Manager برای مدیریت منابعی مثل فایل‌ها، اتصال‌ها و منابع موقتی استفاده می‌شود.
معمولاً با with از آن استفاده می‌کنیم.
🔹 with چیست؟
مثلاً برای کار با فایل:
with open("data.txt", "r") as file:
content = file.read()

وقتی بلوک with تمام شود، Python فایل را مدیریت و در صورت نیاز می‌بندد.
🔹 __enter__ و __exit__
یک کلاس می‌تواند با این دو متد به یک Context Manager تبدیل شود:
class MyContext:
def __enter__(self):
print("Start")
return self

def __exit__(self, exc_type, exc_value, traceback):
print("End")

حالا:
with MyContext():
print("Inside")

خروجی:
Start
Inside
End

🔹 ترتیب اجرا
وقتی می‌نویسیم:
with MyContext():
print("Inside")

تقریباً این اتفاق می‌افتد:
__enter__()
↓
کد داخل with
↓
__exit__()

یعنی:
__enter__ → ورود به Context
__exit__ → خروج از Context

🔹 return self در __enter__
می‌توانیم مقدار برگشتی __enter__ را بعد از as دریافت کنیم:
class MyContext:
def __enter__(self):
print("Start")
return "Hello"

def __exit__(self, exc_type, exc_value, traceback):
print("End")


with MyContext() as value:
print(value)

خروجی:
Start
Hello
End

چون مقدار برگشتی __enter__ داخل value قرار می‌گیرد.
🔹 مدیریت خطا با __exit__
__exit__ اطلاعات مربوط به خطای احتمالی را دریافت می‌کند:
def __exit__(self, exc_type, exc_value, traceback):
...

اگر خطایی رخ ندهد:
exc_type  → None
exc_value → None
traceback → None

اگر داخل with خطایی رخ دهد، اطلاعات خطا به __exit__ ارسال می‌شود.
مثلاً:
class MyContext:
def __enter__(self):
print("Start")

def __exit__(self, exc_type, exc_value, traceback):
print("Error:", exc_value)


with MyContext():
print(10 / 0)

در __exit__ اطلاعات خطای تقسیم بر صفر دریافت می‌شود.
🔹 جلوگیری از انتشار خطا
مقدار برگشتی __exit__ اهمیت دارد.
اگر __exit__ مقدار:
True

برگرداند، Python خطایی را که داخل with رخ داده سرکوب می‌کند.
مثلاً:
class MyContext:
def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
print("Error:", exc_value)
return True


with MyContext():
print(10 / 0)

print("Program continues")

خروجی:
Error: division by zero
Program continues

چون:
return True

باعث می‌شود خطا دوباره به بیرون از with منتقل نشود.
⚠️ اگر True برنگردانیم
اگر __exit__ مقدار False یا None برگرداند، خطا سرکوب نمی‌شود و به بیرون منتقل خواهد شد.
پس به‌طور خلاصه:
return True       → خطا سرکوب می‌شود
return False/None → خطا ادامه پیدا می‌کند

این رفتار باید آگاهانه استفاده شود؛ چون سرکوب کردن یک خطای واقعی ممکن است باعث شود مشکل اصلی دیده نشود.
🧠 خلاصه
with        → استفاده از Context Manager
__enter__ → هنگام ورود
__exit__ → هنگام خروج
as → دریافت مقدار برگشتی __enter__

__exit__:
True → سرکوب خطا
False/None → انتشار خطا

ترتیب کلی:
with
↓
__enter__()
↓
کد داخل with
↓
__exit__()

اگر خطایی رخ دهد، اطلاعات آن به __exit__ ارسال می‌شود.
🎯 تمرین
کلاس Timer را طوری کامل کن که هنگام ورود و خروج پیام مناسب چاپ کند:
class Timer:
def __enter__(self):
# code

def __exit__(self, exc_type, exc_value, traceback):
# code


with Timer():
print("Working...")

خروجی مورد انتظار:
Timer started
Working...
Timer finished

✅ پاسخ کامل تمرین
class Timer:
def __enter__(self):
print("Timer started")
return self

def __exit__(self, exc_type, exc_value, traceback):
print("Timer finished")


with Timer():
print("Working...")

خروجی:
Timer started
Working...
Timer finished

در اینجا با ورود به with:
__enter__()

اجرا می‌شود و پیام شروع را چاپ می‌کند.
سپس کد داخل with اجرا می‌شود:
print("Working...")

و در پایان:
__exit__()

اجرا شده و پیام پایان را چاپ می‌کند.
پست ۱۲۰ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۲۱
کنترل خطا در Context Manager با __exit__
در پست قبل دیدیم که __exit__ هنگام خروج از with اجرا می‌شود.
حالا می‌خواهیم دقیق‌تر ببینیم چطور می‌توانیم خطا را بررسی کنیم و تصمیم بگیریم آن را سرکوب کنیم یا نه.
🔹 بررسی نوع خطا
سه پارامتر __exit__ اطلاعات خطا را دریافت می‌کنند:
class MyContext:
def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
print(exc_type)
print(exc_value)
print(traceback)

اگر داخل with خطایی رخ دهد، این مقادیر اطلاعات مربوط به آن خطا را در اختیارمان قرار می‌دهند.
مثلاً:
with MyContext():
print(10 / 0)

در اینجا:
exc_type  → نوع خطا
exc_value → اطلاعات خطا
traceback → اطلاعات محل وقوع خطا

🔹 تشخیص یک خطای خاص
می‌توانیم بررسی کنیم خطا از نوع مشخصی است یا نه:
class MyContext:
def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
if exc_type is ZeroDivisionError:
print("Cannot divide by zero")
return True

حالا:
with MyContext():
print(10 / 0)

print("Program continues")

خروجی:
Cannot divide by zero
Program continues

چون __exit__ مقدار True برگرداند، خطای ZeroDivisionError سرکوب شد.
🔹 فقط خطای موردنظر را مدیریت کنیم
بهتر است هر خطایی را بدون بررسی سرکوب نکنیم.
مثلاً:
class MyContext:
def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
if exc_type is ZeroDivisionError:
print("Division by zero is not allowed")
return True

return False

در این حالت:
ZeroDivisionError → مدیریت و سرکوب می‌شود
سایر خطاها → به بیرون منتقل می‌شوند

این روش کنترل بیشتری روی خطاها می‌دهد.
⚠️ نکته مهم
اگر هیچ خطایی رخ ندهد:
exc_type
exc_value
traceback

همگی None هستند.
بنابراین می‌توانیم ابتدا بررسی کنیم آیا خطایی وجود دارد یا نه:
if exc_type is not None:
print("An error occurred")

🧠 خلاصه
exc_type  → نوع Exception
exc_value → مقدار/پیام Exception
traceback → اطلاعات مسیر خطا

return True
→ خطا سرکوب می‌شود

return False / None
→ خطا ادامه پیدا می‌کند

🎯 تمرین
کلاس زیر را طوری کامل کن که فقط ValueError را مدیریت کند:
class SafeContext:
def __enter__(self):
print("Start")

def __exit__(self, exc_type, exc_value, traceback):
# اگر ValueError بود:
# پیام "ValueError handled" را چاپ کن
# و خطا را سرکوب کن

# سایر خطاها باید ادامه پیدا کنند


with SafeContext():
raise ValueError("Invalid value")

print("Program continues")

خروجی مورد انتظار:
Start
ValueError handled
Program continues

پست ۱۲۱ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۲۲
ساخت Context Manager با contextlib
تا اینجا Context Manager را با __enter__ و __exit__ ساختیم.
اما Python راه ساده‌تری هم دارد:
contextlib

در بسیاری از موارد لازم نیست یک کلاس کامل بنویسیم.
🔹 @contextmanager
از contextmanager برای ساخت Context Manager با یک تابع استفاده می‌کنیم:
from contextlib import contextmanager

@contextmanager
def my_context():
print("Start")

yield

print("End")

حالا می‌توانیم از آن با with استفاده کنیم:
with my_context():
print("Inside")

خروجی:
Start
Inside
End

🔹 yield چه نقشی دارد؟
در اینجا:
@contextmanager
def my_context():
print("Start")

yield

print("End")

کد قبل از yield هنگام ورود به with اجرا می‌شود.
کد بعد از yield هنگام خروج از with اجرا می‌شود.
یعنی:
ورود به with
↓
کد قبل از yield
↓
yield
↓
کد داخل with
↓
کد بعد از yield

🔹 ارسال مقدار به with
می‌توانیم از yield یک مقدار هم برگردانیم:
from contextlib import contextmanager

@contextmanager
def get_message():
print("Start")

yield "Hello Python"

print("End")

حالا:
with get_message() as message:
print(message)

خروجی:
Start
Hello Python
End

مقدار yield داخل message قرار می‌گیرد.
🔹 چرا contextlib مفید است؟
برای Context Managerهای ساده، این روش معمولاً خواناتر از نوشتن:
__enter__()
__exit__()

است.
مثلاً:
@contextmanager
def database_connection():
print("Connect")

yield

print("Disconnect")

استفاده:
with database_connection():
print("Working with database")

⚠️ نکته مهم
@contextmanager از ما می‌خواهد تابع حداقل یک yield داشته باشد.
بخش قبل از yield نقش ورود را دارد و بخش بعد از آن نقش خروج را.
🧠 خلاصه
@contextmanager → ساخت Context Manager با تابع
yield → نقطه انتقال کنترل به کد داخل with
قبل از yield → ورود
بعد از yield → خروج

🎯 تمرین
یک Context Manager به نام open_session بساز که خروجی زیر را ایجاد کند:
Session started
Working...
Session closed

شروع کد:
from contextlib import contextmanager

@contextmanager
def open_session():
# code


with open_session():
print("Working...")

پست ۱۲۲ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۲۳
مدیریت خطا در contextmanager
در پست قبل با @contextmanager آشنا شدیم.
حالا می‌خواهیم ببینیم اگر داخل with خطایی رخ دهد، چگونه می‌توانیم آن را مدیریت کنیم.
🔹 try و except داخل Context Manager
می‌توانیم try و except را اطراف yield قرار دهیم:
from contextlib import contextmanager

@contextmanager
def my_context():
print("Start")

try:
yield
except ValueError:
print("ValueError handled")

print("End")

حالا:
with my_context():
print("Inside")
raise ValueError("Invalid value")

خروجی:
Start
Inside
ValueError handled
End

🔹 چرا except بعد از yield قرار دارد؟
وقتی برنامه به:
yield

می‌رسد، کنترل موقتاً به کد داخل with منتقل می‌شود.
اگر آن کد خطایی ایجاد کند:
raise ValueError("Invalid value")

خطا به بخش try در Context Manager برمی‌گردد.
پس:
__enter__ / شروع
↓
try
↓
yield
↓
کد داخل with
↓
Exception
↓
except
↓
ادامه Context Manager

🔹 اگر خطا را مدیریت نکنیم چه می‌شود؟
مثلاً:
from contextlib import contextmanager

@contextmanager
def my_context():
print("Start")

try:
yield
finally:
print("End")

اگر بنویسیم:
with my_context():
raise ValueError("Something went wrong")

بخش finally اجرا می‌شود:
Start
End

اما ValueError همچنان به بیرون منتقل می‌شود.
🔹 finally برای Cleanup
یکی از کاربردهای مهم Context Manager این است که عملیات پایانی حتماً انجام شود.
برای این کار finally بسیار مهم است:
from contextlib import contextmanager

@contextmanager
def resource():
print("Resource opened")

try:
yield
finally:
print("Resource closed")

حتی اگر داخل with خطا رخ دهد:
with resource():
print("Working...")
raise ValueError("Error")

باز هم:
Resource opened
Working...
Resource closed

سپس خطا به بیرون منتقل می‌شود.
⚠️ تفاوت مهم except و finally
except   → برای مدیریت یک خطا
finally → برای اجرای قطعی عملیات پایانی

مثلاً:
try:
...
except ValueError:
...
finally:
...

ممکن است از except برای مدیریت خطا و از finally برای آزاد کردن منابع استفاده کنیم.
🧠 خلاصه
@contextmanager
↓
yield
↓
کد داخل with
↓
خطا؟
↙️ ↘️
بله خیر
↓ ↓
except finally
↓
cleanup

در Context Managerها معمولاً:
try/finally

برای cleanup بسیار مهم است.
🎯 تمرین
Context Manager زیر را کامل کن تا:
پیام Start هنگام ورود چاپ شود.
کد داخل with اجرا شود.
حتی اگر خطایی رخ داد، End حتماً چاپ شود.
from contextlib import contextmanager

@contextmanager
def my_context():
# code


with my_context():
print("Working...")
raise ValueError("Something went wrong")

خروجی مورد انتظار:
Start
Working...
End

✅ پاسخ کامل تمرین
from contextlib import contextmanager


@contextmanager
def my_context():
print("Start")

try:
yield
finally:
print("End")


with my_context():
print("Working...")
raise ValueError("Something went wrong")

خروجی:
Start
Working...
End

اما یک نکته مهم وجود دارد:
با وجود اینکه End چاپ می‌شود، خطای:
ValueError: Something went wrong

همچنان به بیرون منتقل می‌شود؛ چون در این مثال except نداریم و خطا را سرکوب نکرده‌ایم.
پس finally باعث مدیریت یا حذف خطا نمی‌شود؛ وظیفه آن این است که کد پایانی را اجرا کند.
در این مثال:
finally:
print("End")

حتی هنگام رخ دادن خطا نیز اجرا می‌شود.
پست ۱۲۳ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۲۴
Context Manager آماده در Python | closing()
تا اینجا یاد گرفتیم چطور Context Manager خودمان را با __enter__، __exit__ و @contextmanager بسازیم.
اما گاهی یک شیء داریم که متد close() دارد، ولی خودش Context Manager نیست.
برای این حالت می‌توانیم از closing() در contextlib استفاده کنیم.
🔹 closing() چیست؟
closing() باعث می‌شود در پایان بلوک with، متد close() شیء به‌صورت خودکار اجرا شود.
مثلاً:
from contextlib import closing

class Resource:
def open(self):
print("Resource opened")

def close(self):
print("Resource closed")


resource = Resource()

with closing(resource):
resource.open()
print("Working...")

خروجی:
Resource opened
Working...
Resource closed

🔹 چرا close() اجرا شد؟
closing() در پایان with تضمین می‌کند که:
resource.close()

اجرا شود.
یعنی تقریباً:
with
↓
کد اصلی
↓
close()

این موضوع برای منابعی که باید بعد از استفاده بسته یا آزاد شوند مفید است.
🔹 دریافت شیء با as
می‌توانیم شیء را مستقیماً با as دریافت کنیم:
from contextlib import closing

resource = Resource()

with closing(resource) as item:
item.open()

در اینجا:
item → همان resource

است.
⚠️ نکته مهم
closing() برای اشیایی مناسب است که متد:
close()

دارند.
اگر شیء چنین متدی نداشته باشد، استفاده از closing() مناسب نیست.
🧠 خلاصه
closing(object)
↓
with
↓
کد داخل with
↓
object.close()

پس closing() یک راه ساده برای استفاده از اشیای دارای close() به‌عنوان Context Manager است.
🎯 تمرین
کد زیر را کامل کن:
from contextlib import closing

class Connection:
def connect(self):
print("Connected")

def close(self):
print("Connection closed")


connection = Connection()

with closing(connection):
connection.connect()
print("Working...")

بدون اجرا، خروجی را حدس بزن.
پست ۱۲۴ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۲۵
مدیریت چند Context Manager با ExitStack
گاهی لازم است چند منبع را هم‌زمان مدیریت کنیم.
مثلاً چند فایل، اتصال یا Resource مختلف.
برای این کار contextlib ابزاری به نام ExitStack دارد.
🔹 مشکل چیست؟
می‌توانیم چند with را داخل هم بنویسیم:
with open("file1.txt") as file1:
with open("file2.txt") as file2:
print("Working...")

اما اگر تعداد Context Managerها پویا باشد، این روش مناسب نیست.
🔹 ExitStack
با ExitStack می‌توانیم Context Managerها را به‌صورت پویا مدیریت کنیم:
from contextlib import ExitStack

with ExitStack() as stack:
file1 = stack.enter_context(open("file1.txt"))
file2 = stack.enter_context(open("file2.txt"))

print("Working...")

در پایان بلوک with، منابع مدیریت‌شده توسط stack به‌صورت خودکار خارج می‌شوند.
🔹 enter_context() چگونه کار می‌کند؟
برای وارد کردن یک Resource به ExitStack از:
stack.enter_context(resource)

استفاده می‌کنیم.
مثلاً:
with ExitStack() as stack:
resource_a = stack.enter_context(Resource("A"))
resource_b = stack.enter_context(Resource("B"))
resource_c = stack.enter_context(Resource("C"))

print("Working...")

هر بار که enter_context() اجرا می‌شود:
__enter__() همان Resource اجرا می‌شود.
مقدار برگشتی __enter__() برگردانده می‌شود.
__exit__() برای زمان خروج توسط ExitStack ثبت می‌شود.
پس:
Resource("A")
↓
A.__enter__()

Resource("B")
↓
B.__enter__()

Resource("C")
↓
C.__enter__()

و هنگام خروج:
C.__exit__()
↓
B.__exit__()
↓
A.__exit__()

یعنی Resourceها به ترتیب ورود وارد می‌شوند، اما به ترتیب معکوس خارج می‌شوند.
🔹 چرا ExitStack مفید است؟
فرض کنیم تعداد فایل‌ها مشخص نیست:
files = ["a.txt", "b.txt", "c.txt"]

می‌توانیم همه را با یک حلقه باز کنیم:
from contextlib import ExitStack

files = ["a.txt", "b.txt", "c.txt"]

with ExitStack() as stack:
opened_files = []

for filename in files:
file = stack.enter_context(open(filename))
opened_files.append(file)

print(len(opened_files))

اینجا تعداد فایل‌ها می‌تواند تغییر کند، بدون اینکه تعداد withها را از قبل مشخص کنیم.
🔹 ترتیب خروج
ExitStack Context Managerها را به ترتیب معکوس ورود خارج می‌کند.
اگر:
A → ورود
B → ورود
C → ورود

باشد، هنگام خروج:
C → خروج
B → خروج
A → خروج

این رفتار شبیه ساختار Stack است:
Last In → First Out

🔹 اضافه کردن Cleanup
ExitStack حتی می‌تواند یک تابع cleanup را هم ثبت کند:
from contextlib import ExitStack

def cleanup():
print("Cleanup done")

with ExitStack() as stack:
stack.callback(cleanup)
print("Working...")

خروجی:
Working...
Cleanup done

تابع cleanup هنگام خروج از with اجرا می‌شود.
⚠️ نکته مهم
ExitStack زمانی بسیار مفید است که:
تعداد Context Managerها از قبل مشخص نیست.
منابع به‌صورت پویا ایجاد می‌شوند.
چند Resource باید با هم مدیریت شوند.
Cleanup باید حتی هنگام رخ دادن Exception انجام شود.
🧠 خلاصه
ExitStack
↓
مدیریت چند Context Manager
↓
enter_context()
↓
__enter__()
↓
مدیریت Resource
↓
__exit__()

و ترتیب خروج:
A → B → C
↓
C → B → A

🎯 تمرین
کد زیر را کامل کن تا سه Resource را با ExitStack مدیریت کند:
from contextlib import ExitStack

class Resource:
def __init__(self, name):
self.name = name

def __enter__(self):
print(f"{self.name} started")
return self

def __exit__(self, exc_type, exc_value, traceback):
print(f"{self.name} closed")


with ExitStack() as stack:
# سه Resource با نام‌های A ،B و C را وارد Stack کن
# سپس عبارت زیر را چاپ کن:
print("Working...")

خروجی مورد انتظار:
A started
B started
C started
Working...
C closed
B closed
A closed

پست ۱۲۵ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۲۶
مدیریت موقت Context Manager با ExitStack
گاهی تعداد Resourceها مشخص نیست یا یک Resource فقط در شرایط خاصی لازم است.
ExitStack اجازه می‌دهد Resourceها را به‌صورت پویا و شرطی وارد مدیریت کنیم.
🔹 ورود شرطی Resource
from contextlib import ExitStack

use_cache = True

with ExitStack() as stack:

if use_cache:
cache = stack.enter_context(Resource("Cache"))

print("Application running")

اگر use_cache برابر False باشد، Resource اصلاً وارد Stack نمی‌شود.
🔹 اضافه کردن چند Resource
with ExitStack() as stack:

if condition_1:
resource_a = stack.enter_context(Resource("A"))

if condition_2:
resource_b = stack.enter_context(Resource("B"))

print("Working...")

Resourceها هنگام خروج، برعکس ترتیب ورود cleanup می‌شوند:
A → B → C
↓
C → B → A

همچنین می‌توانیم تعداد متغیری Resource را با حلقه مدیریت کنیم:
resources = ["A", "B", "C"]

with ExitStack() as stack:
for name in resources:
stack.enter_context(Resource(name))

print("All resources are active")

🔹 اگر هنگام ورود Resource خطا رخ دهد؟
ممکن است __enter__() یک Resource خطا ایجاد کند:
class Resource:

def __init__(self, name):
self.name = name

def __enter__(self):
print(f"{self.name} started")

if self.name == "D":
raise RuntimeError("D failed")

return self

def __exit__(self, exc_type, exc_value, traceback):
print(f"{self.name} closed")

حالا:
with ExitStack() as stack:
a = stack.enter_context(Resource("A"))
d = stack.enter_context(Resource("D"))

print("Working...")

خروجی:
A started
D started
RuntimeError: D failed
A closed

چون A قبلاً با موفقیت وارد Stack شده، در پایان cleanup می‌شود.
اما D به دلیل خطا در __enter__() وارد Stack نشده است.
🔹 مدیریت خطا با try/except
می‌توانیم خطای ورود را مدیریت کنیم:
with ExitStack() as stack:

try:
a = stack.enter_context(Resource("A"))
d = stack.enter_context(Resource("D"))

except RuntimeError as error:
print("Failed to enter resource:", error)

print("Working...")

خروجی:
A started
D started
Failed to enter resource: D failed
Working...
A closed

اینجا:
try/except → مدیریت خطا
ExitStack → مدیریت Resourceها

A همچنان توسط ExitStack cleanup می‌شود.
⚠️ نکته مهم
اگر نخواهیم خطا را متوقف کنیم، آن را در except مدیریت می‌کنیم.
اگر بخواهیم خطا دوباره به بیرون منتقل شود:
except RuntimeError as error:
print(error)
raise

🧠 خلاصه
enter_context()
↓
__enter__()
↓
موفق → ثبت __exit__
خطا → try/except
↓
Cleanup Resourceهای قبلی

مزیت ExitStack:
مدیریت پویا، شرطی و امن چند Resource، حتی زمانی که هنگام ورود یکی از آن‌ها خطا رخ دهد.

🎯 تمرین
کد را کامل کن:
from contextlib import ExitStack

with ExitStack() as stack:

try:
# Resource A را وارد کن

# Resource D را وارد کن

pass

except RuntimeError as error:
print("Failed to enter resource:", error)

print("Working...")

خروجی مورد انتظار:
A started
D started
Failed to enter resource: D failed
Working...
A closed

پست ۱۲۶ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۲۷
ساخت Context Manager با @contextmanager
تا اینجا Context Manager را با کلاس و متدهای __enter__() و __exit__() ساختیم.
اما Python روش ساده‌تری هم دارد:
from contextlib import contextmanager

با @contextmanager می‌توانیم یک تابع را به Context Manager تبدیل کنیم.
🔹 ساخت یک Context Manager ساده
from contextlib import contextmanager

@contextmanager
def my_context():
print("Start")

yield

print("End")

حالا می‌توانیم از آن با with استفاده کنیم:
with my_context():
print("Working...")

خروجی:
Start
Working...
End

🔹 yield چه نقشی دارد؟
در Context Manager ساخته‌شده با @contextmanager:
@contextmanager
def my_context():
# قبل از with
yield
# بعد از with

کد قبل از yield هنگام ورود اجرا می‌شود.
کد بعد از yield هنگام خروج اجرا می‌شود.
یعنی:
__enter__
↓
قبل از yield
↓
کد داخل with
↓
بعد از yield
↓
__exit__

🔹 استفاده از as
می‌توانیم از yield یک مقدار برگردانیم:
from contextlib import contextmanager

@contextmanager
def resource():
print("Opening")

data = "Python"
yield data

print("Closing")

حالا:
with resource() as value:
print(value)

خروجی:
Opening
Python
Closing

مقداری که yield می‌کند، داخل متغیر value قرار می‌گیرد.
⚠️ نکته مهم
@contextmanager از ما می‌خواهد تابع شامل yield باشد.
ساختار کلی:
@contextmanager
def my_context():
# setup

yield value

# cleanup

بخش setup قبل از yield و بخش cleanup بعد از آن قرار می‌گیرد.
🎯 تمرین
یک Context Manager به نام database_connection بساز که:
قبل از yield چاپ کند:
Connecting...

داخل with این پیام چاپ شود:
Query running...

بعد از yield چاپ کند:
Connection closed.

پست ۱۲۷ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۲۸
مدیریت خطا در @contextmanager
در پست قبل یاد گرفتیم با @contextmanager یک Context Manager بسازیم.
حالا باید ببینیم اگر داخل with خطایی رخ دهد، چطور آن را مدیریت کنیم.
🔹 استفاده از try/except
می‌توانیم yield را داخل try قرار دهیم:
from contextlib import contextmanager

@contextmanager
def my_context():
print("Start")

try:
yield
except ValueError:
print("Value error handled")

print("End")

حالا:
with my_context():
print("Working...")
raise ValueError("Invalid value")

خروجی:
Start
Working...
Value error handled
End

خطا در محل yield به Context Manager برمی‌گردد و except می‌تواند آن را مدیریت کند.
🔹 اگر خطا را مدیریت نکنیم
مثلاً:
@contextmanager
def my_context():
print("Start")

try:
yield
finally:
print("Cleanup")

اگر داخل with خطا رخ دهد:
with my_context():
raise ValueError("Something went wrong")

finally اجرا می‌شود، اما خطا همچنان به بیرون منتقل می‌شود.
Start
Cleanup
ValueError

⚠️ except و finally
except  → برای مدیریت خطا
finally → برای cleanup که باید در هر حالت اجرا شود

در Context Managerها معمولاً cleanup را در finally قرار می‌دهیم.
🧠 خلاصه
@contextmanager
def my_context():
try:
# setup
yield
except ValueError:
# handle error
pass
finally:
# cleanup
pass

🎯 تمرین
کد را کامل کن تا خطای ValueError مدیریت شود:
from contextlib import contextmanager

@contextmanager
def database():
print("Connecting...")

try:
yield
except ValueError:
print("Invalid query")
finally:
print("Connection closed")


with database():
print("Query running...")
raise ValueError("Bad query")

پست ۱۲۸ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۲۹
Context Manager با مدیریت چند Resource
گاهی یک Context Manager خودش باید چند Resource را هم‌زمان مدیریت کند.
مثلاً برنامه‌ای که هم‌زمان به یک فایل و یک اتصال دیگر نیاز دارد.
🔹 مدیریت چند Resource
می‌توانیم داخل یک Context Manager از ExitStack استفاده کنیم:
from contextlib import contextmanager
from contextlib import ExitStack

@contextmanager
def resources():
with ExitStack() as stack:
file = stack.enter_context(open("data.txt", "w"))

print("Resources opened")

yield file

print("Resources closing")

حالا:
with resources() as file:
file.write("Hello Python")

در اینجا:
resources()
↓
ExitStack
↓
باز شدن Resourceها
↓
yield
↓
کد داخل with
↓
cleanup

🔹 چرا ExitStack داخل Context Manager؟
چون ممکن است تعداد Resourceها ثابت نباشد:
@contextmanager
def open_files(file_names):

with ExitStack() as stack:

files = []

for name in file_names:
file = stack.enter_context(open(name, "w"))
files.append(file)

yield files

حالا:
files = ["a.txt", "b.txt", "c.txt"]

with open_files(files) as opened_files:
for file in opened_files:
file.write("Hello")

هر تعداد فایل که در لیست باشد، ExitStack آن‌ها را مدیریت می‌کند.
⚠️ نکته مهم
@contextmanager ساختار Context Manager را ساده می‌کند.
ExitStack هم برای مدیریت چند Resource پویا بسیار مناسب است.
ترکیب این دو زمانی مفید است که یک Context Manager خودش نیاز به مدیریت چند Resource داشته باشد.
🧠 خلاصه
@contextmanager
↓
ساخت Context Manager ساده
+
ExitStack
↓
مدیریت چند Resource
↓
cleanup خودکار

🎯 تمرین
یک Context Manager به نام open_files بنویس که:
لیستی از نام فایل‌ها دریافت کند.
همه فایل‌ها را با ExitStack باز کند.
لیست فایل‌های بازشده را با yield برگرداند.
بعد از خروج از with، همه فایل‌ها خودکار بسته شوند.
پست ۱۲۹ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۳۰
Context Manager با __enter__ و __exit__ در کلاس‌ها
تا اینجا Context Manager را با @contextmanager و ExitStack دیدیم.
حالا یک نکته مهم دیگر: می‌توانیم یک کلاس Context Manager بسازیم که علاوه بر مدیریت ورود و خروج، اطلاعات یا وضعیت داخلی خودش را هم نگه دارد.
🔹 ساخت Context Manager کلاسی
class Database:
def __init__(self, name):
self.name = name

def __enter__(self):
print(f"Connecting to {self.name}")
return self

def __exit__(self, exc_type, exc_value, traceback):
print(f"Closing {self.name}")

استفاده:
with Database("MyDB") as db:
print(f"Using {db.name}")

خروجی:
Connecting to MyDB
Using MyDB
Closing MyDB

🔹 چرا return self؟
در:
with Database("MyDB") as db:

مقداری که __enter__() برمی‌گرداند، داخل db قرار می‌گیرد.
پس:
return self

باعث می‌شود خود شیء را داخل db داشته باشیم.
🔹 مدیریت خطا
__exit__() اطلاعات خطا را دریافت می‌کند:
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None:
print("An error occurred")

print("Closing database")

اگر داخل with خطایی رخ دهد، exc_type و exc_value اطلاعات آن خطا را در اختیارمان قرار می‌دهند.
اگر خطایی رخ ندهد:
exc_type = None
exc_value = None
traceback = None

⚠️ نکته مهم
اگر __exit__() مقدار True برگرداند، Exception سرکوب می‌شود.
def __exit__(self, exc_type, exc_value, traceback):
print("Closing database")
return True

اما اگر False یا None برگرداند، خطا به بیرون منتقل می‌شود.
پس return True را فقط زمانی استفاده کن که عمداً می‌خواهی خطا را مدیریت و متوقف کنی.
🧠 خلاصه
__enter__() → ورود و آماده‌سازی
yield/as → در اختیار قرار دادن Resource
__exit__() → خروج و Cleanup

🎯 تمرین
یک کلاس FileManager بساز که:
نام فایل را در __init__ دریافت کند.
در __enter__() فایل را باز کند.
خود فایل را با return برگرداند.
در __exit__() فایل را ببندد.
استفاده:
with FileManager("data.txt") as file:
file.write("Hello Python")

پست ۱۳۰ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۳۱
Context Manager برای فایل‌ها با __enter__ و __exit__
در پست قبل یک Context Manager کلاسی ساختیم.
حالا آن را روی فایل‌ها پیاده می‌کنیم تا ببینیم چطور می‌توانیم باز کردن و بستن فایل را خودمان مدیریت کنیم.
🔹 ساخت FileManager
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None

def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file

def __exit__(self, exc_type, exc_value, traceback):
self.file.close()

حالا می‌توانیم از آن با with استفاده کنیم:
with FileManager("data.txt", "w") as file:
file.write("Hello Python")

ترتیب اجرا:
__enter__()
↓
باز شدن فایل
↓
کد داخل with
↓
__exit__()
↓
بسته شدن فایل

🔹 اگر داخل with خطا رخ دهد؟
حتی اگر داخل with خطایی ایجاد شود، __exit__() اجرا می‌شود:
with FileManager("data.txt", "w") as file:
file.write("Hello")
raise ValueError("Something went wrong")

قبل از خروج، فایل بسته می‌شود.
بنابراین __exit__() محل مناسبی برای cleanup است.
⚠️ نکته مهم
در Python برای کار معمول با فایل‌ها نیازی نیست خودمان چنین کلاسی بنویسیم.
روش استاندارد همان است:
with open("data.txt", "w") as file:
file.write("Hello Python")

این مثال فقط برای درک بهتر نحوه کار Context Manager است.
🧠 خلاصه
__enter__() → باز کردن Resource
with → استفاده از Resource
__exit__() → cleanup و بستن Resource

🎯 تمرین
کلاس FileManager را طوری تغییر بده که اگر داخل with خطایی رخ داد، پیام زیر را چاپ کند:
An error occurred

اما فایل همچنان در __exit__() بسته شود.
پست ۱۳۱ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۳۲
Context Manager و مدیریت خطا در __exit__
در پست قبل دیدیم که __exit__() حتی هنگام رخ دادن خطا هم اجرا می‌شود.
حالا می‌خواهیم از اطلاعات خطا داخل __exit__() استفاده کنیم.
🔹 اطلاعات خطا در __exit__
متد __exit__() سه پارامتر مهم دریافت می‌کند:
def __exit__(self, exc_type, exc_value, traceback):

این پارامترها شامل:
exc_type   → نوع Exception
exc_value → پیام Exception
traceback → اطلاعات محل رخ دادن خطا

اگر خطایی رخ نداده باشد، هر سه مقدار None هستند.
🔹 بررسی وجود خطا
class MyContext:

def __enter__(self):
print("Start")

def __exit__(self, exc_type, exc_value, traceback):

if exc_type is not None:
print("Error:", exc_value)

print("Cleanup")

استفاده:
with MyContext():
print("Working...")
raise ValueError("Invalid value")

خروجی:
Start
Working...
Error: Invalid value
Cleanup

🔹 جلوگیری از انتشار خطا
اگر __exit__() مقدار True برگرداند، Exception دیگر به بیرون منتقل نمی‌شود:
def __exit__(self, exc_type, exc_value, traceback):

if exc_type is not None:
print("Error handled")
return True

در این حالت برنامه بعد از with ادامه پیدا می‌کند.
⚠️ نکته مهم
اگر فقط می‌خواهیم خطا را مشاهده کنیم و اجازه دهیم Exception به بیرون منتقل شود، نباید True برگردانیم.
def __exit__(self, exc_type, exc_value, traceback):
print("Cleanup")

اما:
return True

یعنی:
این Exception را مدیریت کردم؛ دیگر آن را به بیرون منتقل نکن.

🧠 خلاصه
exc_type   → نوع خطا
exc_value → اطلاعات خطا
traceback → محل رخ دادن خطا

return True → خطا سرکوب می‌شود
return None → خطا ادامه پیدا می‌کند

🎯 تمرین کامل
یک Context Manager به نام SafeContext بساز که رفتار زیر را داشته باشد:
در __enter__() پیام زیر را چاپ کند:
Context started

اگر هیچ خطایی رخ نداد، در __exit__() پیام زیر را چاپ کند:
Context finished

اگر ValueError رخ داد:
پیام خطا را چاپ کند.
خطا را با return True مدیریت کند.
اگر Exception دیگری رخ داد:
پیام Unknown error را چاپ کند.
خطا را به بیرون منتقل کند.
کد اولیه:
class SafeContext:

def __enter__(self):
# کد را کامل کن
pass

def __exit__(self, exc_type, exc_value, traceback):
# کد را کامل کن
pass

حالا این دو حالت را تست کن:
حالت اول
with SafeContext():
print("Working...")
raise ValueError("Invalid value")

خروجی مورد انتظار:
Context started
Working...
Error: Invalid value

حالت دوم
with SafeContext():
print("Working...")
raise TypeError("Wrong type")

در این حالت باید:
Context started
Working...
Unknown error

نمایش داده شود و TypeError همچنان به بیرون منتقل شود.
پست ۱۳۲ از دوره جامع Python 🐍