6. برنامهنویسی شیگرا قسمت اول : اشیاء و متدها#
6.1. مقدمه#
در گذشته، بیشتر زبان های برنامهنویسی مانند (Fortran، C، MATLAB و غیره) بر اساس پارادایمی به نام «برنامهنویسی رویهای» طراحی شده بودند. در این پارادایم، برنامه به صورت مجموعهای از دستورالعمل ها و توابع نوشته میشود که به ترتیب اجرا شده و دادهها را پردازش میکند.
این پارادایم به شرح زیر عمل میکند:
برنامه دارای یک وضعیت است که متناظر با مقادیر متغیرهای خود است.
توابع فراخوانی میشوند تا بر روی این وضعیت عمل کرده و آن را تغییر دهند.
خروجیهای نهایی از طریق دنبالهای از فراخوانیهای توابع تولید میشوند.
دو پارادایم مهم دیگر، برنامهنویسی شیگرا (OOP) و برنامهنویسی تابعی هستند.
در پارادایم OOP، دادهها و توابع در قالب “اشیاء” با یکدیگر ترکیب میشوند، و توابع در این زمینه به عنوان متدها شناخته میشوند.
متدها برای تغییر دادههای موجود در شیء فراخوانی میشوند.
به یک لیست پایتون فکر کنید که حاوی داده است و متدهایی مانند
()appendو()popدارد که داده را تغییر میدهند.
زبانهای برنامهنویسی تابعی، بر اساس ایده ترکیب توابع ساخته شدهاند.
پس پایتون در کدام یک از این دستهها قرار میگیرد؟
در واقع پایتون یک زبان عملگرا است که به جای پیروی سخت گیرانه از یک رویکرد خالص، سبکهای شیگرا، تابعی و رویهای را با یکدیگر ترکیب میکند.
از یک طرف، این ویژگی به پایتون و کاربرانش اجازه میدهد تا جنبههای خوب پارادایمهای مختلف را انتخاب کنند.
و از طرف دیگر، همین نبود خلوص پارادایمی ممکن است گاهی اوقات منجر به برخی سردرگمیها شود.
خوشبختانه اگر بدانیم که در سطح بنیادی، پایتون یک زبان شیگرا است، این سردرگمی تا حد زیادی به حداقل میرسد.
منظور ما این است که، در پایتون، همه چیز یک شیء است.
در این درس، توضیح میدهیم که این گزاره به چه معناست و چرا اهمیت دارد.
ما از کتابخانه خارجی زیر استفاده خواهیم کرد:
!pip install rich
6.2. اشیاء#
در پایتون، یک شیء مجموعهای از دادهها و دستورالعملهای نگهداریشده در حافظه کامپیوتر است که شامل موارد زیر میشود:
یک نوع(Type)
یک شناسه منحصر به فرد(Unique Identity)
داده (یعنی محتوا)
متدها(Methods)
این مفاهیم به ترتیب تعریف و بررسی میشوند.
6.2.1. نوع (Type)#
پایتون انواع مختلفی از اشیاء را فراهم میکند تا بتواند دستههای مختلفی از داده ها را پشتیبانی کند.
به عنوان مثال:
s = 'This is a string'
type(s)
str
x = 42 # حالا بیایید یک عدد صحیح ایجاد کنیم
type(x)
int
نوع یک شیء برای بسیاری از عبارات مهم است.
به عنوان مثال، عملگر جمع بین دو رشته متنی به معنای به هم پیوند دادن آنها است:
'300' + 'cc'
'300cc'
از طرف دیگر، بین دو عدد به معنای جمع معمولی است:
300 + 400
700
عبارت زیر را در نظر بگیرید:
'300' + 400
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 1
----> 1 '300' + 400
TypeError: can only concatenate str (not "int") to str
در اینجا ما در حال ترکیب انواع متفاوتی از مقادیر هستیم، و برای پایتون مشخص نیست که آیا کاربر میخواهد:
'300'را به عدد صحیح تبدیل کند و سپس آن را به400اضافه کند، یا400را به رشته تبدیل کند و سپس آن را به'300'متصل کند.
برخی زبانها ممکن است سعی کنند حدس بزنند، اما پایتون یک زبان تایپ قوی (strongly typed) است.
نوع داده اهمیت زیادی دارد و تبدیل ضمنی نوع در پایتون به ندرت انجام میشود.
در نتیجه پایتون به جای حدس زدن، با نشان دادن یک خطای
TypeErrorپاسخ میدهد.
برای جلوگیری از این خطا، باید با تغییر نوع داده مربوطه، منظور خود را به صورت صریح وشفاف مشخص کنید.
به عنوان مثال:
int('300') + 400 # برای جمع به عنوان اعداد، رشته را به عدد صحیح تبدیل کنید
700
6.2.2. شناسه (Unique Identity)#
در پایتون، هر شیء یک شناسه منحصر به فرد دارد که به پایتون (و ما) کمک میکند تا شیء را ردیابی و تشخیص دهیم.
شناسه یک شیء را میتوان از طریق تابع ()id به دست آورد:
y = 2.5
z = 2.5
id(y)
140285257082704
id(z)
140285679755664
در این مثال، y و z با اینکه مقدار یکسانی (یعنی 2.5) دارند، اما آنها یک شیء نیستند.
شناسه یک شیء در واقع فقط آدرس شیء در حافظه است.
6.2.3. محتوای شیء: دادهها و ویژگیها#
اگر x = 42 را قرار دهیم، یک شیء از نوع int ایجاد میکنیم که حاوی داده 42 است.
در واقع، همانطور که مثال زیر نشان میدهد، حاوی اطلاعات بیشتری نیز هست:
x = 42
x
42
x.imag
0
x.__class__
int
وقتی پایتون این شیء که از نوع عدد صحیح است را ایجاد میکند، اطلاعات کمکی مختلفی مانند قسمت موهومی(imaginary) و نوع(type) شیء را با آن ذخیره میکند.
هر نامی که بعد از نقطه بیاید ویژگی(attribute) شیء سمت چپ نقطه نامیده میشود.
به عنوان مثال،
imagو__class__ویژگیهایxهستند.
از این مثال میبینیم که اشیاء دارای ویژگیهایی هستند که حاوی اطلاعات کمکی هستند.
همچنین، ویژگیهایی وجود دارند که مانند توابع عمل میکنند و متد(methods) نامیده میشوند
این ویژگیها مهم هستند، بنابراین بیایید آنها را به طور عمیق بررسی کنیم.
6.2.4. متدها#
متدها توابعی هستند که به طور مستقیم با یک شیء مرتبط شده و جزئی از آن شیء محسوب میشوند.
به طور رسمی، متدها ویژگیهای اشیاء هستند که قابل فراخوانی هستند، یعنی ویژگیهایی که میتوان آن ها را مانند توابع فراخوانی کرد.
x = ['foo', 'bar']
callable(x.append)
True
callable(x.__doc__)
False
متدها معمولاً بر روی دادههای موجود در شیئی که به آن تعلق دارند عمل میکنند، یا آن داده را با دادههای دیگر ترکیب میکنند:
x = ['a', 'b']
x.append('c')
s = 'This is a string'
s.upper()
'THIS IS A STRING'
s.lower()
'this is a string'
s.replace('This', 'That')
'That is a string'
بخش بزرگی از قابلیتهای پایتون حول فراخوانیهای متدها سازماندهی شده است.
به عنوان مثال، کد زیر را در نظر بگیرید:
x = ['a', 'b']
x[0] = 'aa' # تغییر آیتم با استفاده از براکت
x
['aa', 'b']
با اینکه به نظر میرسد که در اینجا از هیچ متدی استفاده نشده باشد، اما در واقع استفاده از براکت ها برای تغییر دادن مقادیر فقط یک رابط ساده و راحت برای فراخوانی یک متد است.
آنچه در واقع اتفاق میافتد این است که پایتون متد __setitem__ را به شرح زیر فراخوانی میکند:
x = ['a', 'b']
x.__setitem__(0, 'aa') # معادل x[0] = 'aa'
x
['aa', 'b']
(اگر بخواهید میتوانید متد __setitem__ را تغییر دهید، به طوری که استفاده از براکت ها برای مقادیر کار کاملاً متفاوتی انجام دهد)
6.3. بازرسی با استفاده از Rich#
یک بسته بسیار کاربردی به نام rich وجود دارد که به ما کمک میکند محتویات یک شیء را به صورت خوانا مشاهده کنیم.
به عنوان مثال:
from rich import inspect
x = 10
inspect(10)
╭────── <class 'int'> ───────╮ │ int([x]) -> integer │ │ int(x, base=10) -> integer │ │ │ │ ╭────────────────────────╮ │ │ │ 10 │ │ │ ╰────────────────────────╯ │ │ │ │ denominator = 1 │ │ imag = 0 │ │ numerator = 10 │ │ real = 10 │ ╰────────────────────────────╯
برای دیدن متدها هم میتوانیم از این روش استفاده کنیم:
inspect(10, methods=True)
╭───────────────────────────────────────────────── <class 'int'> ─────────────────────────────────────────────────╮ │ int([x]) -> integer │ │ int(x, base=10) -> integer │ │ │ │ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ │ 10 │ │ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │ │ │ │ denominator = 1 │ │ imag = 0 │ │ numerator = 10 │ │ real = 10 │ │ as_integer_ratio = def as_integer_ratio(): Return a pair of integers, whose ratio is equal to the original int. │ │ bit_count = def bit_count(): Number of ones in the binary representation of the absolute value of self. │ │ bit_length = def bit_length(): Number of bits necessary to represent self in binary. │ │ conjugate = def conjugate(): Returns self, the complex conjugate of any int. │ │ from_bytes = def from_bytes(bytes, byteorder='big', *, signed=False): Return the integer represented by │ │ the given array of bytes. │ │ is_integer = def is_integer(): Returns True. Exists for duck type compatibility with float.is_integer. │ │ to_bytes = def to_bytes(length=1, byteorder='big', *, signed=False): Return an array of bytes │ │ representing an integer. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
در واقع متدهای بیشتری هم وجود دارند، و شما میتوانید با اجرای inspect(10, all=True) آنها را مشاهده کنید.
6.4. یک معمای کوچک#
در این سخنرانی ادعا کردیم که پایتون، در ذات خود یک زبان شیگرا است.
اما در اینجا مثالی وجود دارد که بیشتر رویهای به نظر میرسد:
x = ['a', 'b']
m = len(x)
m
2
اگر پایتون شیگرا است، چرا از ()x.len استفاده نمیکنیم؟
پاسخ به این نکته مربوط میشود به اینکه پایتون به قابلیت خوانایی و سبک ثابت و یکسان اهمیت میدهد.
در پایتون، معمولا کاربران اشیاء سفارشی میسازند، ما نیز نحوه انجام این کار را بعداً بررسی خواهیم.
معمولا کاربران متدهایی به اشیاء خود اضافه میکنند که طول شیء را، به طوریکه برای شیء مناسب باشد، اندازهگیری کنند.
هنگام نامگذاری چنین متدی، انتخابهای طبیعی ()len و ()length هستند.
اگر برخی از کاربران ()len و برخی دیگر ()length را انتخاب کنند، سبک ناسازگار و ناهماهنگ شده و به خاطر سپردن آن سخت تر خواهد بود.
برای جلوگیری از این امر، سازنده پایتون تصمیم گرفت ()len را به عنوان یک تابع داخلی اضافه کند، تا تأکید کند بر اینکه استفاده از ()len قراردادی است.
حالا، با گفتن همه اینها، پایتون همچنان در پشت صحنه شیگرا است.
در واقع، لیست x که پیشتر بررسی شد، دارای متدی به نام ()__len__ است.
و تنها کاری که تابع ()len انجام میدهد فراخوانی این متد است.
به عبارت دیگر، معادل کد زیر است:
x = ['a', 'b']
len(x)
2
و
x = ['a', 'b']
x.__len__()
2
6.5. خلاصه#
نکته اصلی این درس این است:
در پایتون، همه چیز در حافظه به عنوان یک شیء در نظر گرفته میشود.
و این فقط شامل لیستها، رشتهها و غیره نیست، بلکه مواردی که کمتر مورد توجه قرار میگیرد را نیز شامل میشود، مانند:
توابع (پس از بارگذاری در حافظه)
ماژولها
فایلهای باز شده برای خواندن یا نوشتن
اعداد صحیح و غیره
به یاد داشتن اینکه همه چیز یک شیء است به شما کمک میکند تا با برنامههای خود تعامل داشته باشید و کد پایتونی واضحی بنویسید.
6.6. تمرینها#
Exercise 6.1
ما قبلاً با نوع داده بولین آشنا شدیم.
با استفاده از آنچه در این درس آموختیم، لیستی از متدهای شیء بولین True را چاپ کنید.
Hint
میتوانید از callable() برای آزمایش اینکه آیا یک ویژگی از یک شیء میتواند به عنوان تابع فراخوانی شود، استفاده کنید
Solution to Exercise 6.1
ابتدا، باید همه ویژگیهای True را پیدا کنیم، که با کد زیر میتوانید پیدا کنید:
print(sorted(True.__dir__()))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']
یا
print(sorted(dir(True)))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']
از آنجایی که نوع داده بولین یک نوع داده اولیه است، میتوانید آن را در مجموعه پیش فرض توابع و نام ها (built-in) نیز پیدا کنید:
print(dir(__builtins__.bool))
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'is_integer', 'numerator', 'real', 'to_bytes']
در اینجا از یک حلقه for برای فیلتر کردن ویژگیهایی که قابل فراخوانی هستند استفاده میکنیم:
attributes = dir(__builtins__.bool)
callablels = []
for attribute in attributes:
# از eval() برای ارزیابی یک رشته به عنوان یک عبارت استفاده کنید
if callable(eval(f'True.{attribute}')):
callablels.append(attribute)
print(callablels)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_count', 'bit_length', 'conjugate', 'from_bytes', 'is_integer', 'to_bytes']