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

metaprogramming — descriptor, __init_subclass__ আর metaclass

Metaprogramming: Attribute Lookup, Descriptors, __init_subclass__, Metaclasses & Introspection

Taught in Bangla, this lesson explains metaprogramming in Python: classes as objects and type(), the attribute lookup order, __getattribute__ versus __getattr__, writing descriptors, why functions and property are descriptors, how a class is created, choosing between class decorators, __init_subclass__ and metaclasses, introspection with inspect, monkeypatching and the risks of eval and exec.

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

  • class কেন object, type(cls) কেন type, আর type(name, bases, ns) দিয়ে চলার সময় class বানানো
  • obj.x লিখলে Python ঠিক কোন ক্রমে খোঁজে, আর __getattribute__ বনাম __getattr__
  • নিজের descriptor দিয়ে যাচাই-করা field (দাম, স্টক, মোবাইল নম্বর), আর ফাংশন, property, classmethod কীভাবে descriptor
  • class জন্মের পুরো ধাপ — __prepare__ থেকে class decorator পর্যন্ত — আর __init_subclass__ দিয়ে plugin registry ও ছোট একটি ORM
  • metaclass কখন সত্যিই লাগে, inspect আর dynamic attribute-এর ব্যবহার, monkeypatching আর eval/exec-এর বিপদ, আর AI-এর লেখা “জাদু” যাচাই

১ · মূল ভাবনা — যে কোড অন্য কোডকে গড়ে

এ পর্যন্ত আপনি যে কোড লিখেছেন, তার কাজ ছিল ডেটা নিয়ে কাজ করা: দাম যোগ করা, ফাইল পড়া, list সাজানো। এবার একটি ভিন্ন প্রশ্ন। ধরুন একটি অনলাইন দোকানের সফটওয়্যারে চল্লিশটি class আছে, আর প্রতিটির দাম ধনাত্মক হতে হবে, স্টক ঋণাত্মক হতে পারবে না, মোবাইল নম্বর ১১ অঙ্কের হতে হবে। প্রতিটি class-এ একই যাচাই আলাদা করে লিখবেন? নাকি এমন কোড লিখবেন, যা অন্য class-গুলোকে গড়ে দেয় — নিয়ম একবার লিখে সব class-এ বসিয়ে দেয়?

দ্বিতীয় পথটির নাম metaprogramming: এমন কোড, যার কাজের বস্তু ডেটা নয়, বরং অন্য কোড — class, ফাংশন, attribute. পর্ব ৩৭-এর decorator ছিল এর প্রথম স্বাদ: একটি ফাংশন, যা আরেকটি ফাংশনকে বদলে দেয়। এই পর্বে আমরা আরও গভীরে যাব — class-এর জন্ম আর attribute পড়া-লেখার একেবারে ভেতরে।

একটি গার্মেন্টস কারখানার কথা ভাবুন। লাইনের অপারেটররা শার্ট সেলাই করেন — এটি সাধারণ কোড, ডেটা নিয়ে কাজ। কিন্তু কারখানায় একজন প্যাটার্ন-মাস্টারও আছেন। তিনি নিজে শার্ট সেলাই করেন না; তিনি কাগজের ছাঁচ বানান, যা দেখে সবাই শার্ট কাটে। আর তিনি একটি নিয়ম বসিয়ে দেন: “প্রতিটি ছাঁচে সেলাইয়ের জন্য এক সেন্টিমিটার বাড়তি জায়গা থাকবে।” একটি নিয়ম, শত শত ছাঁচে। Python-এ class হলো ছাঁচ, object হলো শার্ট, আর metaprogramming হলো প্যাটার্ন-মাস্টারের কাজ — ছাঁচ কীভাবে তৈরি হবে আর কেমন আচরণ করবে, তা ঠিক করা।

ভিত্তি: class-ও একটি object

পর্ব ০৫-এ শিখেছেন, Python-এ সবকিছুই object. পর্ব ৩৪-এ দেখেছেন, class-ও এর বাইরে নয়: type(450) হলো int, আর type(int) হলো type. আপনার নিজের Product class-এর ক্ষেত্রেও type(Product) হলো type. অর্থাৎ type হলো সেই class, যার object-গুলো নিজেরাই class — একে বলে metaclass. আর type(type)? সেটিও type — শৃঙ্খলটি এখানে নিজের গায়ে এসে থামে।

এর একটি চমকপ্রদ ফল: class বাক্যটি আসলে type-কে ডাকার একটি সুন্দর লেখা। নিচের দুটি অংশ একই class বানায়:

# সাধারণ class বাক্য দিয়ে
class Bkash:
    fee = 1.85
    def charge(self, amount):
        return amount * self.fee / 100

# একই জিনিস, type() ডেকে — চলার সময়
def charge(self, amount):
    return amount * self.fee / 100

Bkash = type("Bkash", (object,), {"fee": 1.85, "charge": charge})
print(type(Bkash), Bkash().charge(1000))   # <class 'type'> 18.5

type-কে তিনটি argument দিলে সে নতুন class বানায়: নাম, parent class-গুলোর tuple, আর class-এর ভেতরের নাম ও মানের একটি dict — যাকে বলে class-এর namespace. বাস্তবে এভাবে class খুব কমই লেখা হয়, কিন্তু এটি জানলে একটি সত্য স্পষ্ট হয়: class তৈরি হয় প্রোগ্রাম চলার সময়, একটি সাধারণ ফাংশন-ডাকের মতো. আর যে প্রক্রিয়া চলার সময় ঘটে, তাতে নিজের কোড ঢোকানো যায়।

তিনটি জায়গায় হাত দেওয়া যায়

attribute পড়া-লেখা

obj.price পড়লে বা লিখলে কী হবে — descriptor, __getattr__, __getattribute__. অংশ ২–৪।

class-এর জন্ম

class তৈরির মুহূর্তে কিছু যোগ বা যাচাই — class decorator, __init_subclass__, metaclass. অংশ ৫–৬।

নিজেকে দেখা

চলার সময় object-এর গঠন পড়া ও বদলানো — getattr, vars, inspect. এর নাম introspection. অংশ ৭।

এগুলো কোনো তাত্ত্বিক খেলনা নয়। পর্ব ৩৪-এর @dataclass, পর্ব ৫১-এর FastAPI যে Pydantic-এর ওপর দাঁড়িয়ে, পর্ব ৫২-এর SQLAlchemy, আর Django-র model — এদের প্রত্যেকটি এই তিন হাতিয়ার দিয়েই বানানো। আপনি যখন class Customer(Model): name = Column(String) লেখেন, তখন পেছনে ঠিক এই পর্বের কৌশলগুলো চলে।

মূল নিয়ম

Metaprogramming-এর প্রথম নিয়ম: সবচেয়ে সরল হাতিয়ার দিয়ে কাজ সারুন. সাধারণ ফাংশন বা class দিয়ে হলে সেটিই। না হলে decorator. তারপর __init_subclass__. Metaclass একেবারে শেষ উপায়। প্রতিটি ধাপ ওপরে উঠলে শক্তি বাড়ে, কিন্তু পড়া, ডিবাগ করা আর টুলের সাহায্য পাওয়া কঠিন হয়।

২ · obj.x লিখলে আসলে কী ঘটে — attribute খোঁজার পথ

পর্ব ৩৪-এ attribute খোঁজার ক্রমটি এক নজরে দেখেছেন। এখানে আমরা সেটিকে যন্ত্রের মতো খুলে দেখব, কারণ এই পর্বের প্রতিটি কৌশল এর ওপর দাঁড়িয়ে। মূল কথা: p.price লেখা একটি সরাসরি dict-খোঁজা নয়। Python আসলে ডাকে type(p).__getattribute__(p, "price") — আর সাধারণ object-এর __getattribute__ একটি নির্দিষ্ট নিয়ম মেনে চলে।

নিয়মটির কেন্দ্রে দুই রকম descriptor (পর্ব ৩৪): যার __set__ বা __delete__ আছে, সে data descriptor; যার শুধু __get__ আছে, সে non-data descriptor. খোঁজা শুরু হয় instance-এ নয়, class আর তার পূর্বপুরুষদের (MRO ক্রমে) দিয়ে — কারণ সেখানে data descriptor থাকলে সে-ই জিতবে। নিচের stepper-এ একটি class-এর চারটি নাম পড়া আর একটি নাম লেখা ধাপে ধাপে দেখুন:

1class Product:2    price = Field(...)     # data descriptor3    shop = "করিম স্টোর"     # সাধারণ মান4    def label(self): ...   # ফাংশন5    def __getattr__(self, name):6        return f"{name}?"78p = Product(450, 12, "01712345678")9p.shop = "রহিম স্টোর"     # instance-এর ঘরে1011p.price12p.shop13Product.shop14p.label15p.colour16p.price = 0
খোঁজার খাতা
প্রশ্ন
class-এ (MRO)
কোন ধরন
p.__dict__
ফল
  1. p.price — data descriptor জেতে। Python প্রথমে type(p).__mro__ ধরে class-গুলোর __dict__-এ price খোঁজে, আর পায় একটি Field object. তার class-এ __set__ আছে, তাই এটি data descriptor. সঙ্গে সঙ্গে Field.__get__(desc, p, Product) ডাকা হয়, আর তার ফলই উত্তর। instance-এর নিজের ঘরে একই নামে কিছু থাকলেও সেটি দেখাই হয় না।
  2. p.shop — instance-এর ঘর জেতে। class-এ shop আছে, কিন্তু একটি সাধারণ string কোনো descriptor নয়। তাই Python এবার p.__dict__ দেখে — লাইন ৯ সেখানে 'রহিম স্টোর' রেখেছে। instance-এর নিজের মান class-এর মানকে ঢেকে দেয় (shadowing).
  3. Product.shop — class attribute. class থেকে সরাসরি পড়লে instance নেই, তাই class-এর মান 'করিম স্টোর'. নতুন কোনো object, যার নিজের shop নেই, সেও এই মানই পাবে — সব object-এর ভাগের মান।
  4. p.label — non-data descriptor. class-এ পাওয়া গেল একটি ফাংশন, যার শুধু __get__ আছে। non-data descriptor-এর আগে instance-এর ঘর দেখা হয়; সেখানে label নেই, তাই ফাংশনের __get__ ডাকা হয়, যা একটি bound method ফেরত দেয় (অংশ ৪-এ বিস্তারিত)।
  5. p.colour — কোথাও নেই। class, instance, পূর্বপুরুষ — কোথাও নেই। তখন __getattribute__ AttributeError তোলে। কিন্তু class-এ __getattr__ লেখা আছে, তাই Python শেষ সুযোগ হিসেবে p.__getattr__("colour") ডাকে, আর ফল 'colour?'. না থাকলে সরাসরি AttributeError.
  6. p.price = 0 — লেখার পথও একই নীতিতে। লেখার সময়ও Python আগে class-এ দেখে: data descriptor আছে? আছে, তাই Field.__set__(desc, p, 0) ডাকা হয়, যা যাচাই করে ValueError তোলে। instance-এর ঘরে কিছুই লেখা হয় না। data descriptor না থাকলে (যেমন p.shop = ...) মান সোজা p.__dict__-এ যেত।
পড়া: data descriptor → instance __dict__ → non-data descriptor / class attribute → __getattr__ → AttributeError

__getattribute__ বনাম __getattr__ — নাম প্রায় এক, কাজ আলাদা

দুটি নাম প্রায়ই গুলিয়ে যায়, অথচ পার্থক্যটি বিশাল:

  • __getattribute__ প্রতিটি পড়ায় চলে। উপরের পুরো খোঁজার নিয়মটিই object.__getattribute__-এর ভেতরে লেখা। একে বদলালে আপনি প্রতিটি attribute পড়ায় ঢুকে পড়বেন — এমনকি নিজের method আর self.__dict__ পড়াতেও। ভেতরে self.কিছু লিখলে সেটি আবার __getattribute__ ডাকে, আর অসীম পুনরাবৃত্তি হয়ে আসে RecursionError: maximum recursion depth exceeded. তাই ভেতরে সবসময় super().__getattribute__(name) ব্যবহার করতে হয়। বাস্তবে এটি প্রায় কখনো বদলানোর দরকার হয় না।
  • __getattr__ শুধু ব্যর্থ হলে চলে। সাধারণ খোঁজা AttributeError-এ শেষ হলেই Python এটি ডাকে। তাই এটি নিরাপদ আর সস্তা: যে attribute পাওয়া যায়, তার গতিতে কোনো প্রভাব নেই।

__getattr__-এর সবচেয়ে প্রচলিত ব্যবহার হলো proxy object — যে object নিজের কাছে না থাকা সব অনুরোধ আরেকটি object-এর কাছে পাঠিয়ে দেয়। যেমন একটি হিসাবের “শুধু-পড়া” রূপ, যা রিপোর্টে দেখানো যাবে কিন্তু বদলানো যাবে না:

class ReadOnly:
    def __init__(self, target):
        object.__setattr__(self, "_target", target)   # নিজের __setattr__ এড়িয়ে

    def __getattr__(self, name):          # নিজের কাছে না পেলে তবেই
        return getattr(self._target, name)

    def __setattr__(self, name, value):   # প্রতিটি লেখা আটকানো
        raise AttributeError(f"{name}: শুধু পড়া যায়")

view = ReadOnly(account)
view.balance          # account.balance পড়ে দেয়
view.balance = 0      # AttributeError: balance: শুধু পড়া যায়

লক্ষ করুন, __init__-এ self._target = target লেখা যায়নি, কারণ সেটিও আমাদের __setattr__ আটকে দিত। তাই object.__setattr__ দিয়ে সরাসরি মূল ব্যবস্থাকে ডাকা হয়েছে। এই ধরনের ছোট ফাঁদই metaprogramming-এর বৈশিষ্ট্য — সাধারণ নিয়মগুলো নিজের কোডের ওপরেও খাটে।

৩ · descriptor protocol — একবার লিখুন, সব class-এ বসান

পর্ব ৩৪-এ PositiveNumber দেখেছেন — একটি নির্দিষ্ট নিয়মের descriptor. এবার নিয়মটিকেও বাইরে থেকে নেওয়া যাক, যাতে একটি Field class দিয়েই দাম, স্টক আর মোবাইল নম্বর — তিন রকম যাচাই হয়। আগে descriptor protocol-এর চারটি method এক জায়গায়:

Methodকখন ডাকা হয়কী পায়
__get__(self, obj, owner)p.price বা Product.price পড়লেobj = object (class থেকে পড়লে None), owner = class
__set__(self, obj, value)p.price = 500 লিখলেকোন object, কোন মান
__delete__(self, obj)del p.price লিখলেকোন object
__set_name__(self, owner, name)class তৈরির মুহূর্তে, একবার (Python 3.6 থেকে)কোন class, কোন নামে বসানো হয়েছে

শেষেরটি, __set_name__, সমস্যার একটি পুরোনো সমাধান। price = Field(...) লাইনে ডান দিকের Field(...) আগে তৈরি হয়, তারপর নামটি বসে — তাই descriptor নিজে জানে না সে কোন নামে আছে। Python 3.6-এর আগে নামটি দুবার লিখতে হতো (price = Field("price", ...)), নইলে একটি metaclass লাগত। এখন class তৈরির সময় Python নিজেই প্রতিটি attribute দেখে: তার __set_name__ আছে কি না — থাকলে ডেকে নাম জানিয়ে দেয়।

প্রথম runnable প্রোগ্রাম। Field একটি যাচাই-ফাংশন আর একটি বার্তা নেয়; বাকিটা descriptor protocol সামলায়। চালানোর আগে অনুমান করুন — শেষ লাইনে price কত থাকবে?

class Field:
    def __init__(self, check, msg):
        self.check, self.msg = check, msg
    def __set_name__(self, owner, name):       # class তৈরির সময় একবার
        self.name = name
    def __get__(self, obj, owner=None):
        return self if obj is None else obj.__dict__[self.name]
    def __set__(self, obj, value):             # প্রতিটি লেখায়
        if not self.check(value):
            raise ValueError(f"{self.name}={value!r}: {self.msg}")
        obj.__dict__[self.name] = value        # একই নামে instance-এর ঘরে

class Product:
    price = Field(lambda v: v > 0, "দাম ০-এর বেশি হতে হবে")
    stock = Field(lambda v: v >= 0, "স্টক ঋণাত্মক হতে পারে না")
    phone = Field(lambda v: len(v) == 11 and v[:2] == "01" and v.isdigit(),
                  "১১ অঙ্কের BD মোবাইল নম্বর নয়")
    def __init__(self, price, stock, phone):
        self.price, self.stock, self.phone = price, stock, phone

p = Product(450, 12, "01712345678")
for name, bad in [("price", 0), ("stock", -3), ("phone", "0171")]:
    try:
        setattr(p, name, bad)
    except ValueError as e:
        print(e)
print(vars(p), type(Product.price).__name__)

আউটপুট চার লাইনের: price=0: দাম ০-এর বেশি হতে হবে, stock=-3: স্টক ঋণাত্মক হতে পারে না, phone='0171': ১১ অঙ্কের BD মোবাইল নম্বর নয়, আর শেষে {'price': 450, 'stock': 12, 'phone': '01712345678'} Field. তিনটি ভুল লেখাই __set__-এ আটকেছে, তাই মানগুলো অক্ষত — price এখনো 450. কয়েকটি সূক্ষ্ম জিনিস লক্ষ করুন। (১) __init__-এর self.price = price-ও __set__ দিয়ে যায় — তাই জন্মের সময়েও যাচাই হয়, পর্ব ৩৪-এর __post_init__-এর মতো আলাদা কিছু লাগে না। (২) মান রাখা হয়েছে instance-এর __dict__-এ একই নামে ('price'), তবু পড়ার সময় descriptor-ই ডাকা হয় — কারণ অংশ ২-এর নিয়মে data descriptor instance-এর ঘরের আগে আসে। (৩) Product.price (class থেকে পড়া) obj=None পায়, তাই descriptor নিজেকেই ফেরত দেয় — এজন্যই শেষে Field ছাপা হলো। (৪) মোবাইল নম্বরের যাচাই এখানে ইচ্ছা করে সরল রাখা; বাস্তবে পর্ব ৩০-এর regex দিয়ে অপারেটর কোডও যাচাই করবেন।

একটি সাধারণ ভুল: descriptor-এর ভেতরে মান রাখা self.value = value দিয়ে। self এখানে descriptor নিজে, আর descriptor class-এ একটিই — সব Product object তাকে ভাগ করে। ফলে একটি পণ্যের দাম বদলালে সব পণ্যের দাম বদলে যাবে। মান সবসময় obj-এর ঘরে রাখুন (obj.__dict__ বা setattr(obj, ...)), descriptor-এ শুধু নিয়ম।

৪ · ফাংশন, property, classmethod — সবই descriptor

এখন মডিউল ৫-এর একটি পুরোনো রহস্যের সমাধান। পর্ব ৩১-এ শিখেছেন, p.label() ডাকলে self আপনাআপনি p হয়ে যায়। কে বসায়? কোনো বিশেষ নিয়ম নয় — ফাংশন object-এর নিজের __get__ method. Python-এর প্রতিটি সাধারণ ফাংশন একটি non-data descriptor. দেখুন ধাপে ধাপে:

  1. খোঁজাProduct.__dict__function পাওয়া গেল
  2. __get__(p, Product)non-data descriptor
  3. bound method__func__ + __self__দুটি একসাথে বাঁধা
  4. ডাকlabel(p)self = p
