درس ۱۷: شی گرایی (OOP) در پایتون: تعریف کلاس و ایجاد شی

شی گرایی (OOP) در پایتون: تعریف کلاس و ایجاد شی

Photo by Sarah Kilian

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

این درس بر ارائه تعاریف مربوط به کلاس (Class) و شی (Object) از مفاهیم شی‌گرایی حاکم در زبان برنامه‌نویسی پایتون تمرکز دارد و مفاهیم باقی مانده در دروس آتی ارائه خواهند شد.

سطح: متوسط



برنامه‌نویسی شی گرا (Object-Oriented Programming)

همان‌طور که پیش‌تر نیز گفته شده است، پایتون یک زبان برنامه‌نویسی چند الگویی (multi-paradigm) است و از الگوهای مختلفی از جمله شی گرایی پشتیبانی می‌کند. شی گرایی یک الگوی برنامه‌نویسی یا روشی برای طراحی کدهای برنامه است.

در این شیوه، کدهای برنامه در قالب موجودیت‌های کوچکی به نام کلاس (Class) به وجود می‌آیند. کلاس‌ها چیزی نیستند جز ابزاری که توسط آن می‌توان داده‌ها و عملیات مرتبط با یکدیگر را در یک دسته و جدا از سایر بخش‌های کد قرار داد. با کمک کلاس‌ها رفتار و عملکرد هر تکه از کد مشخص است و با سایر بخش‌های کد تداخل پیدا نمی‌کند. ایجاد یک کلاس به معنی ایجاد یک نوع (Type) جدید در برنامه می‌باشد که می‌توان چندین شی (Object) یا نمونه (Instance) از آن نوع ایجاد کرد. یک برنامه شی‌گرا حاصل ارتباط و تعامل اشیا مختلف ایجاد شده در آن است.

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

تعریف کلاس (Class)

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

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

کلمه کلیدی تعریف کلاس - class - یک دستور اجراپذیر (Executable Statement) است، همانند دستور def برای توابع. یک کلاس پیش از اجرای دستور خود هیچ تاثیری در برنامه ندارد. این شرایط سبب می‌شود که حتی بتوان یک کلاس را در میان بدنه دستور شرط (if) یا درون بدنه یک تابع تعریف کرد.

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

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

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

هر چیزی در پایتون یک شی است. حتی کلاس‌ها، با اجرای دستور تعریف کلاس، یک شی از نوع type در حافظه ایجاد می‌گردد و از نام کلاس برای اشاره به آن شی استفاده می‌شود.

نکته

پیشنهاد PEP 8: برای نوشتن نام کلاس از شیوه CapitalizedWords استفاده شود.

نکته

کلاس‌ها نیز همانند توابع حوزه (Scope) خود را دارند - درس دوازدهم. با تعریف هر کلاس یک حوزه محلی جدید در برنامه پایتونی تعریف می‌گردد.

نکته

یک کلاس، تعریف کننده صفات (ویژگی‌ها) - که به عنوان Attribute شناخته می‌شوند - و رفتار‌های (عملیات) - که به عنوان Method شناخته می‌شوند - اشیایی است که از آن ایجاد خواهد شد. در واقع نام کلاس معرف نوع (Type) اشیای خود است.

به بیانی ساده‌تر، هر کلاس می‌تواند شامل تعدادی تابع و متغیر در داخل بدنه خود باشد. که به متغیرها: Attribute و به توابع: Method گفته می‌شود. هر یک از این Attributeها و Methodها انواعی دارند که در ادامه شرح داده خواهد شد.

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

>>> class DerivedClassName(BaseClassName):
...     pass
...
>>>
>>> class MyClassName():
...     pass
...
>>>

نمونه‌سازی (Instantiation)

به عملیات ایجاد یک شی از کلاس، نمونه‌سازی (Instantiation) گفته می‌شود. کلاس چیزی جز تکه کدی نوشته شده نیست و جایی در حافظه ندارد، این اشیا ایجاد شده از کلاس هستند که در حافظه (Memory) قرار می‌گیرند. نمونه‌سازی از یک کلاس در زبان پایتون به صورت زیر انجام می‌شود:

>>> class Sample:
...     pass
...
>>>

