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

Python কীভাবে চলে — interpreter, bytecode আর সবকিছুই object

How Python Runs — Interpreter, Bytecode & “Everything Is an Object”

Taught in Bangla, this lesson opens the hood on Python: the pipeline from source to bytecode, inspecting bytecode with dis and __pycache__, the virtual machine, other implementations, why everything is an object with type, identity and value, names as labels, reference counting, garbage collection and the GIL.

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

  • Run চাপার পর আপনার কোড কোন কোন ধাপ পার হয় — source থেকে token, AST, bytecode, তারপর VM
  • “Compiled না interpreted?” প্রশ্নের সঠিক উত্তর, আর CPython ছাড়া অন্য Python implementation কোনগুলো
  • “সবকিছুই object” কথাটির অর্থ, আর নাম ও object-এর পার্থক্য — যার ওপর পুরো কোর্স দাঁড়িয়ে
  • Object কখন তৈরি হয়, কখন মুছে যায় — reference counting ও garbage collector
  • Python কেন বিশুদ্ধ লুপে ধীর অথচ NumPy-তে দ্রুত, আর GIL আসলে কী

১ · Run চাপলে আসলে কী ঘটে

আগের পর্বে আপনি প্রথম প্রোগ্রাম লিখেছেন, চালিয়েছেন, error-ও পড়েছেন। টার্মিনালে python hello.py লিখে Enter চাপলেন, আর মুহূর্তেই লেখা ভেসে উঠল। মনে হয় যেন Python সরাসরি আপনার লেখা পড়ে কাজ করে ফেলল। আসলে ভেতরে কয়েকটি ধাপ ঘটে গেছে — এত দ্রুত যে আপনি টেরই পাননি।

এই পর্বটি একটু আলাদা। এখানে নতুন কোনো syntax শিখবেন না। বরং একটি মানসিক মডেল তৈরি করবেন — Python ভেতরে কীভাবে ভাবে। পরের প্রতিটি পর্ব (ভ্যারিয়েবল, list, ফাংশন, class) এই মডেলের ওপর দাঁড়াবে। এখানে যা বুঝবেন, তা দিয়েই পরে অনেক “অদ্ভুত” আচরণের ব্যাখ্যা পাবেন।

প্রথম প্রশ্ন: CPU কি Python বোঝে? না। CPU বোঝে শুধু নিজের machine code. আপনার print("Hi") আর CPU-র মাঝখানে একজন মধ্যস্থতাকারী আছে। সেই মধ্যস্থতাকারী হলো interpreter — একটি প্রোগ্রাম, যা আপনার Python কোড পড়ে চালায়। আপনি যে python কমান্ড চালান, সেটিই এই interpreter.

আপনার কোড থেকে CPU পর্যন্ত

Python source → bytecode → virtual machine → CPU

  1. আপনি লেখেনhello.pyমানুষ পড়তে পারে
  2. CompilerbytecodePython-এর নিজস্ব নির্দেশ
  3. Virtual machineceval loopএক নির্দেশ এক নির্দেশ করে
  4. CPUmachine codeVM-এর নিজের কোড চলে
ছবি ৫.১ — Python-এর compiler আর virtual machine দুটোই একই python প্রোগ্রামের ভেতরে থাকে।
মূল ধারণা

“Compiled” আর “interpreted” দুটি আলাদা বাক্স নয় — এটি একটি বর্ণালি (spectrum). একদিকে C, যেখানে চালানোর আগেই পুরো কোড machine code হয়ে যায়। অন্যদিকে একেবারে পুরোনো BASIC interpreter, যা প্রতিবার লাইনটির লেখা পড়ে অর্থ বের করত। Python মাঝামাঝি: আগে compile করে bytecode বানায়, তারপর সেই bytecode interpret করে। তাই “Python কি compiled না interpreted?” প্রশ্নের সঠিক উত্তর — “CPython দুটোই করে: bytecode-এ compile, তারপর VM-এ interpret.”

একটি রেস্টুরেন্টের রান্নাঘরের কথা ভাবুন। মালিক বাংলায় একটি লম্বা রেসিপি লিখে দিলেন। হেড শেফ প্রথমে সেটি পড়ে ছোট ছোট কাজের কার্ডে ভাগ করলেন — “পেঁয়াজ কাটো”, “তেল গরম করো”, “৫ মিনিট নাড়ো”। এরপর একজন রাঁধুনি কার্ডগুলো একটার পর একটা নিয়ে কাজ করেন। রেসিপি হলো আপনার .py ফাইল, কাজের কার্ড হলো bytecode, আর কার্ড ধরে ধরে কাজ করা রাঁধুনি হলো virtual machine. রেসিপি না বদলালে কার্ড আবার লিখতে হয় না — এটাই .pyc ফাইলের রহস্য, যা অংশ ৩-এ দেখবেন।

১৯৮৯ সালের ডিসেম্বরে নেদারল্যান্ডসের CWI গবেষণাগারে Guido van Rossum Python লেখা শুরু করেন; প্রথম প্রকাশ ১৯৯১ সালে। Guido interpreter-টি লিখেছিলেন C ভাষায় — সেই মূল interpreter-ই আজকের CPython, যা python.org থেকে আপনি ডাউনলোড করেন। শুরু থেকেই এটি কোডকে bytecode-এ অনুবাদ করে তারপর চালাত। অর্থাৎ “Python-এর bytecode” কোনো নতুন সংযোজন নয়, এটি ভাষার জন্ম থেকেই আছে।

২ · Source থেকে bytecode — ভেতরের পাইপলাইন

C কোর্সের টুলচেইনে চারটি আলাদা প্রোগ্রাম (preprocessor, compiler, assembler, linker) পরপর কাজ করে। Python-এ সব ধাপ একই python প্রোগ্রামের ভেতরে ঘটে, তাই চোখে পড়ে না। কিন্তু ধাপগুলো ঠিকই আছে। নিচের ছোট প্রোগ্রামটির দ্বিতীয় লাইন total = qty * 40 কীভাবে রূপ বদলায়, দেখুন। “পরে” চাপুন:

  1. Sourceshop.pyআপনার লেখা
  2. Tokenizertokensশব্দে ভাঙা
  3. ParserASTব্যাকরণের গাছ
  4. Compilerbytecodeকাজের কার্ড
  5. VMcevalকার্ড ধরে কাজ
shop.pyআপনি (বা AI) যা লিখেছেন
qty = 3
total = qty * 40
print(total)
tokensলাইনটি অর্থপূর্ণ টুকরোয় ভাঙা
NAME    'total'
OP      '='
NAME    'qty'
OP      '*'
NUMBER  '40'
NEWLINE
ASTকোনটা কার অংশ — গাছের আকারে
Assign
 ├─ target: Name 'total'  (এখানে রাখো)
 └─ value:  BinOp  *
             ├─ Name 'qty'   (মান আনো)
             └─ Constant 40
bytecodePython 3.14-এর নির্দেশ
LOAD_NAME       qty
LOAD_SMALL_INT  40
BINARY_OP       5 (*)
STORE_NAME      total
TerminalVM সব নির্দেশ চালাল
$ python shop.py
120
  1. ধাপ ১ — Source. আপনার ফাইল আসলে শুধু লেখা — অক্ষরের একটি সারি। Python-এর কাছে এখনো total কোনো নাম নয়, 40 কোনো সংখ্যা নয়; সবই অক্ষর।
  2. ধাপ ২ — Tokenizer: শব্দে ভাঙা। প্রথমে লেখাটিকে ছোট অর্থপূর্ণ টুকরো বা token-এ ভাঙা হয় — এটা নাম, এটা চিহ্ন, এটা সংখ্যা। বাংলা বাক্য পড়ার সময় আমরাও আগে শব্দ আলাদা করি। Indentation-ও এখানে token হয়ে যায় (INDENT, DEDENT) — Python-এ ফাঁকা জায়গা কেন এত গুরুত্বপূর্ণ, তার শুরু এখানেই।
  3. ধাপ ৩ — Parser: ব্যাকরণের গাছ। Parser token-গুলো সাজিয়ে একটি গাছ বানায়, যার নাম AST (Abstract Syntax Tree). গাছটি বলে: “এটি একটি assignment; ডান দিকে একটি গুণ; গুণের দুই পাশে qty আর 40.” ব্যাকরণ ভুল হলে (যেমন বন্ধনী খোলা রেখে দেওয়া) এই ধাপেই SyntaxError আসে — কোনো লাইন চলার আগেই।
  4. ধাপ ৪ — Compiler: কাজের কার্ড। Compiler গাছ ঘুরে ঘুরে সহজ, ছোট নির্দেশ বানায় — bytecode. প্রতিটি নির্দেশের একটি নাম (opcode) আছে: “qty-র মান আনো”, “40 আনো”, “গুণ করো”, “ফলটি total নামে রাখো”। এগুলো CPU-র নির্দেশ নয় — Python-এর নিজস্ব কাল্পনিক যন্ত্রের নির্দেশ।
  5. ধাপ ৫ — VM: কার্ড ধরে কাজ। এবার virtual machine নির্দেশগুলো একটার পর একটা চালায়। qty নামে খুঁজে 3 পায়, 40-এর সাথে গুণ করে 120 বানায়, total নামটি সেই 120-এ লাগায়। পরের লাইনের print 120 ছাপে। Runtime-এর সব error (NameError, ZeroDivisionError) এই ধাপেই ঘটে।