ধাপ ১class-এ যা আছে, তা একটি সাধারণ ফাংশন
f = Product.__dict__["label"]
type(f)                  # <class 'function'>
hasattr(f, "__get__")    # True  → descriptor
hasattr(f, "__set__")    # False → non-data
ধাপ ২p.label লিখলে Python যা ডাকে
# p.__dict__-এ 'label' নেই, তাই:
f.__get__(p, Product)
# Product.label লিখলে obj নেই:
f.__get__(None, Product)  # → ফাংশনটি নিজেই
ধাপ ৩নতুন object: method
m = p.label
type(m).__name__         # 'method'
m.__func__ is f          # True — আসল ফাংশন
m.__self__ is p          # True — বাঁধা object
ধাপ ৪ডাকলে self প্রথম argument
p.label()
# = m()
# = m.__func__(m.__self__)
# = Product.label(p)
ধাপ ৫অন্য descriptor, অন্য বাঁধন
@classmethod  → __get__ বাঁধে class-কে (cls)
@staticmethod → __get__ দেয় খালি ফাংশন
@property     → __get__ ডাকে getter-কে
# নিয়ম একটাই: __get__ যা ফেরত দেয়, সেটাই উত্তর
  1. class-এ একটি সাধারণ ফাংশন। def label(self) class-এর ভেতরে লিখলে কোনো “method” তৈরি হয় না — তৈরি হয় একটি সাধারণ function object, যা Product.__dict__-এ থাকে। তার class-এ __get__ আছে কিন্তু __set__ নেই, তাই এটি non-data descriptor.
  2. পড়ার সময় __get__ ডাকা। p.label লিখলে অংশ ২-এর নিয়মে instance-এর ঘরে কিছু না পেয়ে Python ফাংশনের __get__(p, Product) ডাকে। আর class থেকে পড়লে (Product.label) obj হয় None, তখন ফাংশনটি নিজেকেই ফেরত দেয় — Python 3-এ তাই Product.label একটি সাধারণ ফাংশন।
  3. bound method তৈরি। __get__ একটি নতুন ছোট object বানায়, যার দুটি ঘর: __func__ (আসল ফাংশন) আর __self__ (p). প্রতিবার p.label লিখলে নতুন একটি method object তৈরি হয় — এ কারণে p.label is p.label হয় False, যদিও == হয় True.
  4. ডাকলে self আসে। bound method ডাকলে সে __self__-কে প্রথম argument হিসেবে বসিয়ে আসল ফাংশন ডাকে: p.label() আসলে Product.label(p). পর্ব ৩১-এর “self আপনাআপনি আসে” কথাটির পেছনে এই এক লাইনের কারসাজি।
  5. বাকি সবাই একই খেলার খেলোয়াড়। পর্ব ৩৭-এর টেবিলে যে @classmethod, @staticmethod, @property দেখেছেন, এরা wrapper ফাংশন নয় — একেকটি descriptor class. classmethod-এর __get__ object-এর বদলে class-কে বাঁধে, staticmethod-এর __get__ কিছুই বাঁধে না, আর property একটি data descriptor, যার __get__ আপনার getter ডাকে।

এক টেবিলে Python-এর তৈরি descriptor-গুলো — যাদের আপনি এতদিন “বিশেষ নিয়ম” ভেবে এসেছেন:

জিনিসধরন__get__ যা দেয়
সাধারণ ফাংশন (method)non-dataobject-এ বাঁধা bound method
classmethodnon-dataclass-এ বাঁধা method
staticmethodnon-dataখালি ফাংশন, কোনো বাঁধন নেই
propertydatagetter-এর ফল; __set__ setter ডাকে (setter না থাকলে AttributeError)
functools.cached_propertynon-dataপ্রথমবার হিসাব করে instance-এর ঘরে একই নামে রেখে দেয়; পরের বার ঘরটিই জেতে
__slots__-এর প্রতিটি নামdataobject-এর ভেতরের নির্দিষ্ট ঘর থেকে মান (member_descriptor)

শেষ সারিটি একটু ব্যাখ্যা চায়। __slots__ = ("a",) লিখলে (পর্ব ৩৪-এর slots=True) Python object-এর জন্য __dict__ বানায় না; বদলে প্রতিটি নামের জন্য object-এর ভেতরে একটি নির্দিষ্ট ঘর রাখে, আর class-এ বসায় একটি data descriptor, যে সেই ঘর পড়ে-লেখে। তালিকার বাইরের নাম লিখতে চাইলে বসানোর মতো কোনো __dict__ নেই, তাই আসে AttributeError. __slots__ নতুন কোনো ভাষা-বৈশিষ্ট্য নয় — descriptor ব্যবস্থার আরেকটি প্রয়োগ।

৫ · একটি class কীভাবে জন্মায় — class বাক্যের ভেতরের ধাপ

অংশ ১-এ দেখেছেন, class বাক্য আসলে type(...) ডাকা। কিন্তু মাঝে আরও কয়েকটি ধাপ আছে, আর প্রতিটি ধাপে একটি করে “হুক” — যেখানে আপনার কোড ঢোকানো যায়। নিচের stepper-এ একটি পেমেন্ট gateway class-এর জন্ম দেখুন। এখানে সব হুক একসাথে লাগানো হয়েছে, যাতে ক্রমটি পরিষ্কার হয় — বাস্তবে একসাথে এত হুক প্রায় কখনো লাগে না। ক্রমটি Python 3.14-এ প্রতিটি হুকে print বসিয়ে যাচাই করা।

1@register2class Bkash(Gateway, metaclass=Meta, code="bkash"):3    fee = Field(lambda v: v >= 0, "ফি ঋণাত্মক নয়")4    def pay(self, amount): ...56# Gateway-এর ভেতরে:7#   def __init_subclass__(cls, code=None, **kw): ...
জন্মের খাতা
metaclass
namespace
class object
Bkash নাম
  1. metaclass ঠিক করা। Python প্রথমে ঠিক করে, কোন metaclass এই class বানাবে। metaclass=Meta লেখা থাকলে সেটি; না থাকলে parent-দের metaclass (সাধারণত type). একাধিক parent-এর metaclass আলাদা হলে এখানেই সমস্যা হতে পারে (অংশ ৬)। code="bkash"-এর মতো বাড়তি keyword-গুলো পরের ধাপগুলোতে পাঠানো হয়।
  2. __prepare__ — খালি খাতা। Meta.__prepare__("Bkash", bases) ডাকা হয়। সে একটি mapping ফেরত দেয়, যেখানে class-এর body-র নামগুলো জমা হবে। type-এর নিজস্বটি একটি সাধারণ খালি dict দেয়। নিজের __prepare__ খুব কমই লাগে।
  3. body চলে। class-এর ভেতরের লাইনগুলো একটি সাধারণ কোডের মতো ওপর থেকে নিচে চলে, আর প্রতিটি নাম ওই খাতায় জমা হয়: fee = একটি Field object, pay = একটি ফাংশন। body-তে একটি print রাখলে সেটি এখনই ছাপা হয় — কেউ object না বানালেও।
  4. Meta.__new__ — class object তৈরি। নাম, parent আর ভরা খাতা নিয়ে Meta.__new__ ডাকা হয়। ভেতরে সে super().__new__ দিয়ে type.__new__ ডাকে, আর সেখানেই আসল class object জন্মায়। এখানে আপনি চাইলে খাতা বদলাতে পারেন — নাম যোগ, বাদ বা যাচাই।
  5. __set_name__ — type.__new__-এর ভেতরে। class তৈরি হওয়ামাত্র, type.__new__ থেকে বেরোনোর আগেই, Python খাতার প্রতিটি মানকে দেখে: __set_name__ আছে? Field-এর আছে, তাই ডাকা হয় fee.__set_name__(Bkash, "fee"). এজন্যই descriptor তার নাম class ব্যবহারের আগেই জেনে যায়।
  6. __init_subclass__ — parent-এর পালা। এখনো type.__new__-এর ভেতরে: নতুন class-এর parent (Gateway)-এর __init_subclass__ ডাকা হয়, নতুন class আর code="bkash" নিয়ে। parent এখানে subclass-কে নিবন্ধন বা যাচাই করতে পারে। লক্ষ করুন, এটি metaclass-এর __init__-এর আগে চলে।
  7. Meta.__init__। __new__ ফিরে আসার পর metaclass-এর __init__ চলে, তৈরি হয়ে যাওয়া class হাতে নিয়ে। এখানে class আর বদলানো হয় না সাধারণত, শুধু শেষ সাজসজ্জা বা নিবন্ধন।
  8. class decorator আর নাম। সবার শেষে @register চলে — পর্ব ৩৭-এর নিয়মে Bkash = register(Bkash). decorator যা ফেরত দেয়, Bkash নামটি সেটাই পায়। এর আগ পর্যন্ত module-এ Bkash নামটি ছিলই না।
metaclass ঠিক করা → __prepare__ → body → Meta.__new__ (ভেতরে __set_name__ → __init_subclass__) → Meta.__init__ → class decorator → নাম

এই ক্রম থেকে একটি ব্যবহারিক শিক্ষা: class decorator সবচেয়ে শেষে চলে, তাই সে সম্পূর্ণ তৈরি class দেখে — সব descriptor নাম পেয়ে গেছে, সব parent হুক চলে গেছে। আর __init_subclass__ চলে metaclass-এর __init__-এর আগে, যা অনেককে অবাক করে। কোন হুক কখন চলে, তা না জেনে দুটি একসাথে ব্যবহার করলে এমন বাগ হয়, যেখানে একটি হুক এমন জিনিস খোঁজে, যা অন্যটি তখনো বসায়নি।

৬ · সরল থেকে কঠিন — class decorator, __init_subclass__, metaclass

class তৈরির সময় কিছু করার তিনটি হাতিয়ার আছে। কোনটি কখন, সেটিই এই অংশের মূল প্রশ্ন।

ধাপ ১ — class decorator: একটি class, একবার

পর্ব ৩৭-এ দেখেছেন, @register class Plugin: ... মানে Plugin = register(Plugin). class decorator সবচেয়ে সরল আর সবচেয়ে স্পষ্ট: যে class-এর ওপরে @ লেখা, শুধু তার ওপরেই কাজ করে, আর পাঠক চোখের সামনেই দেখেন যে কিছু একটা হচ্ছে। @dataclass এর সবচেয়ে বড় উদাহরণ। এর সীমা একটাই: subclass-এ এটি আপনাআপনি চলে না। @register class Gateway লিখলে class Bkash(Gateway) নিবন্ধিত হবে না, যদি না প্রতিটি subclass-এর ওপরেও @register লেখেন — আর কেউ একজন ভুলবেনই।

ধাপ ২ — __init_subclass__: প্রতিটি subclass, আপনাআপনি

এখানেই __init_subclass__ (Python 3.6, PEP 487). parent class-এ একবার লিখলে, প্রতিটি নতুন subclass তৈরির সময় এটি নিজে থেকে চলে — নাতি-পুতি সব প্রজন্মে। পর্ব ৩৪-এ এর একটি ছোট রূপ দেখেছেন। এবার বাস্তবের মতো একটি plugin registry: প্রতিটি gateway নিজের কোড (code="bkash") দেয়, মাঝের সাধারণ class নিবন্ধিত হয় না, আর pay() না লিখলে class তৈরির মুহূর্তেই error:

class Gateway:
    registry = {}

    def __init_subclass__(cls, code=None, **kwargs):
        super().__init_subclass__(**kwargs)      # সহযোগিতা: অন্য parent-কেও সুযোগ
        if code is None:
            return                               # মাঝের সাধারণ class — নিবন্ধন নয়
        if not callable(getattr(cls, "pay", None)):
            raise TypeError(f"{cls.__name__}: pay() method নেই")
        Gateway.registry[code] = cls