>>> sample_object = Sample()  # Instantiation

>>> type(sample_object)
<class '__main__.Sample'>

در زبان برنامه‌نویسی پایتون با فراخوانی نام کلاس - همچون فراخوانی یک تابع - یک شی از آن کلاس ایجاد می‌گرد.

از هر کلاس می‌توان بی‌نهایت نمونه‌سازی داشت. هر شی از یک کلاس، حوزه (Scope) مخصوص به خود را دارد که جدا از دیگر اشیا آن کلاس خواهد بود. بنابراین اشیا هر کلاس کاملا مستقل و ایزوله (isolated) از یکدیگر هستند.

 >>> class Sample:
...     pass
...
>>>
>>> obj_1 = Sample()
>>> obj_2 = Sample()

>>> id(obj_1)
139936512966840
>>> id(obj_2)
139936512967008

همانطور که از خروجی تابع آماده (built-in) id نیز مشخص است [اسناد پایتون] هر شی جدید از کلاس، identity یا هویتی مستقل از دیگر اشیا داشته و در مکانی جداگانه از حافظه قرار داده شده است.

>>> class Sample:
...     pass
...
>>> obj = Sample()


>>> type(obj)
<class '__main__.Sample'>

>>> type(obj) == Sample
True


>>> isinstance(obj, Sample)
True

در زبان پایتون دو شیوه رایج برای بررسی نوع یک شی وجود دارد. یک راه استفاده از تابع آماده (built-in) type است [اسناد پایتون] که پیش‌تر از آن استفاده می‌کردیم و راه دیگر استفاده از تابع آماده (built-in) isinstance می‌باشد [اسناد پایتون] این تابع دو آرگومان می‌پذیرد که به ترتیب شی و نوع مورد نظر هستند، در صورتی که شی از نوع دریافت شده باشد، مقدار True و در غیر این صورت False برمی‌گرداند.

سازنده (Constructor)

در مبحث شی‌گرایی، هنگام ساخت یک شی (ایجاد یک نمونه جدید)، به صورت خودکار یک متد از داخل کلاس مورد نظر فراخوانی می‌شود. به این متد، سازنده (Constructor) گفته می‌شود. فراخوانی خودکار این متد به برنامه‌نویس این امکان را می‌دهد که در صورت تمایل بتواند چگونگی ایجاد شی جدید را مدیریت یا در همان هنگام ساخت، شخصی‌سازی نماید.

از طرفی هر کلاس در زبان برنامه‌نویسی پایتون شامل یک سری متد خاص می‌باشد که نام تمام آن‌ها با دو کاراکتر خط‌زیرین (Underscore or Underline _) شروع و نیز پایان می‌یابد همانند: __init__ - در کامیونیتی پایتون به دو کاراکتر خط‌زیرین در کنار هم به اصطلاح Dunder (Double underscores) گفته می‌شود - به این متدهای خاص در پایتون به اصطلاح Special Methods ،Dunder Methods یا Magic Methods گفته می‌شود. [اسناد پایتون] باید توجه داشت که تمام این متدها یک پیاده‌سازی پیش‌فرض در پایتون دارند و الزامی برای پیاده‌سازی از طرف برنامه‌نویس وجود ندارد.

در فرآیند نمونه‌سازی از یک کلاس پایتون، به ترتیب دو متد خاص درگیر هستند: __new__ [اسناد پایتون] و __init__ [اسناد پایتون]

متد __new__ در زمان ایجاد شی و دقیقا برای ایجاد شی فراخوانی می‌شود، خروجی این متد یک شی جدید از آن کلاس می‌باشد. این متد از نوع Static Method است - در بخش بعدی شرح داده خواهد شد - نخستین پارامتر این متد ،کلاسی است که قرار است از آن یک شی ایجاد گردد و پارامترهای دیگر که می‌توانند حاوی مقادیری باشند که در زمان نمونه‌سازی ارسال شده است.

