مقدمه‌ای بر برنامه‌نویسی تابعی در پایتون

ترجمه و تالیف : ارسطو عباسی
تاریخ انتشار : 13 خرداد 98
خواندن در 4 دقیقه
دسته بندی ها : پایتون

بیشتر ما با پایتون به عنوان یک زبان برنامه‌نویسی شئ‌گرا آشنا شدیم. زبانی که برنامه‌های ما در آن با استفاده از یک مجموعه کلاس و شئ ساخته می‌شوند. در حالیکه برنامه‌نویسی با استفاده از این پارادایم بسیار ساده و برای شروع کار لذت بخش است اما راه‌های دیگری نیز برای نوشتن کدهای پایتونی وجود دارد که می‌توان از آن‌ها استفاده کرد. زبان‌های دیگری مانند جاوا کار کردن با پارادایمی غیر از حالت شئ‌گرا را برای شما بسیار دشوار می‌کنند. اما پایتون به شما به سادگی اجازه می‌دهد که از تفکر شئ‌گرایی فاصله بگیرید و به سمت پارادایم دیگری بروید.

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

در این مطلب ما موضوعات زیر را بررسی می‌کنیم:

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

مقایسه پارادایم شئ‌گرا با تابعی

راحت‌ترین راه برای آنکه بتوانیم برنامه‌نویسی تابعی را توضیح بدهیم این است که آن را با یک پارادایم که قبلا با آن آشنایی داشته‌ایم مقایسه کنیم: پارادایم شئ‌گرا. تصور کنید که می‌خواهیم یک برنامه شمارنده خط را درست کنیم. این برنامه یک کلاس است که یک فایل را دریافت می‌کند، آن را می‌خواند و تعداد خطوط آن را حساب می‌کند. با استفاده از یک کلاس برنامه شما شبیه به حالت زیر خواهد بود:

class LineCounter:

    def __init__(self, filename):

        self.file = open(filename, 'r')

        self.lines = []

    

    def read(self):

        self.lines = [line for line in self.file]

    

    def count(self):

        return len(self.lines)

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

برای آنکه هر دوی این مفاهیم بتوانند کار بکنند، نیاز است که بتوانیم وضعیت شئ‌های موجود را به صورت مکرر تغییر دهیم. این تغییر روی خصوصیت lines بعد از اجرا کردن متد read() اتفاق می‌افتد. به عنوان یک مثال، در اینجا می‌توانید شیوه استفاده ما از این کلاس را مشاهده بکنید:

# example_file.txt contains 100 lines.

lc = LineCounter('example_file.txt')

print(lc.lines)

>> []

print(lc.count())

>> 0

# The lc object must read the file to

# set the lines property.

lc.read()

# The `lc.lines` property has been changed.

# This is called changing the state of the lc

# object.

print(lc.lines)

>> [['Hello world!', ...]]

print(lc.count())

>> 100

تغییرات مدام این شئ هم می‌تواند خوب باشد و هم می‌تواند بد باشد. برای آنکه متوجه شویم که چرا تغییر یک وضعیت می‌تواند تاثیر بدی داشته باشد نیاز است که یک جایگزین را معرفی کنیم. حالت جایگزین آن است که ما برنامه را به صورت یک زنجیره از توابع بنویسیم.

def read(filename):

    with open(filename, 'r') as f:

        return [line for line in f]

def count(lines):

    return len(lines)

example_lines = read('example_log.txt')

lines_count = count(example_lines)

کار با توابع خالص

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

توابعی که در بالا مشاهده کردید را pure functions یا توابع خالص می‌نامند. یک مثال از حالت pure functions و non pure functions را می‌توانید در زیر مشاهده بکنید:

# Create a global variable `A`.

A = 5

def impure_sum(b):

    # Adds two numbers, but uses the

    # global `A` variable.

    return b + A

def pure_sum(a, b):

    # Adds two numbers, using

    # ONLY the local function inputs.

    return a + b

print(impure_sum(6))

>> 11

print(pure_sum(4, 6))

>> 10

یکی از مزیت‌های بسیار بزرگی که توابع خالص برای ما فراهم می‌کنند این است که تاثیرات جانبی یا side effects را نمی‌پذیرند. تاثیرات جانبی زمانی اتفاق می‌افتند که یک عملیات تابعی از خارج scope خودش تاثیر می‌گیرد و از آن‌ جا ورودی‌هایی می‌گیرد. برای مثال زمانی که در یک تابع، یک عملیات دیگر که حال خودش می‌تواند یک شئ، متد و… باشد را فراخوانی کنیم در این حالت side effect اتفاق می‌افتد:

def read_and_print(filename):

    with open(filename) as f:

        # Side effect of opening a

        # file outside of function.

        data = [line for line in f]

    for line in data:

        # Call out to the operating system

        # "println" method (side effect).

        print(line)