class MobileWallet(Gateway):                     # code নেই, তাই তালিকায় নেই
    fee = 1.85

class Bkash(MobileWallet, code="bkash"):
    def pay(self, amount): return f"bKash: {amount} টাকা"

class Nagad(MobileWallet, code="nagad"):
    def pay(self, amount): return f"Nagad: {amount} টাকা"

class Card(Gateway, code="card"):
    def pay(self, amount): return f"Card: {amount} টাকা"

Gateway.registry["nagad"]().pay(500)             # 'Nagad: 500 টাকা'
class Rocket(Gateway, code="rocket"): pass       # TypeError: Rocket: pay() method নেই

তিনটি জিনিস লক্ষ করুন। (১) class লাইনের code="bkash" একটি সাধারণ keyword argument হয়ে __init_subclass__-এ পৌঁছায় — অংশ ৫-এর ধাপ ১ আর ৬। (২) Bkash সরাসরি Gateway-এর সন্তান নয়, নাতি; তবু হুকটি চলেছে। (৩) super().__init_subclass__(**kwargs) লাইনটি বাদ দেবেন না: একাধিক parent থাকলে প্রত্যেকের হুক চলার সুযোগ এভাবেই আসে (পর্ব ৩৩-এর cooperative super())। নতুন পেমেন্ট পদ্ধতি যোগ করতে এখন শুধু একটি class লিখলেই হয় — কোনো কেন্দ্রীয় তালিকা হাতে বদলাতে হয় না। একে বলে plugin architecture.

এবার দ্বিতীয় runnable প্রোগ্রাম: __set_name__ আর __init_subclass__ মিলিয়ে একটি খুদে ORM, যা class-এর field পড়ে নিজে থেকে SQL টেবিলের সংজ্ঞা লেখে আর একটি সাধারণ __init__ দেয় — পর্ব ৫২-এর SQLAlchemy যা করে, তার একটি খেলনা রূপ।

class Column:
    def __init__(self, sql_type):
        self.sql_type = sql_type
    def __set_name__(self, owner, name):     # descriptor না হলেও ডাক আসে
        self.name = name

class Model:
    def __init_subclass__(cls, table=None, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.columns = [v for v in vars(cls).values() if isinstance(v, Column)]
        cols = ", ".join(f"{c.name} {c.sql_type}" for c in cls.columns)
        cls.ddl = f"CREATE TABLE {table or cls.__name__.lower()} ({cols})"
    def __init__(self, **values):
        for col in self.columns:
            setattr(self, col.name, values.get(col.name))

class Customer(Model, table="customers"):
    name = Column("TEXT")
    phone = Column("TEXT")
    due = Column("REAL")

print(Customer.ddl)
c = Customer(name="রহিম", due=250.0)
print(c.name, c.phone, c.due, Customer.due.sql_type)
print(vars(c))

আউটপুট তিন লাইনের: CREATE TABLE customers (name TEXT, phone TEXT, due REAL), তারপর রহিম None 250.0 REAL, তারপর {'name': 'রহিম', 'phone': None, 'due': 250.0}. যা ঘটল: class Customer তৈরির সময় প্রথমে প্রতিটি Column-এর __set_name__ চলল (তাই প্রত্যেকে নিজের নাম জানে), তারপর Model.__init_subclass__ vars(cls) থেকে Column-গুলো সংগ্রহ করল। Python 3.6 থেকে class body-র নামের ক্রম রক্ষিত থাকে, তাই কলামের ক্রম লেখার ক্রমের মতোই। একটি শিক্ষণীয় বিষয়: Column-এর __get__/__set__ নেই — সে descriptor-ই নয়, তবু __set_name__ পেয়েছে। __init__-এ setattr মান রেখেছে instance-এর ঘরে, আর অংশ ২-এর নিয়মে instance-এর ঘর সাধারণ class attribute-এর আগে আসে — তাই c.due হলো 250.0, কিন্তু Customer.due এখনো Column object, যার sql_type আমরা পড়তে পারলাম। যে phone দেওয়া হয়নি, সে None.

ধাপ ৩ — metaclass: class তৈরির পুরো প্রক্রিয়ার মালিকানা

metaclass হলো type-এর একটি subclass. এর চারটি method, প্রতিটি অংশ ৫-এর একটি ধাপের মালিক:

  • __prepare__(mcls, name, bases, **kw): body-র জন্য খাতা দেয়। একটি বিশেষ mapping দিয়ে body-তে একই নাম দুবার লেখা ধরা যায়, বা লেখার ক্রম মনে রাখা যায়।
  • __new__(mcls, name, bases, ns, **kw): class object তৈরি করে; খাতা বদলানো যায়।
  • __init__(cls, name, bases, ns, **kw): তৈরি class-কে সাজায়।
  • __call__(cls, *args, **kwargs): এটি class তৈরিতে নয়, object তৈরিতে চলে। Config() লেখা মানে type(Config).__call__(Config), যা ভেতরে __new__ আর __init__ ডাকে। এটি বদলালে object তৈরির নিয়মই বদলে যায়।

শেষেরটির একটি প্রচলিত উদাহরণ singleton — যে class-এর object পুরো প্রোগ্রামে একটিই হবে, যেমন একটি কনফিগারেশন:

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):          # Config() লিখলে এটি চলে
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Config(metaclass=SingletonMeta):
    def __init__(self):
        print("config loaded")

a = Config()     # config loaded
b = Config()     # কিছুই ছাপা হয় না — আগেরটিই ফেরত
print(a is b)    # True

কাজ করে — কিন্তু সৎভাবে বলতে গেলে, Python-এ singleton-এর জন্য metaclass প্রায় কখনো সেরা উত্তর নয়। একটি module নিজেই singleton (প্রথম import-এর পর cache হয়, পর্ব ২২), তাই config.py-তে একটি object বানিয়ে রাখা, বা functools.cache দেওয়া একটি get_config() ফাংশন অনেক সরল আর পরীক্ষায় সহজে বদলানো যায়। উদাহরণটি দেখানো হলো __call__ বোঝাতে।

metaclass-এর লড়াই — conflict

metaclass-এর একটি বড় ব্যবহারিক সমস্যা: দুটি লাইব্রেরি নিজের নিজের metaclass ব্যবহার করলে তাদের class মিলিয়ে একটি class বানানো যায় না।

class M1(type): pass
class M2(type): pass
class A(metaclass=M1): pass
class B(metaclass=M2): pass

class C(A, B): pass
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

বার্তাটির মানে: C-এর metaclass এমন হতে হবে, যা M1 আর M2 দুটিরই subclass (বা তাদের একটি নিজেই)। এখানে এমন কেউ নেই, তাই Python সিদ্ধান্ত নিতে পারে না কোনটি C বানাবে। সমাধান হলো class M3(M1, M2) বানিয়ে metaclass=M3 দেওয়া — কিন্তু তখন দুটি অপরিচিত metaclass একসাথে ঠিকঠাক চলবে, এর কোনো নিশ্চয়তা নেই। এই সমস্যাকে বলে metaclass conflict, আর এটিই PEP 487 আনার একটি বড় কারণ: __init_subclass__ সাধারণ method, তাই সাধারণ inheritance-এর নিয়মে মিলেমিশে চলে।

তাহলে metaclass কখন সত্যিই লাগে?

Zen of Python-এর লেখক Tim Peters-এর নামে একটি উক্তি বহুল প্রচলিত, যা পর্ব ৩৪-এও দেখেছেন: “Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't.” আধুনিক Python-এ metaclass লাগে মূলত তখন, যখন __init_subclass__ বা decorator সেই ধাপে পৌঁছাতে পারে না:

  • body চলার আগে বা চলার সময় নিয়ন্ত্রণ: __prepare__ দিয়ে বিশেষ খাতা দেওয়া। Python-এর নিজের enum এটি ব্যবহার করে — Enum-এর metaclass (EnumType, 3.11-এর আগে নাম ছিল EnumMeta) body-তে একই নাম দুবার দিলে ধরতে পারে।
  • class-এর নিজের আচরণ বদলানো: যেমন len(Color) বা for c in Color — class-এর ওপর dunder method কাজ করাতে সেগুলো metaclass-এ লিখতে হয়, কারণ পর্ব ৩২-এর নিয়মে Python dunder খোঁজে object-এর type-এ, আর class-এর type হলো তার metaclass.
  • isinstance/issubclass-এর নিয়ম বদলানো: পর্ব ৩৫-এর ABCMeta এভাবেই “virtual subclass” সম্ভব করে।
কাজপ্রথম পছন্দ
একটি attribute-এর যাচাই বা হিসাব@property
একই যাচাই অনেক attribute বা class-এনিজের descriptor + __set_name__
নির্দিষ্ট কয়েকটি class-এ method যোগ বা নিবন্ধনclass decorator
প্রতিটি subclass নিবন্ধন বা যাচাই__init_subclass__
class body-র খাতা নিয়ন্ত্রণ, class-এর ওপর operatormetaclass (শেষ উপায়)

৭ · introspection — চলার সময় নিজেকে দেখা আর বদলানো

metaprogramming-এর তৃতীয় হাতিয়ার: object-কে প্রশ্ন করা — এর কী কী attribute আছে, এই ফাংশন কী কী argument নেয়? প্রোগ্রামের নিজের গঠন পড়াকে বলে introspection, আর পড়ার সঙ্গে বদলানোও যোগ হলে অনেকে বলেন reflection.

নাম যখন একটি string

সাধারণ কোডে attribute-এর নাম আপনি লেখার সময়ই জানেন: p.price. কিন্তু নামটি যদি আসে একটি ফাইল, ফর্ম বা কমান্ড থেকে, string হিসেবে? তখন চারটি built-in ফাংশন:

  • getattr(obj, "price") = obj.price; তৃতীয় argument দিলে না পেলে সেটি ফেরত দেয়: getattr(obj, "discount", 0).
  • setattr(obj, "price", 500) = obj.price = 500 (descriptor থাকলে __set__ সহ — প্রথম runnable প্রোগ্রামের লুপ এটিই করেছে)।
  • hasattr(obj, "price") — আসলে getattr চেষ্টা করে দেখে AttributeError আসে কি না। তাই property-র ভেতরে অন্য কোনো AttributeError হলে এটি ভুল করে False বলতে পারে।
  • vars(obj) = obj.__dict__; আর dir(obj) দেয় ব্যবহারযোগ্য সব নামের তালিকা, class-এর আর পূর্বপুরুষদের নামসহ।

inspect — ফাংশনের নকশা পড়া

