সরাসরি পর্বে যান
পর্ব ১৩ · ৫৫-এর মধ্যে · মডিউল ২

ফাংশন — def, return, default আর keyword argument

Functions — def, return, Default & Keyword Arguments

Taught in Bangla, this lesson teaches Python functions: defining with def, returning one or several values, print versus return, what happens on the call stack, positional, keyword and default arguments, docstrings, type hints, pure functions, functions as objects and a first look at recursion.

এই পর্বে যা শিখবেন

  • ফাংশন কেন দরকার, আর def, parameter, argument, return — প্রতিটির আসল কাজ
  • print বনাম return-এর পার্থক্য, None কোথা থেকে আসে, আর একাধিক মান ফেরত দেওয়া
  • Positional, keyword ও default argument; সঙ্গে * (keyword-only) আর / (positional-only)
  • ফাংশন ডাকলে ভেতরে কী ঘটে — call stack, frame আর local variable; recursion-এর প্রথম ঝলক
  • ভালো ফাংশনের গুণ — docstring, type hint, pure function, assert দিয়ে পরীক্ষা, আর AI-এর লেখা ফাংশন যাচাই

১ · ফাংশন কী — নাম দেওয়া, বারবার ব্যবহারযোগ্য রেসিপি

আগের পর্বগুলোতে আপনি অনেক ফাংশন ব্যবহার করেছেন: print(), len(), input(), round(). কেউ একজন এগুলো আগেই লিখে রেখেছেন, আপনি শুধু নাম ধরে ডেকেছেন। এই পর্বে আপনি নিজেই ফাংশন বানাবেন। মডিউল ২-এর এটাই শেষ পর্ব, আর সম্ভবত সবচেয়ে গুরুত্বপূর্ণ — কারণ বড় প্রোগ্রাম আসলে অনেকগুলো ছোট ফাংশনের সমষ্টি।

একটি function হলো নাম দেওয়া, বারবার ব্যবহারযোগ্য একটি রেসিপি. রেসিপিতে উপকরণ ঢোকে, নির্দিষ্ট ধাপে কাজ হয়, শেষে একটি পদ বের হয়। ফাংশনেও তাই: ইনপুট ঢোকে (parameter), ভেতরে কাজ হয়, ফল বের হয় (return value). রেসিপি একবার ঠিকঠাক লিখে রাখলে প্রতিবার নতুন করে ভাবতে হয় না — শুধু বলেন “ওই রেসিপিটা বানাও”।

পাড়ার চায়ের দোকানের কথা ভাবুন। মামাকে প্রতিবার বোঝাতে হয় না “পানি ফোটান, পাতা দিন, দুধ দিন, চিনি দুই চামচ”। আপনি শুধু বলেন “এক কাপ দুধ চা, চিনি কম”। “দুধ চা” হলো ফাংশনের নাম, “চিনি কম” হলো argument, আর হাতে পাওয়া কাপটি হলো return value. ভেতরে মামা কীভাবে বানালেন, তা আপনার জানার দরকার নেই — শুধু জানেন কী দিলে কী পাবেন। এই “ভেতরটা লুকিয়ে শুধু ব্যবহারের নিয়ম জানানো”কে বলে abstraction, যা পর্ব ০২-এ সমস্যা ভাঙার সময় দেখেছিলেন।

ফাংশন কেন দরকার? তিনটি বড় কারণ:

  • নাম দেওয়া: price * 1.15 দেখে বোঝা কঠিন এটা কী। add_vat(price) পড়লেই বোঝা যায়। ভালো নাম একটি ধারণাকে এক শব্দে বেঁধে ফেলে।
  • একবার পরীক্ষা, বহুবার ব্যবহার: VAT-এর হিসাব যদি প্রোগ্রামের ২০ জায়গায় কপি করা থাকে, হার বদলালে ২০ জায়গায় বদলাতে হবে — আর একটা বাদ পড়বেই। ফাংশনে থাকলে বদল এক জায়গায়।
  • ভাগ করে ভাবা: “বিল তৈরি” একটি বড় কাজ। একে ভাঙুন — “দাম যোগ”, “ছাড় বাদ”, “VAT যোগ”, “রসিদ ছাপা”। প্রতিটি আলাদা ফাংশন, প্রতিটি আলাদাভাবে বোঝা ও পরীক্ষা করা যায়।

প্রথম ফাংশনের গঠন

def add_vat(price, rate):          # def + নাম + (parameter) + কোলন
    total = price * (1 + rate)     # body — চার space ভেতরে
    return total                   # ফল ফেরত

bill = add_vat(500, 0.15)          # ডাকা: argument 500 আর 0.15
print(bill)                        # 575.0

লাইন ধরে দেখুন। def একটি keyword, যা বলে “এখানে একটি ফাংশন সংজ্ঞায়িত হচ্ছে”। এরপর নাম (add_vat — PEP 8 অনুযায়ী ছোট হাতের অক্ষর আর আন্ডারস্কোর), বন্ধনীর ভেতরে parameter-এর নাম, শেষে কোলন। এর নিচে indentation দিয়ে লেখা অংশটুকুই ফাংশনের body — if বা for-এর মতোই। Indentation যেখানে শেষ, ফাংশনও সেখানে শেষ।

মূল ধারণা

Parameter হলো ফাংশনের সংজ্ঞায় লেখা খালি ঘরের নাম (price, rate). Argument হলো ডাকার সময় পাঠানো আসল মান (500, 0.15). ফর্মের “নাম: ______” ঘরটি parameter; আপনি যা লিখে জমা দেন, তা argument. ডাকার মুহূর্তে Python প্রতিটি parameter নামকে সংশ্লিষ্ট argument object-এর সাথে জুড়ে দেয় — পর্ব ০৫-এর সেই লেবেল-ছবি, শুধু নতুন একটি লেবেল।

আরেকটি গুরুত্বপূর্ণ বিষয়: def লাইনটি চলার সময় ফাংশনের ভেতরের কোড চলে না. def শুধু একটি function object তৈরি করে আর add_vat নামটি তার গায়ে লাগায়। ভেতরের কোড চলে তখনই, যখন আপনি বন্ধনী দিয়ে ডাকেন — add_vat(500, 0.15). রেসিপি লিখে রাখা আর রান্না করা আলাদা কাজ। এজন্যই ফাংশনের ভেতরে বানান-ভুল থাকলেও ডাকার আগ পর্যন্ত NameError দেখা দেয় না।

ফাংশনের জীবন — সংজ্ঞা থেকে ফল পর্যন্ত

argument ঢোকে, body চলে, return value বের হয়

  1. Argument500, 0.15ডাকার সময় পাঠানো মান
  2. Parameterprice, rateমানগুলোর ভেতরের নাম
  3. Bodyprice * (1 + rate)আসল কাজ
  4. Return575.0ডাকা জায়গায় ফেরত
ছবি ১৩.১ — ফাংশনের বাইরে থেকে দেখা যায় শুধু দুই প্রান্ত: কী দিলেন আর কী পেলেন।

২ · return — ফল ফেরত দেওয়া, একাধিক মান আর None

return দুটি কাজ একসাথে করে: ফাংশন সেখানেই থামায়, আর একটি মান ডাকা জায়গায় পাঠায়. return-এর পরের লাইন আর চলে না। ডাকা জায়গায় add_vat(500, 0.15) পুরো লেখাটি যেন সেই মান দিয়ে বদলে যায় — তাই একে ভ্যারিয়েবলে রাখা যায়, অন্য হিসাবে বসানো যায়, এমনকি অন্য ফাংশনের argument হিসেবেও পাঠানো যায়: print(add_vat(500, 0.15)).

একাধিক মান ফেরত — আসলে একটি tuple

অনেক সময় একটি হিসাব থেকে দুটি ফল দরকার — যেমন BMI-র সংখ্যা আর তার শ্রেণি। Python-এ লেখা যায় return bmi, "স্বাভাবিক". দেখে মনে হয় দুটি মান ফেরত যাচ্ছে, কিন্তু আসলে কমা দিয়ে একটি tuple তৈরি হয় — দুটি মানের একটি প্যাকেট — আর সেই একটি tuple-ই ফেরত যায়। ডাকার জায়গায় value, category = bmi_report(...) লিখলে প্যাকেটটি খুলে দুটি নামে বসে যায়। একে বলে unpacking. Tuple আর unpacking-এর পুরো গল্প পর্ব ১৫-এ।

যে ফাংশন কিছু return করে না — None

যদি ফাংশনে return না-ই থাকে, অথবা শুধু return লেখা থাকে কোনো মান ছাড়া? তখন Python নিজে থেকেই একটি বিশেষ object ফেরত দেয়: None. এর মানে “কোনো মান নেই”। None নিজেও একটি object (type NoneType), আর পুরো প্রোগ্রামে এর একটিই কপি থাকে — তাই None পরীক্ষা করা হয় is দিয়ে: if result is None:. অর্থাৎ Python-এ প্রতিটি ফাংশন ডাক কিছু না কিছু ফেরত দেয় — কখনো আপনার দেওয়া মান, কখনো None.

