درس ۰۵: مفهوم شی‌گرایی

این درس به توضیح مفاهیم پایه «برنامه‌نویسی شی‌گرا» اختصاص یافته است و آخرین درس از سطح‌بندی «پایه» در این کتاب می‌باشد. هدف از این درس آشنایی خوانندگان با مفاهیم عمومی شی‌گرایی بوده و نه آموزش آن؛ جزییات بیشتر از برنامه‌نویسی شی‌گرا به همراه آموزش پیاده‌سازی مفاهیم آن در زبان پایتون توسط درس‌هایی دیگر بررسی خواهد شد. در این درس همچنین به ساختار اشیا و کلاس‌ها در زبان پایتون اشاره‌ شده است که پیش‌نیاز دروس آتی خواهد بود.

سطح: پایه



برنامه‌نویسی شی‌گرا

«برنامه‌نویسی شی‌گرا» (Object-Oriented Programming) یا به اختصار OOP یک الگو یا شیوه تفکر در برنامه‌نویسی است که برگرفته از دنیای واقعی بوده و از دهه ۱۹۶۰ میلادی مطرح گشته است. به زبانی که از این الگو پشتیبانی کند، «زبان شی‌گرا» گفته می‌شود؛ Simula 67 و Smalltalk نخستین زبان‌های برنامه‌نویسی شی‌گرا هستند. ایده شی‌گرایی در پاسخ به برخی از نیازها که الگوهای موجود پاسخ‌گو آن‌ها نبودند به وجود آمد؛ نیازهایی مانند: توانایی حل تمامی مسائل پیچیده (Complex)، «پنهان‌سازی داده» (Data Hiding)، «قابلیت استفاده مجدد» (Reusability) بیشتر، وابستگی کمتر به توابع، انعطاف بالا و...

رویکرد برنامه‌نویسی شی‌گرا «از پایین به بالا» (Bottom-Up) است؛ یعنی ابتدا واحدهایی کوچک از برنامه ایجاد می‌شوند و سپس با پیوند این واحدها، واحدهایی بزرگتر و در نهایت شکلی کامل از برنامه به وجود می‌آید. برنامه‌نویسی شی‌گرا در قالب دو مفهوم «کلاس» (Class) و «شی» (Object) ارایه می‌گردد. هر کلاس واحدی از برنامه است که تعدادی داده و عملیات‌ را در خود نگه‌داری می‌کند و هر شی نیز حالتی (State) مشخص از یک کلاس می‌باشد.

در برنامه‌نویسی شی‌گرا، هر برنامه در قالب موجودیت‌های کوچکی که در واقع همان اشیا هستند و با یکدیگر تعامل دارند در نظر گرفته می‌شود. برای داشتن این اشیا می‌بایست ابتدا کلاس‌های برنامه را تعریف نماییم؛ هر کلاس «رفتار» (Behavior) و «صفات» (Attributes) اشیایی که قرار است از آن ایجاد شوند را تعریف می‌کند. از یک کلاس می‌توان هر تعداد که بخواهیم شی ایجاد نماییم. هر شی بیانگر یک «حالت» یا یک «نمونه» (Instance) از کلاس خود است.

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

به هر شی کلاس، یک نمونه از آن کلاس گفته می‌شود و هر زمان که یک شی از کلاسی ایجاد می‌گردد در واقع یک نمونه از آن ساخته می‌شود. به این عمل در شی‌گرایی «نمونه‌سازی» (Instantiation) گفته می‌شود. بر همین اساس دو نوع کلاس در شی‌گرایی وجود دارد: ۱- کلاس‌های عادی که توانایی نمونه‌سازی دارند و به آن‌ها ”Concrete Class“ گفته می‌شود ۲- کلاس‌هایی که توانایی نمونه‌سازی ندارند و به آن‌ها ”Abstract Class“ گفته می‌شود.

یکی از مفاهیم دیگر در برنامه‌نویسی شی‌گرا، «کپسوله‌سازی» (Encapsulation) است. کپسوله‌سازی به معنی قرار دادن عناصر یک ساختار در قالب موجودیتی جدید می‌باشد. در برنامه‌نویسی شی‌گرا با ایجاد هر نمونه از کلاس، عناصر آن (صفات و رفتارها) در قالب یک موجودیت جدید به نام «شی» قرار می‌گیرد. کپسوله‌سازی در شی‌گرایی امکانی است برای پنهان‌سازی داده‌ها؛ در این شرایط اشیا بدون اینکه از درون یکدیگر و چگونگی کارکرد هم کوچکترین آگاهی داشته باشند به تعامل با یکدیگر می‌پردازند.

