পাঠ ৪৯ · ৫৮-এর মধ্যে · মডিউল ১১
Home / Courses / Concepts of Programming Languages & Compiler Design / প্যারামিটার পাসিং

প্যারামিটার পাসিং মেকানিজম

Parameter passing mechanisms
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • Pass-by-value কীভাবে caller-এর ডেটাকে isolate রাখে
  • Pass-by-reference কীভাবে caller-এর ডেটায় সরাসরি মিউটেশন ঘটায়
  • Pass-by-name কেন বিরল, এবং এর সারপ্রাইজিং কনসিকোয়েন্স কী
  • একই modifier ফাংশন দিয়ে by-value ও by-reference-এর পার্থক্য বাস্তব কোডে যাচাই

১ · অ্যাক্টিভেশন রেকর্ডের প্যারামিটার স্লট — কীভাবে ভরা হয়

M11/L48-এ দেখা গেছে, প্রতিটি অ্যাক্টিভেশন রেকর্ডে একটি প্যারামিটার স্লট থাকে। কিন্তু ঠিক কীভাবে caller-এর আর্গুমেন্ট মান সেই স্লটে "ভরা" হয় — সেই ভিন্ন-ভিন্ন কৌশলগুলোই এই পাঠের বিষয়। ভাষাভেদে এই কৌশল ভিন্ন, এবং এই পার্থক্যের সরাসরি, বাস্তব প্রভাব আছে callee কী করতে পারবে তার উপর।

Pass-by-value
Callee একটি কপি পায়। Isolation guarantee — callee কখনো caller-এর অরিজিনাল বদলাতে পারে না।
Pass-by-reference
Callee caller-এর storage-এর একটি alias পায়। Callee-এর মিউটেশন caller-এও দেখা যায়।
Pass-by-name
প্রতিবার ব্যবহারে এক্সপ্রেশন পুনরায় ইভালুয়েট হয় — সাইড-ইফেক্ট থাকলে প্রতিবার ভিন্ন ফল দিতে পারে।

২ · Pass-by-value — কপি ও আইসোলেশন

Pass-by-valuePass-by-valueCallee আর্গুমেন্টের মানের একটি কপি পায় — callee-এর পরিবর্তন caller-এর অরিজিনালকে প্রভাবিত করে না। -এ callee আর্গুমেন্টের মানের একটি কপি পায় — callee নিজের কপিতে যা-ই পরিবর্তন করুক না কেন, caller-এর অরিজিনাল ভ্যারিয়েবল অপরিবর্তিত থাকে। এটি একটি গুরুত্বপূর্ণ আইসোলেশন গ্যারান্টি: একটি ফাংশন কল করার পরও caller নিশ্চিত থাকতে পারে তার নিজের ডেটা অক্ষত আছে (যদি না ফাংশনটি স্পষ্টভাবে একটি নতুন মান রিটার্ন করে caller নিজেই সেটি assign করে)।

৩ · Pass-by-reference — সরাসরি একই স্টোরেজে অ্যাক্সেস

Pass-by-referencePass-by-referenceCallee caller-এর অরিজিনাল ভ্যারিয়েবলের একটি রেফারেন্স/alias পায় — একই আন্ডারলাইং স্টোরেজে কাজ করে। -এ callee caller-এর অরিজিনাল ভ্যারিয়েবলের একটি রেফারেন্স (কার্যত একটি alias) পায় — callee যা পরিবর্তন করে তা সরাসরি caller-এর অরিজিনালেও প্রতিফলিত হয়, কারণ উভয়েই একই আন্ডারলাইং স্টোরেজে কাজ করছে, কপিতে নয়। এটি একটি সত্যিকারের ভিন্ন, বেশি permissive সিমান্টিক্স — একটি ফাংশন ইচ্ছাকৃতভাবে (ভালো বা মন্দ, উভয় অর্থেই) তার caller-এর ভ্যারিয়েবল বদলে দিতে পারে এই মেকানিজমের মাধ্যমে।

৪ · Pass-by-name — ঐতিহাসিক, বিরল, বিস্ময়কর

Pass-by-namePass-by-nameআর্গুমেন্ট এক্সপ্রেশনটি নিজেই (তার ইভালুয়েটেড মান নয়) পাস হয়, এবং callee-এর ভেতরে প্রতিবার ব্যবহারের সময় নতুন করে ইভালুয়েট হয়। একটি ঐতিহাসিকভাবে গুরুত্বপূর্ণ কিন্তু এখন বিরল মেকানিজম — এখানে আর্গুমেন্ট এক্সপ্রেশনটি নিজেই (তার ইভালুয়েটেড মান নয়, এবং একটি সাধারণ রেফারেন্সও নয়) পাস হয়, এবং callee-এর ভেতরে প্যারামিটারটি যতবার ব্যবহৃত হয় ততবারই সেই এক্সপ্রেশন নতুন করে ইভালুয়েট হয়। এর বাস্তব, বিস্ময়কর ফলাফল: যদি আর্গুমেন্ট এক্সপ্রেশনে সাইড-ইফেক্ট থাকে, বা এমন একটি ভ্যারিয়েবল রেফার করে যা callee চলাকালীন বদলে যায়, তাহলে প্রতিটি ব্যবহার ভিন্ন ভিন্ন ফলাফল দিতে পারে — এটাই মূল কারণ কেন আধুনিক বেশিরভাগ ভাষা এই মেকানিজম এড়িয়ে চলে।

