تمرین پایتون پیشرفته: ۱۵ تمرین با جواب، از شی‌گرایی تا دکوراتور و ژنراتور - مجله آموزش برنامه‌نویسی داناورس
آموزش برنامه‌نویسی بازبینی علمی: مجید مهربخش ۱۲ دقیقه مطالعه

تمرین پایتون پیشرفته: ۱۵ تمرین با جواب، از شی‌گرایی تا دکوراتور و ژنراتور

بعد از پایتون مقدماتی، بیشتر آدم‌ها جایی گیر می‌کنند که مفهوم‌ها را خوانده‌اند ولی وقتی باید خودشان کلاس، دکوراتور یا ژنراتور بنویسند، نمی‌دانند از کجا شروع کنند. راه عبور از این مرحله فقط تمرین است. این صفحه ۱۵ تمرین پایتون پیشرفته دارد که به ترتیب سختی چیده شده‌اند و هر کدام جواب کامل، خروجی واقعی و توضیح دارد.

این تمرین‌ها برای کسی است که پایتون مقدماتی (متغیر، شرط، حلقه، تابع، لیست و دیکشنری) را بلد است و می‌خواهد سطحش را بالا ببرد؛ برای آمادگی امتحان درس پایتون پیشرفته، مصاحبه‌ی شغلی یا قبل از شروع یک دوره‌ی پیشرفته. اگر هنوز نمی‌دانید پایتون پیشرفته دقیقاً شامل چه مباحثی است، اول آموزش پایتون پیشرفته: بعد از مقدماتی چه یاد بگیرم؟ را بخوانید.

چطور از این تمرین‌ها استفاده کنید؟

  • اول خودتان بنویسید. صورت مسئله را بخوانید و حداقل ۱۵ دقیقه روی آن کار کنید، بعد جواب را ببینید. خواندن جواب بدون تلاش، تقریباً هیچ چیز یاد نمی‌دهد.
  • جواب را اجرا کنید و تغییر دهید. همه‌ی کدها با پایتون ۳ اجرا و خروجی‌شان همین‌جا آمده است. یک خط را عوض کنید و پیش‌بینی کنید چه می‌شود.
  • جواب شما لازم نیست شبیه جواب ما باشد. در پایتون معمولاً چند راه درست هست؛ مهم این است که کد شما خوانا باشد و درست کار کند.

شی‌گرایی

تمرین ۱: حساب بانکی

کلاس 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 و پروژه‌های حرفه‌ای، در کلاس زنده و آنلاین با بازبینی کد توسط منتور تمرین می‌شود. بازبینی کد همان چیزی است که در خودآموزی بیشتر از همه کم است: کسی که بگوید کد شما کار می‌کند ولی چرا خوانا نیست.

سوالات رایج

این تمرین‌ها برای امتحان پایتون پیشرفته مناسب است؟

مباحث این صفحه، یعنی شی‌گرایی، دکوراتور، ژنراتور، مدیریت خطا و کار با فایل، در بیشتر سرفصل‌های پایتون پیشرفته هست. ولی شکل سوال‌های هر امتحان به منبع درس بستگی دارد؛ برای امتحان کلاسی، سوال‌های نمونه‌ی خود استاد یا منبع درس را هم حتماً تمرین کنید.

با کدام نسخه‌ی پایتون اجرا کنم؟

همه‌ی کدها با پایتون ۳ و فقط با کتابخانه‌های استاندارد نوشته شده‌اند و نصب چیز اضافه‌ای لازم ندارند.

اگر نتوانستم بیشتر تمرین‌ها را حل کنم چه کنم؟

نگران نباشید؛ یعنی هنوز به مرور مباحث مقدماتی، به‌خصوص تابع و دیکشنری، نیاز دارید. بعد از مرور، تمرین‌ها را از اول شروع کنید. تمرین ۱ تا ۵ پایه‌ی بقیه است.

پایتون پیشرفته
پیشنهاد ویژه یادگیری

پایتون پیشرفته

ارتقا از سطح برنامه‌نویس به مهندس نرم‌افزار.

مشاهده دوره و ثبت‌نام

مقالات مرتبط

برچسب‌ها: #آموزش برنامه‌نویسی #داناورس

بحث و تبادل نظر 0 دیدگاه

💭

هنوز دیدگاهی ثبت نشده است. شما اولین نفر باشید!

مشاوره و پشتیبانی در واتس‌اپ