স্ট্যান্ডার্ড লাইব্রেরির inspect module আরও গভীরে দেখে। inspect.signature(func) (Python 3.3 থেকে, PEP 362) একটি ফাংশনের পুরো signature দেয় — parameter-এর নাম, default, annotation. inspect.getmembers(cls, inspect.isfunction) একটি class-এর সব ফাংশন নামসহ (বর্ণানুক্রমে) দেয়। এগুলো দিয়ে একটি ছোট CLI কমান্ড-ব্যবস্থা বানানো যায়, যেখানে ফাংশনের annotation দেখে ব্যবহারকারীর লেখা string আপনাআপনি সঠিক type-এ বদলায় — পর্ব ৫৩-এর Typer লাইব্রেরি ঠিক এই ধারণায় চলে:

import inspect

COMMANDS = {}
def command(func):                       # পর্ব ৩৭-এর registry decorator
    COMMANDS[func.__name__] = func
    return func

@command
def send(to: str, amount: int = 100):
    return f"{amount} টাকা → {to}"

def run(line):
    name, *words = line.split()
    func = COMMANDS[name]
    sig = inspect.signature(func)        # (to: str, amount: int = 100)
    bound = sig.bind(*words)             # শব্দগুলো parameter-এ বসানো
    for pname, text in bound.arguments.items():
        bound.arguments[pname] = sig.parameters[pname].annotation(text)
    return func(*bound.args, **bound.kwargs)

run("send 01712345678 500")    # '500 টাকা → 01712345678'  (500 এখন int)
run("send")                    # TypeError: missing a required argument: 'to'

sig.bind ঠিক সেই নিয়মে argument মেলায়, যে নিয়মে Python নিজে ফাংশন ডাকে — কম বা বেশি argument হলে একই রকম TypeError. একই কৌশলে ছোট একটি dependency injection ব্যবস্থাও বানানো যায়: ফাংশনের parameter-এর নাম পড়ে (db, notifier) সেই নামের জিনিস খুঁজে দেওয়া। FastAPI-র Depends আর pytest-এর fixture (পর্ব ৪২) এভাবে parameter-এর নাম দেখেই ঠিক করে কী পাঠাতে হবে।

Python 3.14-এ annotation

Python 3.14 থেকে (PEP 649 আর 749) annotation আর def বা class তৈরির মুহূর্তে হিসাব হয় না; প্রথমবার কেউ পড়তে চাইলে তবেই হিসাব হয়। ফলে customer: Customer লেখা যায় Customer সংজ্ঞায়িত হওয়ার আগেই, কোনো error ছাড়া। কিন্তু তখনো Customer তৈরি না হলে Order.__annotations__ পড়লে NameError আসে। নিজের metaprogramming কোডে annotation পড়তে নতুন annotationlib module-এর annotationlib.get_annotations(cls, format=annotationlib.Format.FORWARDREF) নিরাপদ — যা হিসাব করা যায় না, তার জায়গায় একটি ForwardRef বসায়। পর্ব ৪০-এ type hint-এর পুরো গল্প।

monkeypatching — চলন্ত প্রোগ্রামে অন্যের কোড বদলানো

Python-এ module আর class-ও object, তাই চলার সময় তাদের attribute বদলে দেওয়া যায়: math.sqrt = lambda x: 42 লিখলে এরপর পুরো প্রোগ্রামে, যে module math.sqrt ডাকে, সবাই 42 পাবে। অন্যের কোড এভাবে চলার সময় বদলানোকে বলে monkeypatching. এর একটি বৈধ জায়গা আছে — টেস্ট: পর্ব ৪২-এ pytest-এর monkeypatch fixture দেখবেন, যা টেস্টের সময় একটি ফাংশন নকল দিয়ে বদলায় আর টেস্ট শেষে নিজে থেকে আগের অবস্থায় ফিরিয়ে দেয়. বাকি সব জায়গায় এটি বিপজ্জনক:

  • দূরের প্রভাব: বদলটি পুরো প্রক্রিয়ায় সবার জন্য, এমনকি সেই লাইব্রেরিগুলোর জন্যও, যারা কিছুই জানে না।
  • অদৃশ্য: math.sqrt-এর ডকুমেন্টেশন পড়ে আপনি বুঝবেন না কেন 42 আসছে; IDE-ও জানে না।
  • ভঙ্গুর: লাইব্রেরির পরের সংস্করণে ভেতরের নাম বদলালে patch নিঃশব্দে কাজ করা বন্ধ করে, বা ভুল জিনিস বদলায়।

eval আর exec — সবচেয়ে ধারালো ছুরি

eval() একটি string-কে Python expression হিসেবে চালায় (eval("2 + 3") → 5), আর exec() পুরো statement বা ফাংশন-সংজ্ঞাও চালাতে পারে। অর্থাৎ চলার সময় নতুন কোড লিখে চালানো — metaprogramming-এর চূড়ান্ত রূপ। স্ট্যান্ডার্ড লাইব্রেরি নিজেও সাবধানে এটি ব্যবহার করে: dataclasses ভেতরে __init__-এর source লেখা string হিসেবে তৈরি করে exec দিয়ে ফাংশন বানায়।

অবিশ্বস্ত ইনপুটে কখনোই eval বা exec নয়। ব্যবহারকারীর লেখা, ফর্মের ঘর, ফাইল বা ওয়েব থেকে আসা কোনো string এদের দিলে আক্রমণকারী যা খুশি চালাতে পারেন — ফাইল মোছা, পাসওয়ার্ড পড়া, সার্ভার দখল। এর নাম arbitrary code execution, সবচেয়ে গুরুতর নিরাপত্তা-দুর্বলতাগুলোর একটি। globals খালি করে দিলেও এটি নিরাপদ হয় না — Python-এর object-জগৎ থেকে বিপজ্জনক জিনিসে পৌঁছানোর বহু পথ আছে। সংখ্যা বা list-এর মতো সরল মান দরকার হলে ast.literal_eval বা json.loads; হিসাবের সূত্র দরকার হলে নিজের ছোট parser. পর্ব ৫৪-এ নিরাপদ Python-এর পুরো আলোচনা।

৮ · বড় লাইব্রেরির ভেতরে — আর জাদুর দাম

এই পর্বের হাতিয়ারগুলো আপনি নিজে হয়তো কম লিখবেন, কিন্তু প্রতিদিন ব্যবহার করবেন। ধারণাগতভাবে দেখুন বড় লাইব্রেরিগুলো কোনটি কোথায় লাগায় (ভেতরের খুঁটিনাটি সংস্করণভেদে বদলায়):

লাইব্রেরিআপনি লেখেনপেছনে যা চলে
dataclasses@dataclass class Product: price: floatclass decorator annotation পড়ে __init__, __repr__, __eq__-এর source তৈরি করে exec দিয়ে বানায়
Django ORMclass Customer(models.Model): name = models.CharField(...)metaclass field-গুলো সংগ্রহ করে টেবিলের তথ্য বানায়; সম্পর্কের field-এ descriptor বসায়, যাতে order.customer লিখলে ডেটাবেস থেকে আনা হয়
SQLAlchemy (পর্ব ৫২)name: Mapped[str] = mapped_column()declarative base class-এর হুক কলাম সংগ্রহ করে; class-এর কলামগুলো descriptor-এ বদলে যায়, তাই Customer.name == "রহিম" একটি SQL শর্ত বানায়
Pydantic (পর্ব ৫১)class Order(BaseModel): total: floatmetaclass annotation পড়ে যাচাইয়ের নিয়ম তৈরি করে; object বানালেই type যাচাই আর রূপান্তর

খেয়াল করুন, Pydantic আর Django এখনো metaclass ব্যবহার করে, কারণ তারা __init_subclass__-এর আগের যুগে জন্ম নিয়েছে বা class-এর ওপর এমন নিয়ন্ত্রণ চায়, যা হুক দিয়ে পাওয়া যায় না। আপনার নিজের কোডে সেই যুক্তি প্রায় কখনো খাটে না।

জাদুর তিনটি দাম

  • পড়া আর ডিবাগ: c.due দেখে মনে হয় একটি সাধারণ attribute; আসলে হয়তো একটি descriptor ডেটাবেসে প্রশ্ন পাঠাচ্ছে। Zen of Python বলে, “Explicit is better than implicit.” যত জাদু, তত বেশি জায়গায় খুঁজতে হয় আসলে কী ঘটছে।
  • টুলের অন্ধত্ব: setattr, type(...) বা exec দিয়ে চলার সময় বানানো নাম IDE-র autocomplete আর mypy-র মতো type checker দেখতে পায় না — তাদের কাছে সেগুলো ভুল বা অস্তিত্বহীন। এজন্য Python 3.11-এ typing.dataclass_transform (PEP 681) এসেছে, যাতে লাইব্রেরি type checker-কে জানাতে পারে “আমার class dataclass-এর মতো আচরণ করে”.
  • গতি: descriptor-ওয়ালা attribute পড়া মানে প্রতিবার একটি Python ফাংশন ডাক, যা সাধারণ attribute পড়ার চেয়ে কয়েক গুণ ধীর। সাধারণ কাজে এটি নগণ্য, কিন্তু লক্ষ লক্ষ বার চলা লুপে টের পাওয়া যায় — পর্ব ৪৬-এ মেপে দেখার পদ্ধতি।
C/C++ জানলে

C-তে চলার সময় নতুন type বানানো বা attribute খোঁজায় হাত দেওয়ার কোনো ব্যবস্থা নেই; কোড-দিয়ে-কোড বানানোর একমাত্র হাতিয়ার preprocessor macro, যা compile-এর আগে লেখা বদলায় (C-র preprocessor ও macro). C++-এর metaprogramming হয় compile-এর সময় — template আর constexpr দিয়ে (C++-এর constexpr ও compile-time হিসাব) — ফলে চলার সময় কোনো খরচ নেই, কিন্তু চলার সময় কিছু বদলানোও যায় না। Python ঠিক উল্টো: সব হয় চলার সময়, তাই নমনীয়তা অনেক বেশি, দামও দিতে হয় চলার সময়। অন্য ভাষার পাঠকদের জন্য: Java-র reflection আর annotation Python-এর inspect আর decorator-এর কাছাকাছি (Java-র annotation ও reflection), তবে Java-তে চলার সময় class-এর গঠন বদলানো অনেক কঠিন।