৫ · কোড: by-value বনাম by-reference যাচাই

Python-এর নিজস্ব ডিফল্ট আচরণ আসলে অবজেক্ট-রেফারেন্স পাস করা — তাই সত্যিকারের pass-by-value দেখানোর জন্য নিচের কোডে স্পষ্টভাবে copy.deepcopy দিয়ে কপি তৈরি করা হয়েছে। এরপর একই modifier_fn ও একই স্টার্টিং লিস্ট দিয়ে দুটো মেকানিজম চালিয়ে caller-এর লিস্টের অবস্থা যাচাই করা হয়েছে।

Python
import copy


def call_by_value(original_list, modifier_fn):
    """callee একটি deep copy-তে কাজ করে -- caller-এর original অস্পৃশ্য থাকে।"""
    local_copy = copy.deepcopy(original_list)
    modifier_fn(local_copy)
    return original_list, local_copy


def call_by_reference(original_list, modifier_fn):
    """callee সরাসরি caller-এর original object-এ কাজ করে।"""
    modifier_fn(original_list)
    return original_list


def append_99(lst):
    lst.append(99)


# --- Pass-by-value ---
caller_list_a = [1, 2, 3]
print(f"call_by_value-এর আগে caller_list_a = {caller_list_a}")
original_a, copy_a = call_by_value(caller_list_a, append_99)
print(f"call_by_value-এর পরে caller_list_a = {caller_list_a}   (অপরিবর্তিত থাকা উচিত)")
print(f"callee-এর ভেতরের কপি ছিল          = {copy_a}   (এখানেই পরিবর্তন ঘটেছে)")
assert caller_list_a == [1, 2, 3]
assert copy_a == [1, 2, 3, 99]

# --- Pass-by-reference ---
caller_list_b = [1, 2, 3]
print(f"\ncall_by_reference-এর আগে caller_list_b = {caller_list_b}")
call_by_reference(caller_list_b, append_99)
print(f"call_by_reference-এর পরে caller_list_b = {caller_list_b}   (পরিবর্তিত হওয়া উচিত)")
assert caller_list_b == [1, 2, 3, 99]

print("\nসারাংশ:")
print(f"  by-value    -> caller অপরিবর্তিত: {caller_list_a == [1, 2, 3]}")
print(f"  by-reference -> caller পরিবর্তিত : {caller_list_b == [1, 2, 3, 99]}")

    
লক্ষ্য করুন — call_by_value ও call_by_reference দুটোই একই modifier_fn (append_99) ব্যবহার করছে। পার্থক্যটা modifier ফাংশনে নেই — পার্থক্যটা caller ও callee-এর মধ্যে কোন object শেয়ার হচ্ছে তাতে: call_by_value একটি সম্পূর্ণ আলাদা কপিতে modifier চালায়, call_by_reference একই, শেয়ার্ড object-এ।
মূল কথা · Key takeaway

Parameter passing মেকানিজম নির্ধারণ করে callee ও caller একই storage শেয়ার করে কিনা। Pass-by-value শেয়ার করে না (আইসোলেশন), pass-by-reference শেয়ার করে (মিউটেশন দৃশ্যমান), আর pass-by-name তো মানই শেয়ার করে না — এক্সপ্রেশন নিজেই শেয়ার করে, যা প্রতিবার নতুন করে ইভালুয়েট হয়ে বিস্ময়কর আচরণ তৈরি করতে পারে।

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

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ উপরের কোডে call_by_value কেন copy.deepcopy ব্যবহার করছে, শুধু সাধারণভাবে local_copy = original_list লিখলে চলত না কেন?

Python-এ local_copy = original_list নতুন কোনো object তৈরি করে না — এটি শুধু একই list object-এর দিকে একটি দ্বিতীয় নাম যোগ করে (Python-এর ডিফল্ট আচরণই object-রেফারেন্স পাস করা)। ফলে local_copy.append(99) করলে সেই একই, শেয়ার্ড object বদলে যেত — অর্থাৎ ভুলবশত pass-by-reference-এর আচরণ দেখাত। copy.deepcopy সত্যিই একটি সম্পূর্ণ নতুন, স্বাধীন object বানায়, যা pass-by-value-এর প্রকৃত isolation semantics-কে সঠিকভাবে সিমুলেট করে।