در مثال بالا دستور with open و حتی دستور print باعث بوجود آمدن side effect می‌شوند.

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

تلاش برای حذف کردن تمام تاثیرات جانبی تلاشی مثبت است همچنین این موضوع روی برنامه‌نویسی ساده‌تر بسیار تاثیرگذار خواهد بود. اما این موضوع را نیز باید در نظر بگیرید که اگر تمام تاثیرات جانبی را از برنامه‌های تان حذف کنید دیگر قادر نخواهید بود که از تابع print یا open استفاده کنید. به همین خاطر این موضوع اجتناب ناپذیر است اما می‌تواند کمتر شود.

در دنیای برنامه‌نویسی تابعی پایتون یکسری میانبرها و امکانات بوجود آمده‌اند که به کاربران این امکان را می‌دهند تا بتوانند بهتر برنامه‌های تابعی را در پایتون ایجاد کنند. در این قسمت بر چند مورد از این امکانات نگاهی می‌کنیم: 

عبارت Lambda

بجای استفاده از def برای تعیین یک تابع می‌توانیم از عبارت lambda استفاده بکنیم. یک مثال از تابع ایجاد شده با استفاده از lambda را می‌توانیم در زیر مشاهده بکنیم:

# Using `def` (old way).

def old_add(a, b):

    return a + b

# Using `lambda` (new way).

new_add = lambda a, b: a + b

old_add(10, 5) == new_add(10, 5)

>> True

lambda در حقیقت راهی برای تعیین توابعی است که ناشناس هستند. در واقع تا زمانی که ما آن‌ها را به یک متغیر نسبت ندهیم، ناشناس می‌مانند. روش تعریف آن نیز به صورت خطی است و محدودیت‌های بسیار زیادی نسبت به حالت def دارد. 

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

unsorted = [('b', 6), ('a', 10), ('d', 0), ('c', 4)]

# Sort on the second tuple value (the integer).

print(sorted(unsorted, key=lambda x: x[1]))

>> [('d', 0), ('c', 4), ('b', 6), ('a', 10)]

تابع Map

در حالیکه استفاده از یک تابع به عنوان ورودی تابعی دیگر در پایتون کاری جدید نیست اما در اکثر زبان‌های برنامه‌نویسی این موضوع به تازگی ساخته شده است. توابعی که به این صورت رفتار می‌کنند را توابع first-class می‌نامند. هر زبانی که قابلیت استفاده از توابع first-class را داشته باشد می‌تواند به صورت فانکشنال یا تابعی نوشته شود. 

اولین تابعی که می‌خواهیم از این دست با آن کار کنیم تابع map است. تابع map() یک لیست را دریافت می‌کند و آن را به صورت یک شئ تکرار پذیر جدید به خروجی می‌فرستد. در مثال زیر شئ جدید تابع first-class را به تمام المان‌ها اعمال می‌کند. 

# Pseudocode for map.

def map(func, seq):

    # Return `Map` object with

    # the function applied to every

    # element.

    return Map(

        func(x)

        for x in seq

    )

در اینجا می‌توانید شیوه استفاده ما از map را برای اضافه کردن عدد ۱۰ یا ۲۰ به هر المان از لیست را مشاهده کنید:

values = [1, 2, 3, 4, 5]

# Note: We convert the returned map object to

# a list data structure.

add_10 = list(map(lambda x: x + 10, values))

add_20 = list(map(lambda x: x + 20, values))

print(add_10)

>> [11, 12, 13, 14, 15]

print(add_20)

>> [21, 22, 23, 24, 25]

تابع filter

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

values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Note: We convert the returned filter object to

# a list data structure.

even = list(filter(lambda x: x % 2 == 0, values))

odd = list(filter(lambda x: x % 2 == 1, values))

print(even)

>> [2, 4, 6, 8, 10]

print(odd)

>> [1, 3, 5, 7, 9]

تابع reduce

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

from functools import reduce

values = [1, 2, 3, 4]

summed = reduce(lambda a, b: a + b, values)

print(summed)

>> 10

مقدمه‌ای بر برنامه‌نویسی تابعی در پایتون

یک موضوع جالب این است که اگر شما برای ورودی تنها یک المان را وارد کنید، تابع reduce همواره فقط مقدار اول لیست را برمی‌گرداند:

from functools import reduce

values = [1, 2, 3, 4, 5]

# By convention, we add `_` as a placeholder for an input

# we do not use.

first_value = reduce(lambda a, _: a, values)

print(first_value)

>> 1

قدم بعدی

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

منبع

گردآوری و تالیف ارسطو عباسی
آفلاین
user-avatar

من ارسطو‌ام :) کافی نیست؟! :)

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید