برنامه نویسی شئ گرا (oop)
در تمام برنامه هایی که تاکنون نوشته ایم، برنامۀ خود را حول تابع طراحی کرده ایم؛ یعنی بلوک هایی از دستورات که داده ها را دست کاری می کنند. این شیوۀ برنامه نویسی، برنامه نویسی روبه محور نامیده می شود. روش دیگری برای سازماندهی برنامه وجود دارد که در آن داده ها و قابلیت های مرتبط با یکدیگر را با هم ترکیب می کنند و و درون چیزی به نام شئ (object) قرار می دهیم. این شیوه، الگوی برنامه نویسی شئ گرا نامیده می شود. در بیشتر مواقع می توانید از برنامه نویسی رو به محور استفاده کنید؛ اما هنگام نوشتن برنامه های بزرگ و یا زمانی که با برنامه ای مواجه هستید که با این روش سازگاری بیشتری دارد، می توانید از تکنیک برنامه نویسی شئ گرا استفاده کنید.
کلاس ها و اشیا دو جنبۀ اصلی برنامه نویسی شئ گرا هستند. یک کلاس، یک نوع (type) جدید ایجاد می کند که اشیا نمونه هایی از آن کلاس هستند. برای درک بهتر، می توان اینگونه تشبیه کرد که می توانید متغیرهایی از نوع int داشته باشید؛ بدین معنا که متغیرهایی که اعداد صحیح را ذخیره می کنند، نمونه ها (اشیائی) از کلاس int هستند.
نکته ای برای برنامه نویسان زبان ایستا
توجه داشته باشید که که حتی اعداد صحیح نیز مانند اشیا (از کلاس
int) در نظر گرفته می شوند. این موضوع با ++C و Java (قبل از نسخه 1.5) متفاوت است؛ زیرا اعداد صحیح از نوع اولیه بومی (primitive native types) هستند.برای اطلاعات بیشتر دربارۀ کلاس،
help(int)را ببینید.برنامه نویسان #C و Java 1.5 این مفهوم را مشابه مفهوم boxing and unboxing خواهند یافت.
اشیا می توانند با استفاده از متغیرهای معمولی، داده هایی را ذخیره کنند که به شئ تعلق دارند. متغیرهایی که به یک شئ یا کلاس تعلق دارند، فیلدها نامیده می شوند. اشیا همچنین می توانند با استفاده از توابعی که به کلاس تعلق دارند، دارای قابلیت و عملکرد باشند. چنین توابعی روش های آن کلاس نامیده می شوند. این اصلاحات اهمیت دارند، زیرا به ما کمک می کنند میان توابع و متغیرهایی که مستقل هستند و آنهایی که به یک کلاس یا شئ تعلق دارند، تمایز قائل شویم. مجموعۀ فیلدها و روش ها را می توان در مجموع ویژگی های آن کلاس نامیم.
فیلدها دو نوع هستند - ممکن است به هر نمونه/شئ از کلاس تعلق داشته باشند یا می توانند به خود کلاس تعلق داشته باشند. این دو نوع به ترتیب متغیرهای نمونه و متغیرهای کلاس نامیده می شوند.
یک کلاس با استفاده از کلمۀ کلیدی class ایجاد می شود. فیلدها و روش های کلاس در یک بلوک دارای تورفتگی لیست می شوند.
The self
روش های کلاس تنها یک تفاوت مشخص با توابع معمولی دارند - آنها باید یک نام اضافی در ابتدای لیست پارامترهای خود داشته باشند؛ اما هنگام فراخوانی روش، مقداری به این پارامتر نمی دهید، پایتون این مقدار را فراهم می کند. این متغیر به خود شئ اشاره می کند و طبق قرارداد، نام آن self است.
اگرچه می توانید هر نامی به این پارامتر بدهید، اکیداً توصیه می کنم از نام selfاستفاده کنید؛ هر نام دیگری نا مناسب تلقی می شود. استفاده از یک نام استاندارد، مزایای زیادی دارد - خوانندۀ برنامه شما بی درنگ آن را تشخیص می دهد و حتی IDE ها (محیط های توسعۀ یکپارچه) نیز در صورت استفاده از self می توانند به شما کمک کنند.
نکته ای برای برنامه نویسان ++C#/Java/C
نام
selfدر پایتون معادل اشاره گرthisدر ++C و مرجعthisدر Java و #C است.
ممکن است برایتان این سؤال پیش آمده باشد که پایتون چگونه مقدار self را فراهم می کند و چرا لازم نیست شما مقداری به آن ارائه دهید. یک مثال این مفهوم را روشن می کند. فرض کنید کلاسی به نام MyClass و نمونه ای از این کلاس به نام myobject دارید. وقتی روشی از این شئ را به صورت myobject.method(arg1 , arg2) را فراخوانی می کنید، پایتون این عبارت را به صورت خودکار به MyClass.method(myobject, arg1, arg2) تبدیل می کند. تمام مفهوم ویژۀ self همین است.
این همچنین بدین معناست که اگر روشی داشته باشید که هیچ آرگومانی دریافت نمی کند، باز هم باید یک آرگومان داشته باشید؛ یعنی self.
کلاس ها (Classes)
ساده ترین کلاس ممکن در مثال زیر نشان داده شده است (آن را با نام oop_simplestclass.py ذخیره کنید):
class Person:
pass # An empty block
p = Person()
print(p)
خروجی:
$ python oop_simplestclass.py
<__main__.Person instance at 0x10171f518>
نحوۀ عملکرد
با استفاده از دستور class و نام کلاس، یک کلاس جدید ایجاد می کنیم. پس از آن، یک بلوک از دستورات دارای تورفتگی قرار می گیرد که بدنۀ کلاس را تشکیل می دهد. در این مثال، یک بلوک خالی داریم که با استفاده از دستور pass مشخص شده است.
سپس با استفاده از نام کلاس و به دنبال آن یک جفت پرانتز، یک شئ/نمونه از این کلاس را ایجاد می کنیم. (در بخش بعدی درباره نمونه سازی بیشتر خواهیم آموخت ). برای بررسی، نوع متغیر را به سادگی با پرینت کردن آن بررسی می کنیم. خروجی به ما می گوید که یک نمونه از کلاس Person در ماژول __main__ داریم.
توجه کنید که آدرس حافظۀ کامپیوتر که شئ شما در آن ذخیره شده است نیز چاپ می شود. این آدرس در رایانۀ شما مقدار متفاوتی خواهد داشت، زیرا پایتون می تواند شئ را هر جایی که فضای کافی پیدا کند، ذخیره کند.
روش ها (Methods)
پیش تر بررسی کردیم که کلاس ها/اشیا می توانند همچون توابع دارای روش باشند، با این تفاوت که یک متغیر اضافی به نام self دارند. اکنون مثالی را بررسی می کنیم (آن را با نام oop_method.py ذخیره کنید).
class Person:
def say_hi(self):
print('Hello, how are you?')
p = Person()
p.say_hi()
# The previous 2 lines can also be written as
# Person().say_hi()
خروجی:
$ python oop_method.py
Hello, how are you?
نحوۀ عملکرد
در اینجا self را در عمل مشاهده می کنید. توجه داشته باشید که روش say_hi پارامتری دریافت نمی کند، اما هنوز self در تعریف تابع قرار دارد.
روش __init__
نام روش های زیادی در کلاس های پایتون وجود دارند که اهمیت ویژه ای دارند. اکنون اهمیت روش __init__ را بررسی می کنیم.
روش __init__ به محض اینکه یک شئ از یک کلاس نمونه سازی (ایجاد)شود، اجرا می شود. این روش برای انجام هرگونه مقداردهی اولیه (اختصاص مقادیر اولیه به شئ) که می خواهید روی شئ انجام دهید، مفید است. توجه کنید که که در ابتدا و انتهای نام این روش، دو زیرخط وجود دارد.
مثال را تحت عنوان oop_init.py ذخیره کنید:
class Person:
def __init__(self, name):
self.name = name
def say_hi(self):
print('Hello, my name is', self.name)
p = Person('Swaroop')
p.say_hi()
# The previous 2 lines can also be written as
# Person('Swaroop').say_hi()
خروجی:
$ python oop_init.py
Hello, my name is Swaroop
نحوۀ عملکرد
در اینجا، روش __init__ را به گونه ای تعریف می کنیم که یک پارامتر name (علاوه بر self معمول) دریافت کند. در اینجا یک فیلد جدید نیز ایجاد می کنیم که آن هم name نام دارد. توجه کنید که این ها دو متغیر متفاوت هستند، حتی اگر هر دو 'name' نامیده شوند. هیچ مشکلی وجود ندارد، زیرا نشانه گذاری نقطه ای self.name به این معناست که چیزی به نام "name" وجود دارد که بخشی از شئ موسوم به "self" است؛ درحالی که name دیگر یک متغیر محلی است. از آنجا که به صراحت مشخص می کنیم به کدام name اشاره داریم، هیچ ابهامی ایجاد نمی شود.
هنگام ایجاد نمونۀ جدید p از کلاس Person، این کار را با استفاده از نام کلاس و سپس آرگومان های موجود در پرانتز انجام می دهیم: p = Person('Swaroop').
ما روش __init__ را به صورت آشکار فراخوانی نمی کنیم.
اهمیت ویژۀ این روش دقیقاً در همین موضوع است.
اکنون می توانبم از فیلد self.name در روش های خود استفاده کنیم؛ همانطور که در روش say_hi نشان داده شده است.
متغیر های کلاس و شئ {class-obj-vars#}
پیش تر دربارۀ قابلیت های کلاس ها و اشیا (یعنی روش ها)، بحث کردیم. اکنون بیایید بخش داده ای آن را بررسی کنیم. بخش داده ای، یعنی فیلدها، چیزی جز متغیرهای معمولی نیستند که به فضاهای نام (namespaces) کلاس ها و اشیا متصل شده اند. این بدان معناست که این نام ها تنها در چارچوب همین کلاس ها و اشیا معتبر هستند. به همین دلیل به آنها فضاهای نام گفته می شود.
دو نوع فیلد وجود دارد - متغیرهای کلاس و متغیرهای شئ؛ این دو بر اساس اینکه متغیرها به ترتیب متعلق به کلاس یا شئ هستند، طبقه بندی می شوند.
متغیرهای کلاس به صورت مشترک مورد استفاده قرار می گیرند - یعنی تمام نمونه های آن کلاس می توانند به آنها دسترسی داشته باشند. تنها یک کپی از متغیر کلاس وجود دارد و هنگامی که هر یک از اشیا تغییری در متغیر کلاس ایجاد کند، این تغییر توسط تمام نمونه های دیگر نیز قابل مشاهده خواهد شد.
متغیرهای شئ متعلق به هر شئ/نمونه منفرد از کلاس هستند. در این حالت، هر شئ کپی مخصوص به خود را از فیلد دارد؛ یعنی این متغیرها با یکدیگر به اشتراک گذاشته نمی شوند و به هیچ شکلی با فیلدی همنام در یک نمونۀ دیگر مرتبط نیستند. مثال زیر در درک بهتر این موضوع به شما کمک می کند (آن را با نام oop_objvar.py ذخیره کنید):
class Robot:
"""Represents a robot, with a name."""
# A class variable, counting the number of robots
population = 0
def __init__(self, name):
"""Initializes the data."""
self.name = name
print("(Initializing {})".format(self.name))
# When this person is created, the robot
# adds to the population
Robot.population += 1
def die(self):
"""I am dying."""
print("{} is being destroyed!".format(self.name))
Robot.population -= 1
if Robot.population == 0:
print("{} was the last one.".format(self.name))
else:
print("There are still {:d} robots working.".format(
Robot.population))
def say_hi(self):
"""Greeting by the robot.
Yeah, they can do that."""
print("Greetings, my masters call me {}.".format(self.name))
@classmethod
def how_many(cls):
"""Prints the current population."""
print("We have {:d} robots.".format(cls.population))
droid1 = Robot("R2-D2")
droid1.say_hi()
Robot.how_many()
droid2 = Robot("C-3PO")
droid2.say_hi()
Robot.how_many()
print("\nRobots can do some work here.\n")
print("Robots have finished their work. So let's destroy them.")
droid1.die()
droid2.die()
Robot.how_many()
خروجی:
$ python oop_objvar.py
(Initializing R2-D2)
Greetings, my masters call me R2-D2.
We have 1 robots.
(Initializing C-3PO)
Greetings, my masters call me C-3PO.
We have 2 robots.
Robots can do some work here.
Robots have finished their work. So let's destroy them.
R2-D2 is being destroyed!
There are still 1 robots working.
C-3PO is being destroyed!
C-3PO was the last one.
We have 0 robots.
نحوۀ عملکرد
این مثال نسبتاً طولانی است، اما به خوبی ماهیت متغیرهای کلاس و شئ را نمایش می دهد. در اینجا population به کلاس Robot تعلق دارد و بنابراین یک متغیر کلاس است. متغیر name به شئ تعلق دارد (با استفاده از self به آن اختصاص داده شده است) و بنابراین یک متغیر شئ است.
بنابراین، به متغیر کلاس population به صورت Robot.population ارجاع می دهیم، نه self.population. در روش های آن شئ به متغیر شئ name با استفاده از نشانه گذاری self.name ارجاع می دهیم. این تفاوت ساده میان متغیرهای کلاس و شئ را به خاطر داشته باشید. همچنین توجه کنید که اگر یک متغیر شئ با متغیر کلاس همنام باشد، متغیر شئ، متغیر کلاس را پنهان خواهد کرد!
به جای Robot.population می توانستیم از self.__class__.population نیز استفاده کنیم؛ زیرا هر شئ از طریق ویژگی self.__class__ به کلاس خود ارجاع داده می شود.
عبارت hoe_many در واقع روشی است که به کلاس تعلق دارد نه به شئ. بنابراین بسته به اینکه لازم باشد بدانیم عضو کدام کلاس هستیم، می توانیم آن را به صورت یک classmethod یا staticmethod تعریف کنیم. از آنجا که به یک متغیر کلاس ارجاع می دهیم، بیایید از classmethod استفاده کنیم.
ما روش how_many را با استفاده از یک decorator به عنوان روش کلاس علامت گذاری کرده ایم.
می توانیم دکوراتورها را به عنوان یک میان بر برای فراخوانی یک تابع پوشاننده در نظر گرفت؛ یعنی تابعی که تابع دیگر را «می پوشاند» تا بتواند پیش یا پس از تابع داخلی، عملی انجام دهد. بنابراین اعمال دکوراتور classmethod@ معادل فراخوانی عبارت زیر است:
how_many = classmethod(how_many)
مشاهده می کنید که روش __init__ برای مقداردهی اولیۀ نمونۀ Robot با یک نام استفاده می شود. در این روش، مقدار population را 1 واحد افزایش می دهیم، زیرا یک ربات دیگر به مجوعه اضافه شده است. همچنین توجه کنید که مقادیر self.name برای هر شئ اختصاصی هستند که این موضوع ماهیت متغیرهای شئ را نمایش می دهد.
به خاطر داشته باشید که باید تنها با استفاده از self به متغیرها و روش های همان شئ ارجاع دهید. این کار ارجاع به ویژگی نامیده می شود.
در این برنامه، استفاده از docstrings را نیز برای کلاس ها و روش ها مشاهده می کنیم. می توانیم در زمان اجرا به docstring کلاس با استفاده از __Robot.__doc و به docstring روش با استفاده از __Robot.say_hi.__doc دسترسی پیدا کنیم.
در روش die، مقدار شمارندۀ Robot.population را به سادگی یک واحد کاهش می دهیم.
تمام اعضای کلاس عمومی هستند. یک استثنا وجود دارد: اگر از اعضای داده ای با نام هایی استفاده کنید که با دو زیرخط در ابتدای نام آغاز می شوند، مانند privatevar__، پایتون از ساز وکاری به نام تغییر نام (name-mangling) استفاده می کند تا این متغیر عملاً به یک متغیر خصوصی تبدیل شود.
بنابراین، قراردادی که در پایتون دنبال می شود این است که هر متغیری که قرار است تنها در داخل کلاس یا شئ مورد استفاده قرار گیرد، باید با یک زیرخط شروع شود و تمام نام های دیگر عمومی هستند و می توانند توسط کلاس ها/اشیای دیگر مورد استفاده قرار گیرند. به خاطر داشته باشید که این تنها یک قرارداد است و پایتون آن را اجبار نمی کند (به جز مورد پیشوند دو زیرخط).
یک نکته برای برنامه نویسان ++C#/Java/C
تمام اعضای کلاس (از جمله اعضای داده ای) عمومی هستند و تمام روش ها در پایتون مجازی هستند.
وراثت (Inheritance)
یکی از مزایای اصلی برنامه نویسی شئ گرا، استفاده مجدد از کد است و یکی از روش های دستیابی به این هدف، ساز و کار وراثت است. وراثت را می توان به بهترین شکل به صورت ایجاد رابطۀ نوع و زیرنوع میان کلاس ها تصور کرد.
فرض کنید می خواهید برنامه ای بنویسید که باید معلمان و دانش آموزان یک کالج را پیگیری کند. آن ها برخی ویژگی های مشترک مانند نام، سن و آدرس دارند. در عین حال ویژگی های خاصی مانند حقوق، دوره ها و مرخصی ها برای معلمان و نمرات و شهریه برای دانش آموزان نیز دارند.
می توانید برای هر نوع، دو کلاس مستقل ایجاد کرده و آن ها را پردازش کنید؛ اما اضافه کردن یک ویژگی مشترک جدید به این معنا خواهد بود که باید آن ویژگی را به هر دو کلاس مستقل اضافه کنید. این کار خیلی زود به ساختاری دست و پاگیر تبدیل می شود.
راهکار بهتر این است که یک کلاس به نام SchoolMember ایجاد کنیم و سپس کلاس های معلمان و دانش آموز را از این کلاس به ارث ببریم؛ یعنی آن ها را به زیرنوع های این نوع (کلاس) تبدیل کنیم و سپس می توانیم ویژگی های خاصی را به این زیرنوع ها اضافه کنیم.
این رویکرد مزایای زیادی دارد. اگر هر قابلیت را در SchoolMember اضافه یا تغییر دهیم، این تغییر به صورت خودکار در زیرنوع ها نیز منعکس می شود. برای مثال، می توانیم یک فیلد جدید برای کارت شناسایی معلمان و دانش آموزان اضافه کنیم، برای این کار لازم است آن را به کلاس SchoolMember بیفزاییم. با این حال، تغییرات در زیرنوع ها بر سایر زیرنوع ها تاثیری نمی گذارند. مزیت دیگر این است که می توانید به یک شئ معلم یا دانش آموز به عنوان یک شئ SchoolMember ارجاع دهید؛ این قابلیت در برخی موقعیت ها، مانند شمارش تعداد اعضای مدرسه، می تواند مفید واقع شود. این مفهوم چندریختی (polymorphism) نامیده می شود؛ یعنی یک زیرنوع می تواند در هر موقعیتی که انتظار یک نوع والد وجود دارد، جایگزین آن شود. به عبارت دیگر، می توان با شئ به عنوان نمونه ای از کلاس والد رفتار کرد.
همچنین توجه کنید که در اینجا کد کلاس والد را مجدداً استفاده می کنیم و نیازی به به تکرار آن در کلاس های مختلف نداریم؛ کاری که در صورت استفاده از کلاس های مستقل مجبور به انجام آن بودیم.
کلاس SchoolMembeer در این موقعیت کلاس پایه یا فراکلاس نامیده می شود. کلاس های Teacher و Student نیز کلاس های مشتق شده یا زیرکلاس ها نامیده می شوند.
اکنون این مثال را به صورت یک برنامه مشاهده می کنیم (آن را با نام oop_subclass.py ذخیره کنید):
class SchoolMember:
'''Represents any school member.'''
def __init__(self, name, age):
self.name = name
self.age = age
print('(Initialized SchoolMember: {})'.format(self.name))
def tell(self):
'''Tell my details.'''
print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=" ")
class Teacher(SchoolMember):
'''Represents a teacher.'''
def __init__(self, name, age, salary):
SchoolMember.__init__(self, name, age)
self.salary = salary
print('(Initialized Teacher: {})'.format(self.name))
def tell(self):
SchoolMember.tell(self)
print('Salary: "{:d}"'.format(self.salary))
class Student(SchoolMember):
'''Represents a student.'''
def __init__(self, name, age, marks):
SchoolMember.__init__(self, name, age)
self.marks = marks
print('(Initialized Student: {})'.format(self.name))
def tell(self):
SchoolMember.tell(self)
print('Marks: "{:d}"'.format(self.marks))
t = Teacher('Mrs. Shrividya', 40, 30000)
s = Student('Swaroop', 25, 75)
# prints a blank line
print()
members = [t, s]
for member in members:
# Works for both Teachers and Students
member.tell()
خروجی:
$ python oop_subclass.py
(Initialized SchoolMember: Mrs. Shrividya)
(Initialized Teacher: Mrs. Shrividya)
(Initialized SchoolMember: Swaroop)
(Initialized Student: Swaroop)
Name:"Mrs. Shrividya" Age:"40" Salary: "30000"
Name:"Swaroop" Age:"25" Marks: "75"
نحوۀ عملکرد
برای استفاده از وراثت، نام کلاس پایه را در یک تاپل قرار می دهیم و آن را پس از نام کلاس در تعریف کلاس می نویسیم؛ برای مثال، class Teacher(SchoolMember). سپس مشاهده می کنیم که روش __init__ از کلاس پایه به طور صریح و با استفاده از متغیر self فراخوانی می شود تا بتوانیم بخش مربوط به کلاس پایه را در یک نمونه از زیرکلاس مقداردهی اولیه کنیم. به خاطر سپردن این نکته بسیار مهم است - از آنجایی که ما یک روش __init__ در زیرکلاس های Teacher و Student تعریف می کنیم، پایتون سازندۀ کلاس پایه SchoolMember را به صورت خودکار فراخوانی نمی کند، باید خودتان آن را به صورت صریح فراخوانی کنید.
در مقابل، اگر در یک زیرکلاس روش __init__ را تعریف نکرده باشیم، پایتون سازندۀ کلاس پایه را به طور خودکار فراخوانی خواهد کرد.
اگرچه می توانستیم با نمونه های Teacher یا Student همانند یک نمونه از SchoolMember رفتار کنیم و با نوشتن Teacher.tell یا Student.tell به روش tell از SchoolMember دسترسی پیدا کنیم، در عوض در هر زیرکلاس یک روش tell دیگر تعریف می کنیم (که بخشی از آن از روش tell کلاس SchoolMember استفاده می کند) تا این روش را متناسب با همان زیرکلاس تنظیم کنیم. از آنجا که این کار را انجام داده ایم، هنگامی که Teacher.tell را می نویسیم، پایتون از روش tell برای آن زیرکلاس استفاده می کند در مقابل فراکلاس. با این حال، اگر در زیر کلاس روش tell وجود نداشته باشد، پایتون از روش tell در فراکلاس استفاده می کند. پایتون همیشه جست وجوی روش ها را ابتدا از نوع واقعی زیر کلاس آغاز می کند و اگر چیزی پیدا نکند، روش ها را در کلاس پایۀ زیرکلاس، یکی پس از دیگری، بررسی می کند؛ ترتیب بررسی همان ترتیبی است که کلاس های پایه در تاپل تعریف کلاس مشخص شده اند (در اینجا تنها یک کلاس پایه داریم، اما می توانید چندین کلاس پایه داشته باشید).
نکته ای دربارۀ اصطلاحات: اگر بیش از یک کلاس در تاپل وراثت لیست شده باشد، به آن وراثت چندگانه گفته می شود.
پارامتر end در تابع print موجود در روش ()tell فراکلاس استفاده شده است تا یک خط پرینت شود و به print بعدی اجازه دهد تا چاپ خود را در همان خط ادامه دهد. این ترفندی است برای جلوگیری از اینکه print در انتهای خروجی خود نماد n\ (کاراکتر خط جدید) را پرینت کند.
جمع بندی
اکنون جنبه های مختلفی از کلاس ها و اشیا و همچنین اصطلاحات گوناگون مرتبط با آن ها را بررسی کردیم. همچنین با مزایا و کاستی های برنامه نویسی شئ گرا آشنا شدیم. پایتون زبانی به شدت شئ گرا است و درک دقیق این مفاهیم در بلند مدت کمک شایانی به شما خواهد کرد.
در ادامه، خواهیم آموخت که چگونه با ورودی/خروجی کار کنیم و چگونه به فایل ها در پایتون دسترسی داشته باشیم.