گفتیم هر کلاس از تعدادی داده و عملیات درون خود نگهداری می‌کند و همچنین گفتیم هر کلاس رفتار و صفات اشیایی که قرار است از آن ایجاد شوند را تعریف می‌کند؛ اکنون با ارایه تعریفی کامل‌تر خواهیم گفت که: هر کلاس از دو بخش «اعضای داده» (Data Members) و «توابع عضو» (Member Functions) تشکیل شده است. اعضای داده در واقع همان متغیر‌های درون کلاس هستند که خصوصیات یا صفات شی را بیان می‌کنند و در شی‌گرایی با عنوان «فیلد» (Field) یا «صفت» (Attribute) از آن‌ها یاد می‌شود. توابع عضو نیز عملیات یا کارهایی هستند که یک شی از کلاس قادر به انجام آن‌ها می‌باشد؛ می‌توان توابع عضو را بیانگر رفتار اشیا کلاس دانست. در شی‌گرایی به این توابع «متد» (Method) گفته می‌شود.

پس از نمونه‌سازی، شی حاوی تمامی اعضای داده و توابع عضوی است که توسط کلاس مربوط به آن تعریف شده است و برای دسترسی به آن‌ها از الگو: «نام شی + نقطه + نام صفت یا متد()» استفاده می‌گردد. همانند:

car_object.color
car_object.drive()

همانطور که در زمان پیاده‌سازی کلاس خواهید دید؛ با ایجاد هر نمونه از کلاس یک متد خاص در آن به صورت خودکار اجرا می‌گردد. این متد «سازنده» (Constructor) نام دارد و کار آن «مقداردهی اولیه» (Initialization) شی است. این کار موجب اطمینان از مقداردهی تمامی اعضای داده پیش از استفاده شی در برنامه می‌گردد.

برای مثال به کلاس خودرو برگردیم و برای آن صفات: رنگ بدنه، ظرفیت باک، بیشینه سرعت و متدهای: راندن، دریافت میزان سوخت، سوخت گیری، تنظیم سرعت، توقف را در نظر بگیریم. اکنون می‌توانیم با تنظیم صفات، نمونه‌ها یا اشیای مورد نظر خود را از این کلاس ایجاد نماییم. برای مثال: دو خودروی آبی با ظرفیت باک ۲۰ لیتر و بیشینه سرعت ۸۰ کیلومتر-ساعت یا یک خودروی صورتی با ظرفیت باک ۴۰ لیتر و بیشینه سرعت ۱۶۰ کیلومتر-ساعت که البته هر سه آن‌ها تمام متدهای کلاس را در خود دارند:

../_images/l05-car-class-sample.jpg ../_images/l05-car-class-object-sample.jpg

تا به اینجا با مفاهیم «کلاس»، «صفت»، «متد»، «شی»، «نمونه‌سازی» و «کپسوله‌سازی» آشنا شده‌ایم؛ در ادامه به توضیح سه مفهوم مهم دیگر از برنامه‌نویسی شی‌گرا که عبارتند از: «وراثت» (Inheritance)، «چندریختی» (Polymorphism) و «انتزاع» یا «تجرید» (Abstraction) خواهیم پرداخت.

وراثت:

وراثت یکی از شکل‌های «قابلیت استفاده مجدد» کد بوده که برنامه‌نویس را قادر می‌سازد تا با ارث‌بری صفات و متدهای یک یا چند کلاس موجود، کلاس‌های جدیدی را ایجاد نماید.

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

به کلاسی که از آن ارث‌بری می‌شود ”Parent Class“ یا ”Base Class“ (کلاس پایه) یا ”Superclass“ و به کلاسی که اقدام به ارث‌بری می‌کند ”Child Class“ (کلاس فرزند) یا ”Derived Class“ یا ”Subclass“ گفته می‌شود.

ارث‌بری توسط «نسبت هست-یک» (IS-A Relationship) بیان می‌شود؛ این نسبت می‌گوید کلاس فرزند یک نوع از چیزی است که کلاس پایه هست. کلاس A از کلاس B ارث‌بری دارد؛ در این حالت می‌گوییم: A is a type of B، یعنی درست است اگر بگوییم: «سیب» یک نوع «میوه» است یا «خودرو» یک نوع «وسیله نقلیه» است ولی توجه داشته باشید که این یک ارتباط یک‌طرفه از کلاس فرزند به کلاس پایه است و نمی‌توانیم بگوییم: «میوه» یک نوع «سیب» است یا «وسیله نقلیه» یک نوع «خودرو» است.