পরীক্ষা ও ইন্টারভিউতে
  • Metaclass কী? type(int) কী? — যে class-এর object নিজেরাই class; ডিফল্ট metaclass type, তাই type(int) হলো type.
  • Descriptor কী, data আর non-data-র পার্থক্য? — __get__/__set__/__delete__ আছে এমন class attribute; __set__ বা __delete__ থাকলে data descriptor, যা instance-এর __dict__-এর আগে জেতে।
  • __getattr__ আর __getattribute__-এর পার্থক্য? — দ্বিতীয়টি প্রতিটি attribute পড়ায় চলে; প্রথমটি শুধু সাধারণ খোঁজা ব্যর্থ হলে।
  • __init_subclass__ কেন metaclass-এর চেয়ে ভালো? — সাধারণ classmethod-এর মতো, inheritance-এ মিলেমিশে চলে, metaclass conflict হয় না, পড়া সহজ।
  • Method-এ self কে বসায়? — ফাংশনের __get__; সে object-কে বেঁধে একটি bound method ফেরত দেয়।

৯ · AI-এর যুগে — AI-এর লেখা “জাদু” যাচাই করা

“প্রতিটি subclass নিবন্ধন করার ব্যবস্থা করে দাও” বললে AI প্রায়ই সরাসরি একটি metaclass লিখে দেয় — কারণ পুরোনো টিউটোরিয়াল আর Stack Overflow উত্তরে, যা তার শেখার ডেটার বড় অংশ, এটাই প্রচলিত ছিল। কোডটি চলে, কিন্তু তিন লাইনের __init_subclass__-এর জায়গায় বিশ লাইনের জাদু, আর পরে অন্য লাইব্রেরির সাথে মেলাতে গেলে metaclass conflict. AI-এর লেখা metaprogramming-এ বারবার যে ভুলগুলো দেখা যায়:

AI প্রায়ই যা করেসমস্যাকী চাইবেন
registry বা যাচাইয়ের জন্য metaclassঅপ্রয়োজনীয় জটিলতা; অন্য metaclass-এর সাথে conflict__init_subclass__ বা class decorator
descriptor-এ self.value = valueসব object একই মান ভাগ করে — একটির দাম বদলালে সবার বদলায়মান obj.__dict__-এ, নাম __set_name__ থেকে
__get__-এ obj is None পরীক্ষা নেইclass থেকে পড়লে (Product.price) crash; ডকুমেন্টেশন টুল ভাঙেclass থেকে পড়লে descriptor নিজেকে ফেরত দেবে
__getattribute__ বদলানো, ভেতরে self.xRecursionError; প্রতিটি attribute পড়া ধীরযথেষ্ট হলে __getattr__; নইলে super().__getattribute__
setattr/exec দিয়ে method বানানোIDE, mypy, ডকুমেন্টেশন — কেউ নামগুলো দেখে নাস্পষ্ট লেখা method, বা অন্তত type stub
ইনপুট পার্স করতে evalarbitrary code executionjson.loads, ast.literal_eval, বা নিজের parser
__init_subclass__-এ super() ডাক নেইmultiple inheritance-এ অন্য parent-এর হুক চলে নাsuper().__init_subclass__(**kwargs)

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

আমি Python 3.12+ ব্যবহার করি। একটি অনলাইন দোকানের জন্য দুটি জিনিস লিখে দাও: (ক) একটি পুনর্ব্যবহারযোগ্য descriptor Field, যা একটি যাচাই-ফাংশন আর বার্তা নেয়; Product class-এ price (> 0), stock (>= 0) আর phone (১১ অঙ্কের, 01 দিয়ে শুরু) — তিনটি field. (খ) একটি পেমেন্ট gateway registry, যেখানে প্রতিটি subclass (bKash, Nagad, Card) class লাইনে code="..." দিয়ে নিজে থেকে নিবন্ধিত হবে। শর্ত: ১. metaclass ব্যবহার করবে না। registry-র জন্য __init_subclass__, আর তাতে super().__init_subclass__(**kwargs) ডাকবে। ২. descriptor __set_name__ দিয়ে নিজের নাম জানবে, মান রাখবে obj.__dict__-এ — descriptor-এর নিজের গায়ে নয়। __get__-এ obj is None হলে descriptor নিজেকে ফেরত দেবে। ৩. eval, exec বা setattr দিয়ে method বানানো চলবে না; সব method স্পষ্ট লেখা থাকবে, যাতে IDE আর mypy দেখতে পায়। ৪. code ছাড়া মাঝের class নিবন্ধিত হবে না; pay() ছাড়া gateway class তৈরির সময়েই TypeError দেবে। ৫. assert দিয়ে দেখাও: দুটি Product-এর দাম আলাদা থাকে, ভুল দামে ValueError আসে, আর registry-তে ঠিক তিনটি চাবি আছে। ৬. শেষে এক অনুচ্ছেদে বলো, কেন এখানে metaclass দরকার নেই।

AI-এর লেখা metaprogramming পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Python 2.2-এর আগে list বা dict থেকে subclass বানানো যেত না কেন? “new-style class” আর type-class একীকরণ কোন সমস্যার সমাধান করেছিল?

Python-এর প্রথম দশকে ভাষার ভেতরে দুটি আলাদা জগৎ ছিল। একদিকে built-in type — int, list, dict — যেগুলো C-তে লেখা, আর যাদের নিজস্ব নিয়ম। অন্যদিকে class — আপনার class বাক্য দিয়ে বানানো, যার সব object একটি সাধারণ “instance” type-এর। দুই জগতের মাঝে দেয়াল: আপনি list-এর subclass বানাতে পারতেন না। একটি “নিজে থেকে সাজানো list” চাইলে UserList-এর মতো একটি wrapper class লিখতে হতো, যা ভেতরে একটি আসল list রেখে প্রতিটি method হাতে ফরোয়ার্ড করত — ধীর, লম্বা, আর isinstance(x, list)-এ ব্যর্থ।

২০০১ সালের Python 2.2-এ Guido van Rossum এই দেয়াল ভাঙেন। তাঁর প্রস্তাব PEP 252 (“Making Types Look More Like Classes”) আর PEP 253 (“Subtyping Built-in Types”), আর সঙ্গে একটি দীর্ঘ প্রবন্ধ — “Unifying types and classes in Python 2.2”. নতুন ব্যবস্থায় সব class-এর মূলে একটি সাধারণ পূর্বপুরুষ এল: object. যে class object (বা কোনো built-in type) থেকে inherit করে, তাকে বলা হলো new-style class; পুরোনো ধাঁচের class-গুলো “classic class” নামে টিকে রইল, যাতে পুরোনো কোড না ভাঙে।

এই একীকরণের সাথে একই প্যাকেজে এসেছিল এই পর্বের প্রায় সবকিছু: descriptor protocol, property, classmethod, staticmethod, __slots__, __getattribute__, object তৈরির জন্য __new__, আর metaclass-কে একটি স্বীকৃত ধারণা হিসেবে। কারণটি যৌক্তিক: built-in type আর class যদি একই জিনিস হয়, তবে built-in-গুলোর যেসব আচরণ আগে C কোডে লুকানো ছিল (যেমন method কীভাবে বাঁধা হয়), সেগুলো একটি প্রকাশ্য, Python থেকে ব্যবহারযোগ্য নিয়মে বর্ণনা করতে হবে। descriptor ঠিক সেই নিয়ম।

পরের ধাপে, Python 2.3-এ new-style class-এর MRO বদলে C3 linearization নেওয়া হয় — একটি পদ্ধতি, যা ১৯৯৬ সালে Dylan ভাষার জন্য একটি গবেষণাপত্রে প্রস্তাবিত হয়েছিল; কারণ পুরোনো depth-first নিয়ম diamond inheritance-এ অসংগত ক্রম দিত (পর্ব ৩৩)। আর দুই ধরনের class পাশাপাশি থাকার বিভ্রান্তি শেষ হয় Python 3.0-তে (২০০৮), যেখানে classic class পুরোপুরি বাদ যায় — এখন প্রতিটি class নিজে থেকেই object-এর সন্তান।

মূল উপলব্ধি: descriptor আর metaclass কোনো আলাদা “উন্নত বৈশিষ্ট্য” হিসেবে যোগ হয়নি; এগুলো এসেছিল ভাষার একটি মৌলিক অসংগতি দূর করার প্রয়োজনে। ভালো নকশায় অনেক সময় একটি সাধারণ নিয়ম বহু বিশেষ নিয়মের জায়গা নেয় — আর সেই নিয়মটি ব্যবহারকারীর হাতেও তুলে দেওয়া যায়।

প্র ০২“সবকিছুই object, class-ও” — এই ভাবনা Smalltalk থেকে কীভাবে এল? Smalltalk-এর metaclass আর Python-এর type-এর মধ্যে মিল আর অমিল কোথায়?

১৯৭০-এর দশকে Xerox PARC গবেষণাগারে Alan Kay, Dan Ingalls, Adele Goldberg ও তাঁদের সহকর্মীরা Smalltalk ভাষা গড়ে তোলেন, যার পরিণত রূপ Smalltalk-80 প্রকাশ্যে আসে ১৯৮০-র দশকের শুরুতে। এর কেন্দ্রীয় ভাবনা ছিল চরম সরলতা: সবকিছুই object, আর সব কাজ হয় object-কে বার্তা পাঠিয়ে. সংখ্যা object, true object, কোডের একটি ব্লক object — আর class-ও object. 3 + 4 মানে 3 নামের object-কে “+ 4” বার্তা পাঠানো।

class যদি object হয়, তবে প্রশ্ন ওঠে: class-টি কোন class-এর instance? Smalltalk-80-এর উত্তর: প্রতিটি class-এর নিজস্ব একটি metaclass থাকে, যা সিস্টেম আপনাআপনি বানায়। Point-এর metaclass হলো “Point class”, আর তাতেই থাকে class-স্তরের method, যেমন নতুন object বানানোর new. ফলে class-এর একটি শ্রেণিবিন্যাসের পাশাপাশি metaclass-এরও একটি সমান্তরাল শ্রেণিবিন্যাস চলে। আর শৃঙ্খলটি শেষ হয় একটি বৃত্তে: সব metaclass হলো Metaclass নামের class-এর instance, আর Metaclass-এর নিজের metaclass আবার Metaclass-এরই instance.

Python-এ একই বৃত্ত অনেক সরলভাবে দেখা যায়: type(type) হলো type; type নিজে object-এর subclass, আর object হলো type-এর instance. দুটি জিনিস একে অপরকে ধরে রেখেছে — আর এই দুটি ভাষার ভেতরে C কোডে বিশেষভাবে বানানো, কারণ সাধারণ নিয়মে ডিম-আগে-না-মুরগি-আগে সমস্যার সমাধান হয় না।

