Function গভীরভাবে

*args, **kwargs, scope, closure, lambda আর সাধারণ ফাঁদ

রোডম্যাপের মাস ১পড়তে প্রায় ১৫ মিনিট

Function হলো একটা নাম দেওয়া কাজের টুকরো, যেটা বারবার ব্যবহার করা যায়। Dart এ তুমি অনেক function লিখেছো, তাই মূল ধারণা নতুন না। কিন্তু Python এর function এর কিছু বিশেষ ক্ষমতা আছে (*args, **kwargs, keyword argument, closure), আর একটা বিখ্যাত ফাঁদ আছে (mutable default) যেটায় প্রায় সবাই একবার পড়ে। FastAPI পুরোটাই function দিয়ে বানানো, তাই এই অধ্যায় খুব জরুরি।

#Function এর গঠন

Python
def calculate_delivery_fee(distance_km: float, is_rainy: bool = False) -> int:
    """দূরত্ব আর আবহাওয়া দেখে delivery fee (টাকায়) বের করে।"""
    base = 30
    per_km = 10
    fee = base + int(distance_km * per_km)
    if is_rainy:
        fee += 20
    return fee


print(calculate_delivery_fee(3.5))                 # 65
print(calculate_delivery_fee(3.5, is_rainy=True))  # 85
  • def দিয়ে শুরু, তারপর নাম, parameter, আর :
  • distance_km: float হলো type hint, -> int মানে ফেরত দেবে int
  • তিনটা উদ্ধৃতি চিহ্নের ভেতরের লেখাটা docstring: function কী করে তার বর্ণনা। VS Code এ function এর উপর mouse রাখলে এটা দেখায়।
  • return না থাকলে function None ফেরত দেয়

Python এ নাম লেখার নিয়ম: function আর variable এর নাম snake_case এ (calculate_delivery_fee), Dart এর মতো camelCase এ না।

#Argument দেওয়ার উপায়

Python
def create_user(name: str, age: int, city: str = "Dhaka", is_admin: bool = False):
    return {"name": name, "age": age, "city": city, "is_admin": is_admin}

create_user("Rahim", 25)                          # positional
create_user(name="Rahim", age=25)                 # keyword
create_user("Rahim", 25, is_admin=True)           # মিশিয়ে, city default থাকবে
create_user(age=25, name="Rahim")                 # keyword এ ক্রম লাগে না

Keyword argument code কে পড়তে সহজ করে। create_user("Rahim", 25, "Sylhet", True) দেখে বোঝা যায় না True কীসের জন্য, কিন্তু is_admin=True দেখলে সাথে সাথে বোঝা যায়।

#শুধু keyword দিয়ে দেওয়া যাবে এমন parameter

* এর পরের parameter গুলো অবশ্যই নাম দিয়ে দিতে হবে। ভুল বোঝাবুঝি এড়াতে এটা খুব কাজের:

Python
def transfer(amount: int, *, from_account: str, to_account: str) -> None:
    print(f"{from_account} থেকে {to_account} এ {amount} টাকা")

transfer(500, from_account="A-1", to_account="B-2")   # ঠিক আছে
# transfer(500, "A-1", "B-2")   ← TypeError, কোনটা from কোনটা to গুলিয়ে যাওয়ার সুযোগই নেই

টাকা পাঠানোর মতো কাজে কোনটা from আর কোনটা to, সেটা গুলিয়ে গেলে ভয়ানক bug। Keyword-only parameter সেই সুযোগ বন্ধ করে।

#*args আর **kwargs

কতগুলো argument আসবে আগে থেকে জানা না থাকলে:

Python
def total(*prices: int) -> int:
    # prices একটা tuple
    return sum(prices)

print(total(100, 250, 75))     # 425

def build_query(**filters: str) -> str:
    # filters একটা dict
    parts = [f"{key}={value}" for key, value in filters.items()]
    return "&".join(parts)

print(build_query(city="Dhaka", category="food", sort="rating"))
# city=Dhaka&category=food&sort=rating

উল্টো দিকেও কাজ করে, list বা dict কে খুলে argument হিসেবে দেওয়া:

Python
prices = [100, 250, 75]
print(total(*prices))

user_data = {"name": "Sara", "age": 28, "city": "Khulna"}
user = create_user(**user_data)    # dict এর key গুলো keyword argument হয়ে যায়

**data দিয়ে dict খুলে দেওয়ার কৌশল FastAPI আর Pydantic এ প্রচুর দেখবে, যেমন User(**request_data)।

#সবচেয়ে বিখ্যাত ফাঁদ: mutable default

এই code টা দেখে অনুমান করো কী print হবে:

Python
def add_item(item: str, cart: list[str] = []) -> list[str]:
    cart.append(item)
    return cart

print(add_item("চাল"))    # ['চাল']
print(add_item("ডাল"))    # ['চাল', 'ডাল']  😱 নতুন খালি cart আশা করেছিলাম

কারণ: default value ([]) একবারই তৈরি হয়, যখন function define করা হয়, প্রতিবার call এর সময় না। তাই সব call একই list share করে।

সঠিক উপায়:

Python
def add_item(item: str, cart: list[str] | None = None) -> list[str]:
    if cart is None:
        cart = []
    cart.append(item)
    return cart

#Scope: কোন variable কোথায় দেখা যায়

Python কোনো নাম খোঁজে এই ক্রমে, সংক্ষেপে LEGB:

B: Built-in print, len, range, ... Python নিজে দেয় G: Global file এর সবচেয়ে বাইরের variable E: Enclosing বাইরের function এর variable L: Local এই function এর ভেতরের variable Python প্রথমে এখানে খোঁজে, না পেলে বাইরের দিকে যায়
নাম খোঁজা হয় ভেতর থেকে বাইরে: Local → Enclosing → Global → Built-in।
Python
VAT_RATE = 0.15          # global

def price_with_vat(price: int) -> float:
    total = price * (1 + VAT_RATE)   # total local, VAT_RATE global থেকে পড়া
    return total

counter = 0
def increment() -> None:
    global counter       # global variable বদলাতে চাইলে বলতে হয়
    counter += 1

উপরের নামের মতো VAT_RATE (সব বড় হাতের) লেখা Python এর রীতি: এটা constant, বদলানো হবে না।

#Function ও একটা value

Python এ function কে variable এ রাখা যায়, argument হিসেবে দেওয়া যায়, return করা যায়। Dart এও তাই, যেমন onPressed: () {...}।

Python
def to_upper(text: str) -> str:
    return text.upper()

def shout(text: str) -> str:
    return text.upper() + "!!!"

def apply_to_all(func, items: list[str]) -> list[str]:
    return [func(item) for item in items]

names = ["rahim", "karim"]
print(apply_to_all(to_upper, names))   # ['RAHIM', 'KARIM']
print(apply_to_all(shout, names))      # ['RAHIM!!!', 'KARIM!!!']

#lambda: নামহীন ছোট function

Python
square = lambda x: x * x     # এভাবে নাম দিয়ে রাখার বদলে def লেখাই ভালো

orders = [{"id": 1, "total": 950}, {"id": 2, "total": 300}, {"id": 3, "total": 1200}]
by_total = sorted(orders, key=lambda o: o["total"])
big = list(filter(lambda o: o["total"] > 500, orders))

lambda শুধু এক লাইনের expression হতে পারে। একটু জটিল হলেই def ব্যবহার করো।

#Closure: function যে মনে রাখে

ভেতরের function বাইরের function এর variable মনে রাখতে পারে, বাইরের function শেষ হয়ে যাওয়ার পরও:

Python
def make_discount(percent: int):
    def apply(price: int) -> int:
        return price - price * percent // 100   # percent মনে রেখেছে
    return apply

eid_offer = make_discount(20)
flash_sale = make_discount(50)

print(eid_offer(1000))    # 800
print(flash_sale(1000))   # 500
make_discount(20) চলে শেষ হয়ে যায় apply function ব্যাগে রাখা: percent = 20 eid_offer নাম পেল
Closure হলো একটা function যে নিজের সাথে কিছু data এর "ব্যাগ" নিয়ে ঘোরে।

Closure কেন জানা দরকার: পরের মাসে decorator শিখবে, যেটা পুরোটাই closure এর উপর দাঁড়ানো। আর FastAPI এর @app.get("/") একটা decorator।

#ভালো function লেখার নিয়ম

  1. একটা function, একটা কাজ। নাম দিয়ে কাজ বোঝা না গেলে, বা নামে "and" লাগলে (validate_and_save_and_email), ভেঙে ফেলো।
  2. ছোট রাখো। ২০-৩০ লাইনের বেশি হলে ভাবো ভাঙা যায় কিনা।
  3. Pure function পছন্দ করো। একই input এ সবসময় একই output, আর বাইরের কিছু বদলায় না। এগুলো test করা সবচেয়ে সহজ।
  4. Side effect আলাদা রাখো। হিসাব একটা function এ, database এ save আরেকটায়। তাহলে হিসাবের অংশ database ছাড়াই test করা যায়।
  5. নাম ক্রিয়াপদ দিয়ে: calculate_total, send_email, get_user।
Python
# খারাপ: হিসাব আর save একসাথে, test করতে database লাগবে
def process_order(order):
    total = sum(i["price"] * i["qty"] for i in order["items"])
    if total > 1000:
        total -= 100
    db.save(order, total)

# ভালো: হিসাব আলাদা, pure
def calculate_total(items: list[dict]) -> int:
    total = sum(i["price"] * i["qty"] for i in items)
    return total - 100 if total > 1000 else total

def process_order(order, db) -> None:
    total = calculate_total(order["items"])
    db.save(order, total)

#অনুশীলন

#নিজেকে যাচাই করো

`def f(x, items=[])` কেন বিপজ্জনক?

Default list একবারই তৈরি হয় function define করার সময়। ফলে প্রতিটা call একই list share করে, আগের call এর data পরের call এ থেকে যায়। সমাধান: default None রেখে ভেতরে নতুন list বানানো।

`*args` আর `**kwargs` ভেতরে কী type হিসেবে আসে?

*args আসে tuple হিসেবে (positional argument গুলো), **kwargs আসে dict হিসেবে (keyword argument গুলো)।

`def transfer(amount, *, to):` এ `*` এর কাজ কী?

* এর পরের parameter (to) কে অবশ্যই নাম দিয়ে দিতে হবে: transfer(500, to="B-2")। এতে argument এর ক্রম গুলিয়ে ভুল করার সুযোগ থাকে না।

Closure কী? এক লাইনে বলো।

এমন একটা ভেতরের function, যে তার বাইরের function এর variable মনে রাখে, বাইরের function শেষ হয়ে যাওয়ার পরও।

Pure function test করা কেন সহজ?

কারণ এটা শুধু input এর উপর নির্ভর করে আর বাইরের কিছু (database, file, global variable) বদলায় না। তাই test এ শুধু input দিয়ে output মিলিয়ে দেখলেই হয়, কোনো setup বা mock লাগে না।

চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।