নিচের প্রোগ্রামে তিনটি বাস্তব ফাংশন একসাথে দেখুন — VAT-এর হিসাব (ডিফল্ট হারসহ), BMI-র মান ও শ্রেণি, আর একটি অভিবাদন যা শুধু ছাপে। চালানোর আগে অনুমান করুন শেষ লাইন কী ছাপবে:

def add_vat(price, rate=0.15):
    """দাম আর VAT-এর হার নিয়ে VAT-সহ মোট দাম ফেরত দেয়।"""
    return round(price * (1 + rate), 2)

def bmi_report(weight_kg, height_m):
    bmi = round(weight_kg / height_m ** 2, 1)
    if bmi < 18.5:
        return bmi, "কম ওজন"
    if bmi < 25:
        return bmi, "স্বাভাবিক"
    return bmi, "বেশি ওজন"

def greet(name):
    print(f"স্বাগতম, {name}!")

print(add_vat(1000))
print(add_vat(1000, rate=0.05))
value, category = bmi_report(68, 1.70)
print(value, category)
result = greet("রিনা")
print(result)

আউটপুট পাঁচ লাইনের: 1150.0, 1050.0, 23.5 স্বাভাবিক, স্বাগতম, রিনা! আর None. প্রথম ডাকে rate পাঠানো হয়নি, তাই default 0.15 বসেছে; দ্বিতীয় ডাকে নাম ধরে rate=0.05 পাঠানো হয়েছে। bmi_report একটি tuple ফেরত দিয়েছে, যা দুটি নামে খুলে বসেছে — আর এটি early return-এর উদাহরণও: শর্ত মিললেই ফাংশন সেখানেই শেষ, তাই elif/else লাগেনি। greet পর্দায় লেখা ছাপিয়েছে, কিন্তু কিছু return করেনি — তাই result-এ এসেছে None. BMI-র সীমা (১৮.৫ আর ২৫) WHO-র সাধারণ শ্রেণিবিভাগ থেকে নেওয়া; এটি শেখার উদাহরণ, চিকিৎসার পরামর্শ নয়।

Early return কেন ভালো? শর্তের ভেতরে শর্ত, তার ভেতরে আরও শর্ত — এমন গভীর ভাঁজ পড়া কঠিন। ভুল ইনপুট বা সহজ ক্ষেত্র শুরুতেই return দিয়ে বিদায় করলে বাকি কোড বাঁ দিকে সোজা থাকে। যেমন: if amount <= 0: return 0 লিখে তারপর আসল হিসাব। পেশাদাররা একে বলেন “guard clause”.

৩ · print বনাম return — নতুনদের সবচেয়ে সাধারণ গোলমাল

পর্দায় দুটোই একই রকম দেখায় বলে নতুন প্রোগ্রামাররা প্রায়ই print আর return গুলিয়ে ফেলেন। কিন্তু দুটির গন্তব্য সম্পূর্ণ আলাদা। print-এর গন্তব্য মানুষের চোখ — লেখাটি টার্মিনালে যায়, প্রোগ্রাম আর সেটি ফেরত পায় না। return-এর গন্তব্য প্রোগ্রামের বাকি অংশ — মানটি ডাকা জায়গায় ফেরত আসে, যেখানে আরও হিসাব হতে পারে। নিচে দুটি প্রায় একই রকম ফাংশন দেখুন, আর ধাপে ধাপে লক্ষ করুন কে কী পায়:

1def show_total(a, b):2    print(a + b)34def get_total(a, b):5    return a + b67x = show_total(2, 3)8y = get_total(2, 3)9print(x, y)
ডাকা জায়গার নাম
x
y
  1. show_total(2, 3) ডাকা হলো। Python লাইন ৭-এ এসে ডান দিক আগে হিসাব করে — তাই ফাংশনটি ডাকা হলো। x এখনো কোনো মান পায়নি।
  2. ভেতরে print চলল। লাইন ২-এ print(a + b) টার্মিনালে 5 ছাপল। লেখাটি মানুষের চোখের জন্য গেল — প্রোগ্রামের হাতে সংখ্যা 5 রইল না।
  3. ফাংশন শেষ, return নেই। Body শেষ হয়ে গেল কিন্তু কোথাও return নেই। তাই Python নিজেই None ফেরত দিল, আর x লেবেলটি None-এ লাগল। পর্দায় 5 দেখলেও x-এ 5 নেই!
  4. get_total(2, 3) ডাকা হলো। লাইন ৫-এ a + b হিসাব হলো, ফল 5 — কিন্তু পর্দায় কিছুই ছাপা হলো না। return নিঃশব্দে কাজ করে।
  5. মান ফেরত এল। 5 ডাকা জায়গায় ফিরে এল, y লেবেলটি 5-এ লাগল। এখন এই মান দিয়ে যোগ, তুলনা, আরেক ফাংশনে পাঠানো — সবই করা যায়।
  6. শেষ print. print(x, y) ছাপল None 5. পর্দার প্রথম 5 এসেছিল ফাংশনের ভেতরের print থেকে; x নিজে খালি হাতে ফিরেছে। যদি এরপর x + 1 লিখতেন, পেতেন TypeError — None-এর সাথে যোগ হয় না।
print → মানুষকে দেখায়, প্রোগ্রাম কিছু পায় না
return → প্রোগ্রামকে মান দেয়, পর্দায় কিছু দেখায় না

তাহলে কোনটি কখন? নিয়মটি সহজ: হিসাবের ফাংশন return করবে; ছাপানোর কাজ করবে একেবারে বাইরের অংশ. একটি GPA ফাংশন যদি নিজেই ছাপিয়ে দেয়, তাহলে সেই GPA দিয়ে মেধাতালিকা সাজানো, সার্টিফিকেটে বসানো বা ওয়েবসাইটে দেখানো — কোনোটিই করা যাবে না। মান return করলে ছাপাতে চাইলে বাইরে print(gpa(...)) লিখলেই হয়; উল্টোটা সম্ভব নয়।

REPL-এর ফাঁদ: REPL-এ >>> get_total(2, 3) লিখলে 5 দেখায় — মনে হয় ফাংশন ছাপিয়েছে। আসলে REPL নিজেই যেকোনো expression-এর ফল দেখায় (শুধু None হলে কিছু দেখায় না)। একই লাইন .py ফাইলে লিখে চালালে কিছুই ছাপা হবে না। তাই “REPL-এ তো দেখাচ্ছিল!” — এটি print আর return গুলিয়ে ফেলার একটি বড় উৎস।

৪ · ফাংশন ডাকলে ভেতরে কী ঘটে — call stack আর frame

একটি ফাংশন আরেকটি ফাংশনকে ডাকতে পারে, সেটি আবার আরেকটিকে। Python কীভাবে মনে রাখে কে কাকে ডেকেছে, আর কাজ শেষে কোথায় ফিরতে হবে? উত্তর: call stack. প্রতিবার কোনো ফাংশন ডাকা হলে Python একটি নতুন frame তৈরি করে — একটি ছোট খাতা, যেখানে লেখা থাকে সেই ডাকের parameter, local variable, আর কোন লাইনে কাজ চলছে। নতুন frame রাখা হয় আগেরগুলোর ওপরে, প্লেটের স্তূপের মতো। return হলে ওপরের frame তুলে ফেলা হয়, আর কাজ চলে নিচের frame-এ, ঠিক যেখানে থেমেছিল।

নিচের প্রোগ্রামে add_vat নিজের কাজের মাঝে round_taka-কে ডাকে। “পরে” চাপুন আর stack-এর ওঠানামা দেখুন:

1def round_taka(amount):2    return round(amount)34def add_vat(price, rate=0.15):5    with_vat = price * (1 + rate)6    return round_taka(with_vat)78total = add_vat(200)9print(total)
Call stack (নিচ থেকে ওপরে)
stack
add_vat
round_taka
total
  1. প্রথম ডাক। লাইন ৮-এ add_vat(200) ডাকা হলো। সবচেয়ে নিচে আছে module-এর frame (ফাইলের মূল অংশ)। তার ওপরে উঠল add_vat-এর নতুন frame: price=200, আর argument না পাওয়ায় rate পেল default 0.15.
  2. Local variable তৈরি। লাইন ৫-এ with_vat তৈরি হলো — শুধু এই frame-এর ভেতরে। মানটি খেয়াল করুন: 229.99999999999997, ঠিক 230 নয়। এটাই পর্ব ০৭-এর float-এর ফাঁদ, আর এজন্যই টাকা গোল করার আলাদা ফাংশন দরকার।
  3. ফাংশনের ভেতর থেকে ফাংশন। লাইন ৬-এ add_vat ডাকল round_taka-কে। add_vat থেমে অপেক্ষা করছে; তার ওপরে উঠল নতুন frame, যেখানে amount নামটি একই float object-কে দেখাচ্ছে।
  4. ভেতরের return. round(amount) দিল 230, আর round_taka সেটি ফেরত দিল। এই মুহূর্তে তার frame তুলে ফেলা হবে — amount নামটি আর কোথাও থাকবে না।
  5. ফিরে এসে আবার return. কাজ ফিরল add_vat-এ, ঠিক লাইন ৬-এ যেখানে থেমেছিল। round_taka(with_vat)-এর জায়গায় এখন 230, আর সেটিই add_vat ফেরত দিচ্ছে।
  6. Frame নামল, মান পৌঁছাল। add_vat-এর frame-ও উঠে গেল, সঙ্গে price, rate, with_vat. শুধু module frame বাকি, আর total লেবেলটি 230-এ লাগল।
  7. ফল ছাপা। লাইন ৯ ছাপল 230. পুরো যাত্রায় stack দুই ধাপ উঠেছে, দুই ধাপ নেমেছে — প্রতিটি ডাকের জন্য একটি frame, প্রতিটি return-এ একটি frame কম।

