Python حرفه‌ای: برنامه‌نویسی تابع‌گرا

Professional Python: Functional programming

18 اسفند 1399
درسنامه درس 15 از سری پایتون حرفه‌ای
Python حرفه ای: برنامه نویسی تابع گرا (قسمت 15)

برنامه نویسی تابع گرا چیست؟

ما در ابتدای این دوره یاد گرفتیم که چطور کد رویه ای (procedural) بنویسیم اما متوجه شدیم که این پارادایم کدنویسی اصلا جالب نیست و مشکلات خودش را دارد. حتما یادتان است که پارادایم به معنی روشی برای نظم دهی به کد ها است. در مرحله بعدی به برنامه نویسی شی گرا (object-oriented programming) رسیدیم که بسیار بهتر از برنامه نویسی رویه ای و مشهور تر از آن است. مسئله اینجاست که این دو تنها پارادایم های کدنویسی موجود نیستند بلکه یک پارادایم بسیار مشهور دیگر به نام برنامه نویسی تابع گرا یا functional programming نیز وجود دارد.

هر دو پارادایم برنامه نویسی شی گرا و تابع گرا مزیت ها و معایب خودشان را دارند و نمی توان گفت که یکی از آن ها بهتر از دیگری است. همچنین می توانیم از هر دو آن ها به طور همزمان استفاده کنیم اما برای درک این مسائل ابتدا باید با مفهوم اصلی برنامه نویسی تابع گرا آشنا بشویم. ما در برنامه نویسی شی گرا کلاس های مختلفی را داشتیم تا قسمت های منطقی کد را از هم جدا کنیم و برنامه نویسی تابع گرا نیز همین روش را دارد اما به جای کلاس ها از توابع استفاده می کند. در برنامه نویسی شی گرا داده ها (attribute های کلاس) و متدها را با هم و در یک کلاس قرار می دادیم اما در برنامه نویسی تابع گرا داده ها و توابع یا متدها از هم جدا می شوند.

اگر یادتان باشد ما در برنامه نویسی شی گرا مبانی چهارگانه خاص خودمان را داشتیم: انتزاع، وراثت، چندریختگی و کپسوله سازی. این در حالی است که برنامه نویسی تابع گرا یک مفهوم اصلی داریم: Pure Function یا «توابع خالص»!

Pure Function یا تابع خالص چیست؟

تابع خالص یعنی تابعی که مانند متدها دارای مقادیر و قابلیت های مختلف و مخفی نباشد. در واقع برای خالص محسوب شدن یک تابع دو شرط وجود دارد:

  • شرط اول توابع خالص این است که اگر یک میلیون بار مقدار خاصی را به آن ها پاس بدهیم، باید در هر دفعه عینا یک خروجی را دریافت کنیم. به عبارت دیگر ورودی یکسان همیشه باعث خروجی یکسان شود.
  • شرط دوم توابع خالص این است که نباید side effect یا «عوارض» تولید کند.

شاید بپرسید عارضه در توابع چیست؟ side effect یا عارضه در توابع کاری است که یک تابع انجام می دهد و چیزی را در خارج از دامنه خودش تغییر می دهد. به طور مثال به تابع زیر توجه کنید:

def multiply(some_list):

    new_list = []

    for item in some_list:

        new_list.append(item * 2)

    return new_list





print(multiply([1, 2, 3]))

این تابع یک لیست را دریافت کرده و سپس اعضای آن را دو برابر می کند. با اجرای کد بالا نتیجه زیر را می گیریم:

[2, 4, 6]

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

def multiply(some_list):

    new_list = []

    for item in some_list:

        new_list.append(item * 2)

    print(new_list)




multiply([1, 2, 3])

ما با اجرای این تابع عینا همان نتیجه قبلی را می گیریم اما print کردن یک عارضه محسوب می شود چرا که چیزی را خارج از تابع تغییر می دهد (یک مقدار را پرینت می کند). مثال دیگری از عارضه این است که درون تابع به متغیری دسترسی پیدا کنیم که خارج از تابع تعریف شده باشد.

تابع ()map

یکی از توابعی که به ما کمک می کند تا به صورت تابع محور فکر کنیم، تابع map است. این تابع دو آرگومان می گیرد: یک تابع و یک iterable (گردش پذیر). زمانی که این تابع را اجرا می کنید، تک تک اعضای گردش پذیر وارد تابع (آرگومان اول) می شوند و می توانیم هر کاری خواستیم را با آن ها انجام بدهیم. در واقع اگر بخواهیم همان مثال قبلی (ضرب اعضای لیست در ۲) را با map انجام بدهیم، کارمان آسان تر می شود:

def multiply_by2(list_items):

    return list_items * 2







new_list = map(multiply_by2, [1, 2, 3])




print(new_list)

من تابع جدیدی به نام multiply_by2 نوشته ام که مقداری را می گیرد و پس از ضرب آن در ۲، مقدار را برمی گرداند. در مرحله بعدی تعریف این تابع را به عنوان آرگومان اول به map داده ایم. توجه کنید که فقط تعریف تابع را به map داده ایم، یعنی نام تابع را بدون پرانتز ذکر کرده ایم. اضافه کردن پرانتز باعث اجرا شدن متد multiply_by2 می شود که هدف ما نیست. ما فقط تعریف این تابع را به map می دهیم و map خودش این تابع را اجرا می کند و تک تک اعضای لیست پاس داده شده (آرگومان دوم) را به آن می دهد. در نهایت مقدار جدید return می شود بنابراین می توانیم آن را درون متغیری به نام new_list قرار بدهیم. با اجرای کد بالا نتیجه زیر را دریافت می کنیم:

<map object at 0x7fc9e7d3f520>

map به صورت خودکار یک شیء را برایمان برمی گرداند بنابراین باید از متدی مثل list استفاده کنیم تا آن را به صورت لیست دریافت کنیم:

def multiply_by2(list_items):

    return list_items * 2



new_list = map(multiply_by2, [1, 2, 3])




print(list(new_list))

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

[2, 4, 6]

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

my_list = [1, 2, 3]







def multiply_by2(list_items):

    return list_items * 2







new_list = map(multiply_by2, my_list)




print(list(new_list))

print(my_list)

به نظر شما با اجرای کد بالا چه نتیجه ای می گیریم؟

[2, 4, 6]

[1, 2, 3]

آیا می دانید چرا؟ به دلیل اینکه تابع map هیچ وقت به داده اصلی (my_list) دست نمی زند بلکه همیشه یک کپی از آن را برمی گرداند. من در کد بالا هم my_list و هم new_list را چاپ کرده ام و شما می بینید که my_list اصلا ویرایش نشده است. این مسئله ثابت می کند که map یک داده خالص است.

تابع ()filter

تابع filter یکی دیگر از توابعی است که به ما کمک می کند برنامه نویسی تابع گرا را بهتر درک کنیم. از نام تابع filter (فیلتر کردن یا جداسازی) می توان حدس زد که کارش چیست. از نظر کارکرد filter شباهت بسیار زیادی به map دارد چرا که یک تابع و یک iterable (گردش پذیر) را می گیرد و آن تابع را روی تک تک اعضای گردش پذیر اجرا می کند اما این بار نمی توانیم هر کاری که خواستیم انجام بدهیم بلکه در تابع پاس داده شده باید True یا False را برگردانیم. اگر برای یکی از اعضای iterable مقدار true را برگردانیم، آن عضو در گردش پذیر باقی می ماند اما اگر false را برگردانیم آن عضو از گردش پذیر حذف شده یا به زبانی دیگر فیلتر می شود.

به طور مثال اگر لیستی از اعداد داشته باشیم و بخواهیم اعداد فرد را از بین آن ها استخراج کنیم، کد زیر را می نویسیم:

my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]







def check_odd(item):

    return item % 2 != 0




new_list = filter(check_odd, my_list)




print(list(new_list))

print(my_list)

ما برای تابع check_odd فقط یک شرط ساده را نوشته ایم. اگر باقی مانده تقسیم item بر عدد ۲ برابر با صفر نباشد (به علامت ! در =! توجه کنید)، یعنی عدد فرد است بنابراین کل این expression به True تبدیل شده و true را return کرده ایم، در غیر این صورت false را return می کنیم. در واقع کل قسمت (item % 2 != 0) یک شرط است. چه شرطی؟ باقی مانده تقسیم item بر ۲ برابر با صفر نباشد. اگر چنین شرطی برقرار باشد، کل قسمت (item % 2 != 0) تبدیل به True شده و در غیر این صورت به false تبدیل می شود. شما می توانستید به جای این روش خلاصه از if و else استفاده کنید اما این روش تمیز تر و خواناتر است. در نهایت این تابع را به filter پاس داده ایم تا اعداد فرد را پیدا کنیم. نتیجه اجرای این کد به صورت زیر خواهد بود:

[1, 3, 5, 7, 9]

[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

با هم می بینیم که filter به لیست اصلی ما (my_list) دست نمی زند بلکه یک مقدار جدید را برمی گرداند. به همین سادگی اعداد ۱ و ۳ و ۵ و ۷ و ۹ را از لیست خود جدا کرده ایم.

تابع ()Zip

تابع سوم این جلسه تابع zip است که دقیقا مانند یک زیپ عمل می کند! یعنی چه؟ یعنی دو یا چند iterable را گرفته و آن ها را دو به دو به هم می چسباند. به مثال زیر توجه کنید:

my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

your_list = [11, 12, 13, 14, 15]




new_list = zip(my_list, your_list)




print(list(new_list))

print(my_list)

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

[(1, 11), (2, 12), (3, 13), (4, 14), (5, 15)]

[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

آیا متوجه کار zip شدید؟ تابع zip در کد بالا دو لیست را گرفته است: my_list و your_list، سپس عضو اول از هر کدام را گرفته و درون یک tuple برمی گرداند، سپس عضو دوم و عضو سوم و الی آخر. شما می توانید به جای دو گردش پذیر از هر تعداد دیگری نیز استفاده کنید. مثال:

my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

your_list = [11, 12, 13, 14, 15]

some_list = [21, 22, 23, 24]




new_list = zip(my_list, your_list, some_list)




print(list(new_list))

print(my_list)

نتیجه:

[(1, 11, 21), (2, 12, 22), (3, 13, 23), (4, 14, 24)]

[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

در ضمن در تمامی این مثال ها باید توجه داشته باشید که داده اصلی ما تغییری نمی کند.

تابع ()reduce

آخرین تابع مورد نظر ما reduce است و به صورت خودکار هسته اصلی پایتون وجود ندارد بلکه درون یکی از ماژول های پایتون است بنابراین برای استفاده از آن باید دستور زیر را اجرا کنیم:

from functools import reduce

ما هنوز به این دستور نرسیده ایم اما فعلا بدانید که این دستور فقط تابعی به نام reduce را از ماژول functools وارد کد های ما می کند. بعدا مفصلا در مورد ماژول ها صحبت می کنیم. حالا به کد زیر توجه کنید:

from functools import reduce




my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]







def accumulator(acc, item):

    print(acc, item)

    return acc + item







new_list = reduce(accumulator, my_list, 0)




print(new_list)

print(my_list)

من در اینجا تابعی به نام accumulator ساخته ام که دو پارامتر را دریافت می کند. پارامتر دوم که همان item است و در توابع قبلی نیز با آن آشنا بودیم اما acc چیست؟ acc یک مقدار خاص است که توسط تابع reduce به این تابع پاس داده می شود. من درون تابع accumulator هر دو مقدار acc و item را چاپ کرده ام تا ببینیم داخل آن ها چیست و سپس آن ها را جمع زده و برگردانده ام. حالا برای صدا زدن reduce به سه آرگومان نیاز داریم: نام تابع، گردش پذیر و مقدار اولیه acc که من صفر را انتخاب کرده ام. به عبارتی acc در هر گردش به اندازه item اضافه خواهد شد. اگر کد بالا را اجرا کنید نتیجه زیر را می گیرید:

0 1

1 2

3 3

6 4

10 5

15 6

21 7

28 8

36 9

45 0

45

[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

در گردش اول acc صفر و item یک است. چرا یک؟ item همان اعضای درون لیست ما است و اولین آیتم درون لیست عدد ۱ بوده است. حالا acc و item جمع می شوند و به گردش بعدی می رویم. در گردش دوم acc برابر یک (عدد acc + item در گردش قبل)  و item (عضو بعدی لیست) برابر ۲ است. در گردش بعدی acc برابر ۳ (جمع ۲ و ۱ در گردش قبل) و item نیز ۳ است و الی آخر. این مسئله تا آنجا ادامه پیدا می کند که دیگر عضوی در لیست نداشته باشیم و عدد ۴۵ برایمان برگردانده می شود. my_list هم مثل همیشه دست نخورده است بلکه یک لیست جدید برایمان برگردانده شده است.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری پایتون حرفه‌ای توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.