Functional Programming
Functional Programming در پایتون شامل توابعی است که دادهها را بدون تغییر state آنها پردازش میکنند. در این فصل با ماژول functools، توابع map/filter، closures، lambda، lru_cache و الگوهای functional آشنا میشویم.
Functional Programming در پایتون شامل توابعی است که دادهها را بدون تغییر state آنها پردازش میکنند. در این فصل با ماژول functools، توابع map/filter، closures، lambda، lru_cache و الگوهای functional آشنا میشویم.
توابع بهعنوان شیء درجه اول
در پایتون، توابع شیء هستند:
def greet(name):
return f"سلام {name}"
# 1) ذخیره در متغیر
say_hi = greet
print(say_hi("علی")) # سلام علی
# 2) ارسال به تابع دیگر
def call_twice(func, arg):
return [func(arg), func(arg)]
print(call_twice(greet, "علی")) # ['سلام علی', 'سلام علی']
# 3) بازگشت از تابع
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
Lambda – توابع ناشناس
# lambda برای توابع کوتاه
square = lambda x: x ** 2
print(square(4)) # 16
# مناسب در توابع high-order
nums = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x ** 2, nums)) # [1, 4, 9, 16, 25]
even = list(filter(lambda x: x % 2 == 0, nums)) # [2, 4]
# مرتبسازی با key
people = [{"name": "علی", "age": 30}, {"name": "سارا", "age": 25}]
people.sort(key=lambda p: p["age"])
# استفاده مناسب: یکخطی، ساده
# عدم استفاده مناسب: lambda x: x.method() if x else None (پیچیده)
map و filter
# map - اعمال تابع روی همه عناصر
prices = [100, 200, 300]
with_tax = list(map(lambda p: p * 1.09, prices))
# [109.0, 218.0, 327.0]
# با چند iterable
list1 = [1, 2, 3]
list2 = [10, 20, 30]
sums = list(map(lambda a, b: a + b, list1, list2))
# [11, 22, 33]
# filter - فیلتر بر اساس شرط
ages = [12, 18, 25, 16, 30]
adults = list(filter(lambda x: x >= 18, ages))
# [18, 25, 30]
# با None - فیلتر falsy values
items = [1, 0, "hello", "", None, [1], []]
truthy = list(filter(None, items))
# [1, 'hello', [1]]
چه زمانی list comprehension بهتر است؟
# معمولاً list comprehension خواناتر است
with_tax = [p * 1.09 for p in prices]
adults = [x for x in ages if x >= 18]
# map/filter وقتی تابع آماده دارید
result = list(map(str.upper, ["a", "b", "c"])) # تمیز
result = [s.upper() for s in ["a", "b", "c"]] # هم تمیز
functools.reduce
from functools import reduce
# مجموع
total = reduce(lambda a, b: a + b, [1, 2, 3, 4, 5])
print(total) # 15
# با مقدار شروع
total = reduce(lambda a, b: a + b, [1, 2, 3], 100)
print(total) # 106
# حداکثر
biggest = reduce(lambda a, b: a if a > b else b, [3, 1, 4, 1, 5, 9])
print(biggest) # 9
# ترکیب دیکشنریها
dicts = [{"a": 1}, {"b": 2}, {"c": 3}]
combined = reduce(lambda d1, d2: {**d1, **d2}, dicts)
print(combined) # {'a': 1, 'b': 2, 'c': 3}
sum، max، min) از reduce سادهتر و سریعترند.
functools.partial
ساخت تابع جدید با مقادیر از پیش تعیینشده برای برخی پارامترها:
from functools import partial
def power(base, exp):
return base ** exp
# ساخت توابع جدید
square = partial(power, exp=2)
cube = partial(power, exp=3)
print(square(5)) # 25
print(cube(3)) # 27
# مثال کاربردی - HTTP client
import requests
api_get = partial(requests.get, timeout=30, headers={"Authorization": "Bearer token"})
# حالا فقط URL لازم است
response = api_get("https://api.example.com/users")
response = api_get("https://api.example.com/products")
Closures – بستهها
Closure یعنی تابع داخلی که به متغیرهای تابع بیرونی دسترسی دارد:
def make_counter(start=0):
count = start
def increment():
nonlocal count # دسترسی به متغیر بیرونی
count += 1
return count
return increment
counter1 = make_counter()
print(counter1()) # 1
print(counter1()) # 2
counter2 = make_counter(100)
print(counter2()) # 101
print(counter2()) # 102
# هر closure حالت خودش را دارد
print(counter1()) # 3
Closure با چند تابع
def make_account(balance=0):
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
if amount > balance:
raise ValueError("موجودی کافی نیست")
balance -= amount
return balance
def get_balance():
return balance
return deposit, withdraw, get_balance
deposit, withdraw, balance = make_account(1000)
deposit(500) # 1500
withdraw(200) # 1300
print(balance()) # 1300
functools.lru_cache
کش خودکار برای توابع pure:
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# بدون کش: O(2^n) - برای 50 دقیقهها طول میکشد
# با کش: O(n) - فوری
print(fibonacci(100)) # سریع
# اطلاعات کش
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)
# پاک کردن کش
fibonacci.cache_clear()
# maxsize=None - کش بدون محدودیت
@lru_cache(maxsize=None)
def expensive(x):
return slow_computation(x)
cache (Python 3.9+) – معادل lru_cache(maxsize=None)
from functools import cache
@cache
def factorial(n):
return 1 if n <= 1 else n * factorial(n-1)
Immutability
در functional programming، دادهها تغییر نمیکنند:
# بد - تغییر in-place
def add_tax_bad(prices):
for i in range(len(prices)):
prices[i] *= 1.09 # داده ورودی تغییر کرد!
# خوب - برگرداندن داده جدید
def add_tax_good(prices):
return [p * 1.09 for p in prices]
original = [100, 200, 300]
with_tax = add_tax_good(original)
# original دست نخورده باقی میماند
# tuple و frozenset برای دادههای immutable
config = ("host", 8080, "production") # tuple
allowed = frozenset(["read", "write"]) # frozenset
# dataclass(frozen=True)
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
p = Point(1, 2)
# p.x = 5 # خطا - immutable
ترکیب توابع
from functools import reduce
def compose(*funcs):
"""ترکیب چند تابع: compose(f, g, h)(x) = f(g(h(x)))"""
def composed(x):
return reduce(lambda acc, f: f(acc), reversed(funcs), x)
return composed
# مثال
add_one = lambda x: x + 1
double = lambda x: x * 2
square = lambda x: x ** 2
# (((5 + 1) * 2) ** 2) = 144
pipeline = compose(square, double, add_one)
print(pipeline(5)) # 144
Pipelining با Generator
# pipeline دادهای - حافظه کم، خوانا
def read_logs(filename):
with open(filename) as f:
for line in f:
yield line.strip()
def filter_errors(lines):
return (line for line in lines if "ERROR" in line)
def parse(lines):
return ({"time": l[:19], "msg": l[20:]} for l in lines)
# ترکیب
errors = parse(filter_errors(read_logs("app.log")))
for error in errors:
print(error)
ماژول operator
توابعی برای عملگرهای استاندارد – مفید در map/filter/reduce:
import operator
from functools import reduce
# بهجای lambdaهای ساده
nums = [1, 2, 3, 4, 5]
# مجموع
total = reduce(operator.add, nums) # بهجای lambda a,b: a+b
# ضرب همه
product = reduce(operator.mul, nums) # 120
# itemgetter - دسترسی به آیتم
from operator import itemgetter
people = [
{"name": "علی", "age": 30},
{"name": "سارا", "age": 25},
]
people.sort(key=itemgetter("age")) # بهجای lambda p: p["age"]
# attrgetter - دسترسی به attribute
from operator import attrgetter
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
people = [Person("علی", 30), Person("سارا", 25)]
people.sort(key=attrgetter("age"))
مثال جامع: پایپلاین داده
from functools import reduce, partial
from operator import itemgetter
# داده ورودی
sales = [
{"product": "فرش کاشان", "price": 5_000_000, "category": "carpet", "qty": 2},
{"product": "گلیم", "price": 1_500_000, "category": "carpet", "qty": 5},
{"product": "تابلو فرش", "price": 8_000_000, "category": "art", "qty": 1},
{"product": "تابلو نقاشی", "price": 3_000_000, "category": "art", "qty": 3},
]
# pipeline functional
def calc_total(item):
return {**item, "total": item["price"] * item["qty"]}
def filter_category(category):
return lambda item: item["category"] == category
# پردازش
carpet_sales = list(map(calc_total, filter(filter_category("carpet"), sales)))
total_carpet_revenue = reduce(
lambda acc, item: acc + item["total"],
carpet_sales,
0
)
print(f"درآمد فرش: {total_carpet_revenue:,}")
# درآمد فرش: 17,500,000
# همان با sum و generator (پایتونتر!)
total = sum(item["price"] * item["qty"]
for item in sales
if item["category"] == "carpet")
بهترین شیوهها
- برای کارهای ساده، list/dict comprehension خواناتر از map/filter است
- بهجای reduce از
sum،max،any،allاستفاده کنید lru_cacheبرای توابع pure که سنگین هستند- توابع pure (بدون side-effect) راحتتر تست میشوند
- tuple و frozenset برای دادههای immutable
- partial برای ساخت specialized version از تابع
جمعبندی
- توابع پایتون شیء درجه اول هستند
- map/filter/reduce ابزارهای functional
- closures برای حالت خصوصی
functools: partial، reduce، lru_cache، cache- operator module برای جایگزینی lambdaهای ساده
- immutability برای کد قابل پیشبینی