Local variable সত্যিই local

ধাপ ৬ থেকে একটি গুরুত্বপূর্ণ নিয়ম বেরিয়ে আসে: ফাংশনের ভেতরে তৈরি নাম — parameter আর ভেতরে assign করা ভ্যারিয়েবল — হলো local variable. এরা থাকে শুধু সেই ডাকের frame-এ, আর frame মুছলে এরাও মুছে যায়। তাই ফাংশনের বাইরে print(with_vat) লিখলে NameError আসবে। একইভাবে, বাইরে price নামে কোনো ভ্যারিয়েবল থাকলেও ভেতরের price তার সাথে ধাক্কা খায় না — দুটি আলাদা খাতায় দুটি আলাদা লেবেল। প্রতিটি ডাক নতুন frame পায়, তাই একই ফাংশন দুবার ডাকলেও আগের ডাকের local variable পরের ডাকে থাকে না। ভেতর থেকে বাইরের নাম পড়া যায় কি না, বদলানো যায় কি না, global ও nonlocal কী — এই পুরো scope-এর নিয়ম (LEGB) পর্ব ২০-এ।

Error এলে Python যে traceback দেখায়, সেটি আসলে call stack-এর একটি ছবি — “module ডেকেছিল add_vat-কে, সে ডেকেছিল round_taka-কে, সেখানে এই লাইনে সমস্যা”। তাই traceback নিচ থেকে ওপরে পড়লে আপনি দেখতে পান কোন পথ ধরে প্রোগ্রাম সমস্যায় পৌঁছেছে। পর্ব ০৪-এর traceback পড়ার কৌশল এখন আরও অর্থবহ হলো।

৫ · Argument পাঠানোর নিয়ম — positional, keyword আর default

এ পর্যন্ত আমরা argument পাঠিয়েছি ক্রম ধরে: প্রথম মান প্রথম parameter-এ, দ্বিতীয় মান দ্বিতীয়তে। একে বলে positional argument. কিন্তু parameter বেশি হলে ক্রম মনে রাখা কঠিন। convert(500, 120, 1) দেখে কে বলবে কোনটি টাকা, কোনটি হার, কোনটি দশমিকের ঘর? তাই Python নাম ধরেও পাঠাতে দেয়: convert(500, rate=120, digits=1) — এটি keyword argument, যা আপনি পর্ব ০৪-এ print(..., sep="-")-তে দেখেছিলেন।

আর default argument মানে সংজ্ঞাতেই একটি মান বসিয়ে রাখা, যেমন rate=0.15. ডাকার সময় সেই argument না দিলে default বসে যায়। এতে সাধারণ ক্ষেত্রে ডাকা সহজ হয়, আবার বিশেষ ক্ষেত্রে বদলানোও যায়। নিচে একটি টাকা-থেকে-ডলার ফাংশনে দেখুন Python কীভাবে argument আর parameter মেলায়:

1def to_dollar(taka, rate=122.0, symbol="$", *, digits=2):2    return f"{symbol}{round(taka / rate, digits)}"34print(to_dollar(500, digits=1, rate=120))
Parameter-এ কী বসল
taka
rate
symbol
digits
  1. ডাকের তিনটি argument. একটি positional (500) আর দুটি keyword (digits=1, rate=120). লক্ষ করুন, keyword-গুলো উল্টো ক্রমে লেখা — তাতে কোনো সমস্যা নেই।
  2. আগে positional, বাঁ থেকে ডানে। Python প্রথমে positional argument-গুলো ক্রম ধরে বসায়। একটিই আছে, তাই 500 গেল প্রথম parameter taka-তে।
  3. তারপর keyword, নাম মিলিয়ে। rate=120 গেল rate-এ, digits=1 গেল digits-এ — ক্রম দেখা হয় না, শুধু নাম। digits লেখা আছে *-এর পরে, তাই এটিকে নাম ছাড়া পাঠানোর উপায়ই নেই।
  4. খালি ঘরে default. symbol-এর জন্য কেউ কিছু পাঠায়নি, তাই সংজ্ঞার default "$" বসল। যদি default ছাড়া কোনো parameter খালি থাকত, তাহলে TypeError: to_dollar() missing 1 required positional argument: 'taka'-এর মতো error আসত।
  5. Body চলল। 500 ÷ 120 = 4.1666…, এক ঘর দশমিকে গোল করলে 4.2, সামনে $. ছাপা হলো $4.2. (১২২ বা ১২০ টাকা — দুটিই শুধু উদাহরণের হার; আসল বিনিময় হার প্রতিদিন বদলায়।)

মেলানোর নিয়ম — যা ভাঙলে error

  • Positional আগে, keyword পরে: to_dollar(rate=120, 500) লিখলে চালানোর আগেই SyntaxError — keyword-এর পরে positional বসানো যায় না।
  • একই parameter দুবার নয়: to_dollar(500, taka=600) লিখলে TypeError: to_dollar() got multiple values for argument 'taka'.
  • অচেনা নাম নয়: to_dollar(500, rat=120) — বানান ভুল — দিলে TypeError: to_dollar() got an unexpected keyword argument 'rat'. Did you mean 'rate'? ভালো দিক: ভুলটা চুপচাপ লুকিয়ে থাকে না, আর Python 3.14 সঠিক নামটিও সুপারিশ করে।
  • সংজ্ঞায় default-ওয়ালা parameter পরে: def f(a=1, b): লেখা যায় না, কারণ তখন f(5)-এর 5 কোথায় বসবে বোঝা যায় না।

Default-এ বদলানো যায় না এমন মান রাখুন

Default মান তৈরি হয় একবারই — def লাইন চলার সময়, প্রতিটি ডাকে নতুন করে নয়। সংখ্যা, string বা None-এর মতো immutable মানে এতে কোনো সমস্যা নেই। কিন্তু default হিসেবে একটি খালি list (items=[]) দিলে সব ডাক একই list ভাগাভাগি করে, আর এক ডাকে যোগ করা জিনিস পরের ডাকেও থেকে যায়।

এক লাইনের সতর্কতা: default হিসেবে কখনো [] বা {} দেবেন না; দিন None, তারপর ভেতরে if items is None: items = []. কেন এমন হয়, তার পুরো ব্যাখ্যা — mutability আর “default-এর ফাঁদ” — পর্ব ১৯ ও পর্ব ২০-এ।

* আর / — কোন argument কীভাবে পাঠানো যাবে, তা ঠিক করা

কখনো আপনি চান কিছু argument অবশ্যই নাম ধরে পাঠাতে হবে। ধরুন transfer(5000, True, False) — True আর False এখানে কী বোঝায়, পড়ে বোঝার উপায় নেই। সংজ্ঞায় একটি একক * বসালে তার পরের সব parameter হয়ে যায় keyword-only: def transfer(amount, *, confirm=True, notify=False). এখন ডাকতে হবে transfer(5000, notify=True) — পড়লেই অর্থ পরিষ্কার। Python-এর নিজের sorted()-এ key আর reverse এভাবেই keyword-only.

উল্টোটাও আছে। সংজ্ঞায় / বসালে তার আগের parameter-গুলো হয়ে যায় positional-only — নাম ধরে পাঠানো যায় না: def bmi(weight, height, /). এতে ফাংশন-লেখক পরে parameter-এর নাম বদলালেও কারও কোড ভাঙে না। এটি এসেছে Python 3.8 থেকে (PEP 570). অনেক built-in ফাংশন আগে থেকেই এমন — len(obj=[1, 2]) লিখে দেখুন, TypeError পাবেন। নতুনদের এগুলো রোজ লিখতে হবে না, তবে ডকুমেন্টেশনে sorted(iterable, /, *, key=None, reverse=False) দেখলে এখন বুঝবেন কোন অংশ কী বলছে।

সামনে যা আসছে: যত খুশি argument নেওয়া যায় এমন ফাংশন — *args আর **kwargs — পর্ব ২০-এ। আর এক লাইনের ছোট নামহীন ফাংশন lambda পর্ব ৩৮-এ।

৬ · ভালো ফাংশনের গুণ — docstring, type hint, pure function আর assert

ফাংশন লেখা সহজ; ভালো ফাংশন লেখা একটি দক্ষতা। ভালো ফাংশন পড়লে বোঝা যায়, ব্যবহার করতে ভয় লাগে না, আর বদলালে কিছু ভাঙে না। চারটি অভ্যাস এই তিনটি গুণ এনে দেয়।