متد __init__ بلافاصله پس از اینکه شی جدید توسط متد __new__ ایجاد گردید و درست قبل از اینکه شی جدید از متد __new__ بازگردانده شود (returned)، فراخوانی می‌گردد. این متد از نوع Instance Method است - در بخش بعدی شرح داده خواهد شد - و بنابراین نخستین پارامتر این متد شی جاری است (همان شی‌ای که توسط __new__ ایجاد گردیده است) و پارامترهای دیگر که برنامه‌نویس در زمان نمونه‌سازی جهت مقدار دهی در شی ارسال می‌کند - توجه داشته باشید که این متد خروجی ندارد (بدون دستور return یا بهتر بگوییم خروجی آن None است) و شی جدید حاصل خروجی متد __new__ خواهد بود.

متاسفانه برخی افراد تازه وارد در زبان پایتون و همینطور برخی آموزش‌ها متد __init__ را به عنوان Constructor کلاس‌های پایتون می‌دانند اما درست این است که در فرآیند نمونه‌سازی در زبان برنامه‌نویسی پایتون، دو متد __new__ و __init__ با یکدیگر کار می‌کنند و نقش سازنده (Constructor) را ایفا می‌کنند. متد __new__ شی را ایجاد (create) و متد __init__ آن را شحصی‌سازی (customize) می‌کند:

 1class Sample:
 2
 3    def __new__(cls, *args, **kwargs):
 4        print("__new__(), Has been called")
 5        print('cls: ', cls)
 6        print('args: ', args)
 7        print('kwargs: ', kwargs)
 8
 9        # create new object
10        obj = super().__new__(cls, *args, **kwargs)
11
12        # return object
13        return obj
14
15    def __init__(self, x=0, y=0):
16        print("__init__(), Has been called")
17        print('self: ', self)
18        self.x = x
19        self.y = y
20
21
22sample_1 = Sample()
23print('-' * 30)
24sample_2 = Sample(3, 6)
25print('-' * 30)
26sample_3 = Sample(x=3, y=6)
__new__(), Has been called
cls:  <class '__main__.Sample'>
args:  ()
kwargs:  {}
__init__(), Has been called
self:  <__main__.Sample object at 0x7fb4580a6470>
------------------------------
__new__(), Has been called
cls:  <class '__main__.Sample'>
args:  (3, 6)
kwargs:  {}
__init__(), Has been called
self:  <__main__.Sample object at 0x7fb4580a64e0>
------------------------------
__new__(), Has been called
cls:  <class '__main__.Sample'>
args:  ()
kwargs:  {'x': 3, 'y': 6}
__init__(), Has been called
self:  <__main__.Sample object at 0x7fb005453438>

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

نکته

  • زبان برنامه‌نویسی پایتون برخلاف برخی از زبان‌های دیگر شی‌گرا به مانند Java، از امکان پیاده‌سازی چندین Constructor پشتیبانی نمی‌کند. البته برنامه‌نویس با روش‌هایی می‌تواند به صورت منطقی به هدف خود برسد!

  • همانطور که بیان شد، هر کلاس پایتون یک پیاده‌سازی پیش‌فرض از دو متد __new__ و __init__ دارد بنابراین الزامی به پیاده‌سازی دو متد __new__ و __init__ برای نمونه‌سازی از کلاس نیست. در اکثر مواقع __new__ پیاده‌سازی نمی‌شود اما زمانی که می‌خواهید در زمان نمونه‌سازی مقادیری در شی تنظیم نمایید، لازم است متد __init__ را پیاده‌سازی نمایید.

  • معمولا __new__ زمانی پیاده‌سازی می‌شود که بخواهیم محدودیت‌هایی در ایجاد شی کلاس مورد نظر ایجاد کنیم. برای نمونه در پیاده‌سازی طرح Singleton [ویکی‌پدیا] یک کلاس.

  • ارسال آرگومان در زمان نمونه‌سازی شی یا همان پیاده‌سازی متد __init__ به برنامه‌نویس این اطمینان را می‌دهد که شی جدید در یک وضعیت درست تنظیم شده است.

  • آرگومان‌های متناظر با پارامتر‌های متد __init__ (به جز self که توسط مفسر پایتون مقداردهی می‌گردد) می‌بایست در زمان نمونه‌سازی و فراخوانی کلاس ارسال گردد.

اشیا با قابلیت فراخوانی (Callable Objects)