کلاس‌ها می‌توانند مستقل باشند ولی هنگامی که وارد رابطه‌های وراثت می‌شوند، یک ساختار سلسله مراتب (Hierarchy) به شکل درخت را تشکیل می‌دهند. برای نمونه به ساختار سلسله مراتب وراثت پایین که مربوط به برخی اشکال هندسی است توجه نمایید، پیکان‌ها نشانگر نسبت is-a هستند.

../_images/l05-Inheritance-Hierarchy-Sample.png

در برنامه‌نویسی شی‌گرا نسبت دیگری نیز با عنوان «نسبت دارد-یک» (HAS-A Relationship) وجود دارد که بیانگر مفهومی به نام «ترکیب» (Composition) است که شکل دیگری از قابلیت استفاده مجدد کد می‌باشد ولی مفهومی متفاوت با وراثت دارد. این نسبت زمانی بیان می‌شود که درون یک کلاس (مانند: C) از کلاس دیگری (مانند: D) نمونه‌سازی شده باشد؛ یعنی شی کلاس C درون خودش شی‌ای از کلاس D را داشته باشد؛ در این حالت می‌گوییم: C has a D. به یاد دارید خواندیم کلاس خودرو از کلاس‌های کوچکتری ساخته شده است؛ مثلا کلاس موتور - یعنی درون این کلاس یک شی از کلاس موتور ایجاد شده است، اکنون می‌توانیم بگوییم: «خودرو» یک «موتور» دارد.

../_images/l05-has-a-Sample.png

چندریختی:

مفهوم چندریختی بیانگر توانایی کلاس فرزند در تعریف متدهایی است که در کلاس پایه موجود می‌باشند. برای نمونه دو کلاس «ماهی» و «گربه» را که هر دو آن‌ها از کلاسی به نام «حیوانات» ارث‌بری دارند را در نظر بگیرید؛ در کلاس حیوانات متدی با عنوان «غذا خوردن» که عملی مشترک در میان تمام حیوانات است وجود دارد ولی از آنجا که چگونگی انجام آن در ماهی و گربه متفاوت است، بنابراین هر دو این کلاس‌ها نیاز دارند تا متد «غذا خوردن» مخصوص خود را داشته باشند - در این جاست که این متد در کلاس‌های فرزند بازتعریف می‌شود، به این عمل ”Method Overriding“ گفته می‌شود. با Override کردن یک متد، متد کلاس پایه زیر سایه متد مشابه در کلاس فرزند قرار می‌گیرد و از نظر اشیا کلاس فرزند پنهان می‌شود.

تجرید:

تجرید در برنامه‌نویسی شی‌گرا به همراه مفهوم چندریختی می‌آید و توسط دو مفهوم «کلاس‌های مجرد» (Abstract Classes) و «متدهای مجرد» (Abstract Methods) ارایه می‌گردد.

«کلاس مجرد» کلاسی است که شامل یک یا چند «متد مجرد» باشد و «متد مجرد» متدی است که اعلان (Declare) شده ولی بدنه آن ‌تعریف (Define) نشده است. کلاس‌های مجرد قابلیت نمونه‌سازی ندارند و نمی‌توان از آن‌ها شی ایجاد نمود؛ چرا که هدف از توسعه آن‌ها قرار گرفتن در بالاترین سطح (یا چند سطح بالایی) درخت وراثت، به عنوان کلاس پایه برای ارث‌بری کلاس‌های پایین‌تر می‌باشد. ایده طراحی کلاس مجرد در تعیین یک نقشه توسعه برای کلاس‌های فرزند آن است؛ تعیین صفات و متدهای لازم ولی واگذاردن تعریف متدها بر عهده کلاس‌های فرزند.

به عنوان نمونه سه کلاس «ماهی»، «گربه» و «کبوتر» را در نظر بگیرید. این کلاس‌ها جدا از رفتارهای خاص خود (مانند: «پرواز کردن» در کبوتر یا «شنا کردن» در ماهی)، در یک سری رفتار به مانند «نفس کشیدن»، «غذا خوردن» و... مشترک هستند. راه درستِ توسعه این کلاس‌ها تعیین یک «کلاس پایه» برای رفتارهای مشترک و ارث‌بری هر سه آن‌ها می‌باشد. ولی از آنجا که هر یک، این رفتارهای مشترک را به گونه‌ای دیگر انجام می‌دهد؛ راه درست‌تر آن است که یک «کلاس مجرد» به عنوان «کلاس پایه» آن‌ها در نظر بگیریم؛ در این حالت هر کدام از کلاس‌ها ضمن دانستن رفتارهای لازم می‌تواند آن‌‌ها را متناسب با خواست خود تعریف نماید.

اشیا در پایتون

علاوه‌بر اینکه پایتون یک زبان برنامه‌نویسی شی‌گراست، ساختار آن نیز بر مبنای شی‌گرایی توسعه یافته است و اینطور بیان می‌شود که هر چیزی در پایتون یک شی است. اشیا، انتزاعِ پایتون برای ارایه «انواع داده‌» (Data Types) هستند. به بیان دیگر تمام داده‌های یک برنامه پایتونی یا به صورت مستقیم یک شی است یا از روابط بین اشیا ایجاد می‌گردد. برای نمونه: 56، "!Hello World"، توابع و... حتی خود کلاس‌ها نیز توسط یک نوع شی ارایه می‌شوند.

هر شی در پایتون حاوی یک «شناسه» (identity)، یک «نوع» (type) و یک «مقدار» (value) است.

  • «شناسه» در زمان ایجاد شی به آن اختصاص می‌یابد و غیر قابل تغییر است. تابع ()id شناسه شی را به صورت یک عدد صحیح برمی‌گرداند که این مقدار در CPython بیانگر نشانی (Address) شی در حافظه (Memory) است:

    >>> id(5)
    140468674877440
    
    >>> num = 0.25
    >>> id(num)
    140468676592120
    
    >>> msg = "Hello World!"
    >>> id(msg)
    140468675425264
    
  • هر شی در پایتون دارای یک «نوع» یا ”type“ است که عملیات قابل پشتیبانی و نیز مقادیر ممکن برای شی را تعریف می‌کند. نوع هر شی توسط تابع ()type قابل مشاهده است و همانند شناسه غیر قابل تغییر می‌باشد:

    >>> # python 3.x
    >>> type(127)
    <class 'int'>
    
    >>> # python 2.x
    >>> type(127)
    <type 'int'>
    

    ملاحظه

    تمام اعداد صحیح (Integers) در پایتون یک شی از نوع int می‌باشند. [با انواع آماده (Built-in) شی در پایتون توسط دروس آینده آشنا خواهید شد.]

  • «مقدار» برخی اشیا در پایتون قابل تغییر است که به این دسته از اشیا ”mutable“ (تغییر پذیر) گفته می‌شود؛ ولی مقدار برخی دیگر قابل تغییر نمی‌باشد (مانند اعداد: شی 127) که به آن‌ها اشیا ”immutable“ (تغییر ناپذیر) می‌گویند.

کلاس‌ها در پایتون

از نسخه 2.2 طراحی کلاس‌ها در پایتون تغییر کرد [New-style Classes] که البته ساختار قدیمی همچنان در نسخه 2x باقی مانده است. [مبنای آموزش در این کتاب طراحی جدید می‌باشد.]

در ساختار جدید مفهوم ”type“ برابر مفهوم ”class“ طراحی شده است. در این ساختار هر کلاس خود یک شی از کلاسی به نام ”type“ می‌باشد و همچنین تمامی کلاس‌ها از کلاسی به نام ”object“ ارث‌بری دارند:

>>> # Python 3.x
>>> num = 3

>>> num.__class__
<class 'int'>

>>> type(num)
<class 'int'>

>>> type(type(num))
<class 'type'>

>>> type(num).__class__
<class 'type'>

>>> type(num).__bases__
(<class 'object'>,)

ملاحظه

صفت __class__ نام کلاس یک شی و صفت __bases__ نام کلاس‌های پایه یک کلاس را نمایش می‌دهد.

تعریف کلاس

در پایتون برای تعریف کلاس از کلمه کلیدی class استفاده می‌گردد؛ همانند الگو پایین:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

کلمه کلیدی تعریف کلاس - class - یک دستور اجراپذیر (Executable Statement) است. یک کلاس پیش از اجرای دستور خود هیچ تاثیری در برنامه ندارد. این شرایط سبب می‌شود که حتی بتوان یک کلاس را در میان بدنه دستور شرط (if) یا درون بدنه یک تابع تعریف کرد. [در پشت صحنه]: با اجرای دستور تعریف کلاس، یک شی از نوع type در حافظه ایجاد می‌گردد و از نام کلاس برای اشاره به آن شی استفاده می‌شود.

بعد از کلمه کلیدی class نام کلاس (به دلخواه کاربر) نوشته می‌شود. سطر نخست تعریف مانند تمام دستورات مرکب (Compound) که به صورت معمول در چند سطر نوشته می‌شوند و سرآیند دارند، به کاراکتر : ختم می‌شود. از سطر دوم با رعایت یکنواخت تورفتگی دستورات بدنه کلاس نوشته می‌شوند:

>>> # Python 3.x

>>> class MyClassName:
...     pass
...
>>>

>>> type(MyClassName)
<class 'type'>

>>> MyClassName.__bases__
(<class 'object'>,)
>>>

ملاحظه

در مواردی که هنوز نمی‌خواهیم دستورات مربوط به بدنه یک دستور به مانند کلاس را بنویسیم؛ می‌توانیم از دستور pass استفاده کنیم. با اجرای این دستور هیچ کاری انجام نمی‌شود.

>>> # Python 2.x

>>> class MyClassName(object):
...     pass
...
>>>

>>> type(MyClassName)
<type 'type'>

>>> MyClassName.__bases__
(<type 'object'>,)
>>>

تمامی کلاس‌ها در پایتون 3x به صورت ضمنی از کلاس object ارث‌بری دارند و نیازی به درج آن توسط برنامه‌نویس نیست؛ ولی در نسخه 2x چنانچه قصد داشته‌ باشیم از طراحی جدید کلاس‌ها پیروی کنیم، می‌بایست به صورت صریح از این کلاس ارث‌بری نماییم.

در بحث ارث‌بری نام کلاس(های) پایه مورد نظر درون پرانتز جلوی نام کلاس نوشته می‌شود. در صورت ارث‌بری از چند کلاس می‌بایست نام آن‌ها را توسط کاما (Comma) از یکدیگر جدا ساخت:

>>> # Python 3.x

>>> class ChildClassName(BaseClassNameOne, BaseClassNameTwo):
...     pass
...
>>>

>>> ChildClassName.__bases__
(<class '__main__.BaseClassNameOne'>, <class '__main__.BaseClassNameTwo'>)

ملاحظه

همانطور که می‌دانیم،‌ __main__ اشاره به نام ماژول دارد.

با دقت در نمونه کد بالا متوجه می‌شوید که دیگر از کلاس object در میان کلاس‌های پایه خبری نیست. دلیل این اتفاق در این است که کلاس فرزند (ChildClassName) اکنون در یک سلسله مراتب وراثت قرار گرفته و کلاس‌های پایه او از این کلاس ارث‌بری دارند.

>>> # Python 2.x

>>> class BaseClassNameOne(object):
...     pass
...
>>>

>>> class BaseClassNameTwo(object):
...     pass
...
>>>

>>> class ChildClassName(BaseClassNameOne, BaseClassNameTwo):
...     pass
...
>>>

>>> ChildClassName.__bases__
(<class '__main__.BaseClassNameOne'>, <class '__main__.BaseClassNameTwo'>)

برای دریافت نام تمام کلاس‌های پایه موجود در سلسله مراتب وراثت یک کلاس مشخص می‌توانیم از تابع ()getmro درون ماژول inspect استفاده نماییم [اسناد پایتون]؛ همانند پایین:

>>> # Python 3.x

>>> import inspect
>>> inspect.getmro(ChildClassName)
(<class '__main__.ChildClassName'>, <class '__main__.BaseClassNameOne'>, <class '__main__.BaseClassNameTwo'>, <class 'object'>)
>>> # Python 2.x

>>> import inspect
>>> inspect.getmro(ChildClassName)
(<class '__main__.ChildClassName'>, <class '__main__.BaseClassNameOne'>, <class '__main__.BaseClassNameTwo'>, <type 'object'>)

ملاحظه

خروجی تابع ()getmro مرتب شده است؛ به این صورت که در یک سلسله مراتب از خود کلاس مورد نظر شروع می‌شود و به کلاس object پایان می‌یابد. کلاس‌های پایه هم سطح نیز بر اساس ترتیب نوشتن آن‌ها در کلاس فرزند مرتب می‌شوند.



😊 امیدوارم مفید بوده باشه

لطفا دیدگاه و سوال‌های مرتبط با این درس خود را در کدرز مطرح نمایید.