১. Docstring আর help()

ফাংশনের def লাইনের ঠিক নিচে তিন কোটে লেখা প্রথম string-টি হলো docstring. Comment-এর সাথে পার্থক্য: Python docstring ফেলে দেয় না, ফাংশনের __doc__-এ রেখে দেয়। তাই help(add_vat) লিখলে বা VS Code-এ ফাংশনের নামের ওপর মাউস রাখলে সেটি দেখায়। ভালো docstring বলে কী করে, কী নেয়, কী ফেরত দেয় — কীভাবে করে তা নয়, সেটা কোডই বলবে।

২. Type hint — নথি হিসেবে

def gpa(all_marks: list[int]) -> float: — এখানে : list[int] আর -> float হলো type hint. এরা পাঠককে (আর editor-কে) জানায় কী ধরনের মান আসবে, কী ফেরত যাবে। কিন্তু Python চলার সময় এগুলো যাচাই করে না — gpa("abc") ডাকলেও hint-এর কারণে কোনো error আসবে না; আসবে ভেতরের কোনো লাইনে গিয়ে। Hint যাচাই করে mypy বা pyright-এর মতো আলাদা টুল, পর্ব ৩৭-এ দেখবেন। আপাতত এগুলোকে ভাবুন জীবন্ত নথি হিসেবে, যা editor-এর auto-complete-কেও সাহায্য করে।

৩. ছোট, এক-কাজের, আর যতটা সম্ভব pure

একটি ফাংশন একটি কাজ করবে — যার নাম এক বাক্যে বলা যায়। “নম্বর থেকে grade point বের করে” ভালো। “নম্বর পড়ে, grade বের করে, ফাইলে লেখে, ইমেইল পাঠায়” — এটি আসলে চারটি ফাংশন। মোটামুটি একটি নিয়ম: ফাংশন যদি পুরো স্ক্রিনে না আঁটে, বা নাম দিতে গিয়ে “and” লাগে, ভাঙার কথা ভাবুন।

আরও ভালো হয় যদি ফাংশনটি pure function হয়: একই ইনপুটে সবসময় একই ফল দেয়, আর বাইরের কিছু বদলায় না। add_vat(1000) সবসময় 1150.0 — এটি pure. অন্যদিকে যে ফাংশন ছাপে, ফাইলে লেখে, ডেটাবেস বদলায় বা বাইরের list-এ যোগ করে, তার আছে side effect. Side effect খারাপ নয় — প্রোগ্রামের কাজই শেষ পর্যন্ত কিছু ছাপা বা সেভ করা। কৌশল হলো দুটিকে আলাদা রাখা: হিসাব pure ফাংশনে, side effect বাইরের পাতলা স্তরে. তাহলে হিসাবের অংশ পরীক্ষা করা অনেক সহজ হয়।

৪. assert দিয়ে এক লাইনে পরীক্ষা

assert একটি statement, যা বলে “এই কথাটি সত্য হতেই হবে”। সত্য হলে কিছু ঘটে না; মিথ্যা হলে AssertionError দিয়ে প্রোগ্রাম থামে। Pure ফাংশন পরীক্ষার জন্য এটি সবচেয়ে সহজ উপায়: assert add_vat(1000) == 1150.0. সীমার মানগুলো (৭৯ আর ৮০, ৩২ আর ৩৩) পরীক্ষা করুন — ভুল সাধারণত সেখানেই লুকায়। পেশাদার পরীক্ষার টুল pytest-ও ভেতরে এই assert-ই ব্যবহার করে; বিস্তারিত পর্ব ৩৯-এ। (সতর্কতা: python -O দিয়ে চালালে assert বাদ পড়ে যায়, তাই ব্যবহারকারীর ইনপুট যাচাইয়ে এটি ব্যবহার করবেন না — সেটা পর্ব ২৪-এর exception-এর কাজ।)

চারটি অভ্যাস একসাথে দেখুন — বাংলাদেশের প্রচলিত ৫-স্কেলে নম্বর থেকে grade point আর GPA. এখানে ফাংশনকে argument হিসেবে পাঠানোর একটি ঝলকও আছে:

def grade_point(marks: int) -> float:
    """নম্বর (0-100) থেকে ৫-স্কেলের grade point ফেরত দেয়।"""
    for cutoff, point in [(80, 5.0), (70, 4.0), (60, 3.5), (50, 3.0), (40, 2.0), (33, 1.0)]:
        if marks >= cutoff:
            return point          # পেলেই সঙ্গে সঙ্গে ফেরত
    return 0.0

def gpa(all_marks: list[int]) -> float:
    total = 0.0
    for m in all_marks:
        total += grade_point(m)
    return round(total / len(all_marks), 2)

assert grade_point(80) == 5.0 and grade_point(79) == 4.0 and grade_point(32) == 0.0
print(grade_point.__doc__)

def by_gpa(student):
    return gpa(student[1])

students = [("রহিম", [85, 72, 64]), ("সুমি", [90, 88, 79]), ("জামিল", [55, 61, 47])]
for name, marks in sorted(students, key=by_gpa, reverse=True):
    print(name, gpa(marks))

আউটপুট: প্রথমে docstring-এর লেখা, তারপর সুমি 4.67, রহিম 4.17, জামিল 2.83. grade_point সীমার তালিকা ওপর থেকে দেখে, প্রথম মিলেই early return করে; কোনোটিতে না মিললে লুপ শেষে 0.0. assert লাইন নিঃশব্দে পার হয়েছে, মানে তিনটি সীমা-পরীক্ষাই সঠিক। শেষ অংশে sorted-কে আমরা by_gpa ফাংশনটিই পাঠিয়েছি — বন্ধনী ছাড়া, অর্থাৎ ডেকে ফল নয়, ফাংশন object নিজে। sorted প্রতিটি ছাত্রের জন্য সেটি ডেকে GPA বের করে সাজিয়েছে, reverse=True (keyword-only) দিয়ে বড় থেকে ছোট। দুটি ফাংশনই pure, তাই পরীক্ষা করা এত সহজ। (বাস্তবের SSC/HSC-তে কোনো বিষয়ে F থাকলে পুরো GPA শূন্য হয়, আর ঐচ্ছিক বিষয়ের আলাদা নিয়ম আছে — উদাহরণটি সেগুলো বাদ দিয়ে সরল করা।)

C/C++ জানলে

C-তে প্রতিটি parameter আর return-এর type লিখতে হয়, ব্যবহারের আগে prototype লাগে, আর একটির বেশি মান ফেরত দিতে pointer বা struct লাগে; Python-এ এর কোনোটিই লাগে না, tuple দিয়েই একাধিক মান ফেরত যায়। Default argument C-তে নেই, C++-এ আছে; keyword argument দুটিতেই নেই। আরেকটি পার্থক্য: C argument-এর মান কপি করে পাঠায়, আর Python পাঠায় object-এর reference — তাই ফাংশনের ভেতরে একটি list বদলালে বাইরেও বদলায় (বিস্তারিত পর্ব ১৯–২০)। Call stack আর frame-এর ধারণা দুই ভাষায় একই; C-র দৃষ্টিকোণ থেকে দেখতে C কোর্সের ফাংশন পর্ব পড়ুন।

৭ · ফাংশনও একটি object — অন্য ফাংশনে পাঠানো

পর্ব ০৫-এ দেখেছিলেন, Python-এ সবকিছুই object. ফাংশনও ব্যতিক্রম নয়। def by_gpa(...) লিখলে একটি function object তৈরি হয়, আর by_gpa নামটি তার লেবেল। তাই একটি সংখ্যা বা string দিয়ে যা করা যায়, ফাংশন দিয়েও তা করা যায়: অন্য নামে রাখা, list বা dict-এ রাখা, অন্য ফাংশনে argument হিসেবে পাঠানো, এমনকি ফাংশন থেকে ফেরত দেওয়া। একে বলে first-class function.

মূল ধারণা

by_gpa (বন্ধনী ছাড়া) = ফাংশন object নিজে, যেন রেসিপির কাগজটি হাতে দেওয়া।
by_gpa(s) (বন্ধনীসহ) = ফাংশনটি ডেকে পাওয়া ফল, যেন রান্না করা খাবার।
sorted(..., key=by_gpa)-এ আমরা কাগজটি দিই, যাতে sorted নিজে প্রয়োজনমতো রান্না করে নেয়। ভুল করে key=by_gpa() লিখলে আগেই ডাকা হয়ে যায় — আর TypeError.

এর বাস্তব প্রয়োগ সর্বত্র। একটি bKash-ধাঁচের মেনু ভাবুন: actions = {"1": send_money, "2": cash_out, "3": check_balance}, তারপর actions[choice]() — লম্বা if/elif-এর বদলে একটি dict. max(products, key=get_price) দামি পণ্য খোঁজে, sorted(names, key=len) নামের দৈর্ঘ্য ধরে সাজায় — len-ও তো একটি ফাংশন। এই ধারণার ওপর দাঁড়িয়ে আছে দুটি বড় বিষয়, যা পরে আসবে: lambda (এক লাইনের নামহীন ফাংশন, পর্ব ৩৮) আর decorator (একটি ফাংশনকে মুড়িয়ে নতুন ফাংশন বানানো, @ চিহ্ন দিয়ে লেখা — পর্ব ৩৫)।

