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 — پست ۱۰۵
تعیین طول شیء با __len__
در پست قبل با __str__ و __repr__ آشنا شدیم.
حالا یکی دیگر از متدهای جادویی (Magic Methods) را یاد می‌گیریم:
__len__

این متد مشخص می‌کند وقتی از len() روی یک شیء استفاده می‌کنیم، چه مقداری برگردانده شود.
🔹 استفاده از __len__
مثلاً می‌توانیم برای یک کلاس Playlist تعداد آهنگ‌ها را مشخص کنیم:
class Playlist:

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

def __len__(self):
return len(self.songs)

حالا:
playlist = Playlist(["Song 1", "Song 2", "Song 3"])

print(len(playlist))

خروجی:
3

🔹 چرا __len__؟
Python نمی‌داند طول یک شیء ساخته‌شده از کلاس ما چقدر است.
با تعریف:
def __len__(self):

مشخص می‌کنیم len() چگونه مقدار طول را محاسبه کند.
مثلاً:
class Team:

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

def __len__(self):
return len(self.members)


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

print(len(team))

خروجی:
4

⚠️ نکته مهم
__len__ باید یک عدد صحیح (int) غیرمنفی برگرداند.
مثلاً این اشتباه است:
def __len__(self):
return "5"

چون "5" یک رشته است، نه عدد.
🔹 ارتباط با bool
یک نکته جالب:
اگر کلاس __bool__ نداشته باشد، Python می‌تواند از __len__ برای تعیین True یا False بودن شیء استفاده کند.
class Team:

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

def __len__(self):
return len(self.members)


team = Team([])

print(bool(team))

خروجی:
False

چون طول شیء 0 است.
اگر عضو داشته باشیم:
team = Team(["Ali"])

print(bool(team))

خروجی:
True

🎯 تمرین
کلاس Cart را کامل کن تا len() تعداد محصولات سبد خرید را برگرداند:
class Cart:

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

def __len__(self):
# کد اینجا


cart = Cart(["Laptop", "Mouse", "Keyboard"])

print(len(cart))

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

پست ۱۰۵ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۰۶
مقایسه اشیاء با __eq__
تا اینجا دیدیم چطور رفتار شیء را با متدهایی مثل __str__ و __len__ کنترل کنیم.
حالا می‌خواهیم مشخص کنیم دو شیء چه زمانی برابر محسوب می‌شوند.
برای این کار از متد جادویی زیر استفاده می‌کنیم:
__eq__

🔹 __eq__ چیست؟
وقتی از == استفاده می‌کنیم، Python می‌تواند از __eq__ برای مقایسه دو شیء استفاده کند.
مثلاً:
class Product:

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

def __eq__(self, other):
return self.name == other.name and self.price == other.price

حالا:
product1 = Product("Laptop", 1200)
product2 = Product("Laptop", 1200)

print(product1 == product2)

خروجی:
True

چون name و price هر دو برابر هستند.
🔹 اگر مقدارها متفاوت باشند
product1 = Product("Laptop", 1200)
product2 = Product("Laptop", 1500)

print(product1 == product2)

خروجی:
False

🔹 چرا این متد مهم است؟
بدون پیاده‌سازی __eq__، دو شیء مستقل معمولاً فقط به دلیل اینکه مقادیر مشابهی دارند، برابر در نظر گرفته نمی‌شوند.
class Product:

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


product1 = Product("Laptop", 1200)
product2 = Product("Laptop", 1200)

print(product1 == product2)

در این حالت نتیجه معمولاً:
False

است؛ چون product1 و product2 دو شیء جداگانه هستند.
🔹 پارامتر other
در این متد:
def __eq__(self, other):

self به شیء سمت چپ و other به شیء سمت راست مقایسه اشاره می‌کند.
مثلاً:
product1 == product2

تقریباً به این شکل بررسی می‌شود:
product1.__eq__(product2)

⚠️ نکته مهم
باید مراقب نوع other باشیم.
مثلاً این کد:
product == 100

ممکن است باعث خطا شود، چون 100 ویژگی‌های name و price را ندارد.
یک روش امن‌تر:
def __eq__(self, other):

if not isinstance(other, Product):
return NotImplemented

return self.name == other.name and self.price == other.price

NotImplemented به Python می‌گوید این مقایسه را با این نوع شیء نمی‌توانیم انجام دهیم.
🎯 تمرین
کلاس User را طوری کامل کن که دو کاربر وقتی username و age یکسان دارند، برابر محسوب شوند:
class User:

def __init__(self, username, age):
self.username = username
self.age = age

def __eq__(self, other):
# کد اینجا


user1 = User("ali", 25)
user2 = User("ali", 25)
user3 = User("sara", 25)

print(user1 == user2)
print(user1 == user3)

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

✅ پاسخ کامل تمرین
class User:

def __init__(self, username, age):
self.username = username
self.age = age

def __eq__(self, other):

if not isinstance(other, User):
return NotImplemented

return (
self.username == other.username
and self.age == other.age
)


user1 = User("ali", 25)
user2 = User("ali", 25)
user3 = User("sara", 25)

print(user1 == user2)
print(user1 == user3)

خروجی:
True
False

در این قسمت:
self.username == other.username

نام کاربری دو شیء را مقایسه می‌کنیم.
و در این قسمت:
self.age == other.age

سن دو شیء را مقایسه می‌کنیم.
در نهایت با and هر دو شرط باید True باشند تا دو کاربر برابر محسوب شوند.
پست ۱۰۶ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۰۷
مقایسه اشیاء با __lt__
در پست قبل یاد گرفتیم با __eq__ مشخص کنیم دو شیء چه زمانی برابر هستند.
حالا می‌خواهیم رفتار عملگر < را برای اشیای کلاس خودمان تعریف کنیم.
برای این کار از:
__lt__

استفاده می‌کنیم.
🔹 __lt__ چیست؟
__lt__ مخفف Less Than است.
یعنی:
<

مثلاً می‌توانیم محصولات را بر اساس قیمت مقایسه کنیم:
class Product:

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

def __lt__(self, other):
return self.price < other.price

حالا:
product1 = Product("Mouse", 20)
product2 = Product("Keyboard", 50)

print(product1 < product2)

خروجی:
True

چون:
20 < 50

🔹 مقایسه برعکس
print(product2 < product1)

خروجی:
False

چون:
50 < 20

صحیح نیست.
🔹 __lt__ چگونه اجرا می‌شود؟
وقتی می‌نویسیم:
product1 < product2

Python تقریباً این متد را فراخوانی می‌کند:
product1.__lt__(product2)

بنابراین:
self

به product1 و:
other

به product2 اشاره می‌کند.
⚠️ نکته مهم
__lt__ فقط رفتار < را تعریف می‌کند.
یعنی با تعریف آن، لزوماً تمام عملگرهای مقایسه‌ای دیگر (>, <=, >=) را برای کلاس تعریف نکرده‌ایم.
هرکدام از این رفتارها متدهای ویژه مربوط به خودشان را دارند.
🔹 مثال کاربردی
می‌توانیم دانش‌آموزها را بر اساس نمره مقایسه کنیم:
class Student:

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

def __lt__(self, other):
return self.score < other.score


student1 = Student("Ali", 15)
student2 = Student("Sara", 18)

print(student1 < student2)

خروجی:
True

چون نمره Ali کمتر از نمره Sara است.
🎯 تمرین
کلاس Book را کامل کن تا دو کتاب بر اساس قیمت با < مقایسه شوند:
class Book:

def __init__(self, title, price):
self.title = title
self.price = price

def __lt__(self, other):
# کد اینجا


book1 = Book("Python", 30)
book2 = Book("Django", 45)

print(book1 < book2)
print(book2 < book1)

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

پست ۱۰۷ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۰۸
مقایسه اشیاء با __gt__
در پست قبل با __lt__ یاد گرفتیم رفتار عملگر < را برای اشیای کلاس خودمان تعریف کنیم.
حالا نوبت عملگر بزرگ‌تر است:
>

برای تعریف این رفتار از متد جادویی:
__gt__

استفاده می‌کنیم.
🔹 __gt__ چیست؟
__gt__ مخفف Greater Than است.
مثلاً می‌توانیم محصولات را بر اساس قیمت مقایسه کنیم:
class Product:

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

def __gt__(self, other):
return self.price > other.price

حالا:
product1 = Product("Laptop", 1200)
product2 = Product("Mouse", 50)

print(product1 > product2)

خروجی:
True