پیش‌تر دیدیم که می‌توان توابع را فراخوانی نمود (درس دروازدهم)، با این کار بدنه تابع اجرا و خروجی متناسب دریافت می‌گردید. در این درس نیز مشاهده کردیم کلاس‌ها نیز در پایتون توانایی فراخوانی دارند که با فراخوانی کلاس، یک شی از آن ایجاد می‌گردد. با استفاده از تابع callable در پایتون می‌توان تشخیص داد که آیا یک شی قابلیت فراخوانی دارد یا خیر [اسناد پایتون]، این تابع در صورتی که شی دریافتی قابلیت فراخوانی (callable) داشته باشد مقدار True و در غیر این صورت False برمی‌گرداند:

>>> def function():
...     pass
...
>>> callable(function)
True

>>> class SampleClass:
...     pass
...
>>> callable(SampleClass)
True

>>> obj = SampleClass()
>>> callable(obj)
False

>>> obj()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'SampleClass' object is not callable

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

همانطور که اشاره شد، کلاس در پایتون چندین متد خاص همانند __new__ و __init__ دارد که به تدریج به آنها آشنا خواهیم شد. یکی دیگر از این متدها __call__ می‌باشد [اسناد پایتون]. این متد نیز همانند متد __init__ از نوع Instance Method (بخش بعدی شرح داده شده است) می‌باشد که با پیاده‌سازی آن در کلاس، اشیای آن کلاس قابلیت فراخوانی پیدا خواهند کرد:

 1class Sample:
 2
 3    def __init__(self, x=0, y=0):
 4        print('------------------- Called __init__()')
 5        self.x = x
 6        self.y = y
 7
 8    def __call__(self, x, y):
 9        print('------------------- Called __call__()')
10        self.x = x
11        self.y = y
12
13
14obj = Sample()
15print('object is callable:', callable(obj))
16print('x =', obj.x)
17
18obj(5, 6)
19print('x =', obj.x)
------------------- Called __init__()
object is callable: True
x = 0
------------------- Called __call__()
x = 5

سطر ۱۸ نمایش فراخوانی یک شی از کلاس Sample می‌باشد - درست به مانند یک تابع!

با فراخوانی یکی شی، به صورت خودکار متد __call__ فراخوانی و آرگومان‌های نظیر ارسال می‌گردند.

از کاربردهای پیاده‌سازی متد __call__ و افزودن قابلیت فراخوانی به یک شی می‌توان به ایجاد کلاس به عنوان دکوراتور (decorator) اشاره کرد (دروس آتی شرح داده خواهد شد) و همچنین کاربردهایی که نیاز می‌شود شی در زمان اجرا initialize یا مقداردهی دوباره داشته باشد، چرا که متد __init__ تنها یکبار در زمان نمونه‌سازی فراخوانی می‌گردد.

صفات (Attributes)

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

  • Instance Attribute

  • Class Attribute

Instance Attribute

به Attributeهای خاص یک شی گفته می‌شود. به هر شی در زبان برنامه‌نویسی پایتون می‌توان با استفاده از سینتکس زیر یک Attribute انتساب داد:

object.attribute_name = value
1class Sample: pass
2
3sample = Sample()
4
5sample.a_new_attribute = 'A New Attribute!'
6
7print(sample.a_new_attribute)
A New Attribute!

هر چیزی در پایتون یک شی است ولی ممکن است مفسر پایتون برای برخی اشیا محدودیت‌هایی در نظر گرفته باشد و شما نتوانید به هر شی‌ای در پایتون Attribute اضافه نمایید. در این لحظه جا دارد اشاره شود به درس چهاردهم (بخش Function Attributes) که در واقع کاری جز افزودن Attribute به شی تابع نبود.

نکته

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

برگردیم به مثال قبل که در آن ما یک کلاس به اسم Sample ایجاد (سطر ۱) و به یک شی از آن - پس از نمونه‌سازی (سطر ۳) - یک Attribute به نام a_new_attribute اضافه کردیم (سطر ۵). این شیوه افزودن Attribute به اشیای کلاس‌هایی که خودمان آن‌ها را تعریف می‌کنیم چندان جالب نیست و ممکن است باعث بروز خطاهایی منطقی در برنامه گردد، بهتر است این کار توسط متد __init__ که در واقع initializer اشیا پایتون است، انجام پذیرد - به نمونه کدهای زیر توجه نمایید:

1class Sample:
2
3        def __init__(self, attribute_value):
4                self.a_new_attribute = attribute_value
5
6sample = Sample()
7
8print(sample.a_new_attribute)
A New Attribute!
 1class Person:
 2
 3        def __init__(self, first_name, last_name, age, gender):
 4                self.first_name = first_name
 5                self.last_name = last_name
 6                self.age = age
 7                self.gender = gender
 8
 9person_1 = Person('Kaneki', 'Ken', 18, 'male')
10person_2 = Person('Haise', 'Sasaki', 19, 'male')
11
12print(person_1.first_name)
13print(person_2.last_name)
Kaneki
Sasaki

در ادامه شرح داده خواهد شد که پارامتر self به شی جاری اشاره دارد و به صورت خودکار توسط مفسر پایتون مقداردهی می‌شود.

Class Attribute

به Attributeهای خاص یک کلاس گفته می‌شود و در واقع متغیرهایی است که درون کلاس و خارج از متدها تعریف می‌گردند. کاربرد این Attributeها به اشتراک گذاشتن یک یا چند مقدار یکسان در بین تمام اشیاست.

تمام اشیای یک کلاس به Class Attributeهای آن کلاس دسترسی دارند:

 1class Sample:
 2    class_attribute = 0
 3
 4print('#' * 10, 'STEP#A')
 5print('LINE 05:', Sample.class_attribute)
 6
 7# Instantiation
 8sample_1 = Sample()
 9sample_2 = Sample()
10
11print('#' * 10, 'STEP#B')
12print('LINE 12:', sample_1.class_attribute)
13print('LINE 13:', sample_2.class_attribute)
14
15print('#' * 10, 'STEP#C')
16
17# Change class_attribute for all objects
18Sample.class_attribute = 1
19
20print('LINE 20:', sample_1.class_attribute)  # Changed!
21print('LINE 21:', sample_2.class_attribute)  # Changed!
22
23print('#' * 10, 'STEP#D')
24
25# WARNING!!! Create a new instance attribute
26sample_2.class_attribute = 2
27
28print('LINE 28:', sample_1.class_attribute)
29print('LINE 29:', sample_2.class_attribute)  # instance attribute!!!
########## STEP#A
LINE 05: 0
########## STEP#B
LINE 12: 0
LINE 13: 0
########## STEP#C
LINE 20: 1
LINE 21: 1
########## STEP#D
LINE 28: 1
LINE 29: 2

نکته

مقدار Class Attributeها هم با استفاده از نام کلاس قابل دستیابی است (سطر ۵) و هم با استفاده از هر یک از اشیا آن کلاس (سطرهای ۱۲ و ۱۳).

نکته

برای تغییر مقدار Class Attribute در داخل کلاس از Class Method - در ادامه شرح داده می‌شود - استفاده می‌شود و در بیرون کلاس با استفاده از نام کلاس به صورت زیر (سطر ۱۸):

ClassName.class_attribute = new_value

نکته

برای تغییر مقدار Class Attributeها، از شی استفاده نکنید، با این کار تنها یک Instance Attribute برای آن شی ایجاد می‌گردد (سطر ۲۶).

متد (Method)

متدها در واقع توابعی هستند که داخل هر کلاس تعریف می‌شوند. هر کلاس پایتون می‌تواند شامل سه نوع متد باشد:

  • Instance Method

    هر زمان در پیاده‌سازی یک کلاس، به شی جاری از کلاس یا Instance Attributeها نیاز داشتیم می‌بایست این نوع متد را پیاده‌سازی کنیم.

  • Class Method

    هر زمان در پیاده‌سازی یک کلاس، به خود کلاس یا Class Attributeها نیاز داشتیم می‌بایست این نوع متد را پیاده‌سازی کنیم.

  • Static Method

    هر زمان در پیاده‌سازی یک کلاس، به کلاس و به اشیای آن کلاس نیازی نداشتیم یا قصد پیاده‌سازی کاری مستقل از رفتار کلی کلاس مورد نظر داشتیم، می‌بایست این نوع متد را پیاده‌سازی کنیم.

 1class Sample:
 2
 3    def instance_method(self):
 4        pass
 5
 6    @classmethod
 7    def class_method(cls):
 8        pass
 9