৮ · নিজেকে ডাকা ফাংশন — recursion-এর প্রথম ঝলক

একটি ফাংশন যদি অন্য ফাংশন ডাকতে পারে, তাহলে নিজেকে ডাকতে পারবে না কেন? পারে — একে বলে recursion. শর্ত একটাই: প্রতিটি ডাকে সমস্যা একটু ছোট হতে হবে, আর কোথাও এমন একটি সহজ অবস্থা থাকতে হবে যেখানে আর নিজেকে ডাকা হয় না — একে বলে base case. Factorial-এর সংজ্ঞাই recursive: ৩! = ৩ × ২!, ২! = ২ × ১!, আর ১! = ১ (এখানে থামা)। নিচে stack-এর ছবিতে দেখুন:

1def factorial(n):2    if n <= 1:3        return 14    return n * factorial(n - 1)56print(factorial(3))
Call stack (ওপরেরটি এখন চলছে)
stack
অপেক্ষায়
ফেরত
  1. factorial(3). n=3, base case নয়। লাইন ৪ বলে 3 * factorial(2) — কিন্তু factorial(2) কত, তা জানতে হলে আগে সেটি ডাকতে হবে। গুণটি অসমাপ্ত রেখে frame অপেক্ষায় রইল।
  2. factorial(2). নতুন frame, নিজের আলাদা n=2. আগের frame-এর n এখনো 3 — দুটি আলাদা খাতা। আবার 2 * factorial(1), আবার অপেক্ষা।
  3. factorial(1) — base case. n <= 1 সত্য, তাই আর নিজেকে ডাকা নয় — সরাসরি 1 ফেরত। এই থামার জায়গা না থাকলে ডাক চলতেই থাকত। Stack এখন সবচেয়ে উঁচু: চারটি frame.
  4. ফেরার পথ শুরু। ওপরের frame উঠে গেল। factorial(2) এবার তার অসমাপ্ত গুণ শেষ করল: 2 * 1 = 2, সেটি ফেরত দিল।
  5. আরও এক ধাপ নিচে। factorial(3) পেল 2, করল 3 * 2 = 6, ফেরত দিল। ডাকার পথে যাওয়া হয়েছিল ওপরে, হিসাব হয়েছে ফেরার পথে।
  6. ফল পৌঁছাল। শুধু module frame বাকি, print ছাপল 6.

Recursion-এর সীমা: প্রতিটি ডাক একটি frame নেয়, তাই stack অসীম বড় হতে পারে না। CPython ডিফল্টে মোটামুটি ১০০০ স্তর পর্যন্ত গভীরে যেতে দেয় (sys.getrecursionlimit() দেখায় 1000); এর বেশি হলে RecursionError: maximum recursion depth exceeded. Base case ভুলে গেলে এটাই দেখবেন। Python অনেক ভাষার মতো “tail call” অপ্টিমাইজ করে না, তাই সাধারণ গোনা বা যোগের কাজে Python-এ লুপই পছন্দের পথ — factorial-এর জন্য তো math.factorial আছেই। Recursion সত্যিকারের কাজে লাগে গাছের মতো গঠনে (ফোল্ডারের ভেতরে ফোল্ডার, JSON-এর ভেতরে JSON) আর ভাগ-করে-জয় অ্যালগরিদমে — সেগুলো পর্ব ৪৫-এ।

পরীক্ষা ও ইন্টারভিউতে
  • Parameter আর argument-এর পার্থক্য? — Parameter হলো সংজ্ঞায় লেখা নাম; argument হলো ডাকার সময় পাঠানো আসল মান।
  • return না থাকলে ফাংশন কী ফেরত দেয়? — None; Python-এ প্রতিটি ফাংশন ডাক কোনো না কোনো মান ফেরত দেয়।
  • print আর return-এর পার্থক্য? — print শুধু পর্দায় লেখা দেখায়, ডাকা জায়গা কিছু পায় না; return মান ফেরত দিয়ে ফাংশন থামায়, পর্দায় কিছু দেখায় না।
  • Python কি একাধিক মান return করতে পারে? — আসলে একটি tuple ফেরত দেয়, যা unpacking দিয়ে কয়েকটি নামে খোলা যায়।
  • Default argument হিসেবে list দেওয়া বিপজ্জনক কেন? — Default একবারই তৈরি হয়, তাই সব ডাক একই list ভাগ করে; None দিয়ে ভেতরে নতুন list বানানোই নিয়ম।

৯ · AI-এর যুগে — AI-এর লেখা ফাংশন যাচাই

“একটা ফাংশন লিখে দাও” — AI-কে সবচেয়ে বেশি এই অনুরোধই করা হয়। AI দ্রুত কাজ চালানোর মতো কোড দেয়, কিন্তু কয়েকটি ভুল বারবার করে, বিশেষ করে যখন অনুরোধ অস্পষ্ট থাকে। এই পর্বের ধারণাগুলো দিয়েই আপনি সেগুলো ধরতে পারবেন:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
৮০–১০০ লাইনের এক বিশাল ফাংশনপড়া, পরীক্ষা আর বদলানো কঠিন; একটি বদলে আরেকটি ভাঙেএক-কাজের ছোট ফাংশনে ভাগ
হিসাবের ফল print করে, return করে নাফল আর কোথাও ব্যবহার করা যায় না; ফেরত আসে Noneহিসাব return করবে, ছাপানো বাইরে
Docstring নেই, বা কোড-পুনরাবৃত্তি করা docstringকী নেয়, কী দেয়, কোন এককে — বোঝা যায় নাকী করে, কী নেয়, কী ফেরত দেয় — এক-দুই লাইনে
Default হিসেবে [] বা {}সব ডাক একই object ভাগ করে, অদ্ভুত বাগNone default, ভেতরে নতুন object
অর্থহীন True/False positional argumentf(x, True, False) পড়ে বোঝা যায় না* দিয়ে keyword-only
বাইরের (global) ভ্যারিয়েবল চুপচাপ বদলায়লুকানো side effect, পরীক্ষা কঠিনইনপুট parameter-এ, ফল return-এ

AI-কে ফাংশন লিখতে বলার সময় শর্তগুলো আগেই বলে দিন। নিচের কাঠামোটি কপি করে নিজের কাজ বসিয়ে ব্যবহার করুন:

আমি Python 3.12+ ব্যবহার করি। একটি ফাংশন লিখে দাও যা একটি দোকানের বিলের মোট দাম, ছাড়ের হার আর VAT-এর হার নিয়ে চূড়ান্ত দাম (টাকায়, পূর্ণসংখ্যায় গোল করা) ফেরত দেয়। শর্ত: ১. হিসাবের ফাংশন মান return করবে, নিজে কিছু print করবে না। ২. প্রতিটি ফাংশন একটি কাজ করবে; দরকার হলে ছোট সহায়ক ফাংশনে ভাগ করো। ৩. প্রতিটি ফাংশনে docstring আর type hint দাও; VAT-এর হার default 0.15, আর হারগুলো keyword-only রাখো। ৪. কোনো default হিসেবে list বা dict ব্যবহার করবে না; global ভ্যারিয়েবল বদলাবে না। ৫. সীমার মানসহ অন্তত ৪টি assert দিয়ে পরীক্ষা লিখে দাও (যেমন ছাড় ০%, ১০০%, দাম ০)। ৬. কোনো অনুমান করলে (যেমন ছাড় আগে না VAT আগে) স্পষ্ট করে লেখো।

AI-এর লেখা ফাংশন পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১ফাংশন বা “subroutine”-এর ধারণা কোথা থেকে এলো? প্রথম দিকের কম্পিউটারে এক জায়গা থেকে কোড ডেকে আবার ফিরে আসা এত কঠিন ছিল কেন?

আজ def লিখে ফাংশন বানানো এত স্বাভাবিক যে মনে হয় এটা সবসময়ই ছিল। কিন্তু প্রথম stored-program কম্পিউটারগুলোর সামনে এটি ছিল একটি আসল প্রকৌশল-সমস্যা। প্রশ্নটা সহজ শোনায়: একটি কোডের টুকরোতে লাফ দিয়ে যাওয়া সহজ, কিন্তু কাজ শেষে কোথায় ফিরতে হবে, তা সেই টুকরোটি জানবে কীভাবে? কারণ তাকে তো প্রোগ্রামের অনেক জায়গা থেকে ডাকা হবে।