چون:
1200 > 50

🔹 مقایسه برعکس
print(product2 > product1)

خروجی:
False

چون:
50 > 1200

صحیح نیست.
🔹 __gt__ چگونه اجرا می‌شود؟
وقتی می‌نویسیم:
product1 > product2

Python از متد مقایسه‌ای مربوط به این عملگر استفاده می‌کند؛ در حالت معمول:
product1.__gt__(product2)

بنابراین:
self

به شیء سمت چپ و:
other

به شیء سمت راست اشاره می‌کند.
🔹 مثال با دانش‌آموز
class Student:

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

def __gt__(self, other):
return self.score > other.score


student1 = Student("Ali", 18)
student2 = Student("Sara", 15)

print(student1 > student2)

خروجی:
True

چون نمره 18 از 15 بیشتر است.
⚠️ نکته مهم
__gt__ فقط رفتار عملگر:
>

را برای کلاس تعریف می‌کند.
برای عملگرهایی مانند:
<   <=   >=

متدهای مقایسه‌ای دیگری وجود دارند.
در ادامه با آن‌ها آشنا می‌شویم.
🎯 تمرین
کلاس Car را کامل کن تا خودروها بر اساس قیمت با > مقایسه شوند:
class Car:

def __init__(self, model, price):
self.model = model
self.price = price

def __gt__(self, other):
# کد اینجا


car1 = Car("BMW", 50000)
car2 = Car("Toyota", 30000)

print(car1 > car2)
print(car2 > car1)

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

پست ۱۰۸ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۰۹
مقایسه اشیاء با __le__ و __ge__
در دو پست قبل با:
__lt__
__gt__

رفتار < و > را برای اشیای کلاس تعریف کردیم.
حالا دو عملگر دیگر را یاد می‌گیریم:
<=
>=

🔹 __le__
__le__ مخفف Less Than or Equal است.
یعنی:
کوچک‌تر یا مساوی

مثلاً:
class Product:

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

def __le__(self, other):
return self.price <= other.price

حالا:
product1 = Product("Mouse", 50)
product2 = Product("Keyboard", 50)

print(product1 <= product2)

خروجی:
True

چون:
50 <= 50

صحیح است.
🔹 __ge__
__ge__ مخفف Greater Than or Equal است.
یعنی:
بزرگ‌تر یا مساوی

مثلاً:
class Product:

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

def __ge__(self, other):
return self.price >= other.price

حالا:
product1 = Product("Laptop", 1200)
product2 = Product("Mouse", 50)

print(product1 >= product2)

خروجی:
True

چون:
1200 >= 50

صحیح است.
🔹 یک مثال کامل
می‌توانیم هر دو متد را در یک کلاس داشته باشیم:
class Student:

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

def __le__(self, other):
return self.score <= other.score

def __ge__(self, other):
return self.score >= other.score


student1 = Student("Ali", 18)
student2 = Student("Sara", 18)

print(student1 <= student2)
print(student1 >= student2)

خروجی:
True
True

چون نمره هر دو دانش‌آموز 18 است.
🧠 متدهای مقایسه‌ای مهم
تا اینجا با این متدها آشنا شدیم:
__eq__  → ==
__lt__ → <
__gt__ → >
__le__ → <=
__ge__ → >=

این متدها به Python می‌گویند عملگرهای مقایسه‌ای با اشیای کلاس ما چگونه رفتار کنند.
🎯 تمرین
کلاس Book را کامل کن تا دو کتاب بر اساس قیمت با <= و >= مقایسه شوند:
class Book:

def __init__(self, title, price):
self.title = title
self.price = price

def __le__(self, other):
# کد اینجا

def __ge__(self, other):
# کد اینجا


book1 = Book("Python", 30)
book2 = Book("Django", 45)

print(book1 <= book2)
print(book1 >= book2)

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

✅ پاسخ کامل تمرین
class Book:

def __init__(self, title, price):
self.title = title
self.price = price

def __le__(self, other):
return self.price <= other.price

def __ge__(self, other):
return self.price >= other.price


book1 = Book("Python", 30)
book2 = Book("Django", 45)

print(book1 <= book2)
print(book1 >= book2)

خروجی:
True
False

در این قسمت:
return self.price <= other.price

قیمت کتاب سمت چپ با قیمت کتاب سمت راست مقایسه می‌شود.
و در:
return self.price >= other.price

