کلاس و شیء
داده و رفتار را کنار هم بگذار: کلاس و شیء، __init__ و self، ویژگی و متد، __str__ و __repr__ برای چاپ خوانا، اعتبارسنجی با raise، ویژگی کلاس، وراثت و super()، بازنویسی متد، و dataclass برای کلاسهای دادهای؛ همراه خطاهای AttributeError و فراموش کردن self.
- حدود ۱۸ دقیقه مطالعه
- ۵ مثال اجراشده
- ۳ تمرین با جواب
- مقدماتی تا متوسط
- بهروزرسانی: ۲۹ مهر ۱۴۰۵
توی این درس یاد میگیری با کلاس نوع دادهی خودت را بسازی. تا اینجا یک سرور را با یک دیکشنری ({"name": ..., "disk": ...}) نشان میدادی و تابعهایش جدا بودند. کلاس داده (ویژگیها) و رفتار (متدها) را در یک جا جمع میکند: web.is_full() بهجای is_full(web). با __init__ و self آشنا میشوی، شیءها را خوانا چاپ میکنی (__str__)، جلوی دادهی غلط را با raise میگیری، با وراثت کلاسهای تخصصیتر میسازی (WebServer از Server)، و با dataclass کلاسهای دادهای را در چند خط مینویسی.
مسئله: دیکشنریهایی که هر چیزی میپذیرند
Section titled “مسئله: دیکشنریهایی که هر چیزی میپذیرند”یک سرور به شکل دیکشنری را هر کسی میتواند هر طور بخواهد بسازد: یکی "disk" مینویسد و یکی "Disk"، یکی عدد میگذارد و یکی متن. تابعهایی که روی سرورها کار میکنند هم در ده فایل پراکندهاند. وقتی برنامه بزرگ شود، دیگر معلوم نیست «یک سرور» دقیقاً چه کلیدهایی دارد و چه کارهایی رویش میشود کرد.
تشبیه: نقشه و خانه
Section titled “تشبیه: نقشه و خانه”کلاس مثل نقشهی معماری یک خانه است: میگوید هر خانه چند اتاق دارد و چه کارهایی در آن میشود کرد، ولی خودش خانه نیست. شیء (object یا instance) یک خانهی واقعی است که از روی نقشه ساخته شده. از یک نقشه میشود صد خانه ساخت؛ همه یک شکلاند ولی هر کدام صاحب، رنگ و اثاث خودش را دارد. self یعنی «همین خانهای که الان دربارهاش حرف میزنیم».
مثالهای عملی
Section titled “مثالهای عملی”مثال ۱: اولین کلاس
Section titled “مثال ۱: اولین کلاس”class Server: def __init__(self, name, ip, disk=0): self.name = name self.ip = ip self.disk = disk
def is_full(self, limit=90): return self.disk > limit
def describe(self): return f"{self.name} ({self.ip}) دیسک {self.disk}٪"
web = Server("web1", "10.0.0.1", 72)db = Server("db1", "10.0.0.5", 95)print(web.name, db.ip)print(web.describe())print(web.is_full(), db.is_full())db.disk = 60print(db.describe(), db.is_full())print(type(web))web1 10.0.0.5web1 (10.0.0.1) دیسک 72٪False Truedb1 (10.0.0.5) دیسک 60٪ False<class '__main__.Server'>قدم به قدم:
class Server:یک نوع جدید به اسمServerتعریف میکند. رسم پایتون: اسم کلاس با حرف بزرگ و بدون_(WebServer).__init__متد ویژهای است که هنگام ساختن هر شیء خودکار اجرا میشود.Server("web1", "10.0.0.1", 72)یک شیء میسازد و__init__را با این آرگومانها صدا میزند.selfپارامتر اول هر متد است و یعنی «همین شیء».self.name = nameیعنی «ویژگیnameهمین شیء را برابر آرگومان بگذار».- ویژگیها (attribute) با نقطه خوانده و نوشته میشوند:
web.name،db.disk = 60. - متدها تابعهاییاند که داخل کلاس تعریف شدهاند.
web.is_full()را صدا میزنی و پایتون خودشwebرا بهجایselfمیدهد؛ برای همینselfرا در صدا زدن نمینویسی.
type(web) میگوید این شیء از نوع Server است؛ همانطور که type(5) میگفت int. در واقع str، list و dict هم کلاساند، و s.upper() و x.append() که تا حالا میزدی همین متدها بودند.
مثال ۲: چاپ خوانا، __str__ و __repr__
Section titled “مثال ۲: چاپ خوانا، __str__ و __repr__”یک شیء ساده را چاپ کنی، چیز بهدردبخوری نمیبینی:
<__main__.Server object at 0x7f0182dff8f0>یعنی «یک شیء Server در این آدرس حافظه». با دو متد ویژه، خودت تعیین میکنی چطور نشان داده شود:
class Server: def __init__(self, name, disk): self.name = name self.disk = disk
def __str__(self): return f"Server {self.name} ({self.disk}%)"
def __repr__(self): return f"Server(name={self.name!r}, disk={self.disk})"
s = Server("web1", 72)print(s)print([s, Server("db1", 95)])Server web1 (72%)[Server(name='web1', disk=72), Server(name='db1', disk=95)]__str__برای آدمها:print(s)وstr(s)از آن استفاده میکنند.__repr__برای برنامهنویسها: وقتی شیء داخل یک لیست چاپ میشود یا در حالت تعاملی اسمش را مینویسی. رسم است شکلی داشته باشد که شبیه کد ساختن همان شیء باشد.!rدر f-string یعنی «repr این مقدار» (برای همین'web1'با کوتیشن آمد).
متدهایی که با دو زیرخط شروع و تمام میشوند (__init__، __str__) را متدهای ویژه (dunder) میگویند: پایتون خودش در موقعیتهای خاص صدایشان میزند.
مثال ۳: شیئی که از خودش محافظت میکند
Section titled “مثال ۳: شیئی که از خودش محافظت میکند”کلاس جای خوبی برای قانونگذاری است: هر تغییری از راه متد، و متد بررسی میکند که تغییر معتبر باشد:
class Account: bank = "LoopX Bank"
def __init__(self, owner, balance=0): self.owner = owner self.balance = balance self.history = []
def deposit(self, amount): if amount <= 0: raise ValueError("مبلغ باید مثبت باشد") self.balance += amount self.history.append(("+", amount))
def withdraw(self, amount): if amount > self.balance: raise ValueError("موجودی کافی نیست") self.balance -= amount self.history.append(("-", amount))
acc = Account("علی", 100_000)acc.deposit(50_000)acc.withdraw(30_000)print(acc.owner, acc.balance, acc.history, Account.bank)try: acc.withdraw(1_000_000)except ValueError as e: print("خطا:", e)print(acc.balance)علی 120000 [('+', 50000), ('-', 30000)] LoopX Bankخطا: موجودی کافی نیست120000withdrawنمیگذارد موجودی منفی شود؛raise(درس قبل) خطا را به صدازننده میدهد و موجودی دستنخورده میماند.historyیک لیست است که هر حساب مال خودش را دارد، چون در__init__(یعنی برای هر شیء جدا) ساخته شده.bankبیرون از__init__و مستقیم داخل کلاس تعریف شده: یک ویژگی کلاس است که بین همهی شیءها مشترک است (Account.bank).
در متد def withdraw(self, amount)، هنگام صدا زدن acc.withdraw(500) پارامتر self چیست؟
جواب درست: خود acc
پایتون شیئی که متد رویش صدا زده شده را خودکار بهعنوان self میدهد.
مثال ۴: وراثت، کلاسهای تخصصیتر
Section titled “مثال ۴: وراثت، کلاسهای تخصصیتر”همهی سرورها اسم و گزارش دارند، ولی «سالم بودن» یک وبسرور با یک دیتابیس فرق دارد. وراثت یعنی کلاس جدید همهچیز کلاس قبلی را به ارث ببرد و فقط تفاوتها را بنویسد:
class Server: def __init__(self, name): self.name = name
def health(self): return "ok"
def report(self): return f"{self.name}: {self.health()}"
class WebServer(Server): def __init__(self, name, port=80): super().__init__(name) self.port = port
def health(self): return f"http://{self.name}:{self.port} پاسخ داد"
class DBServer(Server): def health(self): return "اتصال دیتابیس برقرار است"
for s in [Server("cache1"), WebServer("web1", 8080), DBServer("db1")]: print(s.report())print(isinstance(WebServer("x"), Server))cache1: okweb1: http://web1:8080 پاسخ دادdb1: اتصال دیتابیس برقرار استTrueclass WebServer(Server):یعنی WebServer فرزند Server است و همهی متدهای آن (report) را دارد.- WebServer و DBServer متد
healthرا بازنویسی (override) کردهاند؛reportکه در کلاس پدر است،self.health()را صدا میزند و برای هر شیء نسخهی مخصوص نوع خودش اجرا میشود. super().__init__(name)کار__init__پدر را انجام میدهد (گذاشتنname) و بعد WebServer ویژگی اضافهی خودش (port) را میگذارد. DBServer__init__ننوشته، پس همان مال پدر استفاده میشود.isinstance(x, Server)میگوید x از نوع Server یا یکی از فرزندانش است.
حلقهی آخر نکتهی اصلی است: کد صدازننده (s.report()) نمیداند و لازم نیست بداند هر سرور چه نوعی است؛ هر شیء خودش میداند چه کند.
مثال ۵: dataclass، کلاس دادهای در چند خط
Section titled “مثال ۵: dataclass، کلاس دادهای در چند خط”خیلی از کلاسها فقط داده نگه میدارند و __init__ و __repr__شان تکراری است. dataclass آنها را خودکار میسازد:
from dataclasses import dataclass, field
@dataclassclass Task: title: str done: bool = False tags: list = field(default_factory=list)
t = Task("بکاپ دیتابیس")t2 = Task("بهروزرسانی سرور", tags=["ops"])print(t)print(t2.title, t2.tags)print(t == Task("بکاپ دیتابیس"))Task(title='بکاپ دیتابیس', done=False, tags=[])بهروزرسانی سرور ['ops']True@dataclass(یک «دکوراتور»؛ کلاس زیرش را تغییر میدهد) از روی فهرست ویژگیها و نوعشان،__init__،__repr__و__eq__(مقایسه با==) را خودکار میسازد.field(default_factory=list)همان راهحل تلهی «لیست بهعنوان پیشفرض» در درس تابع است: هر Task لیست خودش را میگیرد.- دو dataclass با مقدارهای یکسان برابرند (
True)؛ دو شیء کلاس معمولی بدون__eq__، فقط اگر همان یک شیء باشند.
برای «یک رکورد با چند فیلد» (کاربر، سفارش، تنظیمات) dataclass معمولاً بهترین انتخاب است.
پشت پرده: self و جستجوی ویژگیها
Section titled “پشت پرده: self و جستجوی ویژگیها”web.is_full() در واقع کوتاهشدهی Server.is_full(web) است: پایتون متد را در کلاس پیدا میکند و شیء را بهعنوان آرگومان اول میدهد. برای همین self اسم خاصی نیست (فقط یک رسم بسیار قوی است) و برای همین اگر در تعریف متد فراموشش کنی، خطای «۰ آرگومان میگیرد ولی ۱ داده شد» میگیری (اشتباهات رایج ۲).
وقتی web.name را میخوانی، پایتون به ترتیب میگردد: اول در ویژگیهای خود شیء (که در __init__ با self. گذاشته شدهاند)، بعد در کلاس (برای همین acc.bank هم کار میکند)، بعد در پدرها به ترتیب وراثت. اولین جایی که پیدا شود برنده است؛ بازنویسی متد در فرزند همینطور کار میکند. اگر هیچجا نبود: AttributeError.
جدولهای مرجع
Section titled “جدولهای مرجع”| کد | معنی |
|---|---|
class Name: |
تعریف کلاس |
def __init__(self, ...): |
ساختن شیء (خودکار) |
self.x = x |
ویژگی شیء |
def method(self, ...): |
متد |
obj = Name(...) |
ساختن شیء |
obj.x / obj.method() |
ویژگی / صدا زدن متد |
class Child(Parent): |
وراثت |
super().__init__(...) |
صدا زدن نسخهی پدر |
isinstance(obj, Name) |
بررسی نوع (با فرزندها) |
@dataclass |
ساختن خودکار init و repr و eq |
| متد ویژه | کی صدا زده میشود |
|---|---|
__init__ |
Name(...) |
__str__ |
print(obj)، str(obj)، f-string |
__repr__ |
حالت تعاملی، داخل لیست، repr(obj) |
__eq__ |
a == b |
__len__ |
len(obj) |
اشتباهات رایج
Section titled “اشتباهات رایج”۱) ویژگیای که وجود ندارد
Section titled “۱) ویژگیای که وجود ندارد”class Server: def __init__(self, name): self.name = names = Server("web1")print(s.ip)Traceback (most recent call last): File "/home/ali/py/attr.py", line 5, in <module> print(s.ip) ^^^^AttributeError: 'Server' object has no attribute 'ip'یا غلط تایپی است، یا آن ویژگی در __init__ ساخته نشده. راهحل: همهی ویژگیها را در __init__ بساز (حتی با مقدار None) تا شکل شیء همیشه یکی باشد.
۲) فراموش کردن self در تعریف متد
Section titled “۲) فراموش کردن self در تعریف متد”class Server: def __init__(self, name): self.name = name def hello(): return "hi"s = Server("web1")print(s.hello())TypeError: Server.hello() takes 0 positional arguments but 1 was given«۱ داده شد» همان شیئی است که پایتون خودکار بهعنوان self فرستاد. راهحل: def hello(self):.
۳) name = name بهجای self.name = name
Section titled “۳) name = name بهجای self.name = name”class Server: def __init__(self, name): name = names = Server("web1")print(s.name)AttributeError: 'Server' object has no attribute 'name'بدون self.، فقط یک متغیر محلی ساخته شد که با تمام شدن __init__ از بین رفت. راهحل: self.name = name.
۴) لیست بهعنوان ویژگی کلاس
Section titled “۴) لیست بهعنوان ویژگی کلاس”اگر history = [] را بیرون از __init__ (مثل bank) بنویسی، همهی حسابها یک لیست مشترک میگیرند و تراکنشهای همه قاطی میشود. راهحل: چیزهای قابلتغییر (لیست، دیکشنری) را در __init__ با self. بساز.
۵) کلاس برای همهچیز
Section titled “۵) کلاس برای همهچیز”برای یک کار ساده (مثلاً تبدیل دما) یک تابع کافی است؛ کلاسی که فقط یک متد و هیچ دادهای ندارد، کد را بیدلیل پیچیده میکند. راهحل: وقتی داده و رفتار به هم گره خوردهاند یا چند شیء همشکل داری، کلاس؛ وگرنه تابع.
کلاس Rectangle بساز با width و height، و دو متد area() و perimeter(). یک __str__ هم بنویس که Rectangle 3×4 چاپ کند. دو مستطیل بساز و مساحت و محیطشان را چاپ کن.
دیدن جواب
class Rectangle: def __init__(self, width, height): self.width = width self.height = height
def area(self): return self.width * self.height
def perimeter(self): return 2 * (self.width + self.height)
def __str__(self): return f"Rectangle {self.width}×{self.height}"
for r in [Rectangle(3, 4), Rectangle(10, 2)]: print(r, "مساحت:", r.area(), "محیط:", r.perimeter())Rectangle 3×4 مساحت: 12 محیط: 14Rectangle 10×2 مساحت: 20 محیط: 24تمرین اصلی: کلاس Inventory (انبار) بساز که یک دیکشنری {کالا: تعداد} نگه دارد و سه متد داشته باشد: add(item, qty)، remove(item, qty) (اگر کالا نبود یا تعداد کافی نبود، ValueError با پیام فارسی) و report() که فهرست را مرتبشده برگرداند. یک برداشت نامعتبر را با try/except امتحان کن.
دیدن جواب
class Inventory: def __init__(self): self.items = {}
def add(self, item, qty): self.items[item] = self.items.get(item, 0) + qty
def remove(self, item, qty): have = self.items.get(item, 0) if qty > have: raise ValueError(f"از {item} فقط {have} عدد موجود است") self.items[item] = have - qty
def report(self): return [f"{name}: {n}" for name, n in sorted(self.items.items())]
inv = Inventory()inv.add("کابل شبکه", 20)inv.add("هارد", 5)inv.remove("کابل شبکه", 8)try: inv.remove("هارد", 9)except ValueError as e: print("خطا:", e)for line in inv.report(): print(line)خطا: از هارد فقط 5 عدد موجود استهارد: 5کابل شبکه: 12برداشت نامعتبر هیچ تغییری نداد (هارد هنوز ۵ است)، چون raise قبل از تغییر موجودی اجرا شد. (ترتیب sorted روی متن فارسی، ترتیب کد یونیکد حرفهاست؛ برای همین «ه» قبل از «ک» آمده.)
با وراثت یک سلسلهمراتب «اعلان» (notification) بساز: کلاس پایهی Notifier با متد send(message) که format(message) را صدا میزند و نتیجه را چاپ میکند؛ و دو فرزند: EmailNotifier(address) که پیام را با To: آدرس قالببندی کند و SlackNotifier(channel) با #کانال. یک لیست از هر دو بساز و با یک حلقه پیام «دیسک db1 پر شد» را به همه بفرست.
دیدن جواب
class Notifier: def format(self, message): return message
def send(self, message): print(self.format(message))
class EmailNotifier(Notifier): def __init__(self, address): self.address = address
def format(self, message): return f"[email] To: {self.address} | {message}"
class SlackNotifier(Notifier): def __init__(self, channel): self.channel = channel
def format(self, message): return f"[slack] #{self.channel} | {message}"
targets = [EmailNotifier("ops@example.com"), SlackNotifier("alerts")]for t in targets: t.send("دیسک db1 پر شد")[email] To: ops@example.com | دیسک db1 پر شد[slack] #alerts | دیسک db1 پر شدsend فقط یک بار در کلاس پایه نوشته شده؛ هر فرزند فقط قالب خودش را تعریف کرده. فردا اگر TelegramNotifier لازم شد، فقط یک کلاس کوچک اضافه میکنی و حلقه دست نمیخورد.
آزمونک
Section titled “آزمونک”فرق کلاس و شیء؟
جواب درست: کلاس نقشه است؛ شیء یک نمونهی ساختهشده از آن
از یک کلاس میشود هر تعداد شیء ساخت.
__init__ کی اجرا میشود؟
جواب درست: هنگام ساختن هر شیء: Server(...)
وظیفهاش گذاشتن ویژگیهای اولیه است.
چرا print(obj) چیزی مثل <__main__.Server object at 0x...> چاپ میکند؟
جواب درست: چون __str__ تعریف نشده
__str__ یا __repr__ بنویس.
در class B(A):، اگر B متد m را تعریف کند و A هم، b.m() کدام را اجرا میکند؟
جواب درست: نسخهی B
فرزند بازنویسی کرده؛ برای نسخهی پدر super().m().
خطای «takes 0 positional arguments but 1 was given» در صدا زدن متد معمولاً یعنی…
جواب درست: self را در تعریف متد فراموش کردهای
پایتون شیء را خودکار بهعنوان آرگومان اول میدهد.
@dataclass چه چیزی را خودکار میسازد؟
جواب درست: __init__ و __repr__ و __eq__ از روی فیلدها
برای کلاسهایی که بیشتر داده نگه میدارند.
جمعبندی
Section titled “جمعبندی”- کلاس نقشه است و شیء نمونهی واقعی آن؛ کلاس داده (ویژگی) و رفتار (متد) را کنار هم میگذارد.
__init__(self, ...)هنگام ساختن اجرا میشود؛self.x = ...ویژگی میسازد؛ متدها همیشهselfرا اول دارند.__str__برای چاپ خوانا و__repr__برای اشکالزدایی؛ متدها جای خوبی برای اعتبارسنجی باraiseهستند.- وراثت:
class Child(Parent):، بازنویسی متد،super().__init__(...)؛ کد صدازننده لازم نیست نوع دقیق را بداند. @dataclassبرای رکوردهای دادهای؛ لیستها را در__init__(یا باfield(default_factory=list)) بساز، نه در سطح کلاس.
| دستور | کاری که میکند |
|---|---|
class Server: | تعریف کلاس |
def __init__(self, name): self.name = name | ساختن ویژگیها |
def is_full(self): return self.disk > 90 | متد |
s = Server("web1"); s.is_full() | ساختن شیء و صدا زدن متد |
def __str__(self): return ... | چاپ خوانا |
class WebServer(Server): | وراثت |
super().__init__(name) | صدا زدن init پدر |
isinstance(s, Server) | بررسی نوع |
@dataclass class Task: title: str | کلاس دادهای سریع |