ইংল্যান্ডের কেমব্রিজ বিশ্ববিদ্যালয়ে Maurice Wilkes-এর দল ১৯৪৯ সালে EDSAC চালু করে — প্রথম দিকের নিয়মিত ব্যবহৃত stored-program কম্পিউটারগুলোর একটি। দলের তরুণ গবেষক David Wheeler এই সমস্যার একটি বুদ্ধিদীপ্ত সমাধান বের করেন, যা পরে “Wheeler jump” নামে পরিচিত হয়। মূল কৌশল: ডাকার আগে মূল প্রোগ্রাম নিজের বর্তমান ঠিকানাটি একটি রেজিস্টারে রেখে subroutine-এ লাফ দেয়। Subroutine-এর প্রথম কাজ হলো সেই ঠিকানা দিয়ে নিজের শেষ নির্দেশটি বদলে ফেলা — যাতে শেষ লাইনটি হয়ে যায় “এই ঠিকানায় ফিরে যাও”। অর্থাৎ কোড নিজেই নিজেকে বদলাত।

এই “closed subroutine” ধারণা থেকে তৈরি হয় subroutine-এর একটি লাইব্রেরি — ভাগ, বর্গমূল, ছাপা ইত্যাদির জন্য পরীক্ষিত টুকরো, যা যে কেউ নিজের প্রোগ্রামে জুড়ে নিতে পারতেন। ১৯৫১ সালে Wilkes, Wheeler ও Stanley Gill এ নিয়ে একটি বই প্রকাশ করেন, যাকে প্রোগ্রামিংয়ের প্রথম পাঠ্যবইগুলোর একটি ধরা হয়। আজকের Python standard library সেই একই চিন্তার উত্তরসূরি: একবার ভালো করে লিখে পরীক্ষা করো, তারপর সবাই ব্যবহার করুক।

Wheeler jump-এর একটি সীমা ছিল, যা এই পর্বের সাথে সরাসরি যুক্ত: ফেরার ঠিকানা রাখা হতো subroutine-এর ভেতরেই, একটিমাত্র জায়গায়। তাই একটি subroutine নিজেকে ডাকলে (recursion) আগের ফেরার ঠিকানা মুছে যেত। সমাধান হলো ফেরার ঠিকানা আর local মান প্রতিটি ডাকের জন্য আলাদা করে একটি stack-এ রাখা — ঠিক যে call stack আপনি এই পর্বের stepper-এ দেখলেন। ১৯৬০ সালের Algol 60 ভাষা recursion-কে ভাষার অংশ করে, আর তা বাস্তবায়নের জন্য stack-ভিত্তিক ডাক মূলধারায় আসে। আজকের প্রায় সব CPU-তে “call” আর “return” নির্দেশ আর একটি হার্ডওয়্যার stack pointer আছে।

মূল উপলব্ধি: ফাংশন কোনো “সাজসজ্জা” নয় — এটি কম্পিউটিংয়ের প্রথম বড় abstraction-গুলোর একটি, যা এসেছিল একটি বাস্তব সমস্যা থেকে: একই কোড বহু জায়গা থেকে ব্যবহার করা আর ঠিক জায়গায় ফেরা। আপনার প্রতিটি return-এর পেছনে আছে ৭৫ বছরেরও বেশি সময়ের প্রকৌশল।

প্র ০২Python কেন return না থাকলে চুপচাপ None ফেরত দেয়? Pascal-এর মতো “procedure” আর “function” আলাদা করলেই কি ভালো হতো না?

অনেক পুরোনো ভাষা দুই ধরনের উপ-প্রোগ্রাম আলাদা রাখত। Pascal-এ procedure কাজ করে কিন্তু মান দেয় না, function মান দেয়। Fortran-এ SUBROUTINE আর FUNCTION. C এই দুটিকে এক করে, তবে মান-না-দেওয়া ফাংশনের return type লিখতে হয় void, আর void ফাংশনের ফল ব্যবহার করতে চাইলে compiler আগেই আটকে দেয়।

Python বেছে নিয়েছে আরও সরল একটি নিয়ম: প্রতিটি ফাংশন ডাক একটি expression, আর প্রতিটি expression-এর একটি মান থাকে. যে ফাংশন অর্থপূর্ণ কিছু ফেরত দেয় না, সে দেয় একটি “শূন্যতার প্রতীক” — None. এর কয়েকটি সুবিধা আছে:

  • একটিই ধারণা: শিক্ষার্থীকে দুই ধরনের উপ-প্রোগ্রাম শিখতে হয় না। সব def, সব ডাক একই নিয়মে চলে।
  • Dynamic ভাষার সাথে মানানসই: Python চলার আগে জানে না কোন ফাংশন কী ফেরত দেবে (একই ফাংশন কোনো শাখায় মান দিতে পারে, কোনোটিতে না)। তাই “মান নেই” প্রকাশ করার জন্য একটি সত্যিকারের object দরকার, যা নিরাপদে নাম দিয়ে ধরা, তুলনা করা আর পাঠানো যায়।
  • REPL-এ পরিষ্কার: REPL ফল None হলে কিছু দেখায় না, তাই print() ডাকার পর পর্দায় বাড়তি “None” ভাসে না।

তবে দামও আছে, আর সেটা এই পর্বেই দেখেছেন: print-করা ফাংশনের ফল ভ্যারিয়েবলে রাখলে কোনো error আসে না, শুধু None আসে — আর সমস্যা ধরা পড়ে অনেক পরে, অন্য কোনো লাইনে TypeError হিসেবে। C-র compiler যা আগেই ধরত, Python তা চলার সময় পর্যন্ত গড়িয়ে দেয়। এজন্যই type hint-এ -> None লেখা আর mypy-র মতো টুল (পর্ব ৩৭) এই ভুল আগেই ধরতে সাহায্য করে।

একটি মজার নকশা-সিদ্ধান্ত এর সাথে যুক্ত। list.sort() list-টিকে জায়গাতেই সাজায় আর ইচ্ছা করেই None ফেরত দেয়। Python-এর অফিসিয়াল FAQ-তে এর কারণ বলা আছে: সাজানো list ফেরত দিলে মানুষ ভাবত এটি নতুন একটি কপি, অথচ মূল list-টিই বদলে গেছে। None ফেরত দিয়ে Python মনে করিয়ে দেয় “এটি একটি side effect”. নতুন সাজানো list চাইলে আছে sorted(), যা Python 2.4-এ যোগ হয়েছিল। নিয়মটি তাই: বদলায় এমন method সাধারণত None দেয়, নতুন কিছু বানায় এমন ফাংশন সেটি return করে।

মূল উপলব্ধি: “সবসময় একটি মান” নীতি ভাষাকে সরল ও একরূপ রাখে, কিন্তু কিছু ভুল চলার সময় পর্যন্ত লুকিয়ে রাখে। None দেখলে নিজেকে জিজ্ঞেস করুন — ফাংশনটি কি ফল দেওয়ার জন্য, নাকি কিছু বদলানোর জন্য?

প্র ০৩Keyword argument কি শুধু সুবিধা, নাকি পড়ার যোগ্যতার (readability) একটি গুরুত্বপূর্ণ হাতিয়ার? তাহলে Python 3.8 উল্টো পথে হেঁটে positional-only parameter আনল কেন?

কোড একবার লেখা হয়, কিন্তু পড়া হয় বহুবার — Python-এর দর্শনে (PEP 20, “The Zen of Python”) এই কথাটা আছে “Readability counts” হিসেবে। Keyword argument এই দর্শনের সরাসরি প্রয়োগ। তুলনা করুন: connect("db.local", 5432, 30, True) বনাম connect("db.local", port=5432, timeout=30, use_ssl=True). দ্বিতীয়টিতে কোনো ডকুমেন্টেশন খুলতে হয় না। Code review-তে ভুলও সহজে চোখে পড়ে — timeout=5432 দেখলেই খটকা লাগে, কিন্তু চারটি সংখ্যার সারিতে উল্টো ক্রম কেউ টেরও পায় না।

সবচেয়ে বিপজ্জনক হলো “boolean trap” — process(data, True, False). এখানে কোনটা কী, পড়ে বোঝার উপায় নেই, আর দুটি উল্টে গেলেও কোনো error আসবে না। এজন্য Python 3.0-এ PEP 3102 যোগ করে keyword-only parameter (*-এর পরে), যাতে ফাংশন-লেখক নিজেই বাধ্য করতে পারেন: “এই argument নাম ছাড়া পাঠানো যাবে না”। sorted(..., reverse=True) তার পরিচিত উদাহরণ।

তাহলে positional-only (PEP 570, Python 3.8, ২০১৯) কেন? কারণ keyword argument-এর একটি লুকানো দাম আছে: parameter-এর নাম ফাংশনের public চুক্তির অংশ হয়ে যায়। কেউ bmi(weight=68, height=1.7) লিখে ফেললে আপনি আর কখনো weight নাম বদলে mass করতে পারবেন না — তাদের কোড ভেঙে যাবে। কিছু ক্ষেত্রে নামের কোনো অর্থই নেই: len(obj)-এ obj নামটি কারও জানার দরকার নেই, pow(x, y)-এ ক্রমটাই অর্থ। আসলে C-তে লেখা অনেক built-in ফাংশন বহু আগে থেকেই positional-only ছিল, আর ডকুমেন্টেশনে সেটি / চিহ্ন দিয়ে দেখানো হতো — কিন্তু বিশুদ্ধ Python-এ তা লেখার উপায় ছিল না। PEP 570 সেই ফাঁক পূরণ করে, যাতে Python-এ লেখা library-ও একই নিশ্চয়তা দিতে পারে।