Source → Tokens → AST → Bytecode → Virtual machine → ফলাফল
প্রথম চার ধাপ “অনুবাদ”, শেষ ধাপ “চালানো”

এই পাইপলাইন জানার সবচেয়ে বড় ব্যবহারিক লাভ — error-এর ধরন দেখে বোঝা যায় সমস্যা কোথায়। নিচের টেবিলটি আগের পর্বের error পড়ার কৌশলের সাথে মিলিয়ে দেখুন:

Errorকোন ধাপ ধরেকখন দেখবেন
SyntaxError, IndentationErrorTokenizer / Parserচালানোর আগেই — ফাইলের একটি লাইনও চলে না
NameErrorVM (চলার সময়)শুধু ভুল লাইনটিতে পৌঁছালে
TypeError (যেমন "5" + 3)VM (চলার সময়)শুধু সেই লাইন চললে
ZeroDivisionErrorVM (চলার সময়)ভাগ আসলে হওয়ার মুহূর্তে

খেয়াল করুন: একটি ফাইলে কোথাও SyntaxError থাকলে প্রথম লাইনের print-ও চলবে না, কারণ পুরো ফাইল আগে compile হয়। কিন্তু NameError থাকলে তার আগের সব লাইন ঠিকঠাক চলে যায়। তাই কোনো শাখা (if-এর ভেতরের কোড) কখনো না চললে তার ভেতরের বানান-ভুল বছরের পর বছর লুকিয়ে থাকতে পারে। এজন্যই পেশাদাররা linter আর টেস্ট ব্যবহার করেন (পর্ব ৩৯ ও ৪৪)।

৩ · Bytecode নিজের চোখে দেখুন — dis আর __pycache__

কথায় না, প্রমাণে বিশ্বাস করুন। Python-এর সাথেই আসে dis (disassembler) module, যা যেকোনো ফাংশনের bytecode পড়ে দেখায়। নিচের কোডটি চালান:

import dis

def add(a, b):
    total = a + b
    return total

dis.dis(add)
print(add(2, 3))
print(type(add.__code__))

প্রথম কলামের সংখ্যা (3, 4, 5) হলো source ফাইলের লাইন নম্বর। লাইন ৪-এর total = a + b তিনটি নির্দেশে ভেঙেছে: LOAD_FAST_BORROW_LOAD_FAST_BORROW একসাথে a আর b-এর মান আনে, BINARY_OP 0 (+) যোগ করে, STORE_FAST ফলটিকে total নামে রাখে। লাইন ৫-এ total আবার এনে RETURN_VALUE দিয়ে ফেরত দেওয়া হয়। শুরুর RESUME হলো ফাংশন শুরুর একটি ভেতরের চিহ্ন। শেষ লাইন দেখায় — compile করা bytecode আসলে code নামের একটি object-এর ভেতরে থাকে, যা প্রতিটি ফাংশনের __code__-এ রাখা আছে।

Version অনুযায়ী আউটপুট বদলায়। উপরের আউটপুট Python 3.14-এর। নিজের কম্পিউটারে অন্য version থাকলে opcode-এর নাম ভিন্ন দেখবেন — যেমন 3.13-এ LOAD_FAST_LOAD_FAST, আর 3.10 বা তার আগে যোগের জন্য BINARY_ADD (3.11 থেকে সব গাণিতিক কাজ মিলে একটি BINARY_OP). এটা কোনো ভুল নয়। Bytecode হলো CPython-এর ভেতরের বিষয়, ভাষার অংশ নয় — তাই প্রায় প্রতিটি নতুন version-এ এটি বদলায়। ভাষার নিয়ম (a + b মানে যোগ) একই থাকে, শুধু ভেতরের যন্ত্রাংশ বদলায়।

Constant folding — compiler আগেই হিসাব সেরে রাখে

Python-এর compiler কিছু ছোট বুদ্ধিও খাটায়। আপনি x = 2 + 3 লিখলে 3.14-এর bytecode-এ কোনো যোগের নির্দেশই থাকে না — compiler নিজেই হিসাব করে সরাসরি 5 বসিয়ে দেয়। একে বলে constant folding. এজন্যই SECONDS_PER_DAY = 60 * 60 * 24 লিখলে চালানোর সময় কোনো খরচ নেই। পড়ার সুবিধার জন্য হিসাবটা খুলে লিখুন; গতি নিয়ে চিন্তা করবেন না।

__pycache__ আর .pyc — অনুবাদ জমিয়ে রাখা

প্রতিবার প্রোগ্রাম চালানোর সময় সব ফাইল নতুন করে compile করলে সময় নষ্ট। তাই আপনার প্রোগ্রাম যখন অন্য কোনো ফাইল import করে, CPython সেই ফাইলের bytecode একটি .pyc ফাইলে লিখে রাখে, __pycache__ নামের ফোল্ডারে। পরের বার ফাইলটি না বদলালে সরাসরি .pyc থেকে bytecode পড়ে নেয় — অনুবাদের ধাপ বাদ।

myshop/
├── main.py
├── billing.py
└── __pycache__/
    └── billing.cpython-314.pyc
  • নামের মাঝে cpython-314 কেন? কারণ bytecode version অনুযায়ী বদলায়। 3.13 আর 3.14 একই ফোল্ডারে পাশাপাশি নিজের নিজের .pyc রাখতে পারে। এই নিয়ম এসেছে Python 3.2 থেকে।
  • main.py-এর .pyc কোথায়? যে ফাইলটি আপনি সরাসরি চালান (python main.py), তার .pyc সাধারণত লেখা হয় না — শুধু import করা module-গুলোর হয়।
  • ফাইল বদলালে? .pyc-এর ভেতরে source ফাইলের সময় আর আকারের তথ্য থাকে। মিল না হলে CPython নিজেই নতুন করে compile করে।
  • মুছে ফেললে কি ক্ষতি? একদম না। পরের বার আবার তৈরি হবে। তবে git-এ এটি রাখা উচিত নয় — .gitignore-এ __pycache__/ যোগ করা প্রচলিত নিয়ম।

.pyc ফাইল প্রোগ্রামকে দ্রুত চালায় না, শুধু দ্রুত শুরু করায়। একবার bytecode তৈরি হলে চালানোর গতি একই। আর .pyc দিয়ে কোড “লুকানো” যায় না — bytecode থেকে প্রায় পুরো source আবার বের করার টুল আছে। গোপন রাখার উপায় হিসেবে এটি নির্ভরযোগ্য নয়।

৪ · Virtual machine — এক নির্দেশ এক নির্দেশ করে

“Virtual machine” শুনে VirtualBox-এর মতো পুরো কম্পিউটার ভাববেন না। এখানে এর মানে অনেক সরল: C-তে লেখা একটি বড় লুপ, যা বারবার একই কাজ করে — পরের bytecode নির্দেশ পড়ো, সেটি কী তা দেখো, সেই অনুযায়ী কাজ করো, আবার পরের নির্দেশে যাও। CPython-এর সোর্সে এই লুপের ফাইলটির নাম ceval.c (“evaluate”). আপনার পুরো Python প্রোগ্রাম আসলে এই লুপের ঘূর্ণন।

CPython-এর VM একটি stack machine. হিসাবের জন্য সে একটি স্তূপ (stack) ব্যবহার করে — যেন টেবিলে একটার ওপর একটা প্লেট রাখা। নতুন মান আসে ওপরে, কাজের সময় ওপর থেকে তোলা হয়। ধরুন a আগে থেকেই 2, b 3. এবার x = a + b-এর চারটি নির্দেশ দেখুন:

1LOAD_NAME   a2LOAD_NAME   b3BINARY_OP   0 (+)4STORE_NAME  x
VM-এর অবস্থা
stack
x
  1. LOAD_NAME a — VM নামের তালিকায় a খুঁজল, পেল 2-এর object. সেটিকে stack-এর ওপরে রাখল। Stack-এ এখন একটি প্লেট।
  2. LOAD_NAME b — একইভাবে b থেকে 3 এনে আগের প্লেটের ওপরে রাখল। Stack: 2-এর ওপর 3.
  3. BINARY_OP + — ওপরের দুটি প্লেট তুলে নিল, যোগ করল। কিন্তু “যোগ” মানে কী, VM নিজে জানে না — সে বাম পাশের object-কে (2, একটি int) জিজ্ঞেস করে “এই type-এ যোগের নিয়ম কী?” int-এর নিয়মে ফল 5. নতুন object 5 রাখা হলো stack-এ। একই নির্দেশ দুটি string পেলে জোড়া লাগাত, দুটি list পেলে নতুন list বানাত।
  4. STORE_NAME x — ওপরের 5 তুলে নিয়ে x নামটি তার গায়ে লাগিয়ে দিল। Stack আবার খালি, এক লাইনের কাজ শেষ। আপনার প্রতিটি লাইন এভাবেই কয়েকটি ছোট নির্দেশে ভেঙে চলে।

ধাপ ৩ খেয়াল করুন — এটাই Python-এর ধীরগতির একটি বড় কারণ, আবার নমনীয়তারও উৎস। C-তে a + b compile হওয়ার সময়েই জানা থাকে দুটি int, তাই একটি CPU নির্দেশেই যোগ হয়ে যায়। Python-এ প্রতিবার VM-কে চলার সময় দেখতে হয় এগুলো কোন type-এর, তারপর সেই type-এর নিয়ম খুঁজতে হয়। এই প্রতিবারের খোঁজাখুঁজিই খরচ।

Python 3.11-এর বড় লাফ — নিজেকে মানিয়ে নেওয়া interpreter

সেই খরচ কমাতে Python 3.11 (২০২২)-এ এসেছে specializing adaptive interpreter (PEP 659). মূল চিন্তা সহজ: বাস্তবে একটি লাইন সাধারণত বারবার একই type-এর মান পায়। লুপে total + price হাজারবার চললে প্রায় প্রতিবারই দুটি int. তাই কয়েকবার চলার পর VM সেই জায়গার সাধারণ BINARY_OP-কে বদলে দেয় একটি বিশেষ নির্দেশে — যা ধরে নেয় “দুটিই int”, শুধু একবার ছোট্ট যাচাই করে, সোজা যোগ করে ফেলে। হঠাৎ অন্য type এলে আবার সাধারণ নির্দেশে ফিরে যায়। ফলাফল ঠিক একই থাকে, শুধু দ্রুত হয়। Python-এর অফিসিয়াল হিসাবে 3.11 আগের version-এর চেয়ে গড়ে প্রায় ২৫% দ্রুত (কাজভেদে ১০ থেকে ৬০%).

JIT — পরীক্ষামূলক পর্যায়ে

পরের ধাপ JIT (Just-In-Time) compiler: বারবার চলা bytecode অংশকে চলার সময়েই আসল machine code-এ বদলে ফেলা। Python 3.13-এ CPython-এ একটি পরীক্ষামূলক JIT যোগ হয়েছে (PEP 744), যা “copy-and-patch” কৌশলে তৈরি। এটি ডিফল্টে বন্ধ, আর বিশেষভাবে build বা চালু না করলে পাবেন না; 3.14-এও এটি পরীক্ষামূলক। এখন পর্যন্ত এর গতির লাভ সীমিত — এটি ভবিষ্যতের ভিত্তি, আজকের জাদু নয়। AI বা পুরোনো ব্লগে “Python-এর কোনো JIT নেই” বা “Python এখন JIT দিয়ে C-র মতো দ্রুত” — দুটো কথাই ভুল।

৫ · এক ভাষা, অনেক implementation

এখানে একটি গুরুত্বপূর্ণ পার্থক্য বুঝে নিন। “Python” হলো একটি ভাষা — নিয়মের একটি বই (Python Language Reference). আর CPython হলো সেই নিয়ম মেনে বানানো একটি প্রোগ্রাম, বা implementation. যেমন “ক্রিকেট” একটি খেলার নিয়ম, আর মিরপুরের শের-ই-বাংলা স্টেডিয়ামের ম্যাচ সেই নিয়মে খেলা একটি আসর — অন্য মাঠেও একই নিয়মে খেলা যায়। CPython হলো reference implementation: নতুন ফিচার প্রথমে এখানেই আসে, আর python.org থেকে এটিই পান।

🐍 CPython

C-তে লেখা মূল interpreter. Bytecode + VM. সবচেয়ে বেশি ব্যবহৃত; NumPy, pandas-এর মতো C-তে লেখা library সবচেয়ে ভালো চলে এখানেই।

python.org-এর Python

⚡ PyPy

নিজস্ব JIT সহ বিকল্প implementation. বিশুদ্ধ Python-এর লম্বা লুপ প্রায়ই কয়েক গুণ দ্রুত চালায়। তবে C-তে লেখা কিছু library-র সাথে মেলাতে ঝামেলা হতে পারে, আর নতুন Python version-এর পেছনে সাধারণত কিছুটা পিছিয়ে থাকে।

লম্বা সময় চলা বিশুদ্ধ-Python কাজ

🔌 MicroPython

খুব ছোট মেমরির microcontroller-এর জন্য হালকা Python — ESP32, Raspberry Pi Pico-র মতো বোর্ডে চলে। সেন্সর পড়া, LED জ্বালানো, ছোট IoT প্রজেক্টে জনপ্রিয়।

ইলেকট্রনিক্স ও IoT

🌐 Pyodide (WebAssembly)

CPython-কেই WebAssembly-তে compile করা, ফলে Python সরাসরি ব্রাউজারের ভেতরে চলে — কোনো সার্ভার ছাড়া। JupyterLite, PyScript এর ওপর তৈরি।

ব্রাউজারে Python

ইতিহাসে আরও দুটি নাম পাবেন: Jython (Python কোড Java-র JVM-এ চালাত) আর IronPython (Microsoft-এর .NET-এ)। দুটোই দীর্ঘদিন Python 2-এর জগতে আটকে ছিল, তাই আজকের নতুন প্রজেক্টে খুব কম দেখা যায়। এই কোর্সে “Python” বললে আমরা CPython 3.14 বোঝাব, আর যেখানে implementation-ভেদে আচরণ বদলায় সেখানে আলাদা করে বলব।

এই পাতার কোড চালানোর বোতাম চাপলে আপনার কোড একটি অনলাইন সার্ভারে CPython 3.14-এ চলে, তারপর আউটপুট ফিরে আসে। আপনার নিজের কম্পিউটারের version ভিন্ন হলে python --version দিয়ে দেখে নিন (পর্ব ০৩)।

৬ · সবকিছুই object — type, identity আর value

এবার পর্বের দ্বিতীয় বড় ধারণা — সম্ভবত পুরো কোর্সের সবচেয়ে গুরুত্বপূর্ণ বাক্য: Python-এ প্রতিটি মান একটি object. সংখ্যা 42 একটি object. লেখা "ঢাকা" একটি object. একটি list, একটি ফাংশন, একটি module, এমনকি int নিজেও (যা একটি class) — সবাই object.

Object মানে মেমরিতে থাকা একটি জিনিস, যার তিনটি বৈশিষ্ট্য আছে:

🏷️ Type — সে কী জাতের

type(x) দিয়ে জানা যায়। Type ঠিক করে object কী কী কাজ পারে — int যোগ করতে পারে, str বড় হাতের অক্ষরে বদলাতে পারে। একটি object-এর type সারাজীবন একই থাকে।

type(42) → int

🪪 Identity — সে কোনটি

প্রতিটি object-এর একটি অনন্য পরিচয়, যেমন মানুষের জাতীয় পরিচয়পত্র নম্বর। id(x) একটি সংখ্যা দেয়, আর a is b জিজ্ঞেস করে “দুটি কি একই object?”

a is b → True / False

📦 Value — ভেতরে কী আছে

Object-এর ভেতরের তথ্য। a == b জিজ্ঞেস করে “দুটির মান কি সমান?” কিছু object-এর value বদলানো যায় (list), কিছুর যায় না (int, str) — বিস্তারিত পর্ব ১৯-এ।

a == b → True / False

id()-এর সংখ্যাটি প্রতিবার চালানোয় বদলাতে পারে, আর CPython-এ এটি সাধারণত মেমরির ঠিকানা। তাই সংখ্যাটি নিয়ে কখনো হিসাব করবেন না বা কোথাও জমিয়ে রাখবেন না। আসল প্রশ্ন সবসময় একটাই — “দুটি নাম কি একই object দেখাচ্ছে?” তার জন্য is ব্যবহার করুন।

“সবকিছুই object” কথাটির সবচেয়ে মজার ফল — ফাংশনও একটি মান. আপনি ফাংশনকে নতুন নাম দিতে পারেন, list-এ রাখতে পারেন, অন্য ফাংশনে পাঠাতে পারেন। sorted(names, key=len)-এ আপনি len ফাংশনটিকেই একটি মান হিসেবে পাঠাচ্ছেন। পরে decorator (পর্ব ৩৫) আর functional tool (পর্ব ৩৮) পুরোটাই এই ধারণার ওপর দাঁড়িয়ে। আর isinstance(x, object) যেকোনো x-এর জন্য True দেয় — কারণ Python-এ সব class শেষ পর্যন্ত object থেকেই আসে।

৭ · নাম আসলে লেবেল, বাক্স নয়

অনেক বইয়ে লেখা থাকে “ভ্যারিয়েবল হলো একটি বাক্স, যার ভেতরে মান রাখা হয়।” C-র জন্য ছবিটা মোটামুটি ঠিক। কিন্তু Python-এর জন্য এই ছবি আপনাকে ভুল পথে নিয়ে যাবে। Python-এ একটি নাম হলো একটি লেবেল বা স্টিকি নোট, যা কোনো object-এর গায়ে লাগানো থাকে। = চিহ্নের কাজ “বাক্সে ভরা” নয় — “এই লেবেলটি ওই object-এর গায়ে লাগাও”। একে বলে binding.

একটি স্কুলের ক্লাসরুম ভাবুন। এক ছাত্রের আসল নাম “মোহাম্মদ রফিকুল ইসলাম”। বাড়িতে সবাই ডাকে “রাফি”, বন্ধুরা ডাকে “রফিক”। তিনটি নাম, কিন্তু মানুষ একজনই। কেউ যদি “রাফির জামায় কালি লেগেছে” বলে, তাহলে “রফিকের জামাতেও” কালি — কারণ একই জামা, একই মানুষ। Python-এ b = a ঠিক এমন: একটি object-এর জন্য দ্বিতীয় একটি ডাকনাম।

এবার এই ধারণা কোডে দেখুন। নিচের ধাপগুলোতে খেয়াল রাখুন — কয়টি list object তৈরি হচ্ছে, আর কয়টি নাম:

1a = [1, 2]2b = a3b.append(3)4print(a)5print(a is b)
নাম → object (সবুজ লাইনটি এইমাত্র চলেছে)
a
b
list #1
মোট list object
  1. Object তৈরি, লেবেল লাগানো। ডান দিকের [1, 2] একটি নতুন list object বানাল (ধরুন তার নাম “list #1”). তারপর a লেবেলটি তার গায়ে লাগল। ক্রম খেয়াল করুন — আগে object, পরে নাম।
  2. b = a — কোনো কপি হয়নি। ডান দিকে a মানে “a যে object দেখায়”। সুতরাং b লেবেলটিও লাগল সেই একই list #1-এর গায়ে। এখনো list object মাত্র একটি, নাম দুটি। একই object-এর একাধিক নামকে বলে alias.
  3. b.append(3) — object নিজেই বদলাল। append list #1-এর ভেতরে 3 যোগ করল। লক্ষ করুন, a বা b কোনো লেবেলই সরেনি — বদলেছে object-এর ভেতরটা। List হলো mutable object, অর্থাৎ তৈরির পরেও তার ভেতর বদলানো যায়।
  4. print(a) → [1, 2, 3]. আপনি a-তে কিছুই করেননি, তবু 3 দেখা যাচ্ছে! নতুনদের কাছে এটাই সবচেয়ে বড় চমক। কিন্তু লেবেলের ছবিতে এটি স্বাভাবিক — a আর b একই জিনিসের দুটি ডাকনাম।
  5. a is b → True. প্রমাণ — দুটি নাম একই object-কে দেখাচ্ছে। আলাদা একটি কপি চাইলে b = a.copy() লিখতে হতো। কখন কপি লাগে, কপি কত গভীর হবে (shallow বনাম deep), তা পুরোপুরি দেখবেন পর্ব ১৯-এ।

লাইব্রেরির ক্যাটালগ দিয়েও ভাবতে পারেন। রাজশাহীর একটি পাবলিক লাইব্রেরিতে “গীতাঞ্জলি”-র একটিমাত্র কপি আছে। ক্যাটালগে বইটি তিনভাবে খোঁজা যায় — লেখকের নামে (রবীন্দ্রনাথ ঠাকুর), বইয়ের নামে (গীতাঞ্জলি), বিষয়ে (কবিতা)। তিনটি কার্ড, কিন্তু সব একই তাকের একই বইটির দিকে নির্দেশ করে। কেউ বইয়ের পাতায় দাগ দিলে যে কার্ড ধরেই খুঁজুন, দাগওয়ালা বইটিই পাবেন। কার্ড হলো নাম, বই হলো object.

এখন নিচের কোডটি চালান। এখানে নাম আর object-এর সম্পর্ক, “দেখতে এক” বনাম “আসলেই এক”, আর ফাংশনও যে object — তিনটিই একসাথে দেখবেন:

a = [1, 2]
b = a              # নতুন list নয় — একই object-এ দ্বিতীয় নাম
b.append(3)
print(a)
print(a is b)

c = [1, 2, 3]      # দেখতে এক, কিন্তু আলাদা object
print(a == c, a is c)

def greet(name):
    return f"সালাম, {name}!"

say = greet        # ফাংশনও object — তাকেও নতুন নাম দেওয়া যায়
print(say("রহিম"))
print(type(42), type("ঢাকা"), type(greet), type(int))
print(isinstance(greet, object), isinstance(int, object))

প্রথম দুই লাইনের আউটপুট [1, 2, 3] আর True — উপরের ডায়াগ্রামের মতোই। তারপর a == c হলো True (মান সমান), কিন্তু a is c হলো False (দুটি আলাদা object, দুটি আলাদা তাকের বই)। say = greet ফাংশনটিকে কপি করেনি, শুধু নতুন লেবেল লাগিয়েছে — তাই say("রহিম") ছাপে “সালাম, রহিম!”। শেষ দুই লাইন দেখায় 42-এর type int, ফাংশনের type function, আর int-এর নিজের type হলো type — class-ও object! আর সবাই object-এর instance, তাই দুটিই True.

Dynamic typing-এর এক ঝলক

লেবেলের ছবি থেকে আরেকটি জিনিস স্বাভাবিকভাবেই আসে। Type থাকে object-এর গায়ে, নামের গায়ে নয়। তাই একই নাম এখন একটি int-এ, পরে একটি str-এ লাগানো যায়: score = 90, তারপর score = "A+". লেবেল সরে গেল, কিন্তু 90 নামের object নিজে কখনো string হয়নি। একে বলে dynamic typing. এর সুবিধা-অসুবিধা আর type hint দিয়ে কীভাবে নিয়ন্ত্রণ করা যায়, তা পরের পর্ব (পর্ব ০৬) আর পর্ব ৩৭-এ দেখবেন।

C/C++ জানলে

C-তে int a = 5; মানে মেমরিতে a নামের একটি নির্দিষ্ট বাক্স, আর b = a মান কপি করে। Python-এর নাম অনেকটা সবসময় কোনো object-এর দিকে তাক করা একটি pointer-এর মতো — b = a শুধু ঠিকানাটি কপি করে, object নয়। পার্থক্য হলো, Python-এ আপনি কখনো ঠিকানা নিয়ে হিসাব করেন না, ঝুলন্ত pointer বা free()-এর ঝামেলাও নেই। C-র pointer-এর ছবিটা পরিষ্কার করতে C কোর্সের pointer পর্ব, আর compile-এর পুরো পাইপলাইন তুলনা করতে C টুলচেইন পর্ব দেখুন।

৮ · Object কখন মুছে যায় — reference counting আর garbage collector

C-তে malloc দিয়ে মেমরি নিলে free দিয়ে নিজেকেই ফেরত দিতে হয়। Python-এ আপনি কখনো object “মুছে” দেন না। তাহলে মেমরি ভরে যায় না কেন? কারণ CPython প্রতিটি object-এর সাথে একটি ছোট গণনা রাখে: এই object-কে এখন কতগুলো reference দেখাচ্ছে. একে বলে reference count. নাম, list-এর ঘর, ফাংশনের argument — সবাই একেকটি reference. গণনা শূন্য হলেই object সঙ্গে সঙ্গে মুছে যায়, মেমরি ফেরত যায়।

1book = ["গীতাঞ্জলি"]2shelf = book3reader = book4del book5shelf = None6reader = None
নাম → object (সংখ্যাগুলো ধারণা বোঝানোর জন্য)
book
shelf
reader
list-এর ref count
  1. Object তৈরি, count = 1. নতুন list object তৈরি হলো, book নামটি তাকে দেখাচ্ছে। একটি reference.
  2. shelf = book → count = 2. নতুন object নয়, শুধু আরেকটি লেবেল। গণনা বাড়ল।
  3. reader = book → count = 3. তিনটি নাম, একটি বই।
  4. del book → count = 2. খুব গুরুত্বপূর্ণ: del object মোছে না, শুধু নামটি মোছে। বইটি এখনো তাকে আছে, কারণ আরও দুটি কার্ড তাকে দেখাচ্ছে। এখন book লিখলে NameError পাবেন।
  5. shelf = None → count = 1. shelf লেবেলটি খুলে None নামের অন্য object-এ লাগানো হলো। বইয়ের গণনা কমল।
  6. reader = None → count = 0. আর কেউ বইটিকে দেখাচ্ছে না, তাই CPython সঙ্গে সঙ্গে list object মুছে মেমরি ফেরত দিল। আসল প্রোগ্রামে সংখ্যাগুলো এর চেয়ে বেশি হতে পারে (interpreter-এর ভেতরেও কিছু reference থাকে), কিন্তু নিয়ম একই — শূন্য হলে বিদায়.

sys.getrefcount(x) নামে একটি ফাংশন সত্যিই আছে, যা এই গণনা দেখায়। কিন্তু এর সংখ্যা version অনুযায়ী বদলায় (ফাংশনে পাঠানোর সময়েই একটি বাড়তি reference তৈরি হয়, আর 3.14-এ কিছু জায়গায় reference না বাড়িয়েই “ধার” নেওয়া হয়), তাই সংখ্যাটি মুখস্থ করার বা তার ওপর নির্ভর করার কিছু নেই। ধারণাটিই আসল।

যেখানে গণনা কাজ করে না — reference cycle

ধরুন দুটি object একে অপরকে দেখাচ্ছে: x = [], y = [x], তারপর x.append(y). এখন del x, y করলে বাইরের কোনো নাম আর এদের দেখায় না, অথচ প্রত্যেকের count অন্তত 1 — একে অপরের কারণে। Reference counting একা এদের কখনো মুছবে না। একে বলে reference cycle.

এজন্য CPython-এ আছে একটি দ্বিতীয় ব্যবস্থা — garbage collector (gc module). এটি মাঝে মাঝে জেগে ওঠে, এমন object-দল খোঁজে যাদের বাইরে থেকে কেউ দেখায় না, শুধু নিজেরা নিজেদের দেখায় — আর তাদের মুছে দেয়। রোজকার কোডে আপনাকে gc নিয়ে কিছুই করতে হবে না। শুধু জেনে রাখুন: বেশিরভাগ object মুছে যায় reference counting-এ, সঙ্গে সঙ্গে; আর চক্রগুলো পরিষ্কার করে gc, পরে কোনো এক সময়।

৯ · Python কেন কখনো ধীর, কখনো দ্রুত — আর GIL

এতক্ষণ যা শিখলেন, তা দিয়েই এখন একটি বহুল আলোচিত প্রশ্নের উত্তর দিতে পারবেন: “Python কি ধীর?” উত্তর — কী করছেন তার ওপর নির্ভর করে।

ধরুন আপনি এক কোটি সংখ্যা যোগ করছেন একটি বিশুদ্ধ Python লুপে। প্রতিবার লুপ ঘোরার সময় VM-কে কয়েকটি bytecode নির্দেশ চালাতে হয়; প্রতিটি যোগে দেখতে হয় type কী; প্রতিটি নতুন ফলের জন্য একটি নতুন int object তৈরি করতে হয় (Python-এর int একটি পূর্ণ object, C-র মতো শুধু ৮ বাইটের সংখ্যা নয়); পুরোনোটির reference count কমিয়ে মুছতে হয়। একই কাজ C-তে কয়েকটি CPU নির্দেশেই হয়ে যায়। তাই এমন লুপে Python সাধারণত C-র চেয়ে কয়েক ডজন গুণ ধীর হতে পারে।

কিন্তু ডেটা সায়েন্স আর AI-এর জগৎ তো Python-এই চলে! রহস্য হলো — ভারী কাজটা Python করে না। NumPy-র মতো library-র ভেতরের অংশ C (আর কিছু Fortran)-এ লেখা, যাকে বলে C extension. আপনি Python-এ একটি লাইন লেখেন — “এই দুই কোটি সংখ্যার array যোগ করো” — আর VM একবারই সেই C ফাংশনকে ডাকে। বাকি পুরো লুপ চলে C-র গতিতে, bytecode-এর খরচ ছাড়া। Python এখানে ম্যানেজার: সিদ্ধান্ত নেয়, কাজ ভাগ করে দেয়; ভারী বোঝা বয় C. এই কৌশল আপনি পর্ব ৪৬-এ NumPy আর pandas-এ হাতে-কলমে দেখবেন, আর পর্ব ৪৩-এ শিখবেন কীভাবে মেপে দেখতে হয় আসলে কোথায় সময় যাচ্ছে।

বিশুদ্ধ Python লুপ — প্রতিটি ধাপে VM-এর খরচ
Library-র একটি ডাক — ভেতরে C-র গতি

GIL — এক পাতায়

CPython-এর একটি বিখ্যাত বৈশিষ্ট্যের নাম GIL (Global Interpreter Lock). সাধারণ CPython-এ একটি প্রক্রিয়ার ভেতরে যতগুলো thread-ই থাকুক, একসাথে শুধু একটি thread Python bytecode চালাতে পারে. কারণটি এই পর্বেই আছে: reference count. দুটি thread একই সময়ে একই object-এর গণনা বদলালে হিসাব গুলিয়ে যেতে পারত, আর ভুল সময়ে object মুছে যেত। প্রতিটি object-এ আলাদা তালা দেওয়ার বদলে পুরো interpreter-এ একটি বড় তালা — সরল ও দ্রুত সমাধান, কিন্তু multi-core CPU-র সব core বিশুদ্ধ Python কোডে কাজে লাগানো যায় না। তবে ফাইল পড়া বা নেটওয়ার্কের অপেক্ষার সময় GIL ছেড়ে দেওয়া হয়, আর NumPy-র মতো অনেক C library-ও ভারী হিসাবের সময় তালা ছাড়ে।

পরিস্থিতি বদলাচ্ছে। PEP 703 অনুযায়ী Python 3.13-এ এসেছে একটি আলাদা, পরীক্ষামূলক free-threaded build — GIL ছাড়া CPython. Python 3.14-এ এই build অফিসিয়ালি সমর্থিত হয়েছে, তবে এখনো ঐচ্ছিক: আলাদা করে ইনস্টল করতে হয় (অনেক সময় নাম python3.14t), ডিফল্ট Python-এ GIL এখনো আছে, আর সব C library এখনো এর জন্য তৈরি নয়। Thread, process আর GIL-এর পুরো আলোচনা পর্ব ৪১-এ।

পরীক্ষা ও ইন্টারভিউতে
  • Python কি compiled না interpreted? — CPython দুটোই করে: source-কে bytecode-এ compile করে, তারপর virtual machine সেই bytecode interpret করে।
  • .pyc ফাইল কী? — Import করা module-এর compile করা bytecode, __pycache__-এ জমা থাকে; এতে প্রোগ্রাম দ্রুত শুরু হয়, দ্রুত চলে না।
  • is আর ==-এর পার্থক্য? — is দেখে দুটি একই object কি না (identity); == দেখে মান সমান কি না (value). সংখ্যা বা string তুলনায় সবসময় ==; is মূলত None-এর জন্য।
  • Python-এ মেমরি কে ফেরত দেয়? — CPython-এ reference counting (count শূন্য হলেই মুছে যায়), আর cycle ধরার জন্য garbage collector.
  • GIL কী, এবং এটি কি সব প্রোগ্রামকে ধীর করে? — একসাথে একটি thread-ই Python bytecode চালাতে পারে; CPU-ভারী multi-thread কোডে বাধা, কিন্তু I/O-নির্ভর কোডে প্রভাব কম।

১০ · AI-এর যুগে — internals নিয়ে AI-এর পুরোনো তথ্য যাচাই

“Python ভেতরে কীভাবে কাজ করে” — এই প্রশ্নে AI চ্যাটবট সাধারণত চমৎকার, আত্মবিশ্বাসী উত্তর দেয়। সমস্যা হলো, এই বিষয়ে ইন্টারনেটের বেশিরভাগ লেখা ২০২২-এর আগের, অর্থাৎ Python 3.11-এর বড় পরিবর্তনের আগের। AI সেই লেখা থেকেই শিখেছে। তাই internals নিয়ে AI-এর উত্তর প্রায়ই এক সময় ঠিক ছিল, এখন পুরোনো.

AI প্রায়ই যা বলেআজকের সঠিক চিত্রকীভাবে যাচাই করবেন
“Python পুরোপুরি interpreted, কোনো compile হয় না”CPython আগে bytecode-এ compile করেdis.dis() চালিয়ে দেখুন
“যোগের opcode হলো BINARY_ADD”3.11 থেকে BINARY_OP; নাম প্রতি version-এ বদলায়নিজের version-এ dis
“GIL কখনো সরানো যাবে না”3.13 থেকে আলাদা free-threaded build আছে (ঐচ্ছিক)python.org-এর “What's New” পাতা
“CPython-এ কোনো JIT নেই”3.13 থেকে পরীক্ষামূলক JIT আছে, ডিফল্টে বন্ধPEP 744 পড়ুন
“ছোট সংখ্যা তুলনায় is ব্যবহার করা যায়”এটি CPython-এর ভেতরের সুবিধা, নিশ্চয়তা নয়; == ব্যবহার করুনভাষার নিয়ম বনাম implementation আলাদা করুন

AI-কে internals নিয়ে প্রশ্ন করলে version বলে দিন, আর প্রমাণ চেয়ে নিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন:

আমি CPython 3.14 ব্যবহার করি। আমাকে সহজ বাংলায় বোঝাও: Python লুপের ভেতরে total = total + price লাইনটি virtual machine কীভাবে চালায়। শর্ত: ১. কোন কথা Python ভাষার নিয়ম আর কোন কথা শুধু CPython-এর implementation-এর বিষয় — আলাদা করে লেবেল দাও। ২. Opcode-এর নাম বললে বলো সেটি কোন version-এর; 3.11-এর আগের তথ্য হলে স্পষ্ট লেখো। ৩. আমি নিজে যাচাই করতে পারি এমন একটি ছোট dis কোড দাও। ৪. যে বিষয়ে তুমি নিশ্চিত নও, সেখানে "নিশ্চিত নই" বলো, আর অফিসিয়াল ডকুমেন্টের কোন পাতায় দেখতে হবে বলো।

Python internals নিয়ে AI-এর উত্তর পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১CPython মেমরি সামলাতে reference counting আর একটি cycle collector — দুটোই রাখে কেন? Java-র মতো শুধু একটি garbage collector রাখলেই তো হতো।

দুটি পদ্ধতির শক্তি আর দুর্বলতা একে অপরের পরিপূরক — তাই CPython দুটোকে জোড়া লাগিয়েছে।

  • Reference counting-এর শক্তি — তাৎক্ষণিকতা ও অনুমেয়তা: গণনা শূন্য হওয়ার মুহূর্তেই object মুছে যায়। ফাইল object-এর শেষ reference গেলেই ফাইলটি বন্ধ হয়ে যায়; মেমরি ব্যবহার মসৃণ থাকে, হঠাৎ বড় বিরতি আসে না। ১৯৯০-এর দশকের শুরুতে, যখন Python জন্মায়, এটি ছিল বানাতে সহজ আর C extension লেখকদের কাছে বোঝা সহজ — যা CPython-এর বিশাল C library-জগতের ভিত্তি।
  • Reference counting-এর দুর্বলতা: প্রথমত, cycle ধরতে পারে না। দ্বিতীয়ত, প্রতিটি নাম বদলের সাথে গণনা বাড়ানো-কমানোর খরচ আছে। তৃতীয়ত, বহু thread একসাথে গণনা বদলালে সুরক্ষা লাগে — GIL-এর জন্ম এখান থেকেই।
  • Cycle collector-এর আগমন: শুরুর দিকে Python-এ cycle ধরার কোনো ব্যবস্থা ছিল না; প্রোগ্রামারকেই সাবধানে চক্র ভাঙতে হতো। Python 2.0 (২০০০ সালে) যোগ করে gc module — একটি generational collector, যা শুধু container object (list, dict, class instance) পরীক্ষা করে, কারণ শুধু এরাই অন্য object-কে ধরে রাখতে পারে। Python 3.14-এ এই collector-কে incremental করা হয়েছে, যাতে বড় প্রোগ্রামে বিরতি আরও ছোট হয়।

তাহলে Java-র মতো শুধু tracing GC নয় কেন? কারণ তাতে C API-র পুরো চুক্তি বদলাতে হতো, আর হাজার হাজার C extension (NumPy থেকে শুরু করে ডেটাবেস driver) ভেঙে যেত। PyPy ঠিক এটাই করেছে — নিজস্ব tracing GC — আর সেজন্যই C extension চালাতে তাকে বাড়তি অনুকরণ-স্তর রাখতে হয়। একটি বাস্তব উদাহরণও আছে: Instagram-এর প্রকৌশলীরা ২০১৭ সালে লিখেছিলেন, তাঁদের বিশাল সার্ভারে cycle collector বন্ধ করে দিয়ে মেমরির অবস্থা উন্নত হয়েছিল; পরে Python 3.7-এ gc.freeze() যোগ হয় অনেকটা সেই অভিজ্ঞতা থেকে। আরেকটি সূক্ষ্ম উন্নতি: Python 3.12 থেকে None, True-এর মতো কিছু object “immortal” — এদের গণনা আর বদলানোই হয় না।

মূল উপলব্ধি: মেমরি ব্যবস্থাপনা কোনো একক “সেরা” কৌশল নয় — তাৎক্ষণিকতা, গতি, সরলতা আর পুরোনো কোডের সাথে মিল, এর মধ্যে ভারসাম্য। CPython-এর নকশা বোঝায়, একবার একটি বড় ecosystem দাঁড়িয়ে গেলে মূল সিদ্ধান্ত বদলানো কত কঠিন।

প্র ০২PyPy বিশুদ্ধ Python কোড অনেক সময় কয়েক গুণ দ্রুত চালায়। তাহলে সবাই PyPy ব্যবহার করে না কেন, আর CPython-ই ডিফল্ট কেন?

PyPy প্রকৌশলের এক অসাধারণ কীর্তি। ২০০০-এর দশকের মাঝামাঝি থেকে একদল গবেষক Python-এর একটি সীমিত রূপ (RPython)-এ একটি Python interpreter লেখেন, আর তাঁদের টুলচেইন সেই interpreter থেকে স্বয়ংক্রিয়ভাবে একটি tracing JIT তৈরি করে। Tracing JIT চলার সময় লক্ষ করে কোন লুপ বারবার ঘুরছে, কোন type আসছে — তারপর সেই নির্দিষ্ট পথের জন্য machine code বানিয়ে ফেলে। ফলে লম্বা সময় ধরে চলা বিশুদ্ধ Python হিসাবে PyPy প্রায়ই CPython-এর চেয়ে কয়েক গুণ দ্রুত।

তবু কেন ডিফল্ট নয়?

  • C extension-এর সাথে মিল: Python-এর শক্তি তার library-জগৎ, যার বড় অংশ CPython-এর C API ধরে লেখা — আর সেই API reference counting আর CPython-এর ভেতরের গঠন ধরে নেয়। PyPy-কে এর অনুকরণ করতে হয়, যা ধীর এবং কখনো অসম্পূর্ণ। যে কাজে সময়ের বেশিরভাগ যায় NumPy-র ভেতরে, সেখানে PyPy-র JIT প্রায় কোনো সুবিধাই দিতে পারে না।
  • Version-এ পিছিয়ে থাকা: নতুন ফিচার প্রথমে CPython-এ আসে; PyPy সাধারণত কিছু version পেছনে থাকে।
  • Warm-up আর মেমরি: JIT-এর শুরুতে শেখার সময় লাগে। ছোট স্ক্রিপ্ট যা এক সেকেন্ডে শেষ হয়, সেখানে লাভ নেই; মেমরিও সাধারণত বেশি লাগে।
  • Reference implementation: CPython হলো ভাষার “আদর্শ”; core developer-রা এখানেই কাজ করেন, operating system আর cloud-এ এটিই আগে থেকে থাকে।

PyPy-র অভিজ্ঞতা কিন্তু বৃথা যায়নি। CPython-এর specializing interpreter আর পরীক্ষামূলক JIT-এর নকশায় PyPy সহ অন্যান্য প্রকল্পের শিক্ষা কাজে লেগেছে — তবে CPython সাবধানে এগোচ্ছে, যাতে C extension-এর চুক্তি না ভাঙে।

মূল উপলব্ধি: প্রযুক্তি জনপ্রিয় হয় শুধু গতিতে নয় — ecosystem, মিল আর সুবিধাও সমান গুরুত্বপূর্ণ। “দ্রুততর বিকল্প কেন জেতেনি?” — এই প্রশ্ন সফটওয়্যার জগতে বারবার আসে, আর উত্তর প্রায়ই একই: পুরোনো কোড ভাঙানোর দাম।

প্র ০৩Python 3.11 হঠাৎ এত দ্রুত হলো কীভাবে? “Faster CPython” প্রকল্পের পেছনের গল্প কী?

বহু বছর ধরে CPython-এর মূলনীতি ছিল সরলতা: interpreter-এর কোড সহজ রাখো, যাতে স্বেচ্ছাসেবীরা বুঝতে ও রক্ষণাবেক্ষণ করতে পারেন। গতি বাড়ত ধীরে ধীরে। ২০২০ সালে core developer Mark Shannon একটি পরিকল্পনা প্রকাশ করেন — কয়েক ধাপে CPython-কে কয়েক গুণ দ্রুত করার উপায়। প্রায় একই সময়ে, অবসর ভেঙে Guido van Rossum Microsoft-এ যোগ দেন, এবং ২০২১ সাল থেকে Microsoft একটি ছোট পূর্ণকালীন দলকে অর্থায়ন করে, যেখানে Guido ও Mark Shannon দুজনেই ছিলেন। প্রকল্পটির নাম হয় “Faster CPython”. সব কাজ খোলাখুলি, CPython-এর মূল কোডেই হয়েছে।

3.11-এ যা এলো:

  • Specializing adaptive interpreter (PEP 659): এই পর্বে দেখেছেন — বারবার একই type পেলে নির্দেশকে বিশেষায়িত করা।
  • সস্তা ফাংশন ডাক: Python ফাংশন ডাকার সময় ভেতরের “frame” তৈরির খরচ অনেক কমানো হয়েছে।
  • দ্রুত শুরু: standard library-র মূল module-গুলোর bytecode আগে থেকে interpreter-এর ভেতরে রাখা, যাতে প্রোগ্রাম শুরুতে সময় কম লাগে।
  • “Zero-cost” exception: try ব্লকে ঢুকতে আর আলাদা খরচ নেই, শুধু exception আসলে ঘটলে খরচ।

ফলাফল — অফিসিয়াল benchmark-এ গড়ে প্রায় ২৫% দ্রুত, কোনো কোড বদল ছাড়াই। পরের version-গুলোতে উন্নতি চলেছে, আর 3.13-এ এসেছে পরীক্ষামূলক JIT (PEP 744, মূল লেখক Brandt Bucher), যা “copy-and-patch” নামে একটি সাম্প্রতিক কৌশল ব্যবহার করে: আগে থেকে তৈরি machine code-এর টেমপ্লেট কপি করে ফাঁকা জায়গায় মান বসিয়ে দেওয়া। শুরুর পরিকল্পনার বড় লক্ষ্যগুলো এখনো পুরোপুরি পূরণ হয়নি — বাস্তব প্রকৌশলে এটাই স্বাভাবিক।

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

মূল উপলব্ধি: বড় ওপেন-সোর্স প্রকল্পে গতি আসে ধারাবাহিক, মাপা উন্নতিতে — benchmark, ছোট ছোট বদল, আর পুরোনো কোডের প্রতি সম্মান। “এক রাতে ১০ গুণ” দাবি সাধারণত বাস্তব নয়।

প্র ০৪“সবকিছুই object” — এই নকশার বাস্তব ফল কী? ফাংশনকে মান হিসেবে পাঠাতে পারা কেন এত শক্তিশালী, আর এর দাম কী?

যখন সবকিছুই object, তখন ভাষার নিয়মগুলো আশ্চর্যরকম একরকম হয়ে যায়। যে কাজ একটি সংখ্যা দিয়ে করা যায়, তা ফাংশন, class বা module দিয়েও করা যায় — নাম দেওয়া, list-এ রাখা, dict-এ রাখা, অন্য ফাংশনে পাঠানো, ফাংশন থেকে ফেরত দেওয়া। কম্পিউটার বিজ্ঞানে বলে ফাংশন এখানে “first-class citizen”.

বাস্তব প্রয়োগ:

  • কাজ বাছাইয়ের টেবিল: একটি bKash-ধাঁচের মেনুতে {"1": send_money, "2": cash_out, "3": check_balance} — ব্যবহারকারী যা চাপবেন, সেই ফাংশন ডাকা হবে। লম্বা if/elif-এর বদলে একটি dict.
  • Callback: sorted(students, key=get_marks) — কীভাবে সাজাতে হবে, সেই নিয়মটাই ফাংশন হিসেবে পাঠানো। বাটন চাপলে কী হবে, সেটাও GUI-তে একটি ফাংশন পাঠিয়ে ঠিক করা হয়।
  • Decorator (পর্ব ৩৫): একটি ফাংশন নিয়ে তাকে মুড়িয়ে নতুন ফাংশন ফেরত দেওয়া — logging, সময় মাপা, লগইন যাচাই। Flask/FastAPI-র @app.get("/") এভাবেই কাজ করে।
  • Introspection: চলার সময় object-কে প্রশ্ন করা যায় — এর type কী, এর কী কী attribute আছে। pytest, dataclass, ORM-এর মতো টুল এর ওপর দাঁড়িয়ে।

দাম কী? প্রথমত, গতি ও মেমরি: সংখ্যা 5-ও একটি পূর্ণ object, যার সাথে type-এর তথ্য আর reference count থাকে — C-র ৪-৮ বাইটের int-এর চেয়ে অনেক বড়। দশ লাখ সংখ্যা Python list-এ রাখলে আসলে দশ লাখ object-এর ঠিকানা রাখা হয়; NumPy array কাঁচা সংখ্যা পাশাপাশি রাখে বলেই এত দ্রুত। দ্বিতীয়ত, alias-এর বিস্ময়: সবকিছু reference বলে একটি mutable object অজান্তে কয়েক জায়গা থেকে বদলে যেতে পারে — পর্ব ১৯ আর ২০-এ এর ফাঁদগুলো দেখবেন।

এই নকশা Python একা আবিষ্কার করেনি — Smalltalk ও Lisp অনেক আগেই এ পথে হেঁটেছিল। Python-এর কৃতিত্ব হলো এই শক্তিকে সাধারণ মানুষের জন্য পড়তে সহজ রূপে আনা।

মূল উপলব্ধি: একটি একক, সামঞ্জস্যপূর্ণ ধারণা (“সবকিছুই object”) ভাষাকে শেখা সহজ ও প্রসারণযোগ্য করে — কিন্তু তার দাম দিতে হয় কাঁচা গতিতে। Python সচেতনভাবে প্রোগ্রামারের সময়কে মেশিনের সময়ের চেয়ে বেশি মূল্য দিয়েছে।

প্র ০৫GIL কেন তৈরি হয়েছিল, এত বছর সরানো যায়নি কেন, আর PEP 703 কীভাবে শেষ পর্যন্ত পথ খুলল?

১৯৯০-এর দশকের শুরুতে Python-এ thread যোগ করার সময় বেশিরভাগ কম্পিউটারে CPU ছিল একটি। তখন একটি বড় তালা — GIL — ছিল সবচেয়ে বুদ্ধিমান সমাধান: interpreter-এর ভেতরের সব কাঠামো (বিশেষ করে reference count) একটি তালায় সুরক্ষিত, C extension লেখকদের thread-নিরাপত্তা নিয়ে ভাবতেই হয় না, আর এক-thread-এর প্রোগ্রাম থাকে দ্রুত। সমস্যা দেখা দিল যখন multi-core CPU সাধারণ হয়ে গেল।

আগের চেষ্টাগুলো:

  • ১৯৯০-এর দশকের শেষে Greg Stein একটি patch বানান, যা GIL সরিয়ে ছোট ছোট তালা বসায়। কিন্তু এক-thread-এর প্রোগ্রাম অনেক ধীর হয়ে যায় — আর বেশিরভাগ প্রোগ্রামই তখন এক-thread-এর।
  • ২০১৬ সালে Larry Hastings-এর “Gilectomy” প্রকল্পও একই দেয়ালে ধাক্কা খায় — reference count বাড়ানো-কমানোকে thread-নিরাপদ করতে গিয়ে গতি পড়ে যায়।
  • Guido-র অবস্থান দীর্ঘদিন স্পষ্ট ছিল: GIL সরাতে গিয়ে এক-thread-এর প্রোগ্রাম ধীর হলে বা C extension ভেঙে গেলে তা গ্রহণযোগ্য নয়।

PEP 703-এর ভিন্নতা: Meta-র প্রকৌশলী Sam Gross ২০২১ সালে “nogil” নামে একটি পূর্ণাঙ্গ পরীক্ষামূলক রূপ দেখান, যেখানে কয়েকটি কৌশল একসাথে কাজ করে — biased reference counting (যে thread object তৈরি করেছে, সে দ্রুত, তালা ছাড়া গণনা বদলায়; অন্য thread-রা ধীর কিন্তু নিরাপদ পথে), কিছু object-কে immortal করা, আর thread-নিরাপদ মেমরি বরাদ্দ। এক-thread-এর গতির ক্ষতি তুলনামূলক কম রেখে GIL সরানো সম্ভব হলো। ২০২৩ সালে Python Steering Council প্রস্তাবটি গ্রহণ করে, তবে সাবধানে: ধাপে ধাপে, আলাদা build হিসেবে, যাতে ecosystem সময় পায়। তাই 3.13-এ পরীক্ষামূলক, 3.14-এ অফিসিয়ালি সমর্থিত কিন্তু ঐচ্ছিক — আর ভবিষ্যতে কখনো ডিফল্ট হবে কি না, তা নির্ভর করছে library-গুলোর প্রস্তুতি আর গতির হিসাবের ওপর।

মূল উপলব্ধি: GIL কোনো “ভুল” ছিল না — তার সময়ের জন্য সেরা trade-off ছিল। হার্ডওয়্যার বদলালে পুরোনো সঠিক সিদ্ধান্তও বোঝা হয়ে দাঁড়ায়, আর লাখো মানুষের ব্যবহৃত সিস্টেমে তা বদলাতে লাগে বছরের পর বছর, ধাপে ধাপে, সবার সম্মতিতে।

অনুশীলন

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

  1. Bytecode অনুমান: see_bytecode.py-এর ফাংশনটি বদলে return a * b - 1 করুন। চালানোর আগে খাতায় লিখুন — কয়টি BINARY_OP দেখবেন, আর কোন ক্রমে? তারপর চালিয়ে মিলিয়ে নিন।

    দুটি BINARY_OP পাবেন — প্রথমে গুণ (*), তারপর বিয়োগ (-)। ক্রমটি stack machine-এর নিয়মে আসে: a আর b এনে গুণ, ফলের ওপরে 1 এনে বিয়োগ, তারপর RETURN_VALUE. গুণ আগে হয়, কারণ parser গাছ বানানোর সময়েই গুণকে বিয়োগের চেয়ে বেশি অগ্রাধিকার দিয়েছে। 3.14-এ 1 আনার নির্দেশের নাম LOAD_SMALL_INT; অন্য version-এ নাম ভিন্ন হতে পারে।

  2. নাম আর object গুনুন: কাগজে লেবেল-ছবি এঁকে বলুন শেষে কী ছাপা হবে — x = [10], y = x, x = x + [20], print(y). তারপর একই কোডে x = x + [20]-এর বদলে x.append(20) দিয়ে আবার ভাবুন।

    প্রথম ক্ষেত্রে ছাপা হবে [10]. কারণ x + [20] একটি নতুন list object বানায়, আর x = ... শুধু x লেবেলটিকে সেই নতুন object-এ সরিয়ে নেয়। y পুরোনো object-এই রয়ে গেছে।

    দ্বিতীয় ক্ষেত্রে ছাপা হবে [10, 20]. append কোনো লেবেল সরায় না, পুরোনো object-এর ভেতরেই যোগ করে — আর y সেই object-ই দেখছে। মূল প্রশ্ন সবসময়: “লেবেল সরল, নাকি object বদলাল?”

  3. __pycache__ খুঁজে দেখুন: নিজের কম্পিউটারে একটি ফোল্ডারে helper.py (ভেতরে একটি ছোট ফাংশন) আর main.py (যা import helper করে) বানান। python main.py চালান। কোন কোন নতুন ফাইল তৈরি হলো? এরপর helper.py-তে একটি লাইন বদলে আবার চালান — .pyc-এর সময় বদলাল কি?

    একটি __pycache__ ফোল্ডার তৈরি হবে, যার ভেতরে helper.cpython-314.pyc (আপনার version অনুযায়ী সংখ্যা ভিন্ন হতে পারে)। main.py-এর কোনো .pyc হবে না, কারণ সেটি সরাসরি চালানো হয়েছে, import হয়নি। helper.py বদলানোর পর আবার চালালে .pyc ফাইলের modified সময় নতুন হবে — CPython দেখেছে source বদলেছে, তাই নতুন করে compile করেছে। ফোল্ডারটি মুছে আবার চালালে আবার তৈরি হবে; কোনো ক্ষতি নেই।

  4. AI অডিট: যেকোনো AI-কে জিজ্ঞেস করুন: “Python-এ x = a + b-এর bytecode কী?” — version উল্লেখ না করে। উত্তরটি এই পর্বের তথ্য আর নিজের dis আউটপুটের সাথে মেলান। তারপর একই প্রশ্ন “CPython 3.14” লিখে করুন। পার্থক্য কী পেলেন?

    প্রায়ই প্রথম উত্তরে BINARY_ADD পাবেন — যা Python 3.10 বা তার আগের নাম। ফাংশনের ভেতরে হলে LOAD_FAST, বাইরে হলে LOAD_NAME — এই পার্থক্যও অনেক সময় গুলিয়ে যায়। Version বলে দিলে উত্তর ভালো হতে পারে, কিন্তু তবু 3.14-এর নতুন নাম (যেমন LOAD_FAST_BORROW) ভুল আসতে পারে, কারণ সেগুলো নতুন। শিক্ষা: internals-এর প্রশ্নে AI-এর উত্তর একটি অনুমান, প্রমাণ নয় — প্রমাণ হলো আপনার নিজের dis আউটপুট।

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

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

ক্রমCPython একটি .py ফাইল চালানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✍️ Source — ফাইলের লেখা পড়া
  2. 🔤 Tokenizer — লেখাকে token-এ ভাঙা
  3. 🌳 Parser — token থেকে AST তৈরি
  4. 📇 Compiler — AST থেকে bytecode
  5. 🔁 Virtual machine — bytecode এক নির্দেশ করে চালানো

CPython একটি .py ফাইল চালানোর সময় আসলে কী করে?

  • পুরো ফাইল সরাসরি CPU-র machine code-এ অনুবাদ করে একটি .exe বানায়
  • প্রথমে bytecode-এ compile করে, তারপর virtual machine সেটি চালায়
  • কোনো অনুবাদ ছাড়াই প্রতিটি লাইনের লেখা পড়ে পড়ে চালায়
  • Python কোডকে Java-তে অনুবাদ করে JVM-এ চালায়

__pycache__ ফোল্ডারের .pyc ফাইলের কাজ কী?

  • source কোড গোপন রাখা, যাতে কেউ পড়তে না পারে
  • প্রোগ্রামকে চলার সময় কয়েক গুণ দ্রুত করা
  • import করা module-এর bytecode জমিয়ে রাখা, যাতে পরের বার দ্রুত শুরু হয়
  • প্রোগ্রামের error-এর তালিকা জমিয়ে রাখা

a = [1, 2], তারপর b = a, তারপর b.append(3). এখন print(a) কী ছাপবে?

  • [1, 2]
  • [1, 2, [3]]
  • AttributeError
  • [1, 2, 3]

a = [1, 2, 3] আর c = [1, 2, 3] আলাদা দুটি লাইনে লেখা হলো। a == c আর a is c-এর মান কী?

  • True আর False
  • True আর True
  • False আর False
  • False আর True

CPython-এ del name লিখলে আসলে কী ঘটে?

  • object-টি সঙ্গে সঙ্গে মেমরি থেকে মুছে যায়, অন্য নাম থাকলেও
  • শুধু নামটি সরে যায় ও reference count কমে; count শূন্য হলে object মুছে যায়
  • object মুছতে হলে এরপর free() ডাকতে হয়
  • কিছুই ঘটে না, del শুধু একটি comment-এর মতো

সাধারণ (ডিফল্ট) CPython-এ GIL-এর প্রভাব কোনটি?

  • প্রতিটি Python প্রোগ্রাম অর্ধেক গতিতে চলে
  • Python-এ thread বানানোই যায় না
  • একসাথে একটিই thread Python bytecode চালাতে পারে, তাই CPU-ভারী কাজ সব core কাজে লাগাতে পারে না
  • Python 3.13 থেকে GIL পুরোপুরি সরিয়ে ফেলা হয়েছে, সব Python-এ

শব্দকোষ

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

মনে রাখুন

  1. CPython কোডকে আগে bytecode-এ compile করে, তারপর virtual machine এক নির্দেশ করে চালায় — compiled আর interpreted একটি বর্ণালি।
  2. পাইপলাইন: Source → Tokens → AST → Bytecode → VM. SyntaxError আসে চালানোর আগে, বাকি error চলার সময়।
  3. dis দিয়ে bytecode দেখা যায়, কিন্তু opcode-এর নাম version অনুযায়ী বদলায়; .pyc শুধু শুরু দ্রুত করে।
  4. সবকিছুই object (type, identity, value); নাম হলো লেবেল — b = a কপি নয়, একই object-এ দ্বিতীয় নাম। is = একই object, == = সমান মান।
  5. Object মুছে যায় reference count শূন্য হলে; চক্র ধরে gc. GIL-এর মূলে এই reference counting, আর 3.14-এ GIL-ছাড়া build ঐচ্ছিক।

পরের পর্বে: মডিউল ১ শেষ! এবার মূল ভাষায় ঢুকছি। আজকের লেবেল-ছবি হাতে নিয়ে দেখব ভ্যারিয়েবল আর type — নাম রাখার নিয়ম, int, float, str, bool, আর dynamic typing আসলে কতটা স্বাধীনতা দেয়।

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

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