برعکس، بررسی می‌کنیم که قیمت کتاب سمت چپ بزرگ‌تر یا مساوی قیمت کتاب سمت راست باشد.
پست ۱۰۹ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۱۰
بررسی نابرابری اشیاء با __ne__
در پست‌های قبل با متدهای مقایسه‌ای مختلف آشنا شدیم.
حالا می‌خواهیم رفتار عملگر:
!=

را برای اشیای کلاس خودمان تعریف کنیم.
برای این کار از متد جادویی:
__ne__

استفاده می‌کنیم.
🔹 __ne__ چیست؟
__ne__ مخفف Not Equal است.
یعنی بررسی می‌کند آیا دو شیء با هم متفاوت هستند یا نه.
مثلاً:
class Product:

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

def __ne__(self, other):
return (
self.name != other.name
or self.price != other.price
)

حالا:
product1 = Product("Laptop", 1200)
product2 = Product("Laptop", 1500)

print(product1 != product2)

خروجی:
True

چون قیمت دو محصول متفاوت است.
🔹 وقتی هر دو مقدار یکسان باشند
product1 = Product("Laptop", 1200)
product2 = Product("Laptop", 1200)

print(product1 != product2)

خروجی:
False

چون دو محصول از نظر name و price یکسان هستند.
🔹 __ne__ چگونه اجرا می‌شود؟
وقتی می‌نویسیم:
product1 != product2

Python می‌تواند متد زیر را فراخوانی کند:
product1.__ne__(product2)

بنابراین:
self  → شیء سمت چپ
other → شیء سمت راست

⚠️ نکته مهم
در مثال بالا از or استفاده کردیم:
self.name != other.name or self.price != other.price

چون اگر حتی یکی از این دو مقدار متفاوت باشد، دو محصول متفاوت محسوب می‌شوند.
مثلاً:
name یکسان + price متفاوت → True
name متفاوت + price یکسان → True
name متفاوت + price متفاوت → True
name یکسان + price یکسان → False

🧠 متدهای مقایسه‌ای تا اینجا
__eq__  → ==
__ne__ → !=
__lt__ → <
__gt__ → >
__le__ → <=
__ge__ → >=

با این متدها می‌توانیم رفتار اصلی عملگرهای مقایسه‌ای را برای کلاس خودمان تعریف کنیم.
🎯 تمرین
کلاس User را کامل کن تا دو کاربر وقتی username یا age متفاوت دارند، متفاوت محسوب شوند:
class User:

def __init__(self, username, age):
self.username = username
self.age = age

def __ne__(self, other):
# کد اینجا


user1 = User("ali", 25)
user2 = User("ali", 25)
user3 = User("sara", 25)

print(user1 != user2)
print(user1 != user3)

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

✅ پاسخ کامل تمرین
class User:

def __init__(self, username, age):
self.username = username
self.age = age

def __ne__(self, other):
if not isinstance(other, User):
return NotImplemented

return (
self.username != other.username
or self.age != other.age
)


user1 = User("ali", 25)
user2 = User("ali", 25)
user3 = User("sara", 25)

print(user1 != user2)
print(user1 != user3)

خروجی:
False
True

در این قسمت:
self.username != other.username

نام کاربری دو شیء مقایسه می‌شود.
و:
self.age != other.age

سن دو شیء مقایسه می‌شود.
از or استفاده می‌کنیم، چون متفاوت بودن حتی یکی از ویژگی‌ها کافی است تا دو کاربر متفاوت محسوب شوند.
پست ۱۱۰ از دوره جامع Python 🐍
1
🐍 آموزش جامع Python — پست ۱۱۱
کنترل عملگر in با __contains__
تا اینجا چندین Magic Method برای کنترل رفتار اشیاء یاد گرفتیم.
حالا می‌خواهیم مشخص کنیم وقتی از عملگر:
in

برای یک شیء استفاده می‌کنیم، Python چگونه بررسی کند که یک مقدار داخل آن شیء وجود دارد یا نه.
برای این کار از:
__contains__

استفاده می‌کنیم.
🔹 __contains__ چیست؟
این متد رفتار عملگر in را برای شیء مشخص می‌کند.
مثلاً یک کلاس Playlist می‌سازیم:
class Playlist:

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

def __contains__(self, song):
return song in self.songs

حالا:
playlist = Playlist([
"Song A",
"Song B",
"Song C"
])

print("Song B" in playlist)
print("Song X" in playlist)

خروجی:
True
False

🔹 not in
وقتی __contains__ را تعریف کنیم، می‌توانیم از not in هم استفاده کنیم:
print("Song X" not in playlist)

خروجی:
True

🔹 __contains__ چگونه اجرا می‌شود؟
وقتی می‌نویسیم:
"Song B" in playlist

Python از رفتار تعریف‌شده در:
playlist.__contains__("Song B")

استفاده می‌کند.
در این متد:
def __contains__(self, song):

پارامتر song همان مقداری است که می‌خواهیم وجود آن را بررسی کنیم.
🔹 مثال با UserGroup
می‌توانیم بررسی کنیم آیا یک نام کاربری در گروه وجود دارد یا نه:
class UserGroup:

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

def __contains__(self, username):
return username in self.users


group = UserGroup(["ali", "sara", "reza"])

print("ali" in group)
print("mina" in group)

خروجی:
True
False

⚠️ نکته مهم
__contains__ باید نتیجه‌ای مناسب برای بررسی وجود داشتن برگرداند؛ معمولاً:
True

یا:
False

در واقع می‌توانیم منطق بررسی را کاملاً مطابق نیاز کلاس خودمان تعریف کنیم.
🧠 چند Magic Method مهم تا اینجا
__str__       → print()
__repr__ → repr()
__len__ → len()
__eq__ → ==
__ne__ → !=
__lt__ → <
__gt__ → >
__le__ → <=
__ge__ → >=
__contains__ → in

این متدها به ما اجازه می‌دهند رفتار طبیعی Python را برای کلاس‌های خودمان شخصی‌سازی کنیم.
🎯 تمرین
کلاس Library را کامل کن تا بتوانیم با in بررسی کنیم آیا یک کتاب در کتابخانه وجود دارد یا نه:
class Library:

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

def __contains__(self, title):
# کد اینجا


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

print("Python" in library)
print("Java" in library)

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

پست ۱۱۱ از دوره جامع Python 🐍
🐍 آموزش جامع Python — پست ۱۱۲
دسترسی به عناصر شیء با __getitem__
تا اینجا یاد گرفتیم با __contains__ رفتار in را برای یک شیء کنترل کنیم.
حالا می‌خواهیم کاری کنیم که بتوانیم با استفاده از [] به عناصر یک شیء دسترسی داشته باشیم.
برای این کار از __getitem__ استفاده می‌کنیم.
🔹 __getitem__
وقتی Python چنین کدی می‌بیند:
object[index]

در واقع می‌تواند متد زیر را صدا بزند:
object.__getitem__(index)

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

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


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

print(playlist[0])
print(playlist[1])

خروجی:
Song A
Song B

در اینجا:
playlist[0]

باعث می‌شود Python از __getitem__ استفاده کند.
🔹 پشتیبانی از Index منفی
چون در مثال بالا دسترسی نهایی به یک list انجام می‌شود، Index منفی هم کار می‌کند:
print(playlist[-1])

خروجی:
Song C

🔹 پشتیبانی از Slice
__getitem__ فقط برای یک Index نیست؛ می‌توانیم Slice هم دریافت کنیم:
print(playlist[0:2])

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

چون مقدار 0:2 به عنوان index به __getitem__ ارسال می‌شود.
⚠️ نکته مهم
__getitem__ خودش الزاماً داده‌ها را نگهداری نمی‌کند.
در مثال ما:
self.songs

داده‌ها را نگهداری می‌کند و __getitem__ فقط نحوه دسترسی به آن‌ها را مشخص می‌کند.
🧠 خلاصه
obj[index]
↓
__getitem__(index)

مثلاً:
playlist[1]

تقریباً معادل:
playlist.__getitem__(1)

است.
🎯 تمرین
کلاس Library را کامل کن تا بتوانیم با Index به کتاب‌ها دسترسی داشته باشیم:
class Library:
def __init__(self, books):
self.books = books

def __getitem__(self, index):
# code


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

print(library[0])
print(library[2])

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

پست ۱۱۲ از دوره جامع Python 🐍
🐍 آموزش جامع 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