আরেকটি সূক্ষ্ম সুবিধা: positional-only parameter থাকলে **kwargs-এর সাথে নামের সংঘাত হয় না (পর্ব ২০-এ এর অর্থ আরও পরিষ্কার হবে)।

তাই দুটি বৈশিষ্ট্য আসলে একই লক্ষ্যের দুই দিক: ফাংশন-লেখক ঠিক করবেন কোন জিনিস চুক্তির অংশ. যেখানে নাম অর্থ বহন করে, সেখানে keyword-only; যেখানে নাম নিছক ভেতরের বিষয়, সেখানে positional-only; বাকিটা দুইভাবেই।

মূল উপলব্ধি: API নকশা মানে শুধু “কাজ করে কি না” নয় — কীভাবে ডাকা হবে, ভবিষ্যতে কী বদলানো যাবে, আর পাঠক এক নজরে কতটা বুঝবে। * আর / ছোট চিহ্ন, কিন্তু এরা সেই নকশার সিদ্ধান্ত কোডেই লিখে রাখে।

প্র ০৪Python recursion-এর গভীরতা মোটামুটি ১০০০-এ সীমিত রাখে কেন? Scheme বা Haskell-এর মতো “tail call optimization” করলে তো এই সীমা লাগত না।

প্রথমে বুঝুন সীমাটি কেন আছে। প্রতিটি ফাংশন ডাক একটি frame নেয়। CPython-এর ইতিহাসের বেশিরভাগ সময়ে একটি Python ডাক ভেতরে C-র stack-ও ব্যবহার করত, আর C stack-এর আকার সীমিত (অপারেটিং সিস্টেম ঠিক করে)। সীমা না থাকলে অসীম recursion গোটা interpreter-কে “segmentation fault” দিয়ে ভেঙে ফেলত — কোনো সুন্দর error ছাড়াই। তাই CPython একটি গণনা রাখে, আর ডিফল্ট ১০০০ পেরোলে পরিষ্কার একটি RecursionError দেয়, যা ধরা ও পড়া যায়। sys.setrecursionlimit() দিয়ে সীমা বাড়ানো যায়, তবে খুব বেশি বাড়ালে আসল stack ফুরিয়ে আবার সেই ক্র্যাশের ঝুঁকি। সাম্প্রতিক version-এ (3.11 থেকে) Python-থেকে-Python ডাক অনেক ক্ষেত্রে আর C stack খায় না, ফলে ভেতরের ব্যবস্থা বদলেছে — কিন্তু ডিফল্ট সীমার ধারণা রয়ে গেছে।

Tail call optimization (TCO) কী? যদি একটি ফাংশনের শেষ কাজই হয় আরেকটি ফাংশন ডাকা (return f(x), কোনো অসমাপ্ত গুণ ছাড়া), তাহলে নতুন frame না বানিয়ে পুরোনোটিই আবার ব্যবহার করা যায়। Scheme-এর মানদণ্ডে এটি বাধ্যতামূলক, তাই সেখানে recursion দিয়ে লুপ লেখাই স্বাভাবিক।

২০০৯ সালের এপ্রিলে Guido van Rossum তাঁর “Neopythonic” ব্লগে দুটি লেখায় ব্যাখ্যা করেন কেন Python TCO করবে না। তাঁর মূল যুক্তিগুলো:

  • Traceback হারিয়ে যায়: frame পুনর্ব্যবহার করলে error-এর সময় পুরো ডাকের পথ আর দেখা যায় না। Python ডিবাগ করার সহজতাকে খুব গুরুত্ব দেয়।
  • এটি শুধু অপ্টিমাইজেশন নয়: একবার কোনো implementation TCO দিলে মানুষ সেটির ওপর নির্ভর করে কোড লিখবে, আর যেখানে TCO নেই (অন্য implementation) সেখানে সেই কোড ভেঙে যাবে। তাই এটি আসলে ভাষার নিয়ম বদলানো।
  • Dynamic নাম: Python-এ return f(x)-এর f চলার সময় অন্য কিছুতে বদলে যেতে পারে, তাই “এটি নিজেকেই ডাকছে” — এমন নিশ্চয়তা compile-এর সময় পাওয়া কঠিন।
  • দর্শন: Guido-র মতে recursion-কে সব প্রোগ্রামিংয়ের ভিত্তি ভাবা একটি তাত্ত্বিক পছন্দ; বাস্তব Python কোডে লুপ বেশি পড়ার যোগ্য।

ফলে Python-এ বাস্তব নিয়ম দাঁড়ায়: গভীরতা যদি ইনপুটের আকারের সমান হয় (যেমন ১০ লাখ উপাদানের list-এ recursion), লুপ লিখুন। গভীরতা যদি ছোট থাকে (যেমন ফোল্ডারের গাছ, যার গভীরতা সাধারণত কয়েক ডজন), recursion চমৎকার ও পরিষ্কার।

মূল উপলব্ধি: ভাষার প্রতিটি সীমার পেছনে একটি মূল্যবোধ থাকে। Python সচেতনভাবে পরিষ্কার traceback আর সব implementation-এ একই আচরণকে recursion-এর স্বাধীনতার চেয়ে বেশি গুরুত্ব দিয়েছে।

প্র ০৫“ফাংশন একটাই কাজ করবে” — এই নিয়ম কতদূর মানা উচিত? অতিরিক্ত ছোট ছোট ফাংশনে ভাগ করারও কি কোনো ক্ষতি আছে?

“Functions should do one thing. They should do it well. They should do it only.” — Robert C. Martin-এর ২০০৮ সালের বই Clean Code-এর এই উক্তি হয়তো সফটওয়্যার জগতের সবচেয়ে বেশি উদ্ধৃত পরামর্শগুলোর একটি। এর মূল যুক্তি শক্ত: ছোট, এক-কাজের ফাংশনের নাম দেওয়া সহজ, পরীক্ষা সহজ, পুনর্ব্যবহার সহজ, আর একটি বদল অন্য কাজে ছড়িয়ে পড়ে না। এই চিন্তার শিকড় আরও পুরোনো — Unix-এর দর্শন (“একটি প্রোগ্রাম একটি কাজ ভালোভাবে করবে”), যা Doug McIlroy-র নামের সাথে যুক্ত।

কিন্তু Martin বইয়ে আরও কড়া পরামর্শ দেন — ফাংশন হবে খুব ছোট, কয়েক লাইনের, আর ভেতরে ফাংশন ভাঙতেই থাকুন যতক্ষণ না আর ভাঙা যায়। এখানেই সমালোচনা শুরু। Stanford-এর অধ্যাপক John Ousterhout তাঁর ২০১৮ সালের বই A Philosophy of Software Design-এ “deep module” ধারণা দেন: ভালো ফাংশন বা module-এর ইন্টারফেস (নাম + parameter) সরল, কিন্তু ভেতরে অনেক কাজ লুকানো। উল্টো দিকে “shallow” ফাংশন — যার ইন্টারফেস প্রায় তার কাজের মতোই জটিল — কোনো জটিলতা লুকায় না, বরং পাঠককে বাড়তি নাম মনে রাখতে বাধ্য করে।

অতিরিক্ত ভাগের বাস্তব ক্ষতি:

  • লাফালাফি করে পড়া: একটি সরল হিসাব বুঝতে দশটি তিন-লাইনের ফাংশনে লাফ দিতে হলে পুরো ছবি মাথায় রাখা কঠিন।
  • নাম তৈরির ক্লান্তি: প্রতিটি টুকরোর জন্য অর্থপূর্ণ নাম পাওয়া যায় না; শেষে আসে process_data_step2-এর মতো অর্থহীন নাম।
  • লুকানো যোগাযোগ: টুকরোগুলো যদি একে অপরের ওপর নির্ভর করে নির্দিষ্ট ক্রমে ডাকা দরকার হয়, তাহলে ভাগ আসলে কিছুই আলাদা করেনি।

তাহলে মাপকাঠি কী? লাইনের সংখ্যা নয়, ধারণা. প্রশ্ন করুন: “এই অংশের কি নিজের একটি অর্থপূর্ণ নাম আছে? একে কি আলাদাভাবে পরীক্ষা করার মানে হয়? কেউ কি এটি অন্য জায়গায় ব্যবহার করবে?” তিনটির উত্তর “না” হলে হয়তো ভাগ করার দরকার নেই। এই পর্বের grade_point আর gpa ভালো ভাগ — প্রত্যেকের নিজস্ব অর্থ আছে, আলাদা পরীক্ষাও করা যায়। AI-এর যুগে এটি আরও প্রাসঙ্গিক: AI একদিকে বিশাল ফাংশন লেখে, আবার “clean code লেখো” বললে অনেক সময় অপ্রয়োজনীয় টুকরোয় ভেঙে দেয়। বিচার আপনার।