প্র ০২ Pass-by-name-এ যদি আর্গুমেন্ট এক্সপ্রেশন হয় a[i] (একটি array ইনডেক্সিং), এবং callee চলাকালীন i-এর মান বদলে যায়, তাহলে কী সমস্যা হতে পারে?

যেহেতু pass-by-name-এ এক্সপ্রেশন a[i] প্রতিবার ব্যবহারের সময় নতুন করে ইভালুয়েট হয়, callee-এর ভেতরে i বদলে গেলে একই প্যারামিটার নাম প্রতিবার ভিন্ন ভিন্ন এলিমেন্ট নির্দেশ করতে পারে — একবার a[2], পরের ব্যবহারে হয়তো a[5]। এটি একটি ক্লাসিক, বাস্তব বাগ-উৎস (একে "Jensen's Device" নামেও ঐতিহাসিকভাবে চেনা যায়) — এবং ঠিক এই কারণেই আধুনিক ভাষাগুলো pass-by-name পরিহার করে, predictable pass-by-value/reference-কেই ডিফল্ট রাখে।

প্র ০৩ যদি append_99-এর বদলে একটি এমন ফাংশন ব্যবহার করা হয় যা প্যারামিটারকে সম্পূর্ণ নতুন একটি list দিয়ে rebind করে (যেমন lst = [0, 0, 0], append নয়), তাহলে pass-by-reference-এও কি caller-এর লিস্ট বদলাবে?

না। lst = [0, 0, 0] callee-এর ভেতরে শুধু স্থানীয় নাম lst-কে একটি সম্পূর্ণ নতুন object-এর দিকে rebind করে — এটি অরিজিনাল, caller-এর object-কে ইন-প্লেস মিউটেট করে না। Pass-by-reference caller-এর object-টিকে মিউটেট (যেমন .append(), [i]=) করলেই caller দেখে — কিন্তু callee-এর স্থানীয় নাম rebind করা সবসময় শুধু সেই স্থানীয় নামকেই প্রভাবিত করে, caller-এর ভ্যারিয়েবলকে নয়। এই সূক্ষ্ম পার্থক্যটি বাস্তব প্রোগ্রামিংয়ের একটি সাধারণ বিভ্রান্তির উৎস।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোডে append_99-এর বদলে একটি নতুন modifier_fn লিখুন যা লিস্টের প্রথম এলিমেন্ট মুছে ফেলে (lst.pop(0)), এবং call_by_value/call_by_reference দুটোতেই চালিয়ে ফলাফল যাচাই করুন।

    একই প্যাটার্ন পুনরাবৃত্তি হবে: call_by_value([1,2,3], remove_first)-এর পর caller-এর লিস্ট [1, 2, 3]-ই থাকবে (অপরিবর্তিত), কিন্তু callee-এর কপি হবে [2, 3]। call_by_reference([1,2,3], remove_first)-এর পর caller-এর লিস্ট সরাসরি [2, 3]-এ বদলে যাবে — মেকানিজমটি operation-নিরপেক্ষ (append হোক বা pop, একই isolation/mutation নিয়ম প্রযোজ্য)।

  2. চিন্তা করুন: Python-এর একটি সাধারণ int ভ্যারিয়েবল (যেমন x = 5) একটি ফাংশনে পাস করলে এবং callee-এর ভেতরে x += 1 করলে caller-এর x কি বদলায়? লিস্টের সাথে এই আচরণের পার্থক্য কেন?

    না, caller-এর x বদলাবে না — কারণ Python-এ integer একটি ইমিউটেবল টাইপ। x += 1 callee-এর ভেতরে আসলে callee-এর স্থানীয় নাম x-কে একটি সম্পূর্ণ নতুন int object-এর দিকে rebind করে (ঠিক প্র-০৩-এর lst = [0,0,0]-এর মতোই) — অরিজিনাল int object মিউটেট হয় না (হতেই পারে না, কারণ int ইমিউটেবল)। লিস্ট (mutable) হলে .append()-এর মতো ইন-প্লেস মিউটেশন সম্ভব বলেই pass-by-reference-এর প্রভাব দৃশ্যমান হয় — এই পার্থক্যটাই দেখায় কেন Python-এর প্যারামিটার-পাসিং আচরণ "value নাকি reference" প্রশ্নের চেয়ে বেশি "টাইপটি mutable কিনা" তার উপর নির্ভর করে বলে মনে হয়।

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

পূর্ববর্তী পাঠ
সাবপ্রোগ্রাম, অ্যাক্টিভেশন রেকর্ড ও কল স্ট্যাক