PYTHON INTERNALS LAB · CPython Memory Model

Python-এ একটা Variable আসলে কী?

"age = 25" লিখলে Python-এর ভেতরে কী ঘটে — namespace, reference, object, refcount সব মিলিয়ে একটা animated memory-diagram দিয়ে বোঝা যাক। (ধরে নিচ্ছি CPython — Python-এর সবচেয়ে জনপ্রিয় implementation)

1age = 25
2name = "Asad"
3print(age) # 25
4print(id(age)) # e.g. 140312825108368

এই পুরো Lab-টা এই দুই লাইন কোডকে ঘিরেই। প্রতিটা ধাপে আমরা দেখব — নাম (name), reference (arrow), আর heap-এর object — এই তিনটা জিনিস আলাদা আলাদা কিন্তু একসাথে কীভাবে কাজ করে।

01
Code
age = 25 লেখা হয়
02
Namespace
"age" নামটা dict-এ ঢোকে
03
Reference
নাম থেকে object-এর দিকে arrow
04
Object
heap-এ 25 নিজেই একটা জিনিস
01

যখন age = 25 লেখা হয় — Object তৈরি হওয়া

Python প্রথমে ডান পাশ (RHS) evaluate করে: 25 একটা literal। CPython compiler এটাকে bytecode-এ কনভার্ট করে —

>>> import dis >>> dis.dis("age = 25") LOAD_CONST 25 # constant পুলে 25 বসানো STORE_NAME age # namespace-এ 'age' bind করা

প্রথমে interpreter heap-এ একটা PyObject বানায় (type=int, value=25, refcount=1) — কিন্তু যদি 25 আগে থেকেই ক্যাশে থাকে, নতুন object বানাতে হয় না (নিচের note দেখো)। এরপর নাম age-কে সেই object-এর সাথে bind করা হয়।

scene · after: age = 25
NAMESPACE (globals dict) age HEAP — PyObject int · id 0x7f9a10c220 · refs 1 25
CPython note — Python -5 থেকে 256 পর্যন্ত ছোট integer-গুলো startup-এই cache করে রাখে (small-int interning)। তাই 25 এই range-এ থাকায় বাস্তবে প্রায়ই নতুন object বানানো লাগে না — আগে থেকে বানানো 25-কেই re-use করা হয়। এটা language spec না, CPython-এর নিজস্ব optimization।
02

নাম কী? — namespace-এ থাকা একটা reference, memory-box না

age নিজে কোনো memory location না। এটা module-এর namespace dict-এ একটা key, যার value হলো heap object-টার একটা reference (pointer)। CPython-এ module-level namespace literally একটা dict — globals() চালিয়ে দেখা যায়:

>>> globals()['age'] 25 >>> globals() {'__name__': '__main__', ..., 'age': 25, 'name': 'Asad'}

Function-এর ভেতরের local variable একটু আলাদা — performance-এর জন্য CPython সেগুলোকে dict না বানিয়ে সরাসরি frame-এর একটা array slot-এ রাখে (STORE_FAST / LOAD_FAST), locals() কল করলে তখন একটা dict বানিয়ে দেখানো হয় মাত্র।

Symbol table vs namespace — "Symbol table" টা compile-time জিনিস (compiler ঠিক করে কোন নাম local/global/free), আর "namespace" হলো runtime-এ সেই নামগুলোর আসল storage (dict বা fast-locals array)। দুটো related কিন্তু এক জিনিস না।
03

Object heap-এ কোথায় থাকে, কীভাবে track হয়

CPython নিজের একটা private heap manage করে (pymalloc)। প্রতিটা object আসলে একটা C struct — শুরুতে থাকে একটা fixed header:

struct PyObject { Py_ssize_t ob_refcnt; // কতগুলো reference এই object-কে point করছে PyTypeObject *ob_type; // type (int, str, list, ...) ... // এরপর actual value/data };

ob_refcnt হলো reference-counting garbage collector-এর হৃদয়। যখনই কোনো নাম বা container object-টাকে ধরে রাখে, refcount বাড়ে; যখন ছেড়ে দেয়, কমে। refcount শূন্যে নামলে object-এর memory সাথে সাথে ফেরত দেওয়া হয় (আর reference cycle-এর জন্য আলাদা একটা cyclic garbage collector আছে)।

04

print(age) — Python কীভাবে খুঁজে পায়

Interpreter bytecode চালায়:

LOAD_NAME age # namespace dict-এ "age" string দিয়ে lookup LOAD_NAME print CALL 1

ধাপে ধাপে: (1) namespace dict-এ "age" key দিয়ে খোঁজা → (2) reference (pointer) পাওয়া → (3) সেই reference অনুসরণ করে heap-এর object-এ পৌঁছানো (dereference) → (4) object-এর type-এর __repr__/__str__ কল করে string বানানো → (5) console-এ দেখানো। "Value" নিজে কখনো namespace-এর ভেতরে বসে থাকে না — সবসময় খোঁজা হয়, তারপর দেখানো হয়।

05

id(age) আসলে কী রিটার্ন করে

Python language spec শুধু এইটুকু guarantee করে: id() এমন একটা integer দেয় যেটা unique এবং object-এর পুরো lifetime জুড়ে constant থাকবে। এটা "মেমরি অ্যাড্রেস" — এই কথাটা spec-এ নেই!

Implementation detail — CPython-এ, ঘটনাচক্রে, id() object-টার আসল C pointer address-ই রিটার্ন করে (hex(id(x)) চালিয়ে দেখলে ঠিক ওই 0x7f9a... address পাওয়া যায়)। কিন্তু PyPy বা Jython-এর মতো implementation-এ id() memory address নাও হতে পারে — সেখানে এটা শুধু একটা counter/hash হতে পারে। তাই সবসময় বলা উচিত: "CPython-এ id() = memory address", "Python spec অনুযায়ী id() = unique constant identifier"।
06

x = 25; y = x — একই Object-কে দুই নাম

x = 25 y = x print(id(x) == id(y)) # True — দুজনেই একই object

y = x মানে x-এর value copy হচ্ছে না — y সরাসরি x যেই object-কে point করে, সেই একই object-কে point করা শুরু করে। ফলে একটাই 25 object, কিন্তু তাকে ধরে রাখা reference এখন দুইটা — refcount 2।

scene · x এবং y একই object-কে point করছে
NAMESPACE x y HEAP — PyObject int · id 0x7f9a10c220 · refs 2 25
07

x = 50 — নতুন object, পুরনো y অক্ষত

এটা rebinding, mutation না। x এখন নতুন একটা object 50-কে point করা শুরু করে। পুরনো 25 object-টা মোছা যায় না, কারণ y এখনও সেটাকে ধরে আছে — তার refcount শুধু 2 থেকে 1-এ নামে।

x = 50 print(x, y) # 50 25 print(id(x) == id(y)) # False
আগে
x y int · id 0x7f9a10c220 · refs 2 25
পরে — x = 50
x y int · id 0x7f9a10c9a0 · refs 1 50 NEW object int · id 0x7f9a10c220 · refs 1 25
08

Mutable vs Immutable — append() কেন আলাদা

a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4] — a-ও বদলে গেল! print(id(a) == id(b)) # True — এখনো একই object

list mutableb.append(4) নতুন object বানায় না, একই list object-এর ভেতরেই পরিবর্তন করে। যেহেতু a আর b দুজনেই সেই একই object-কে point করছিল, দুজনের চোখেই পরিবর্তনটা দেখা যায়। এটা int-এর rebinding (ধাপ 07) থেকে সম্পূর্ণ আলাদা — সেখানে নতুন object তৈরি হয়েছিল, এখানে একই object পরিবর্তিত হচ্ছে।

