Faragiri

همهٔ زبان‌ها

آموزش وراثتبرنامه‌نویسی شی‌گرا

همان متد با همان super، یک بار پنج درصد می‌دهد و یک بار پانزده. تفاوت در کلاسی است که هیچ‌کدام از این دو نامش را نبرده‌اند.

۴بخش
۳دقیقه خواندن
۴عمق

یک نمونه، چهار لایه

صفی که super در آن جلو می‌رود

هر لایه زیر لایه بالای خودش نشسته و super همیشه یکی پایین‌تر را صدا می‌زند، هر جای این صف که نوشته شده باشد.

  1. Vijeهیچ متدی ندارد و فقط ترتیب را می‌سازدجایگاه ۱
  2. Bashgahپنج واحد اضافه می‌کند و بعدی را صدا می‌زندجایگاه ۲
  3. Omdeده می‌دهد، و همان بعدی است که باشگاه به آن رسیدجایگاه ۳
  4. Takhfifپایه، و وقتی باشگاه تنها باشد بعدی همین استجایگاه ۴

این صف مال کلاس Vije است و نه مال Bashgah. همان Bashgah در ترکیب دیگری صف دیگری می‌گیرد، و متدش یک نویسه هم عوض نمی‌شود.

یک کلاس تخفیف باشگاه که پنج واحد به تخفیف بعدی اضافه می‌کند: def darsad(self): return 5 + super().darsad(). صدا زدنش به‌تنهایی عدد ۵ می‌دهد، چون super() به کلاس پایه می‌رسد که صفر برمی‌گرداند.

حالا یک کلاس تخفیف ویژه بسازید که هم از باشگاه ارث می‌برد و هم از تخفیف عمده، و هیچ متدی هم ندارد: class Vije(Bashgah, Omde): pass. همان متد باشگاه، بدون یک نویسه تغییر، حالا ۱۵ می‌دهد. آن super() این بار به کلاس پایه نرفت، به تخفیف عمده رفت که ده می‌دهد.

دلیلش در یک فهرست است: [c.__name__ for c in Vije.__mro__] جواب می‌دهد ['Vije', 'Bashgah', 'Omde', 'Takhfif', 'object']. این ترتیب حل متد است و super() یعنی «بعدی در همین فهرست»، نه «والد من». برای همین یک کلاس می‌تواند رفتار متدی را عوض کند که هیچ‌جا نامش را نبرده است.

و همین باعث می‌شود صدا زدن مستقیم غلط باشد

وسوسه این است که به‌جای super().darsad() نوشته شود Takhfif.darsad(self)، که برای وراثت تک‌خطی همان کار را می‌کند. لحظه‌ای که کلاسی مثل تخفیف ویژه ساخته شود، آن نسخه دستی ترتیب را دور می‌زند و تخفیف عمده هیچ‌وقت اجرا نمی‌شود، بی‌آنکه خطایی بدهد. super() بدون آرگومان تنها شکلی است که در هر دو حالت درست می‌ماند. پیامد دومش این است که متدی که فقط روی کلاس پایه تعریف شده هم درست کار می‌کند: mablagh که داخلش self.darsad() را صدا می‌زند، روی یک نمونه تخفیف ویژه با مبلغ چهارصد و هشتاد هزار عدد ۷۲۰۰۰ می‌دهد. آن self.darsad از بالای همان فهرست دوباره شروع به گشتن می‌کند و مشتق‌ترین نسخه را پیدا می‌کند، نه نسخه‌ای که در همان فایل کنارش نوشته شده.

سه پرسشی که پیش از ارث بردن باید جواب داد

ارث‌بری برای رابطه «هست» است و نه برای رابطه «دارد». یک ترمینال بی‌تماس یک ترمینال کارتی هست؛ اما یک سفارش یک مشتری نیست، مشتری دارد، و ارث بردن در حالت دوم فقط راهی برای دسترسی راحت به متدهاست که بعدا هزینه می‌دهد.

پرسش دوم این است که آیا نسخه فرزند همه‌جا می‌تواند جای والد بنشیند بی‌آنکه چیزی بشکند. اگر فرزند شرطی اضافه می‌کند که والد نداشت یا خطایی می‌دهد که والد نمی‌داد، این وراثت غلط است و پنج اصل، و اینکه هرکدام جلوی چه چیزی را می‌گیرد همین را با نام می‌گوید. پرسش سوم این است که آیا اصلا رفتار مشترکی هست یا فقط داده مشترک؛ اگر فقط قرار است بگویید چیزی چه کاری بلد است، نام بردن یک توانایی، بدون نام بردن پیاده‌سازی ارزان‌تر است.

جایی که کار گیر می‌کند، درخت کلاس‌های عمیق است

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

ابزار، و سالی که این حرف درست است

در سال ۲۰۲۶، برای دیدن همین ترتیب __mro__ و help در خود مفسر کافی‌اند و رایگان، و جای دنبال کردن دستی زنجیره کلاس‌ها در چند فایل را گرفته‌اند. برای گرفتن اشتباه‌های وراثت پیش از اجرا، یک بررسی‌کننده نوع مثل mypy امضای متد بازنویسی‌شده را با نسخه والد مقایسه می‌کند و ناسازگاری را همان‌جا می‌گوید؛ متن‌باز است و پس از نصب به شبکه کاری ندارد.

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

کنار این

بالای این