آموزش برنامه نویسی فانکشنال در پایتون
# چه موقع و چگونه از برنامه نویسی فانکشنال در پایتون استفاده کنیم؟
برنامه نویسی فانکشنال یک الگوی برنامه نویسی است که روش اصلی محاسبه در آن ارزیابی توابع است. در این آموزش، با برنامه نویسی فانکشنال در پایتون آشنا خواهید شد.
برنامه نویسی فانکشنال معمولاً نقش کمرنگی در کد های پایتون دارد اما خوب است که با آن آشنا شویم. حداقل مزیت آن این است که هنگام خواندن کد های دیگران راحت تر آن ها را متوجه خواهید شد. حتی میتوانید در صورت لزوم از قابلیت های برنامه نویسی فانکشنال در کد خود نیز بهره ببرید.
یک تابع خالص تابعی است که مقدار خروجی آن صرفاً از مقادیر ورودی آن پیروی میکند( بدون هیچگونه عوارض جانبی قابل مشاهده). در برنامه نویسی فانکشنال، یک برنامه به طور کامل از ارزیابی توابع خالص تشکیل شده است. محاسبات با فراخوانی تابع های تودرتو یا مرکب، بدون ایجاد تغییر در حالت یا داده های متغیر، اجرا میشوند.
بسیاری از زبان های برنامه نویسی، تا حدی، از برنامه نویسی فانکشنال پشتیبانی میکنند. در بعضی از زبان ها، تقریباً تمامی قسمت های یک کد از الگوی فانکشنال پیروی میکند. هاسکل(Haskell) یکی از این نمونه هاست. در مقابل، پایتون در کنار برنامه نویسی فانکشنال، از سایر مدل های برنامه نویسی نیز پشتیبانی میکند.
درست است که تعریف دقیق برنامه نویسی فانکشنال تا حدودی پیچیده است اما هدف ما در اینجا ارائه تعریف دقیق آن نیست. بلکه در این مقاله، کاربرد برنامه نویسی فانکشنال در پایتون را به شما نشان خواهیم داد.
ویدیو پیشنهادی: ویدیو آشنایی عمیق با function در پایتون
# پایتون تا چه اندازه از برنامه نویسی فانکشنال پشتیبانی میکند؟
برای پشتیبانی از برنامه نویسی فانکشنال، یک تابع باید دارای قابلیت های زیر باشد:
- بتواند تابع دیگری را به عنوان آرگومان خود بپذیرد
- بتواند تابع دیگری را به عنوان خروجی به فراخوان کننده برگرداند
پایتون در دو مورد بالا بسیار خوب عمل میکند. همانطور که قبلاً در این مجموعه آموخته اید، همه چیز در یک برنامه پایتون، یک آبجکت یا یک شئ محسوب میشود. تمامی آبجکت ها در پایتون دارای ساختار تقریبا یکسانی میباشند و توابع نیز استثناء نیستند.
در پایتون، توابع شهروندان درجه یک هستند. این بدان معناست که توابع دارای همان خصوصیاتی هستند که مقادیری مانند رشته ها و اعداد دارند. هر کاری که بتوان با یک رشته یا عدد انجام داد را میتوانید با یک تابع نیز انجام دهید.
به عنوان مثال، میتوانید یک تابع را به یک متغیر اختصاص دهید. سپس میتوانید از آن متغیر به عنوان یک تابع استفاده کنید:
>>> def func():
... print("I am function func()!")
...
>>> func()
I am function func()!
>>> another_name = func
>>> another_name()
I am function func()!
انتساب another_name = func در خط 8، ارجاع جدیدی به تابع func() را ایجاد میکند و اسم آن را another_name میگذارد. پس از این انتساب، همانطور که در خطوط 5 و 9 مشاهده میکنید، میتوانید این تابع را با نام func و یا another_name صدا بزنید.
میتوانید یک فانکشن را با استفاده از دستور print() و به شکل یک آبجکت داده مرکب، مانند لیست، در کنسول نمایش دهید. حتی میتوانید آن را به عنوان کلید یک لیست یا دیکشنری استفاده کنید:
>>> def func():
... print("I am function func()!")
...
>>> print("cat", func, 42)
cat <function func at 0x7f81b4d29bf8> 42
>>> objects = ["cat", func, 42]
>>> objects[1]
<function func at 0x7f81b4d29bf8>
>>> objects[1]()
I am function func()!
>>> d = {"cat": 1, func: 2, 42: 3}
>>> d[func]
2
تابع func() در شرایط بالا همانند مقادیر "cat" و 42 عمل میکند و مفسر پایتون به خوبی آن را ترجمه میکند.
توجه: اینکه با آبجکت ها چه کاری را میتوانید و چه کاری را نمیتوانید انجام دهید، تا حدود زیادی به شرایط برنامه بستگی دارد. به طور مثال برخی عملیات فقط روی آبجکت های خاصی قابل اجرا هستند.
به طور مثال میتوانید دو آبجکت عدد صحیح را با هم جمع کنید یا دو آبجکت رشته ای را با عملگر بعلاوه (+) به هم پیوند دهید. اما عملگر بعلاوه برای آبجکت های از نوع تابع، تعریف نشده است.
برای اهداف فعلی، ذکر همین نکته کافیست که توابع در پایتون دو معیار مفید برای برنامه نویسی فانکشنال، که قبل تر به آن اشاره کردیم، را پشتیبانی میکنند.
میتوانید یک تابع را به عنوان آرگومان به تابع دیگری منتقل کنید:
>>> def inner():
... print("I am function inner()!")
...
>>> def outer(function):
... function()
...
>>> outer(inner)
I am function inner()!
آنچه در مثال فوق اتفاق میافتد، به شرح زیر است:
- دستور موجود در خط 9، تابع inner() را به عنوان آرگومان به تابع outer() میدهد.
- درون تابع outer()، پایتون تابع inner() را به پارامتر تابعی “function” گره میزند.
- بنابراین تابع outer() میتواند مستقیما Inner() را با function فراخوانی کند.
نام این عمل ترکیب توابع است.
یادداشت فنی: پایتون برای تسهیل بسته بندی یک تابع در درون یک تابع دیگر، یک میانبر به نام decorator را ارائه میکند. برای اطلاعات بیشتر، Primer on Python Decorators را مطالعه نمایید.
هنگامیکه یک تابع را به عنوان آرگومان به تابع دیگری منتقل میکنید، گاهی اوقات از تابع منتقل شده به نام callback یا فراخوانی برگشتی نیز یاد میشود زیرا کال بک کردنِ یک تابعِ درونی باعث ایجاد تغییرات در رفتار تابع بیرونی میشود.
یک مثال خوب در این مورد، تابع sorted() پایتون است. به طور معمول، اگر به آن یک لیست از مقادیر رشته ای را بدهید، این تابع آن ها را به ترتیب حروف الفبا مرتب میکند.
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals)
['dog', 'ferret', 'gecko', 'vole']
با این وجود، تابع sorted() یک آرگومان اختیاری به نام key نیز دریافت میکند که تابع کال بکی را که برای مرتب سازی مقادیر استفاده میشود، تعیین میکند. به طور مثال میتوانید رشته ها را بر اساس طول آنها مرتب نمایید.
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=len)
['dog', 'vole', 'gecko', 'ferret']
ویدیو پیشنهادی: ویدیو آموزش متدهای sort و sorted در پایتون
تابع sorted() آرگومان اختیاری دیگری نیز دریافت میکند که ترتیب مرتب سازی را برعکس میکند. اما میتوانید همین کار را با تعریف کردن تابع کال بک دلخواهی انجام دهید که منطق len() را برعکس میکند.
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=len, reverse=True)
['ferret', 'gecko', 'vole', 'dog']
>>> def reverse_len(s):
... return -len(s)
...
>>> sorted(animals, key=reverse_len)
['ferret', 'gecko', 'vole', 'dog']
همانطور که میتوانید یک تابع را به عنوان آرگومان به یک تابع دیگر منتقل کنید، یک تابع خود میتواند تابع دیگری را نیز به عنوان مقدار بازگشتی خود تعیین کند:
>>> def outer():
... def inner():
... print("I am function inner()!")
...
... # Function outer() returns function inner()
... return inner
...
>>> function = outer()
>>> function
<function outer.<locals>.inner at 0x7f18bc85faf0>
>>> function()
I am function inner()!
>>> outer()()
I am function inner()!
آنچه در مثال فوق اتفاق میافتد به شرح زیر است:
- از خط 2 تا 3: تابع outer() یک تابع محلی یا local به نام inner() تعریف میکند.
- خط 6: outer() تابع inner() را به عنوان مقدار خروجی خود برمیگرداند.
- خط9: مقدار خروجی تابع outer() در متغیری به نام function ذخیره میشود.
با توجه به چیزی که گفته شد، میتوانید تابع inner() را به طور غیر مستقیم از طریق متغیر function فراخوانی کنید (در خط 12 نشان داده شده است). همچنین میتوانید این تابع را به طور غیر مستقیم بدون انتساب میانی، از خروجی تابع outer() فراخوانی کنید(در خط 15 نشان داده شده است)
همانطور که میبینید، پایتون تمامیابزار های لازم برای پشتیبانی از برنامه نویسی فانکشنال را به همراه خود دارد. اما قبل از آنکه به برنامه نویسی فانکشنال بپردازیم، یک مفهوم دیگر مانده است که بهتر است با آن آشنا شویم: lamda expression
ویدیو پیشنهادی: ویدیو آموزش لامبدا(lambda) در پایتون
# تعریف توابع ناشناس با استفاده از lambda
به طور کل، برنامه نویسی فانکشنال چیزی جز فراخوانی توابع و انتقال آنها به توابع دیگر نیست. بنابراین طبیعتاً شامل تعریف کردن توابع بسیاری میشود. همانطور که در آموزشهای قبلی این مجموعه مشاهده کردهاید، همیشه میتوانید یک تابع را به روش معمول تعریف کنید.
هرچند، گاهی اوقات راحت تر است که تابعی را بدون اختصاص دادن نامی به آن تعریف کنیم. در پایتون میتوانید این کار را با یک عبارت لامبدا (lambda) انجام دهید.
ساختار عبارت لامبدا به شکل زیر است:
lambda <parameter_list>: <expression>
اجزای عبارات لامبدا به شکل زیر است:
- lambda: کلمه کلیدی که یک عبارت lambda را معرفی میکند.
- <parameter_list>: لیست نامِ پارامترهای ورودی که با ویرگول از هم جدا شده اند.
- دو نقطه: علامتی که <parameter_list> را از <expression>جدا میکند
- <expression>: عبارتی که معمولاً شامل نام های موجود در <parameter_list> است
مقدار عبارت لامبدا دقیقاً همانند تابعی که با کلمه کلیدی def تعریف شده باشد، یک تابع قابل فراخوانی است. عبارت لامبدا آرگومان ها را، همانطور که توسط <parameter_list> مشخص شده است، دریافت میکند و همانطور که با <expression> نشان داده شده است، یک مقدار خروجی را بر میگرداند.
یک مثال کوتاه با هم ببینیم:
>>> lambda s: s[::-1]
<function <lambda> at 0x7fef8b452e18>
>>> callable(lambda s: s[::-1])
True
عبارت خط 1 فقط بیان لامبدا است. در خط 2، پایتون مقدار expression را نشان میدهد، که میتوانید ببینید که یک تابع است
تابع callable()، که یک تابع داخلی پایتون است، هنگامی مقدار True را برمیگرداند که آرگومان ورودی به آن قابل فراخوانی باشد و در غیر اینصورت مقدار False را خروجی میکند. خطوط 4 و 5 نشان میدهد که مقدار برگشتی از عبارت لامبدا یک عبارت قابل فراخوانی است. همانطور که یک تابع باید باشد.
در این مورد، پارامتر لیست از یک پارامتر واحد s تشکیل شده است. عبارت بعدی s[::-1] یک سینتکس جدا کننده است که کارکتر های موجود در s را به ترتیب از آخر به اول برمیگرداند. پس، عبارت لامبدا یک تابع موقت و بی نام تعریف میکند که یک رشته را به عنوان آرگومان میگیرد و معکوس آن رشته را برمیگرداند.
آبجکتی که با استفاده از عبارت لامبدا تعریف شده باشد، همانند یک تابع استاندارد یا هر آبجکت دیگری در پایتون، شهروند درجه یک محسوب میشود:
>>> reverse = lambda s: s[::-1]
>>> reverse("I am a string")
'gnirts a ma I'
این کار عملا معادل تعریف کردن reverse() با کلمه کلیدی def است.
>>> def reverse(s):
... return s[::-1]
...
>>> reverse("I am a string")
'gnirts a ma I'
>>> reverse = lambda s: s[::-1]
>>> reverse("I am a string")
'gnirts a ma I'
فراخوانی های تابع در خطوط 4 و 8 دقیقا مانند هم عمل میکنند.
با این وجود، انتساب یک عبارت لامبدا به یک متغیر قبل از فراخوانی آن، الزامی نیست. همچنین میتوانید تابع تعریف شده با عبارت لامبدا را مستقیما فراخوانی کنید.
>>> (lambda s: s[::-1])("I am a string")
'gnirts a ma I'
در اینجا مثال دیگری آورده ایم:
>>> (lambda x1, x2, x3: (x1 + x2 + x3) / 3)(9, 6, 6)
7.0
>>> (lambda x1, x2, x3: (x1 + x2 + x3) / 3)(1.4, 1.1, 0.5)
1.0
در این مورد، پارامتر های ما x1,x2 و x3 هستند و expression ما x1 +x2 + x3 /3 است. این یک تابع ناشناس لامبدا است که میانگین سه عدد را محاسبه میکند.
مثالی که در آن یک تابع reverse_len() را تعریف کردیم را به یاد میاورید؟
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> def reverse_len(s):
... return -len(s)
...
>>> sorted(animals, key=reverse_len)
['ferret', 'gecko', 'vole', 'dog']
در اینجا هم میتوانیم از تابع لامبدا استفاده کنیم.
>>> animals = ["ferret", "vole", "dog", "gecko"]
>>> sorted(animals, key=lambda s: -len(s))
['ferret', 'gecko', 'vole', 'dog']
یک عبارت لامبدا معمولاً دارای لیستی از پارامترها است، اما الزامی در آن نیست. شما میتوانید یک تابع لامبدا را بدون پارامتر نیز تعریف کنید. مقدار بازگشتی تابع به پارامترهای ورودی وابسته نیست:
>>> forty_two_producer = lambda: 42
>>> forty_two_producer()
42
توجه داشته باشید که فقط میتوانید توابع نسبتاً ابتدایی را با لامبدا تعریف کنید. مقدار برگشتی از یک عبارت لامبدا فقط میتواند یک عبارت واحد باشد. یک عبارت لامبدا نمیتواند شامل عباراتی مانند انتساب یا بازگشت باشد، همچنین نمیتواند حاوی ساختارهای کنترلی مانند for، while، if، else، یا def نیز باشد.
در آموزش های قبلی آموختید که یک تابع تعریف شده با کلید واژه def میتواند چندین مقدار را برگرداند. اگر دستور return در یک تابع دارای چندین مقدار باشد، که با ویرگول از هم جدا شده اند، پایتون آن مقادیر را بسته بندی کرده و به عنوان چند تایی (tuple)برمیگرداند:
>>> def func(x):
... return x, x ** 2, x ** 3
...
>>> func(3)
(3, 9, 27)
این بسته بندی ضمنی، با تابع ناشناس لامبدا کار نمیکند:
>>> (lambda x: x, x ** 2, x ** 3)(3)
<stdin>:1: SyntaxWarning: 'tuple' object is not callable; perhaps you missed a comma?
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
اما میتوانید به طور دستی یک چندتایی را در تابع لامبدا خروجی کنید. برای این کار باید صراحتا چندتایی را با پرانتز مشخص کنید. همچنین میتوانید یک لیست یا دیکشنری را در یک تابع لامبدا برگردانید:
>>> (lambda x: (x, x ** 2, x ** 3))(3)
(3, 9, 27)
>>> (lambda x: [x, x ** 2, x ** 3])(3)
[3, 9, 27]
>>> (lambda x: {1: x, 2: x ** 2, 3: x ** 3})(3)
{1: 3, 2: 9, 3: 27}
یک عبارت لامبدا فضای نامی محلی خود را دارد، بنابراین یکسان بودن نام پارامتر های آن با نام های موجود در فضای نامی سراسری مشکلی به وجود نمیآورد. یک عبارت لامبدا میتواند به متغیر های موجود در فضای نامی سراسری دسترسی پیدا کند، اما نمیتواند آنها را ویرایش کند.
اما یک مورد عجیب وجود دارد که باید در مورد آن آگاه باشید. اگر نیازی به درج عبارت لامبدا در یک رشته قالب بندی شده تحت اللفظی (f-string) پیدا کردید، باید آن را در داخل پرانتز قرار دهید:
>>> print(f"--- {lambda s: s[::-1]} ---")
File "<stdin>", line 1
(lambda s)
^
SyntaxError: f-string: invalid syntax
>>> print(f"--- {(lambda s: s[::-1])} ---")
--- <function <lambda> at 0x7f97b775fa60> ---
>>> print(f"--- {(lambda s: s[::-1])('I am a string')} ---")
--- gnirts a ma I ---
اکنون دیگر میدانید که چگونه یک تابع ناشناس را با استفاده از عبارت لامبدا تعریف کنید.
دیگر وقت آن است که به برنامه نویسی فانکشنال در پایتون بپردازیم. خواهید دید که استفاده از توابع لامبدا به ویژه هنگام نوشتن کد فانکشنال، کار ما را بسیار راحت تر خواهد کرد!
پایتون دو تابع داخلی، map() و filter() را ارائه میکند که متناسب با الگوی برنامه نویسی فانکشنال است. تابع سومینیز وجود داشت، reduce()، اما دیگر بخشی از زبان اصلی نیست. با این وجود هنوز در ماژولی به نام functools در دسترس است. هر یک از این سه توابع، تابع دیگری را به عنوان یکی از آرگومان های خود دریافت میکنند.
ویدیو پیشنهادی: ویدیو آموزش فانکشن های map, filter, reduce در پایتون
# به کار گیری تابع در یک تکرارشونده با استفاده از map()
اولین تابعی که به آن میپردازیم، تابع map() است که یک تابع داخلی پایتون است. با استفاده از map()، میتوانید یک تابع را روی هر عنصر یک تکرارشونده اعمال کنید و خروجی تابع map() یک تکرارشونده حاوی نتایج است. این ویژگی برای نوشتن کد های مختصر عالی است زیرا تابع map() میتواند جایگزین بسیار خوبی برای یک حلقه باشد.
+ فراخوانی تابع map() با یک تکرارشونده
سینتکس فراخوانی تابع map() با یک تکرارشونده به شکل زیر است:
map(<f>, <iterable>)
map(<f>, <iterable>) یک تکرارشونده را برمیگرداند که حاوی نتایج اعمال تابع f روی تک تک عناصر تکرارشونده ورودی است.
به این مثال توجه کنید. فرض کنید که شما تابع reverse() را، تابعی که یک رشته را به عنوان ورودی دریافت میکند و معکوس آن را برمیگرداند، با استفاده از [::-1] تعریف کرده اید:
>>> def reverse(s):
... return s[::-1]
...
>>> reverse("I am a string")
'gnirts a ma I'
اگر لیستی از رشته ها دارید، میتوانید از map() برای اعمال تابع reverse() روی تمامی عناصر آن لیست استفاده کنید.
>>> animals = ["cat", "dog", "hedgehog", "gecko"]
>>> iterator = map(reverse, animals)
>>> iterator
<map object at 0x7fd3558cbef0>
اما حواستان باشد! Map() یک لیست را برنمیگرداند. بلکه یک تکرارشونده را خروجی میکند که به آن map object میگویند. برای بدست آوردن مقادیر این تکرارشونده میتوانید آن را در یک حلقه قرار داده یا از تابع list() استفاده کنید.
>>> iterator = map(reverse, animals)
>>> for i in iterator:
... print(i)
...
tac
god
gohegdeh
okceg
>>> iterator = map(reverse, animals)
>>> list(iterator)
['tac', 'god', 'gohegdeh', 'okceg']
استفاده از حلقه روی تکرارشونده نهایی، معکوس عناصر لیست animals را برمیگرداند.
در این مثال، تابع reverse تابع بسیار کوتاهی است و شاید در خارج از تابع map() اصلا به آن نیازی نداشته باشید. به جای شلوغ کردن کدتان ، میتوانید از عبارت لاندا برای تعریف یک تابع ناشناس استفاده کنید.
>>> animals = ["cat", "dog", "hedgehog", "gecko"]
>>> iterator = map(lambda s: s[::-1], animals)
>>> list(iterator)
['tac', 'god', 'gohegdeh', 'okceg']
>>> # Combining it all into one line:
>>> list(map(lambda s: s[::-1], ["cat", "dog", "hedgehog", "gecko"]))
['tac', 'god', 'gohegdeh', 'okceg']
اگر تکرارشونده شامل عناصری باشد که تناسبی با تابع مشخص شده نداشته باشد، پایتون یک Exception تولید میکند:
>>> list(map(lambda s: s[::-1], ["cat", "dog", 3.14159, "gecko"]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in <lambda>
TypeError: 'float' object is not subscriptable
در این مورد، تابع لامبدا یک آرگومان از نوع رشته ای را دریافت میکند و آن را حرف به حرف جداسازی میکند. اما دومین عنصر لیست بالا عدد 3.14159 است که یک آبجکت از نوع float است که قابل جداسازی نیست. پس یک خطای TypeError به وجود میآید.
در قسمت آموزش توابع رشته ای، با str.join() آشنا شدید. این تابع دو رشته را از یک تکرارشونده با هم الحاق میکند که با رشته ای مشخص از هم تمیز داده شده اند:
>>> "+".join(["cat", "dog", "hedgehog", "gecko"])
'cat+dog+hedgehog+gecko'
این کد به شرطی که عناصر لیست همگی رشته باشند، به خوبی عمل میکند. در غیر این صورت، یک TypeError Exception تولید میکند.
>>> "+".join([1, 2, 3, 4, 5])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, int found
یک راه برطرف کردن این مشکل استفاده از یک حلقه است. با استفاده از حلقه for میتوانید یک لیست جدید ایجاد کنید که شامل نمایش رشته ای اعداد در لیست اصلی باشد. سپس میتوانید با خیال راحت از.join() استفاده کنید:
>>> strings = []
>>> for i in [1, 2, 3, 4, 5]:
... strings.append(str(i))
...
>>> strings
['1', '2', '3', '4', '5']
>>> "+".join(strings)
'1+2+3+4+5'
با توجه به اینکه تابع map() تابع مورد نظر را روی تمامی عناصر یک لیست اعمال میکند، میتوان بدون استفاده از حلقه نیز این کار را انجام داد. در این مورد میتوانید از map() برای اعمال تابع str() روی عناصر لیست، قبل از الحاق آن ها به یکدیگر، استفاده کنید.
>>> "+".join(map(str, [1, 2, 3, 4, 5]))
'1+2+3+4+5'
map(str, [1, 2, 3, 4, 5]) یک تکرارشونده را خروجی میکند که شامل لیستی از رشته ها است: ["1", "2", "3", "4", "5"]. پس میتوانید به راحتی آن ها را به تابع.join() بدهید.
اگر چه map() هدف ما را در مثال بالا برآورده کرده است اما پایتونیک(!) تر است که ما از یک لیست برای جایگزینی حلقه در این مورد استفاده کنیم.
دوره پیشنهادی: دوره آموزش الگوریتمنویسی در پایتون
+ فراخوانی تابع map() با چندین تکرارشونده
یک شکل دیگر از تابع map() وجود دارد که بیش از یک آرگومان تکرارشونده دریافت میکند.
map(<f>, <iterable₁>, <iterable₂>, ..., <iterableₙ>)
map(<f>, <iterable1>, <iterable2>,..., <iterablen>) applies تابع <f> را روی عناصر هر یک از تکرارشونده ها به طور موازی اعمال میکند و یک تکرارشونده حاوی نتایج را برمیگرداند.
تعداد آرگومان های <iterablei> باید با تعداد آرگومان های تابع <f> برابر باشد. تابع <f> ابتدا روی عناصر اول تمامی تکرارشونده ها اجرا میشود و خروجی آن به عنوان اولین عنصر تکرارشوندهِ خروجی ذخیره میگردد. سپس روی عناصر دوم تکرارشونده ها اجرا میشود و عنصر دوم تکرارشوندهِ خروجی حاصل میشود. این روند برای تکرارشونده های بعدی نیز تکرار میشود.
این مثال به شما در درک بهتر مفهوم بالا کمک میکند:
>>> def f(a, b, c):
... return a + b + c
...
>>> list(map(f, [1, 2, 3], [10, 20, 30], [100, 200, 300]))
[111, 222, 333]
در این مثال، تابع f() سه آرگومان ورودی دارد. به همین ترتیب، سه لیست به عنوان ورودی به تابع map() نیز داده شده است: لیست های [1, 2, 3], [10, 20, 30], [100, 200, 300].
اولین عنصر خروجی، حاصل اعمال تابع f روی عناصر اول سه لیست ورودی است: f(1, 10, 100).
دومین عنصر خروجی، حاصل اجرای f(2, 20, 200) و سومین عنصر خروجی حاصل اجرای f(3, 30, 300) است که در دیاگرام زیر نمایش داده شده است:
خروجی تابع map() یک تکرارشونده است که با استفاده از تابع list()، لیست [111,222,333] را برمیگرداند. در اینجا باز هم به دلیل کوتاه بودن تابع f() میتوانیم از لاندا استفاده کنیم:
>>> list(
... map(
... (lambda a, b, c: a + b + c),
... [1, 2, 3],
... [10, 20, 30],
... [100, 200, 300]
... )
... )
در مثال بالا از پرانتز های اضافه برای نوشتن عبارت لامبدا استفاده کردیم. گذاشتن این پرانتز ها ضروری نیست اما کد شما را خواناتر میکند.
# انتخاب عناصر یک تکرارشونده با استفاده از filter()
تابع filter() به شما این امکان را میدهد که عناصر یک تکرارشونده را، با توجه به تابع ارزیابی، انتخاب یا فیلتر کنید. نحوه به کارگیری filter() به شکل زیر است:
filter(<f>, <iterable>)
filter(<f>, <iterable>) تابع <f> را روی هر عنصر تکرارشونده <iterable> اعمال میکند و خروجی آن نیز یک تکرارشونده است که حاوی عناصریست که خروجی تابع ارزیابی <f> برای آن ها truth بوده است. در مقابل، این تابع عناصری را که خروجی اجرای آن ها در تابع <f> ، false است را کنار میگذارد.
در مثال پایین، تابع ارزیابی greater_than_100(x) هنگامی مقدار true را برمیگرداند که x بزرگتر از 100 باشد.
>>> def greater_than_100(x):
... return x > 100
...
>>> list(filter(greater_than_100, [1, 111, 2, 222, 3, 333]))
[111, 222, 333]
در این مورد، تابع ارزیابی greater_than_100() برای عناصر 111و222 و 333 مقدار true را برمیگرداند پس این عناصر در تکرارشونده خروجی حضور دارند اما عناصر 1 و2و3 انتخاب نمیشوند. مانند مثال قبل، میتوانیم تابع ارزیابی را با عبارت لاندا نیز تعریف کنیم:
>>> list(filter(lambda x: x > 100, [1, 111, 2, 222, 3, 333]))
[111, 222, 333]
مثال بعدی کاربرد تابع range() را نشان میدهد. Range(n) یک تکرارشونده را برمیگرداند که شامل اعداد صحیح از نوع Integer از 0 تا n-1 است. در مثال زیر از تابع filter() برای انتخاب کردن اعداد زوج و فیلتر کردن اعداد فرد استفاده شده است:
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> def is_even(x):
... return x % 2 == 0
...
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]
>>> list(filter(lambda x: x % 2 == 0, range(10)))
[0, 2, 4, 6, 8]
در اینجا یک مثال از توابع داخلی رشته ای را مشاهده میکنید:
>>> animals = ["cat", "Cat", "CAT", "dog", "Dog", "DOG", "emu", "Emu", "EMU"]
>>> def all_caps(s):
... return s.isupper()
...
>>> list(filter(all_caps, animals))
['CAT', 'DOG', 'EMU']
>>> list(filter(lambda s: s.isupper(), animals))
['CAT', 'DOG', 'EMU']
تابع s.isupper() هنگامی که تمامیکارکتر های یک رشته حروف بزرگ الفبا باشد، True و اگر تنها یکی از حروف آن حرف کوچک الفبا باشد، مقدار False را بازمیگرداند.
مقاله پیشنهادی: آموزش اتصال به mongodb با پایتون
# تقلیل یک تکرارشونده به یک مقدار واحد با استفاده از reduce()
تابع reduce() هر بار روی دو عنصر یک تکرارشونده اجرا میشود و آن ها را به یک مقدار تبدیل میکند.
Reduce() یکی از توابع داخلی پایتون بود. اما ظاهرا گایدو ون راسوم (Guido van Rossom) – خالق زبان پایتون – این تابع را دوست نداشت و خواهان حذف کامل این تابع از پایتون بود. او در مورد تابع reduce() چنین میگوید:
خب! حالا در مورد reduce(). این یکی از اون مواردیه که من به شدت ازش تنفر دارم. جدا از چند مثال محدود که شامل + یا * میشوند، تقریبا هر موقع که میبینم یک تابع reduce() با یک آرگومان تابعیِ نه چندان ساده استفاده شده، باید خودکار و کاغذ بردارم و دیاگرام بکشم تا ببینیم اصلا چه چیز هایی به عنوان ورودی به این تابع داده میشه تا حالا بعد بفهمم که خود reduce() قراره چیکار کنه! تو ذهن من، کاربرد تابع reduce() به عملگرهای انجمنی محدود میشه و همیشه بهتره که حلقه های انجمنی را جدا و صریح بنویسیم. (منبع)
در واقع گایدو میخواست تا هر سه تابع reduce(),map() و filter() را از پایتون حذف کند! کسی نمیداند در سر او چه میگذرد اما معتقد بود که list comprehension، تمامیکار هایی را که میتوانیم با این سه تابع انجام دهیم را پوشش میدهد.
همانطور که دیدید، تابع map() و filter() به عنوان توابع داخلی و پیش فرض پایتون باقی مانده اند اما reduce() دیگر تابع داخلی نیست. با این حال، همانطور که خواهید دید، در ماژول کتابخانه استاندارد موجود است.
برای استفاده از reduce() نیاز دارید تا آن را از یک ماژول به نام functools وارد کنید. این کار به چندین روش انجام میشود که روش پیش رو ساده ترین آن هاست:
from functools import reduce
مفسر تابع reduce() را در فضای نامیسراسری قرار میدهد و ما میتوانیم از آن استفاده کنیم. مثال هایی که در ادامه خواهید دید با این پیش فرض از تابع reduce() استفاده میکنند.
+ فراخوانی تابع reduce() با دو آرگومان
ساده ترین نوع فراخوانی reduce()، فراخوانی آن با یک آرگومان از نوع تابع و یک آرگومان از نوع تکرارشونده است:
reduce(<f>, <iterable>)
reduce(<f>, <iterable>) از تابع <f> استفاده میکند که باید دقیقا دو آرگومان ورودی داشته باشد که به ترتیب عناصر تکرارشونده را با هم ترکیب کند. برای شروع، تابع reduce() تابع <f> را روی دو عنصر اول تکرارشونده <iterable> اجرا میکند. سپس نتیجه حاصل با عنصر سوم تکرارشونده ترکیب میشود و این روند تا رسیدن به آخرین عنصر تکرار میشود. سپس reduce() مقدار نهایی را برمیگرداند.
در واقع گایدو راست میگفت که بهترین و ساده ترین نوع استفاده از تابع reduce() همان عملگرهای انجمنی هستند. پس ما هم با عملگر + شروع میکنیم.
>>> def f(x, y):
... return x + y
...
>>> from functools import reduce
>>> reduce(f, [1, 2, 3, 4, 5])
15
نتیجه نهایی کد بالا 15 است که به شکل زیر بدست آمده است:
این راهی نسبتا طولانی برای بدست آوردن مجموع اعداد موجود در لیست است. با اینکه به خوبی عمل میکند اما یک راه کوتاه تر وجود دارد. تابع داخلی sum() در پایتون برای اینکار تعبیه شده است:
>>> sum([1, 2, 3, 4, 5])
15
یادتان باشد که عملگرِ بعلاوه، رشته ها را نیز با هم الحاق میکند. پس اگر لیستی از رشته ها را به عنوان ورودی به تابع reduce() و در نتیجه به تابع f() بدهیم، رشته ی نهایی به صورت زیر خواهد بود:
>>> reduce(f, ["cat", "dog", "hedgehog", "gecko"])
'catdoghedgehoggecko'
و باز هم راه ساده تری برای این کار وجود دارد! از تابع داخلی.join() پایتون استفاده کنید:
>>> "".join(["cat", "dog", "hedgehog", "gecko"])
'catdoghedgehoggecko'
حال بیاید به جای عملگر بعلاوه از عملگر ستاره یا ضرب استفاده کنیم. فاکتوریل یک عدد صحیح مثبت به صورت زیر تعریف شده است:
میتوان تابع فاکتوریل را با استفاده از reduce() و range() به صورت زیر پیاده سازی کرد:
>>> def multiply(x, y):
... return x * y
...
>>> def factorial(n):
... from functools import reduce
... return reduce(multiply, range(1, n + 1))
...
>>> factorial(4) # 1 * 2 * 3 * 4
24
>>> factorial(6) # 1 * 2 * 3 * 4 * 5 * 6
720
البته یک راه سریع تر برای این کار نیز وجود دارد. از تابع factorial() در ماژول math پایتون استفاده کنید:
>>> from math import factorial
>>> factorial(4)
24
>>> factorial(6)
720
به عنوان مثال آخر این بخش، فرض کنید که میخواهید بزرگترین عدد یک لیست را پیدا کنید. پایتون تابع داخلی max() را برای اینکار فراهم کرده است اما میتوانید با reduce() نیز این کار را انجام دهید:
>>> max([23, 49, 6, 32])
49
>>> def greater(x, y):
... return x if x > y else y
...
>>> from functools import reduce
>>> reduce(greater, [23, 49, 6, 32])
49
توجه کنید که در هر یک از مثال های بالا تابع ورودی تابع reduce() یک تابع یک خطی است. پس میتوانیم از عبارت لامبدا نیز استفاده کنیم:
>>> reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
15
>>> reduce(lambda x, y: x + y, ["foo", "bar", "baz", "quz"])
'foobarbazquz'
>>> def factorial(n):
... from functools import reduce
... return reduce(lambda x, y: x * y, range(1, n + 1))
...
>>> factorial(4)
24
>>> factorial(6)
720
>>> reduce((lambda x, y: x if x > y else y), [23, 49, 6, 32])
49
این روشی مناسب برای جلوگیری از قرار دادن یک تابع غیر ضروری در فضای نامیاست. اما از طرف دیگر، هنگامیکه از عبارت لامبدا استفاده میکنید خوانایی کدتان کاهش مییابد و فهمیدن کدتان برای اشخاص دیگر سخت تر میشود. پس باید سعی کنیم بین خوانایی و راحتی تعادلی ایجاد کنیم.
مقاله پیشنهادی: دوره های آموزش پروژه محور و پیشرفته پایتون
+ فراخوانی تابع reduce() با مقدار اولیه
راه دیگری برای فراخوانی تابع reduce() وجود دارد که در آن علاوه بر دو آرگومان قبلی، یک مقدار اولیه نیز به تابع داده خواهد شد:
reduce(<f>, <iterable>, <init>)
<init> یک مقدار اولیه را برای فرآیند ترکیب مشخص میکند. در اولین فراخوانی تابع <f>، آرگومان های ورودی تابع f، مقدار <init> و عنصر اول تکرارشونده <iterable> است. سپس نتیجه حاصل از آن ها با عنصر سوم تکرارشونده ترکیب میشود و به همین ترتیب ادامه پیدا میکند:
>>> def f(x, y):
... return x + y
...
>>> from functools import reduce
>>> reduce(f, [1, 2, 3, 4, 5], 100) # (100 + 1 + 2 + 3 + 4 + 5)
115
>>> # Using lambda:
>>> reduce(lambda x, y: x + y, [1, 2, 3, 4, 5], 100)
115
دنباله فراخوانی تابع f به شکل زیر است:
میتوانید به راحتی و بدون استفاده ازreduce() نیز این کار را انجام دهید:
>>> 100 + sum([1, 2, 3, 4, 5])
115
همانطور که در مثال های بالا مشاهده کردید، حتی در مواردی که میتوان از reduce() استفاده کرد، معمولا یک راه ساده تر و پایتونیک تر برای رسیدن به نتیجه یکسان وجود دارد. شاید اکنون بتوانیم دلیل گایو را برای حذف reduce() از هسته پایتون بهتر درک کنیم.
با این وجود، تابع reduce() هنوز هم یکی از مهم ترین توابع هاست. در توضیحات اولیه این قسمت گفتیم که reduce() عناصر را با هم ترکیب میکند و یک خروجی واحد را برمیگرداند اما آن خروجی میتواند یک آبجکت مرکب مثل لیست یا چندتایی نیز باشد. به همین دلیل، تابع reduce() تابع سطح بالایی است که میتوان بسیاری از توابع را با استفاده از آن پیاده سازی کرد.
به طور مثال حتی تابع map() را میتوان با reduce() پیاده سازی کرد:
>>> numbers = [1, 2, 3, 4, 5]
>>> list(map(str, numbers))
['1', '2', '3', '4', '5']
>>> def custom_map(function, iterable):
... from functools import reduce
...
... return reduce(
... lambda items, value: items + [function(value)],
... iterable,
... [],
... )
...
>>> list(custom_map(str, numbers))
['1', '2', '3', '4', '5']
میتوانید تابع filter() را نیز با استفاده از reduce() پیاده سازی کنید:
>>> numbers = list(range(10))
>>> numbers
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> def is_even(x):
... return x % 2 == 0
...
>>> list(filter(is_even, numbers))
[0, 2, 4, 6, 8]
>>> def custom_filter(function, iterable):
... from functools import reduce
...
... return reduce(
... lambda items, value: items + [value] if function(value) else items,
... iterable,
... []
... )
...
>>> list(custom_filter(is_even, numbers))
[0, 2, 4, 6, 8]
در واقع هر تابعی را، که روی دنباله ای از عناصر اجرا میشود، میتوان با تابع reduce() پیاده سازی کرد.
# نتیجه گیری
برنامه نویسی فانکشنال یک الگوی برنامه نویسی است که روش اصلی محاسبه در آن، ارزیابی توابع خالص است. اگرچه پایتون در درجه اول یک زبان فانکنشنال نیست، اما خوب است که با lambda، map()، filter() و reduce() آشنا باشید؛ زیرا آنها میتوانند به شما در نوشتن کدی مختصر، سطح بالا و با قابلیت اجرای موازی کمک کنند. همچنین آنها را در کدی که دیگران نوشته اند خواهید دید.
اگر مقاله بالا را دوست داشتید، پیشنهادی میکنیم به مطالب زیر هم نگاهی بیندازید:
دوره اول آموزش پروژه محور پایتون