অমিলটি নকশার দর্শনে। Smalltalk-এ প্রতিটি class-এর নিজস্ব metaclass, যাতে প্রত্যেকের class-method আলাদা থাকে। Python-এ প্রায় সব class একটিই metaclass ভাগ করে — type — আর class-method পাওয়া যায় descriptor (classmethod) দিয়ে, metaclass ছাড়াই। নিজের metaclass তখনই লাগে, যখন class-এর নিজের আচরণ বদলাতে হয়। এই ধারায় আরেকটি প্রভাবশালী কাজ হলো Common Lisp-এর Metaobject Protocol — Gregor Kiczales ও সহলেখকদের ১৯৯১ সালের বই The Art of the Metaobject Protocol, যা দেখায় কীভাবে একটি ভাষার নিজের object-ব্যবস্থাকেই প্রোগ্রাম দিয়ে বদলানো যায়।

মূল উপলব্ধি: “class-ও object” একটি ছোট বাক্য, কিন্তু এটি মেনে নিলে ভাষা নিজের সম্পর্কে কথা বলতে পারে। Python Smalltalk-এর এই উত্তরাধিকার নিয়েছে, কিন্তু ব্যবহারিকতার খাতিরে বেশিরভাগ ক্ষমতা লুকিয়ে রেখেছে একটি ডিফল্ট — type — এর পেছনে।

প্র ০৩PEP 487 একসাথে দুটি জিনিস এনেছিল — __init_subclass__ আর __set_name__. এর আগে এই কাজগুলো কীভাবে হতো, আর Python 3.6-এর আরেকটি পরিবর্তন কীভাবে __prepare__-এর প্রধান ব্যবহারও প্রায় অপ্রয়োজনীয় করে দিল?

Python 3.6-এর (২০১৬) আগে দুটি খুব সাধারণ কাজের জন্য metaclass লিখতে হতো। প্রথমটি subclass নিবন্ধন বা যাচাই — plugin registry, প্রতিটি model class-কে একটি তালিকায় তোলা। দ্বিতীয়টি descriptor-কে তার নাম জানানো — price = Field()-এ Field জানতই না সে price; তাই হয় নাম দুবার লিখতে হতো, নয়তো একটি metaclass class তৈরির পর namespace ঘুরে প্রতিটি field-কে নাম বলে দিত। Django, SQLAlchemy-র মতো লাইব্রেরি তাদের metaclass-এর ভেতরে এমন কাজই করত।

সমস্যা হলো metaclass ছোঁয়াচে: একটি class-এর metaclass থাকলে তার সব subclass সেটি পায়, আর দুটি ভিন্ন metaclass-এর class মেশাতে গেলে অংশ ৬-এর conflict. ফলে দুটি লাইব্রেরির base class একসাথে inherit করা প্রায়ই অসম্ভব হয়ে উঠত, শুধু এই কারণে যে দুজনেই নিবন্ধনের মতো ছোট একটি কাজের জন্য metaclass লিখেছে।

Martin Teichmann-এর PEP 487, “Simpler customisation of class creation”, পর্যবেক্ষণ করে যে বাস্তবের বেশিরভাগ metaclass আসলে এই দুটি কাজই করে। তাই এটি দুটি সাধারণ হুক প্রস্তাব করে, যা ডিফল্ট type নিজেই ডাকবে: parent-এ __init_subclass__, আর প্রতিটি attribute-এ __set_name__. এগুলো সাধারণ method, তাই সাধারণ inheritance আর super()-এর নিয়মে মিলেমিশে চলে, কোনো conflict ছাড়াই। এর আগে Nick Coghlan-এর PEP 422 একই ধরনের লক্ষ্য নিয়ে একটি হুক প্রস্তাব করেছিল; সেটি পরে PEP 487-এর পক্ষে প্রত্যাহার করা হয়।

একই সংস্করণে আরেকটি নীরব পরিবর্তন: Eric Snow-এর PEP 520 নিশ্চিত করে যে class body-র নামগুলো লেখার ক্রমে সংরক্ষিত থাকবে (সঙ্গে CPython-এর dict-এর নতুন বাস্তবায়নও ক্রম রাখত, যা Python 3.7-এ সব dict-এর জন্য ভাষার নিয়ম হয়)। আগে ORM-গুলো __prepare__ দিয়ে একটি OrderedDict ফেরত দিত, শুধু যাতে টেবিলের কলাম লেখার ক্রমে থাকে। এখন সেটি এমনিতেই হয় — এই পর্বের খুদে ORM-এর কলাম-ক্রম তার প্রমাণ।

মূল উপলব্ধি: একটি শক্তিশালী সাধারণ হাতিয়ার (metaclass) থাকলে মানুষ সেটি ছোট কাজেও ব্যবহার করে, আর তার জটিলতার দাম সবাইকে দিতে হয়। বাস্তব ব্যবহার পর্যবেক্ষণ করে সবচেয়ে সাধারণ কাজগুলোর জন্য সরল হুক দেওয়া — ভাষা-নকশার একটি পরিণত কৌশল।

প্র ০৪“Explicit is better than implicit” — Zen of Python-এর এই নীতি আর metaprogramming-এর জাদু কি পরস্পরবিরোধী? একটি লাইব্রেরি কখন জাদুর দাম দেওয়ার যোগ্য?

Tim Peters-এর লেখা Zen of Python (PEP 20, import this)-এর দ্বিতীয় বাক্য “Explicit is better than implicit”, আর পরে আসে “Readability counts” আর “Special cases aren't special enough to break the rules”. প্রথম দেখায় metaprogramming এর সরাসরি বিরোধী: c.due পড়লে ডেটাবেসে প্রশ্ন যাচ্ছে, অথচ লেখায় তার কোনো চিহ্ন নেই — এটাই implicit.

কিন্তু বাস্তবে প্রশ্নটি “জাদু ভালো না খারাপ” নয়, বরং “জাদুর সীমানা কোথায় আর কতটা পূর্বানুমেয়”. @dataclass জাদু, কিন্তু তার আচরণ ছোট, ডকুমেন্টেড আর সবার পরিচিত — আপনি জানেন সে ঠিক কী বানাবে। অন্যদিকে, যে কোড setattr দিয়ে চলার সময় কয়েকশো method বানায়, বা monkeypatch দিয়ে অন্য লাইব্রেরির আচরণ বদলায়, সেখানে কোনো পাঠক আর একটি লাইন দেখে বুঝতে পারেন না কী ঘটছে। Ruby on Rails-এর সম্প্রদায় দীর্ঘদিন “convention over configuration” আর প্রচুর runtime জাদু পছন্দ করেছে; Python-এর সংস্কৃতি সাধারণত বেশি সাবধানী — যদিও Django-ও প্রথম দিকে বেশ কিছু জাদু রেখেছিল এবং পরে তার একাংশ সরল করে।

জাদুর একটি সুস্পষ্ট বাস্তব উদাহরণ gevent লাইব্রেরি: monkey.patch_all() ডাকলে স্ট্যান্ডার্ড লাইব্রেরির socket, thread, sleep — সব বদলে যায় তার নিজের সংস্করণে, যাতে পুরোনো synchronous কোড না বদলেই একসাথে হাজার সংযোগ সামলাতে পারে। ফল চমকপ্রদ, কিন্তু এর ডকুমেন্টেশন নিজেই সতর্ক করে যে patch যত দ্রুত সম্ভব, অন্য module import-এর আগে করতে হবে — নইলে কিছু module আসল আর কিছু module বদলানো সংস্করণ ধরে রাখে, আর এমন বাগ আসে যা বোঝা দুঃসাধ্য।

আরেকটি দাম পড়ে টুলের ওপর। type checker আর IDE কোড না চালিয়ে বিশ্লেষণ করে; চলার সময় তৈরি জিনিস তারা দেখে না। তাই Django আর Pydantic-এর জন্য mypy-র আলাদা plugin তৈরি হয়েছে, আর Python 3.11-এ PEP 681 dataclass_transform যোগ করেছে — লাইব্রেরি যেন স্পষ্ট ঘোষণা দিয়ে জাদুটি টুলের কাছে “explicit” করতে পারে। অর্থাৎ ভাষা নিজেই জাদু আর স্পষ্টতার মাঝে সেতু বানাচ্ছে।

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

প্র ০৫Django-র ORM আর Pydantic — দুটি বিখ্যাত লাইব্রেরি কীভাবে metaprogramming-এর ওপর দাঁড়িয়ে আছে, আর তাদের নকশা থেকে কী শেখার আছে?

Django জন্ম নেয় যুক্তরাষ্ট্রের Lawrence Journal-World পত্রিকার ওয়েব দলে, Adrian Holovaty আর Simon Willison-এর হাতে, আর ২০০৫ সালে open source হিসেবে প্রকাশিত হয়। এর ORM-এর লক্ষ্য ছিল: ব্যবহারকারী শুধু লিখবেন class Customer(models.Model): name = models.CharField(max_length=100) — বাকি সব, অর্থাৎ টেবিলের নাম, কলাম, SQL, যাচাই, admin প্যানেল, Django নিজে বুঝে নেবে।

এটি সম্ভব হয় এই পর্বের হাতিয়ারে। models.Model-এর পেছনে একটি metaclass (ModelBase) আছে। class তৈরির সময় সে namespace ঘুরে প্রতিটি field খুঁজে বের করে, তাকে class-এর সাথে যুক্ত করে আর নাম জানায়, তারপর _meta নামে একটি object বানায় — যেখানে টেবিলের নাম, সব field-এর তালিকা, সম্পর্ক সব জমা থাকে। প্রতিটি model-এর জন্য আলাদা DoesNotExist exception-ও এখানে তৈরি হয়। আর ForeignKey-এর মতো সম্পর্কের field-এ বসানো হয় descriptor, যাতে order.customer পড়লে পেছনে একটি ডেটাবেস প্রশ্ন যায় আর ফল cache হয়। এমনকি Customer.objects-ও একটি descriptor-এর পেছনে রাখা, যা object থেকে পড়লে ইচ্ছা করে error দেয় — যাতে customer.objects লেখার ভুল ধরা পড়ে।