10    @staticmethod
11    def static_method():
12        pass

متد شی (Instance Method)

رایج‌ترین نوع متد در پایتون است. برای ایجاد این متد نیازی به دکوراتور (Decorator‌ - درس سیزدهم) نیست. همانطور که از نام این متد مشخص است این متد تنها از سوی اشیا یک کلاس قابل استفاده است. همانطور که پیش‌تر صحبت شد، هر شی از کلاس صفات خاص خود را دارد (Instance Attributes) که از این متدها می‌توان برای دستیابی و دستکاری آن‌ها استفاده کرد.

این نوع متد همواره می‌بایست حداقل یک پارامتر داشته باشد. پارامتر نخست که معمولا self نام‌گذاری می‌شود حاوی شی جاری از کلاس است - در واقع همان شی ای که این متد را فراخوانی کرده است. این مقدار همواره از سوی مفسر پایتون ارسال می‌گردد و نیازی به ارسال از سوی برنامه‌نویس ندارد:

 1class Sample:
 2
 3    def __init__(self, char='*'):
 4        self.character = char
 5
 6    def multiply_print(self, count=1):
 7        print(self.character * count)
 8
 9
10sample_1 = Sample()  # Instantiating a new Object
11
12sample_1.multiply_print()
13sample_1.multiply_print(10)
14
15print('-' * 30)
16
17sample_2 = Sample('#')  # Instantiating a new Object
18
19sample_2.multiply_print()
20sample_2.multiply_print(10)
*
**********
------------------------------
#
##########

گفته شده که متد __init__ جزیی از مفهوم Constructor کلاس‌های پایتون بوده و برای شخصی‌سازی یک شی در زمان ایجاد آن به کار می‌رود و کاربرد معمول آن افزودن Attribute به شی است. در نمونه کد بالا، این متد یک پارامتر char دریافت می‌کند - این پارامتر مقدار پیش‌فرض * را دارد، بنابراین ارسال آرگومان متناظر برای آن اجباری نیست (تابع در پایتون - درس دوازدهم). با این کار می‌توانیم در زمان نمونه‌سازی شی، یک Attribute با نام character در آن تعریف نماییم (سطر ۴). ما می‌خواهیم مقدار Attribute یا صفت character از هر شی را به تعداد دلخواه چاپ نماییم، از آنجا که این مقدار یک صفتِ متعلق به شی است و در ازای هر شی این مقدار می‌تواند متفاوت باشد پس ما برای این کار می‌بایست که یک Instance Method در بدنه کلاس تعریف کنیم (متد multiply_print ) - چرا که تنها در این صورت است که می‌توانیم به self دسترسی داشته باشیم و مقدار صفت character را از آن دستیابی کنیم.

نکته

Instance Method‌ها تنها می‌توانند توسط اشیا فراخوانی شوند. روند فراخوانی یک متد توسط شی نیز به صورت نام شی + کاراکتر .‍‍ + نام متد می‌باشد.

متد کلاس (Class Method)

این نوع متد همواره می‌بایست حداقل یک پارامتر داشته باشد. پارامتر نخست که معمولا cls نام‌گذاری می‌شود حاوی کلاس جاری است - در واقع این متد هیچ اطلاعاتی از اشیا کلاس ندارد و تنها کلاس را می‌شناسد و Class Attributeها را دستیابی و دستکاری می‌کند. مقدار cls نیز همانند self همواره از سوی مفسر پایتون ارسال می‌گردد و نیازی به ارسال از سوی برنامه‌نویس ندارد. این متد با استفاده از دکوراتور (Decorator‌ - درس سیزدهم) classmethod@ ایجاد می‌شود [اسناد پایتون]:

 1class Student:
 2    school_name = 'My School'
 3
 4    def __init__(self, name, family):
 5        self.name = name
 6        self.family = family
 7
 8    @classmethod
 9    def school_info(cls):
10        print(cls)
11        return f'name: {cls.school_name}'
12
13print(Student.school_info())
14print('-' * 30)
15print(Student('My Name', 'My Family').school_info())
<class '__main__.Student'>
name: My School
------------------------------
<class '__main__.Student'>
name: My School