মূল উপলব্ধি: “এক কাজ” মানে “এক ধারণা”, “কয়েক লাইন” নয়। ভালো ফাংশন জটিলতা লুকায়; খারাপ ভাগ শুধু জটিলতা ছড়িয়ে দেয়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — অনুমান করা, পড়া আর মিলিয়ে নেওয়াই আসল কাজ।

  1. আউটপুট অনুমান: খাতায় লিখুন কী ছাপা হবে, তারপর চালিয়ে মিলিয়ে নিন — def double(x): print(x * 2), তারপর a = double(4), তারপর print(a), শেষে print(double(1)).

    চারটি লাইন ছাপা হবে: 8, None, 2, None. প্রথম ডাকে ফাংশনের ভেতরের print ছাপে 8, কিন্তু return নেই, তাই a-তে None; পরের লাইন সেটাই ছাপে। শেষ লাইনে প্রথমে ভেতরের ডাক চলে (ছাপে 2), তারপর বাইরের print সেই ডাকের ফল — None — ছাপে। ঠিক করতে print(x * 2)-এর বদলে return x * 2 লিখুন; তখন আউটপুট হবে 8 আর 2.

  2. Argument মেলানো: def ticket(name, seat, *, coach="A", meal=False). নিচের চারটি ডাকের কোনগুলো চলবে, কোনগুলো error দেবে, আর কেন? (ক) ticket("Rafi", 12) (খ) ticket("Rafi", 12, "B") (গ) ticket(seat=12, name="Rafi", meal=True) (ঘ) ticket(name="Rafi", 12)

    (ক) চলবে — coach="A", meal=False default থেকে। (খ) TypeError — coach আছে *-এর পরে, তাই keyword-only; তৃতীয় positional argument বসানোর জায়গা নেই। ঠিক রূপ: ticket("Rafi", 12, coach="B"). (গ) চলবে — সব keyword, তাই ক্রম যা খুশি। (ঘ) SyntaxError — keyword argument-এর পরে positional বসানো যায় না, আর এটি চালানোর আগেই ধরা পড়ে।

  3. একটি ফাংশন লিখুন (এবং পরীক্ষা করুন): taka_to_dollar(taka, *, rate) লিখুন, যা টাকাকে ডলারে বদলে দুই ঘর দশমিকে গোল করে ফেরত দেয়। Docstring দিন, আর অন্তত তিনটি assert লিখুন — যার একটি taka=0.

    একটি সম্ভাব্য সমাধান:

    def taka_to_dollar(taka: float, *, rate: float) -> float:
        """টাকাকে ডলারে বদলায়; rate মানে ১ ডলার = কত টাকা।"""
        return round(taka / rate, 2)
    
    assert taka_to_dollar(1220, rate=122) == 10.0
    assert taka_to_dollar(0, rate=122) == 0.0
    assert taka_to_dollar(500, rate=120) == 4.17

    rate-কে keyword-only রাখায় কেউ ভুল করে taka_to_dollar(122, 1220) লিখে দুটি উল্টে দিতে পারবেন না। লক্ষ করুন, default হার রাখিনি — বিনিময় হার প্রতিদিন বদলায়, তাই ভুল পুরোনো হার নিঃশব্দে বসে যাওয়ার চেয়ে প্রতিবার স্পষ্ট করে বলা নিরাপদ। rate=0 দিলে কী হবে? ZeroDivisionError — সেটি সামলানো শিখবেন পর্ব ২৪-এ।

  4. AI অডিট: যেকোনো AI-কে শুধু এটুকু বলুন: “Python-এ একটি ফাংশন লেখো যা ছাত্রদের নম্বরের তালিকা নিয়ে গড়, সর্বোচ্চ আর সর্বনিম্ন বের করে।” কোনো শর্ত দেবেন না। ফলাফলটি এই পর্বের চেকলিস্ট ধরে মিলিয়ে দেখুন।

    সাধারণত যা পাবেন: ফাংশনটি ফলগুলো নিজেই print করে, return করে না; অথবা ঠিকই tuple return করে কিন্তু docstring নেই। খালি list দিলে কী হবে, সেটা প্রায়ই ভাবা হয় না — len শূন্য হলে গড়ের ভাগে ZeroDivisionError. অনেক সময় ইনপুট নেওয়া, হিসাব আর ছাপা — সব এক ফাংশনে থাকে। এবার এই পর্বের prompt-কাঠামো ব্যবহার করে একই অনুরোধ করুন, আর দুটি উত্তর পাশাপাশি রাখুন। শিক্ষা: AI-এর কোডের মান অনেকটাই নির্ভর করে আপনি কোন শর্ত চাইছেন তার ওপর — আর সেই শর্তগুলো জানতে হলে আপনাকেই ফাংশনের মূলনীতি জানতে হবে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমPython-এ total = add_vat(price) লাইন চলার সময় যা যা ঘটে, সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 Argument-এর মান বের করা হয় (price কোন object-কে দেখায়)
  2. 🧱 Call stack-এ নতুন frame তৈরি হয়, parameter নাম argument-এর সাথে জোড়ে
  3. ⚙️ ফাংশনের body লাইন ধরে চলে, local variable তৈরি হয়
  4. 📤 return মান ফেরত দেয়, frame মুছে যায়
  5. 🏷️ ফেরত আসা মানের গায়ে total নামটি লাগে

Python-এ কোনো ফাংশনের body-তে return statement না থাকলে ডাকার পর সেটি কী ফেরত দেয়?

  • 0
  • একটি error, কারণ return বাধ্যতামূলক
  • None
  • body-র শেষ লাইনের মান

def f(x): print(x + 1) সংজ্ঞায়িত। এরপর y = f(4) আর print(y) চালালে টার্মিনালে কী দেখা যাবে?

  • 5 আর তারপর 5
  • 5 আর তারপর None
  • শুধু None
  • 5 আর তারপর TypeError

def pay(amount, fee=0.0, *, note="") সংজ্ঞায়িত। নিচের কোন ডাকটি বৈধ?

  • pay(500, 10, "rent")
  • pay(fee=10, 500)
  • pay(fee=10, note="rent")
  • pay(500, note="rent")

একটি Python ফাংশনে return total, count লেখা থাকলে আসলে কী ফেরত যায়?

  • দুটি মানের একটি tuple
  • শুধু total, count বাদ পড়ে
  • SyntaxError, কারণ return-এ একটিই মান দেওয়া যায়
  • দুটি মানের একটি list

sorted(students, key=by_marks)-এ by_marks-এর পরে বন্ধনী নেই কেন?

  • বন্ধনী ঐচ্ছিক, দুইভাবেই একই কাজ হয়
  • by_marks একটি string, তাই বন্ধনী লাগে না
  • ফাংশনটিকে ডাকা নয়, ফাংশন object নিজেকে পাঠানো হচ্ছে, যাতে sorted প্রয়োজনমতো ডাকে
  • এটি একটি ভুল, বন্ধনী ছাড়া কোড চলবে না

Base case ছাড়া একটি recursive ফাংশন CPython-এ চালালে সাধারণত কী ঘটে?

  • প্রোগ্রাম চিরকাল চলতে থাকে, কোনো error ছাড়া
  • প্রায় ১০০০ স্তর গভীরে গিয়ে RecursionError আসে
  • চালানোর আগেই SyntaxError আসে
  • Python নিজে থেকে recursion-কে লুপে বদলে চালিয়ে দেয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. ফাংশন হলো নাম দেওয়া, বারবার ব্যবহারযোগ্য রেসিপি: parameter-এ ইনপুট, body-তে কাজ, return-এ ফল। def শুধু তৈরি করে; চলে ডাকলে।
  2. print মানুষকে দেখায়, return প্রোগ্রামকে মান দেয়। return না থাকলে ফেরত আসে None; একাধিক মান মানে একটি tuple.
  3. Argument যায় ক্রম ধরে বা নাম ধরে; positional আগে, keyword পরে। Default-এ শুধু immutable মান (None, সংখ্যা, string); *-এর পরে keyword-only, /-এর আগে positional-only.
  4. প্রতিটি ডাকে call stack-এ নতুন frame, নিজের local variable সহ; return-এ frame মুছে যায়। Recursion-এ এই stack-ই বাড়ে — CPython-এর সীমা প্রায় ১০০০, তাই সাধারণ কাজে লুপ।
  5. ভালো ফাংশন ছোট, এক-ধারণার, docstring-সহ, যতটা সম্ভব pure; assert দিয়ে সীমার মান পরীক্ষা করুন। ফাংশনও object — key=by_gpa-এর মতো পাঠানো যায়।

পরের পর্বে: মডিউল ২ শেষ — আপনি এখন ভাষার মূল সব হাতিয়ার জানেন! মডিউল ৩ শুরু হচ্ছে ডেটা স্ট্রাকচার দিয়ে। প্রথমেই list — বদলানো যায় এমন ক্রম: index আর slicing, append বনাম +, আর একটি list ফাংশনে পাঠালে কেন বাইরেও বদলে যেতে পারে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান