Function গভীরভাবে
*args, **kwargs, scope, closure, lambda আর সাধারণ ফাঁদ
Function হলো একটা নাম দেওয়া কাজের টুকরো, যেটা বারবার ব্যবহার করা যায়। Dart এ তুমি অনেক function লিখেছো, তাই মূল ধারণা নতুন না। কিন্তু Python এর function এর কিছু বিশেষ ক্ষমতা আছে (*args, **kwargs, keyword argument, closure), আর একটা বিখ্যাত ফাঁদ আছে (mutable default) যেটায় প্রায় সবাই একবার পড়ে। FastAPI পুরোটাই function দিয়ে বানানো, তাই এই অধ্যায় খুব জরুরি।
#Function এর গঠন
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)) # 85defদিয়ে শুরু, তারপর নাম, parameter, আর:distance_km: floatহলো type hint,-> intমানে ফেরত দেবে int- তিনটা উদ্ধৃতি চিহ্নের ভেতরের লেখাটা docstring: function কী করে তার বর্ণনা। VS Code এ function এর উপর mouse রাখলে এটা দেখায়।
returnনা থাকলে functionNoneফেরত দেয়
Python এ নাম লেখার নিয়ম: function আর variable এর নাম snake_case এ (calculate_delivery_fee), Dart এর মতো camelCase এ না।
#Argument দেওয়ার উপায়
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 গুলো অবশ্যই নাম দিয়ে দিতে হবে। ভুল বোঝাবুঝি এড়াতে এটা খুব কাজের:
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 আসবে আগে থেকে জানা না থাকলে:
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 হিসেবে দেওয়া:
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 হবে:
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 করে।
সঠিক উপায়:
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:
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: () {...}।
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
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 শেষ হয়ে যাওয়ার পরও:
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)) # 500Closure কেন জানা দরকার: পরের মাসে decorator শিখবে, যেটা পুরোটাই closure এর উপর দাঁড়ানো। আর FastAPI এর @app.get("/") একটা decorator।
#ভালো function লেখার নিয়ম
- একটা function, একটা কাজ। নাম দিয়ে কাজ বোঝা না গেলে, বা নামে "and" লাগলে (
validate_and_save_and_email), ভেঙে ফেলো। - ছোট রাখো। ২০-৩০ লাইনের বেশি হলে ভাবো ভাঙা যায় কিনা।
- Pure function পছন্দ করো। একই input এ সবসময় একই output, আর বাইরের কিছু বদলায় না। এগুলো test করা সবচেয়ে সহজ।
- Side effect আলাদা রাখো। হিসাব একটা function এ, database এ save আরেকটায়। তাহলে হিসাবের অংশ database ছাড়াই test করা যায়।
- নাম ক্রিয়াপদ দিয়ে:
calculate_total,send_email,get_user।
# খারাপ: হিসাব আর 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 ভরে যাবে।