بعد از پایتون مقدماتی، بیشتر آدمها جایی گیر میکنند که مفهومها را خواندهاند ولی وقتی باید خودشان کلاس، دکوراتور یا ژنراتور بنویسند، نمیدانند از کجا شروع کنند. راه عبور از این مرحله فقط تمرین است. این صفحه ۱۵ تمرین پایتون پیشرفته دارد که به ترتیب سختی چیده شدهاند و هر کدام جواب کامل، خروجی واقعی و توضیح دارد.
این تمرینها برای کسی است که پایتون مقدماتی (متغیر، شرط، حلقه، تابع، لیست و دیکشنری) را بلد است و میخواهد سطحش را بالا ببرد؛ برای آمادگی امتحان درس پایتون پیشرفته، مصاحبهی شغلی یا قبل از شروع یک دورهی پیشرفته. اگر هنوز نمیدانید پایتون پیشرفته دقیقاً شامل چه مباحثی است، اول آموزش پایتون پیشرفته: بعد از مقدماتی چه یاد بگیرم؟ را بخوانید.
چطور از این تمرینها استفاده کنید؟
- اول خودتان بنویسید. صورت مسئله را بخوانید و حداقل ۱۵ دقیقه روی آن کار کنید، بعد جواب را ببینید. خواندن جواب بدون تلاش، تقریباً هیچ چیز یاد نمیدهد.
- جواب را اجرا کنید و تغییر دهید. همهی کدها با پایتون ۳ اجرا و خروجیشان همینجا آمده است. یک خط را عوض کنید و پیشبینی کنید چه میشود.
- جواب شما لازم نیست شبیه جواب ما باشد. در پایتون معمولاً چند راه درست هست؛ مهم این است که کد شما خوانا باشد و درست کار کند.
شیگرایی
تمرین ۱: حساب بانکی
کلاس BankAccount بسازید که موجودی را نگه دارد و متدهای deposit و withdraw داشته باشد. واریز مبلغ منفی و برداشت بیشتر از موجودی باید با ValueError رد شود.
جواب:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("amount must be positive")
self.balance += amount
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("insufficient funds")
self.balance -= amount
acc = BankAccount("Sara", 100)
acc.deposit(50)
acc.withdraw(30)
print(acc.balance)
try:
acc.withdraw(500)
except ValueError as err:
print("Error:", err)
خروجی:
120
Error: insufficient funds
نکتهی اصلی این تمرین این است که کلاس از دادهی خودش محافظت میکند: بهجای اینکه هر جای برنامه موجودی را مستقیم کم کند، همهی تغییرها از متدها میگذرد و قانونها یک جا نوشته میشوند.
تمرین ۲: شکلها با ارثبری و چندریختی
کلاس پایهی Shape با متد area بسازید و دو کلاس Rectangle و Circle از آن ارث ببرند. بعد فهرستی از شکلهای مختلف بسازید و مساحت همه را با یک حلقه چاپ کنید.
جواب:
import math
class Shape:
def area(self):
raise NotImplementedError
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
shapes = [Rectangle(3, 4), Circle(1), Rectangle(2, 5)]
for shape in shapes:
print(type(shape).__name__, round(shape.area(), 2))
خروجی:
Rectangle 12
Circle 3.14
Rectangle 10
حلقه نمیداند هر شکل چیست و لازم هم نیست بداند؛ فقط area را صدا میزند و هر کلاس جواب خودش را میدهد. به این چندریختی (Polymorphism) میگویند. اگر فردا کلاس Triangle اضافه شود، حلقه هیچ تغییری لازم ندارد.
تمرین ۳: بردار با متدهای ویژه
کلاس Vector دوبعدی بسازید که با عملگر + جمع شود، با == مقایسه شود، با abs طولش محاسبه شود و هنگام چاپ به شکل خوانا نمایش داده شود.
جواب:
import math
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
def __abs__(self):
return math.hypot(self.x, self.y)
def __repr__(self):
return f"Vector({self.x}, {self.y})"
v = Vector(3, 4) + Vector(1, 2)
print(v)
print(v == Vector(4, 6))
print(abs(Vector(3, 4)))
خروجی:
Vector(4, 6)
True
5.0
متدهایی که با دو زیرخط شروع و تمام میشوند (Dunder Methods) به کلاس اجازه میدهند مثل نوعهای داخلی پایتون رفتار کند. __repr__ را همیشه بنویسید؛ عیبیابی را خیلی راحتتر میکند.
تمرین ۴: دما با property
کلاس Temperature بسازید که دما را به سلسیوس نگه دارد. دمای کمتر از صفر مطلق (۲۷۳٫۱۵- درجه) نباید پذیرفته شود و فارنهایت باید بدون ذخیرهی جداگانه محاسبه شود.
جواب:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("below absolute zero")
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.fahrenheit)
t.celsius = 100
print(t.fahrenheit)
try:
t.celsius = -300
except ValueError as err:
print("Error:", err)
خروجی:
77.0
212.0
Error: below absolute zero
با property بیرون کلاس هنوز مینویسیم t.celsius = 100، ولی پشت صحنه اعتبارسنجی اجرا میشود. حتی مقداری که در __init__ داده میشود هم از همان setter میگذرد.
تمرین ۵: سازندهی جایگزین با classmethod
کلاس Date سه مقدار سال، ماه و روز دارد. یک classmethod بنویسید که تاریخ را از رشتهای مثل 1405-07-10 بسازد، و یک staticmethod که بگوید ماه معتبر است یا نه.
جواب:
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, text):
year, month, day = (int(part) for part in text.split("-"))
return cls(year, month, day)
@staticmethod
def is_valid_month(month):
return 1 <= month <= 12
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
d = Date.from_string("1405-07-10")
print(d)
print(Date.is_valid_month(13))
خروجی:
Date(1405, 7, 10)
False
classmethod کلاس را بهعنوان اولین ورودی میگیرد و برای «راه دوم ساختن شیء» استفاده میشود. staticmethod هیچکدام را نمیگیرد و فقط تابعی است که از نظر معنی به کلاس تعلق دارد.
توابع پیشرفته
تمرین ۶: ورودیهای متغیر با args و kwargs
تابع report بنویسید که هر تعداد نمره را بگیرد و میانگین را چاپ کند، و هر تعداد اطلاعات اضافهی نامدار (مثل نام و کلاس) را هم کنار آن نشان دهد.
جواب:
def report(*scores, **info):
average = sum(scores) / len(scores)
details = ", ".join(f"{key}={value}" for key, value in info.items())
print(f"average={average:.2f} ({details})")
report(18, 17, 19.5, name="Ali", grade=11)
report(15, 20, name="Mina")
خروجی:
average=18.17 (name=Ali, grade=11)
average=17.50 (name=Mina)
*scores همهی ورودیهای بینام را در یک تاپل جمع میکند و **info ورودیهای نامدار را در یک دیکشنری. اسمها قراردادیاند؛ چیزی که اهمیت دارد ستارههاست.
تمرین ۷: شمارنده با closure
تابع make_counter بنویسید که یک تابع برگرداند؛ هر بار که تابع برگشتی صدا زده شود، عدد بعدی را بدهد. دو شمارندهی مستقل بسازید.
جواب:
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
first = make_counter()
second = make_counter()
print(first(), first(), first())
print(second())
خروجی:
1 2 3
1
تابع داخلی متغیر count را حتی بعد از تمام شدن make_counter به خاطر دارد؛ به این closure میگویند. بدون nonlocal پایتون فکر میکند count یک متغیر محلی تازه است و خطا میدهد.
تمرین ۸: دکوراتور شمارش فراخوانی
دکوراتور count_calls بنویسید که بشمارد هر تابع چند بار صدا زده شده، بدون اینکه نام و توضیح تابع اصلی از بین برود.
جواب:
import functools
def count_calls(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
wrapper.calls += 1
return func(*args, **kwargs)
wrapper.calls = 0
return wrapper
@count_calls
def greet(name):
"""Say hello."""
return f"Hello {name}"
greet("Reza")
greet("Neda")
print(greet.calls)
print(greet.__name__, "-", greet.__doc__)
خروجی:
2
greet - Say hello.
دکوراتور تابعی است که تابع میگیرد و تابع تازهای برمیگرداند. functools.wraps نام و توضیح تابع اصلی را روی تابع تازه کپی میکند؛ بدون آن، greet.__name__ میشد wrapper.
تمرین ۹: دکوراتور با ورودی
دکوراتور repeat(n) بنویسید که تابع را n بار اجرا کند.
جواب:
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(3)
def beep():
print("beep")
beep()
خروجی:
beep
beep
beep
دکوراتوری که ورودی میگیرد سه لایه دارد: لایهی بیرونی ورودی را میگیرد، لایهی میانی خود تابع را و لایهی داخلی جای تابع اجرا میشود. @repeat(3) اول repeat(3) را اجرا میکند و نتیجهاش را روی تابع میگذارد.
تمرین ۱۰: مرتبسازی با چند معیار
فهرستی از دانشآموزان (نام و نمره) را اول بر اساس نمره از زیاد به کم، و در نمرههای برابر بر اساس نام به ترتیب الفبا مرتب کنید.
جواب:
students = [
{"name": "Sara", "score": 18},
{"name": "Ali", "score": 19},
{"name": "Amir", "score": 18},
{"name": "Neda", "score": 20},
]
ranked = sorted(students, key=lambda s: (-s["score"], s["name"]))
for position, s in enumerate(ranked, start=1):
print(position, s["name"], s["score"])
خروجی:
1 Neda 20
2 Ali 19
3 Amir 18
4 Sara 18
کلید مرتبسازی میتواند یک تاپل باشد؛ پایتون اول عضو اول را مقایسه میکند و در تساوی سراغ عضو دوم میرود. منفی کردن نمره، ترتیب نزولی را بدون reverse=True میسازد، که اگر به کار میرفت ترتیب نامها را هم برعکس میکرد.
ژنراتور و پیمایشگر
تمرین ۱۱: دنبالهی فیبوناچی با ژنراتور
ژنراتوری بنویسید که اعداد فیبوناچی را بیپایان تولید کند. بعد ده عدد اول و اولین عدد بزرگتر از هزار را چاپ کنید.
جواب:
import itertools
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
print(list(itertools.islice(fibonacci(), 10)))
print(next(n for n in fibonacci() if n > 1000))
خروجی:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
1597
ژنراتور با yield هر بار فقط یک عدد میسازد و حافظهای برای کل دنباله اشغال نمیکند؛ برای همین میتواند بیپایان باشد. islice فقط به تعداد لازم از آن برمیدارد.
تمرین ۱۲: پیمایشگر شمارش معکوس
کلاس Countdown بسازید که بشود با for روی آن حلقه زد و از عدد دادهشده تا یک شمارش معکوس کند.
جواب:
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
for n in Countdown(5):
print(n, end=" ")
print()
خروجی:
5 4 3 2 1
هر چیزی که __iter__ و __next__ داشته باشد در حلقهی for قابل استفاده است. پایان پیمایش با StopIteration اعلام میشود. همین کار را با یک ژنراتور کوتاهتر هم میشود نوشت؛ نسخهی کلاسی برای فهمیدن سازوکار حلقهی for است.
خطا، context manager و داده
تمرین ۱۳: زمانسنج با context manager
کلاس Timer بسازید که با with استفاده شود و زمان اجرای کد داخل بلوک را اندازه بگیرد.
جواب:
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.perf_counter() - self.start
return False
with Timer() as timer:
total = sum(range(1_000_000))
print(total)
print(timer.elapsed < 5)
خروجی:
499999500000
True
__enter__ هنگام ورود به with و __exit__ هنگام خروج اجرا میشود، حتی اگر داخل بلوک خطا رخ دهد. برگرداندن False یعنی خطا (اگر بود) پنهان نشود. باز کردن فایل با with open(...) هم دقیقاً همین سازوکار را دارد.
تمرین ۱۴: آمار کلمهها با Counter و defaultdict
در یک متن، سه کلمهی پرتکرار را پیدا کنید و کلمهها را بر اساس حرف اولشان گروهبندی کنید.
جواب:
from collections import Counter, defaultdict
text = "python is fun and python is powerful and fast"
words = text.split()
for word, count in Counter(words).most_common(3):
print(word, count)
groups = defaultdict(list)
for word in sorted(set(words)):
groups[word[0]].append(word)
for letter, items in groups.items():
print(letter, "->", ", ".join(items))
خروجی:
python 2
is 2
and 2
a -> and
f -> fast, fun
i -> is
p -> powerful, python
Counter شمارش را در یک خط انجام میدهد و defaultdict(list) لازم نمیداند قبل از افزودن، وجود کلید را بررسی کنید. هر دو در ماژول استاندارد collections هستند و در مصاحبهها زیاد پرسیده میشوند.
پروژهی پایانی
تمرین ۱۵: کتابخانهی کوچک با dataclass و JSON
سیستم کوچک کتابخانه بسازید: کتابها عنوان، نویسنده و وضعیت امانت دارند. امکان افزودن، امانت دادن و برگرداندن کتاب باشد و همهی کتابها در فایل JSON ذخیره و دوباره خوانده شوند. امانت دادن کتابی که قبلاً امانت رفته باید خطا بدهد.
جواب:
import json
import os
import tempfile
from dataclasses import asdict, dataclass
@dataclass
class Book:
title: str
author: str
borrowed: bool = False
class Library:
def __init__(self):
self.books = {}
def add(self, book):
self.books[book.title] = book
def borrow(self, title):
book = self.books[title]
if book.borrowed:
raise ValueError(f"{title} is already borrowed")
book.borrowed = True
def give_back(self, title):
self.books[title].borrowed = False
def save(self, path):
with open(path, "w", encoding="utf-8") as f:
json.dump([asdict(b) for b in self.books.values()], f, ensure_ascii=False)
@classmethod
def load(cls, path):
library = cls()
with open(path, encoding="utf-8") as f:
for item in json.load(f):
library.add(Book(**item))
return library
library = Library()
library.add(Book("Dune", "Frank Herbert"))
library.add(Book("Clean Code", "Robert Martin"))
library.borrow("Dune")
path = os.path.join(tempfile.gettempdir(), "library.json")
library.save(path)
restored = Library.load(path)
print("Borrowed:", ", ".join(b.title for b in restored.books.values() if b.borrowed))
try:
restored.borrow("Dune")
except ValueError as err:
print("Error:", err)
خروجی:
Borrowed: Dune
Error: Dune is already borrowed
این پروژه تقریباً همهی مباحث بالا را کنار هم میگذارد: کلاس، classmethod برای ساختن کتابخانه از فایل، with برای کار امن با فایل و مدیریت خطا. dataclass متدهای __init__ و __repr__ را خودکار میسازد و asdict شیء را برای ذخیره در JSON به دیکشنری تبدیل میکند. برای تمرین بیشتر، تاریخ امانت و جریمهی دیرکرد را اضافه کنید.
بعد از این ۱۵ تمرین
اگر بیشتر این تمرینها را خودتان حل کردید، پایهی پایتون پیشرفته را دارید و وقت آن است که این مهارتها را در یک پروژهی واقعی به کار ببرید. چند پیشنهاد:
- پروژهی کتابخانهی تمرین آخر را با دیتابیس SQLite بهجای فایل JSON بازنویسی کنید.
- برای کلاسهای تمرینهای ۱ تا ۴ با ماژول
unittestیا کتابخانهی pytest تست بنویسید. - یکی از ۱۰ پروژهی پایتون برای مبتدیها را با شیگرایی و مدیریت خطا از نو بنویسید؛ تفاوت کد قبل و بعد را خواهید دید.
بعد از پایتون پیشرفته معمولاً یکی از مسیرهای تخصصی انتخاب میشود: توسعهی وب، تحلیل داده یا هوش مصنوعی. اگر هوش مصنوعی برایتان جذاب است، آموزش پایتون برای هوش مصنوعی را ببینید.
در دورهی پایتون پیشرفتهی داناورس همین مباحث، یعنی شیگرایی، ساختمان داده، کار با API و پروژههای حرفهای، در کلاس زنده و آنلاین با بازبینی کد توسط منتور تمرین میشود. بازبینی کد همان چیزی است که در خودآموزی بیشتر از همه کم است: کسی که بگوید کد شما کار میکند ولی چرا خوانا نیست.
سوالات رایج
این تمرینها برای امتحان پایتون پیشرفته مناسب است؟
مباحث این صفحه، یعنی شیگرایی، دکوراتور، ژنراتور، مدیریت خطا و کار با فایل، در بیشتر سرفصلهای پایتون پیشرفته هست. ولی شکل سوالهای هر امتحان به منبع درس بستگی دارد؛ برای امتحان کلاسی، سوالهای نمونهی خود استاد یا منبع درس را هم حتماً تمرین کنید.
با کدام نسخهی پایتون اجرا کنم؟
همهی کدها با پایتون ۳ و فقط با کتابخانههای استاندارد نوشته شدهاند و نصب چیز اضافهای لازم ندارند.
اگر نتوانستم بیشتر تمرینها را حل کنم چه کنم؟
نگران نباشید؛ یعنی هنوز به مرور مباحث مقدماتی، بهخصوص تابع و دیکشنری، نیاز دارید. بعد از مرور، تمرینها را از اول شروع کنید. تمرین ۱ تا ۵ پایهی بقیه است.