نکته

این نوع متد (Class Method) را می‌توان هم با استفاده از نام کلاس دستیابی کرد (سطر ۱۳) و هم با استفاده از اشیای آن کلاس (سطر ۱۵)، در واقع دکوراتور classmethod@ کارهای لازم برای نادیده گرفتن شی و ارسال مقدار پارامتر cls را انجام می‌دهد.

متد ایستا (Static Method)

این نوع متد با استفاده از دکوراتور (Decorator‌ - درس سیزدهم) staticmethod@ ایجاد می‌شود [اسناد پایتون]. این نوع متد پایتون، نه از اشیا اطلاعاتی دارد و نه حتی از کلاس. در واقع به این نوع متد، نه مقدار self ارسال می‌شود و نه cls:

 1class Student:
 2    school_name = 'My School'
 3
 4    def __init__(self, name, family):
 5        self.name = name
 6        self.family = family
 7
 8    @classmethod
 9    def school_info(cls):
10        print(cls)
11        return f'name: {cls.school_name}'
12
13    @staticmethod
14    def info():
15        return "This is a student class"
16
17print(Student.info())
18print('-' * 30)
19print(Student('My Name', 'My Family').info())
This is a student class
------------------------------
This is a student class

نکته

این نوع متد (Static Method) را می‌توان هم با استفاده از نام کلاس دستیابی کرد (سطر ۱۷) و هم با استفاده از اشیای آن کلاس (سطر ۱۹)، در واقع دکوراتور staticmethod@ کارهای لازم برای نادیده گرفتن شی و کلاس مربوط را انجام می‌دهد.

مقدار Hash یک شی و کاربرد آن در پایتون

به صورت کلی یک Hash در واقع عددی است که در ازای داده‌ای مشخص برآورد می‌گردد. داده‌های مشابه دارای مقدار hash یکسانی خواهند بود و از طرفی یک تغییر جزئی در داده منجر به تولید یک مقدار hash کاملا متفاوت می‌شود. مقدار hash از یک تابع هَش [ویکی‌پدیا] به دست می‌آید که مسئولیت آن تبدیل داده ورودی به hash رمزگذاری شده است. واضح است که تعداد داده‌ها می‌تواند بسیار بیشتر از تعداد مقادیر قابل تولید hash باشد، بنابراین دو داده ممکن است مقدار hash یکسان داشته باشند که به آن Hash collision می‌گویند. در واقع اگر دو شی hash یکسان داشته باشند، لزوماً دارای ارزش یکسانی نیستند (برابر نیستند).

در زبان برنامه‌نویسی پایتون،‌ تابع hash [اسناد پایتون] یک شی hashable (قابل hash) را دریافت و مقدار hash آن را بر می‌گرداند:

>>> a = 5
>>> hash(a)
5
>>> hash(5)
5

>>> hash(999999999999999999)
999999999999999999
>>> hash(99999999999999999999999999999999999999)
244469275760665570

>>> a = 'saeid'
>>> hash(a)
4007074958086188072

>>> hash('PYTHON')
-6387242471900568301

>>> hash('PYTHoN')
-6457932607787762593
  • گاهی ممکن است مقدار hash برابر با یک عدد منفی محاسبه گردد، مقدار hash منفی نیز در پایتون معتبر می‌باشد.

  • با دوباره اجرا کردن برنامه یا اسکریپت ممکن است به نتایج دیگری از مقدار hash برسید. در واقع تظمین یکتایی مقدار hash تولید شده در پایتون تنها در ازای حیات هر proccess یا «اجرای برنامه» پابرجا خواهد بود.

شی hashable

گفتیم ورودی تابع hash پایتون می‌بایست یک شی hashable باید. کدام اشیا در پایتون hashable هستند؟ تمامی اشیای که از نوع immutable (تغییرناپذیر - مراجعه شود به بخش دسته‌بندی از درس هشتم) هستند و همچنین اشیایی که متد خاص __hash__ [اسناد پایتون] را پیاده‌سازی کرده باشند:

>>> class Sample:
...     pass
...
>>> obj = Sample()
>>> hash(obj)
-9223363243335467036
>>> obj.__hash__()
-9223363243335467036

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