Pydantic তৈরি করেন Samuel Colvin, ২০১৭ সালে, Python-এর type hint কাজে লাগানোর একটি নতুন ধারণা নিয়ে: annotation শুধু টুলের জন্য নয়, চলার সময় যাচাইয়ের জন্যও। class Order(BaseModel): total: float লিখলে BaseModel-এর metaclass annotation পড়ে প্রতিটি field-এর যাচাই আর রূপান্তরের নিয়ম বানায়; Order(total="250") দিলে "250" হয়ে যায় 250.0, আর "abc" দিলে স্পষ্ট বার্তাসহ error. ২০২৩ সালের Pydantic 2-তে যাচাইয়ের মূল অংশ Rust-এ লেখা pydantic-core-এ সরানো হয়, গতির জন্য — Python-এর metaprogramming নকশা ঠিক করে, ভারী কাজ করে দ্রুত ভাষা। FastAPI (পর্ব ৫১) এর ওপর দাঁড়িয়ে, যা ফাংশনের signature inspect দিয়ে পড়ে ঠিক করে কোন ডেটা কোথা থেকে আসবে।

দুটি লাইব্রেরির শিক্ষা একই: জটিলতা এক জায়গায় জমা, সরলতা সবার জন্য. metaclass, descriptor আর introspection-এর কঠিন কোড লাইব্রেরির ভেতরে একবার লেখা, পরীক্ষিত আর ডকুমেন্টেড; ব্যবহারকারী লেখেন সাধারণ, ঘোষণামূলক (declarative) class. আর দুটিই সময়ের সাথে টুলের কথা ভেবেছে — type checker plugin আর স্পষ্ট type hint যোগ করে।

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

অনুশীলন

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

  1. কে জিতবে: একটি class-এ price = property(lambda self: 100) আর shop = "করিম স্টোর" আছে। একটি object p-এর জন্য লেখা হলো p.__dict__["price"] = 5 আর p.__dict__["shop"] = "রহিম স্টোর". এখন p.price আর p.shop কী দেবে? আর p.price = 7 লিখলে কী হবে?

    p.price দেবে 100: property একটি data descriptor, তাই instance-এর ঘরের 5 উপেক্ষিত। p.shop দেবে 'রহিম স্টোর': class-এর string descriptor নয়, তাই instance-এর ঘর জেতে। p.price = 7 দেবে AttributeError — property-র setter নেই, আর লেখার সময়ও data descriptor আগে দেখা হয়, তাই মানটি instance-এর ঘরে চুপচাপ লেখাও হয় না। অংশ ২-এর ধাপ ১, ২ আর ৬ — তিনটিই এখানে।

  2. ছাপার ক্রম: একটি parent class-এর __init_subclass__-এ print("A"); একটি descriptor-এর __set_name__-এ print("B"); একটি class decorator-এ print("C"); আর subclass-এর body-তে একটি print("D"). subclass-টি decorator-সহ, descriptor রেখে তৈরি হলো। কোন ক্রমে ছাপা হবে?

    D, B, A, C. প্রথমে body চলে (D). তারপর type.__new__-এর ভেতরে প্রথমে __set_name__ (B), তারপর parent-এর __init_subclass__ (A). সবার শেষে class decorator (C). অংশ ৫-এর stepper-এর ধাপ ৩, ৫, ৬, ৮। মনে রাখার সহজ উপায়: আগে খাতা ভরা, তারপর সদস্যদের নাম জানানো, তারপর বাবা-মাকে জানানো, শেষে বাইরের মোড়ক।

  3. হাতিয়ার বাছুন: প্রতিটি কাজের জন্য সবচেয়ে সরল হাতিয়ার বলুন — (ক) Student-এর gpa ০ থেকে ৪-এর মধ্যে রাখা; (খ) দশটি model class-এর ত্রিশটি field-এ “খালি string নয়” যাচাই; (গ) প্রতিটি নতুন রিপোর্ট class নিজে থেকে একটি তালিকায় তোলা; (ঘ) শুধু তিনটি নির্দিষ্ট class-এ একটি to_json() method যোগ করা।

    (ক) @property আর setter — একটি class, একটি attribute, descriptor লাগে না। (খ) নিজের descriptor (NonEmpty) __set_name__-সহ — একবার লিখে ত্রিশ জায়গায়। (গ) base class-এ __init_subclass__ — প্রতিটি subclass আপনাআপনি, কেউ ভুলে যাওয়ার সুযোগ নেই। (ঘ) class decorator (@jsonable), বা আরও সরলভাবে একটি mixin class (পর্ব ৩৩) — যেহেতু মাত্র তিনটি নির্দিষ্ট class, আর পাঠক ওপরে চিহ্নটি দেখতে পান। কোনোটিতেই metaclass দরকার নেই।

  4. একটি descriptor লিখুন (এবং AI-কে যাচাই করুন): Choice(*allowed) নামের একটি descriptor লিখুন, যা শুধু নির্দিষ্ট মান নেয় — যেমন একটি অর্ডারের status = Choice("pending", "paid", "shipped"); অন্য মান দিলে ValueError. তারপর একই কাজ কোনো AI-কে দিয়ে লিখিয়ে অংশ ৯-এর চেকলিস্ট ধরে মিলিয়ে নিন।

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

    class Choice:
        def __init__(self, *allowed):
            self.allowed = allowed
        def __set_name__(self, owner, name):
            self.name = name
        def __get__(self, obj, owner=None):
            return self if obj is None else obj.__dict__.get(self.name)
        def __set__(self, obj, value):
            if value not in self.allowed:
                raise ValueError(f"{self.name}: {value!r} চলবে না; চলবে {self.allowed}")
            obj.__dict__[self.name] = value
    
    class Order:
        status = Choice("pending", "paid", "shipped")

    মূল বিষয়গুলো: মান রাখা হয়েছে obj.__dict__-এ, descriptor-এর গায়ে নয় — তাই দুটি অর্ডারের status আলাদা থাকে। __set_name__ নাম জানায়, তাই error বার্তায় field-এর নাম আসে। obj is None হলে descriptor নিজেকে ফেরত দেয়, তাই Order.status.allowed পড়ে অনুমোদিত মানগুলো দেখা যায় (একটি ফর্মের drop-down বানাতে কাজে লাগে)। AI-এর কোডে খেয়াল করুন: self.value-তে মান রাখা হয়েছে কি না, আর কোনো metaclass ঢুকে পড়েছে কি না। বাস্তবে এই কাজের জন্য স্ট্যান্ডার্ড লাইব্রেরির enum-ও একটি ভালো বিকল্প।

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

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

ক্রমPython-এ একটি decorator-সহ subclass তৈরি হওয়ার সময় ঘটনাগুলো যে ক্রমে ঘটে, সেই ক্রমে সাজান (▲▼ চেপে সরান)

  1. metaclass-এর __prepare__ খালি namespace দেয়
  2. class body চলে, নামগুলো namespace-এ জমা হয়
  3. metaclass-এর __new__ class object তৈরি করে
  4. attribute-গুলোর __set_name__ ডাকা হয়
  5. parent-এর __init_subclass__ চলে
  6. class decorator চলে আর নামটি বসে

Python-এ type(int) আর type(type) কী দেয়?

  • object আর object
  • type আর type
  • <class 'int'> আর type
  • দ্বিতীয়টিতে TypeError

একটি class-এ x নামে একটি data descriptor আছে, আর একটি object-এর __dict__-এও 'x' চাবিতে একটি মান রাখা। obj.x পড়লে কী হয়?

  • descriptor-এর __get__ ডাকা হয়, তার ফল আসে
  • instance-এর __dict__-এর মান আসে
  • AttributeError, কারণ নামটি দুই জায়গায়
  • class-এর __getattr__ ডাকা হয়

Python-এ একটি class-এ __getattr__ লেখা থাকলে সেটি কখন ডাকা হয়?

  • প্রতিটি attribute পড়ার সময়
  • প্রতিটি attribute লেখার সময়
  • শুধু সাধারণ খোঁজা ব্যর্থ হয়ে AttributeError এলে
  • class তৈরির সময় একবার

Python-এ একটি class-এর জন্মের সময় নিচের কোনটি সবার শেষে চলে?

  • descriptor-এর __set_name__
  • parent-এর __init_subclass__
  • metaclass-এর __init__
  • class decorator

Python-এ p.label() ডাকলে self প্যারামিটারে p বসে কোন ব্যবস্থায়?

  • parser p.label()-কে লেখার সময়েই label(p)-এ বদলে দেয়
  • ফাংশনের __get__ একটি bound method ফেরত দেয়
  • object তৈরির সময় প্রতিটি method-এর কপি p.__dict__-এ রাখা হয়
  • metaclass-এর __call__ প্রতিটি ডাকে self বসায়

class A(metaclass=M1) আর class B(metaclass=M2), যেখানে M1 আর M2 দুটি সম্পর্কহীন metaclass. class C(A, B): pass লিখলে কী হয়?

  • Python নিজে থেকে M1 আর M2 মিলিয়ে নতুন metaclass বানায়
  • প্রথম parent-এর metaclass M1 ব্যবহার হয়
  • class তৈরির সময়েই TypeError: metaclass conflict
  • class তৈরি হয়, কিন্তু object বানালে error

শব্দকোষ

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

মনে রাখুন

  1. Class-ও object, চলার সময় type বানায়; class বাক্য = type(name, bases, ns), আর type(type) হলো type.
  2. Attribute পড়ার ক্রম: data descriptor → instance __dict__ → non-data descriptor / class attribute → __getattr__. __getattribute__ প্রতিটি পড়ায় চলে; __getattr__ শুধু ব্যর্থ হলে।
  3. Descriptor = __get__/__set__/__set_name__; নিয়ম descriptor-এ, মান obj.__dict__-এ। ফাংশন, property, classmethod, staticmethod, __slots__ — সবাই descriptor.
  4. Class-এর জন্ম: __prepare__ → body → Meta.__new__ (ভেতরে __set_name__, __init_subclass__) → Meta.__init__ → class decorator.
  5. সরল হাতিয়ার আগে: property → descriptor → class decorator → __init_subclass__ → metaclass. getattr/inspect সাবধানে, monkeypatch শুধু টেস্টে, অবিশ্বস্ত ইনপুটে eval/exec কখনোই নয়।

পরের পর্বে: মডিউল ৫ শেষ — class থেকে metaclass পর্যন্ত Python-এর অবজেক্ট-জগতের ভেতরের যন্ত্র এখন আপনার চেনা। মডিউল ৬ শুরু হচ্ছে আরেকটি protocol দিয়ে: with open(...) লিখলে ফাইল কে বন্ধ করে, __enter__/__exit__ কী, আর নিজের context manager কীভাবে বানাবেন।

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

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