আগে — b = a
a b list · id 0x7f9a20b110 · refs 2 [1, 2, 3]
পরে — b.append(4)
a b list · id 0x7f9a20b110 (same!) · refs 2 [1, 2, 3, 4]
09

কেন "variable stores a value" কথাটা পুরোপুরি accurate না

এই বাক্যটা ইঙ্গিত দেয় যেন variable একটা container, আর value তার ভেতরে বসে আছে — C-এর মতো। কিন্তু Python-এ:

• Variable আসলে একটা নাম (name), namespace-এর একটা entry — এর নিজের কোনো storage slot নেই যেখানে value "বসে" থাকে।
• একই object-কে একাধিক নাম point করতে পারে (ধাপ 06) — তাহলে value কোন "variable-এর ভেতরে" আছে?
• একটা object-কে কোনো নামই point না করলে (সব reference হারালে), সেটা garbage collected হয়ে যায় — value variable-নির্ভর না, object independently বাঁচে।

তাই বেশি accurate বাক্য: "একটা variable হলো namespace-এ একটা নাম, যেটা heap-এ থাকা একটা independent object-কে reference (bind) করে।"

10

C/C++ vs Python — মূল পার্থক্য

C / C++ — value semantics
STACK FRAME int x 25 &x = 0x7ffee3a2c15c

variable-এর নিজস্ব fixed-size memory address আছে, value সরাসরি সেখানেই থাকে। y = x করলে পুরো 25 বাইট-বাই-বাইট copy হয়ে যায় — দুটো আলাদা address, দুটো আলাদা কপি।

Python (CPython) — reference semantics
NAMESPACE x HEAP int 25

variable মানেই একটা reference — value কখনোই variable-এর "ভেতরে" থাকে না, সবসময় আলাদা heap object-এ থাকে। y = x শুধু reference copy করে, value না।

11

পুরো Process — এক নজরে Flow

Python Code age = 25 Variable Name age Namespace / Symbol Table globals()['age'] Reference pointer → heap object Object in Memory PyObject: type=int, refcnt Value 25 print(variable) print(age) Python finds the object LOAD_NAME → dereference Value is displayed 25
12

৫ মিনিটের Real-life Analogy

লকার-রুম ভাবো। প্রতিটা locker (heap object) নিজেই একটা জিনিস — তার নিজস্ব একটা নাম্বার আছে (id/address), আর তার ভেতরে জিনিসপত্র (value) আছে। কিন্তু locker নিজে কোনো নাম জানে না।

তুমি একটা sticky-note নেমপ্লেট লেখো "age" আর সেটা কোনো একটা locker-এর গায়ে সাঁটিয়ে দাও (bind করো) — এখন সবাই জানে "age" মানে ওই locker। আরেকটা নেমপ্লেট "y"-ও চাইলে একই locker-এর গায়ে সাঁটানো যায় (y = x) — locker একটাই, নেমপ্লেট দুইটা। নেমপ্লেট সরিয়ে অন্য লকারে লাগালে (x = 50), পুরনো locker আর তার জিনিস অক্ষত থাকে, যদি অন্য কোনো নেমপ্লেট এখনও তাতে লাগানো থাকে। কোনো নেমপ্লেট না থাকলে সেই locker খালি করে অন্য কাউকে দিয়ে দেওয়া হয় (garbage collected)।

age 25 name "Asad" no nameplate → garbage collected
13

Live Sandbox — নিজে লিখে দেখাও (classroom demo)

নিচের script-টা লাইন-বাই-লাইন চালাও — প্রতিটা assignment/append হওয়ার সাথে সাথে namespace + heap diagram নিজে থেকে আপডেট হবে, refcount আর garbage-collection-ও দেখা যাবে। সমর্থিত syntax: x = 25, x = "text", x = [1,2,3], y = x, x.append(4)

Script

    Memory (live)

    NAMESPACEHEAP— খালি, Run চাপো —