کاربرد hash در پایتون

۱) ساختار Hash table [ویکی‌پدیا]

ساختمان داده دو نوع دیکشنری (dict) و مجموعه (set) در زبان برنامه‌نویسی پایتون بر پایه Hash table ایجاد شده است. در نتیجه سرعت دستیابی عناصر در آن‌ها بسیار بیشتر از دستیابی در شی لیست (List) می‌باشد. در نوع داده دیکشنری، hash کلیدها محاسبه و از آن برای دستیابی مقدار مربوطه استفاده می‌شود، برای همین است که کلیدها در دیکشنری حتما می‌بایست از نوع hashable باشند ولی برای مقادیر هیچ محدودیتی وجود ندارد. نوع داده مجموعه نیز تنها می‌تواند شامل تعدادی شی hashable و یکتا (غیر تکراری) باشد.

۲) مقایسه دو شی

نکته

  • اگر دو شی با یکدیگر برابر باشند، آنگاه مقدار hash آن‌ها نیز برابر خواهد بود.

  • اگر مقدار hash دو شی با یکدیگر برابر باشد، آنگاه ممکن است آن دو شی نیز با یکدیگر برابر باشند.

طی دروس آینده با مبحث Operator Overloading آشنا خواهید شد ولی در اینجا تنها کافی است بدانید که هرگاه دو شی توسط عملگر == مقایسه گردند، متد __eq__ [اسناد پایتون] به صورت خودکار فراخوانی خواهد شد. البته این متد نیز مانند باقی متدهای خاص پایتون، به صورت ضمنی یک پیاده‌سازی پیش‌فرض از خود دارد. در واقع خروجی این متد نتیجه مقایسه برابر بودن دو شی را برمی‌گرداند.

بین دو متد __eq__ و __hash__ روابطی حاکم است که باید بدانیم:

  • اگر متد __eq__ را پیاده‌سازی کنید ولی متد __hash__ را خیر، آنگاه اشیای کلاس مذکور hashable نخواهند بود.

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

  • در حالت پیش‌فرض پایتون، True بودن خروجی متد __eq__ برای دو شی x و y یعنی x == y به معنی برقرار بودن دو شرط:‌ x is y و hash(x) == hash(y) می‌باشد.

  • در حالت پیش‌فرض پایتون، تمام اشیای یک کلاس نابرابر و دارای مقدار hash متفاوت هستند، مگر اینکه ملاک مقایسه یک شی، خودش باشد.

به دو نمونه کد زیر توجه نمایید:

مقایسه شی در حالت پیش‌فرض:

 1class Student:
 2    def __init__(self, name, score):
 3        self.name = name
 4        self.score = score
 5
 6obj_1 = Student('Saeid', 70)
 7obj_2 = Student('Saeid', 90)
 8
 9print('Single Object     :', obj_1 == obj_1)
10print('Same Objects      :', obj_1 == Student('Saeid', 70))
11print('Different Objects :', obj_1 == obj_2)
Single Object     : True
Same Objects      : False
Different Objects : False

مقایسه شی به همراه شخصی‌سازی دو متد مذکور:

 1class Student:
 2    def __init__(self, name, score):
 3        self.name = name
 4        self.score = score
 5
 6    def __eq__(self, other):
 7        return self.name == other.name and self.score == other.score
 8
 9    def __hash__(self):
10        return hash((self.name, self.score))
11
12obj_1 = Student('Saeid', 70)
13obj_2 = Student('Saeid', 90)
14
15print('Single Object     :', obj_1 == obj_1)
16print('Same Objects      :', obj_1 == Student('Saeid', 70))
17print('Different Objects :', obj_1 == obj_2)
Single Object     : True
Same Objects      : True
Different Objects : False

در این مثال هر دو Attribute کلاس Student از نوع immutable بودند، بنابراین از خود آن‌ها برای مقایسه و محاسبه مقدار hash استفاده کردیم. به هر حال منطق پیاده‌سازی این دو متد بر اساس مسئله مطرح شده، بر عهده برنامه‌نویس می‌باشد. حتی می‌توانید از مقدار تابع ()id یا همان id(self) بهره بگیرید.



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