অধ্যায় 22

মডিউল আর ইমপোর্ট

নিজের লেখা ফাইলকে আরেক ফাইল থেকে ব্যবহার করা, import করলে আসলে কী ঘটে, `if __name__ == "__main__"` কেন লাগে, স্ট্যান্ডার্ড লাইব্রেরি, আর ফাইলের নাম দিয়ে মডিউল ঢেকে ফেলার ফাঁদ।

33 মিনিটPython 3.12
  1. 1সমস্যা
  2. 2বোঝা
  3. 3উদাহরণ
  4. 4অনুমান
  5. 5নিজে করা
  6. 6কঠিন করা

যে সমস্যাটা আমরা সমাধান করছি

গত অধ্যায়ে আমরা একটা line_total ফাংশন লিখেছিলাম। এখন দ্বিতীয় একটা প্রোগ্রাম লিখতে গিয়ে সেটাই আবার দরকার।

সবচেয়ে সহজ উপায় — কপি করে বসিয়ে দেওয়া। সেটা কাজও করে, প্রথম দিন।

তারপর করের হার বদলায়। আপনি একটা ফাইলে ঠিক করেন, অন্যটার কথা মনে থাকে না। এখন দুটো প্রোগ্রাম একই অর্ডারের দুটো আলাদা মোট দেখাচ্ছে, আর কোনটা ঠিক তা বলার উপায় নেই — কারণ দুটোই "ঠিক", যার যার ফাইল অনুযায়ী।

সমস্যাটা কপি করার পরিশ্রম নয়। সমস্যাটা হলো সত্যটা এখন দুই জায়গায়।

python
import pricing

print(pricing.TAX_RATE)
print(pricing.line_total(15.0, 3))
text
0.15
51.75

একটা ফাইল, আর যত প্রোগ্রাম দরকার তারা সবাই সেটাকেই ব্যবহার করে। হার বদলালে এক জায়গায় বদলায়।

এই অধ্যায়টা সেই এক লাইন — import — আর তার পেছনে আসলে কী ঘটে।

এই অধ্যায় শেষে আপনি পারবেন

  • নিজের লেখা একটা ফাইলকে আরেকটা ফাইল থেকে ইমপোর্ট করতে
  • import x, from x import y আর import x as z-এর পার্থক্য বলতে
  • বলতে ইমপোর্ট করার মুহূর্তে আসলে কী ঘটে, আর সেটা কতবার ঘটে
  • if __name__ == "__main__": কেন লেখা হয় তা ব্যাখ্যা করতে
  • স্ট্যান্ডার্ড লাইব্রেরি থেকে মডিউল আনতে
  • পাইথন কোথায় খোঁজে তা জানতে, আর ফাইলের নাম দিয়ে একটা মডিউল ঢেকে ফেলার ফাঁদ এড়াতে

পূর্বশর্ত: স্কোপ আর পরিবর্তনীয় ডিফল্ট।


মডিউল মানে কেবল একটা ফাইল

pricing.py নামের একটা ফাইল থাকলেই সেটা pricing নামের একটা মডিউল। আলাদা করে কিছু ঘোষণা করতে হয় না, কোনো বিশেষ লাইন লিখতে হয় না — ফাইলটা থাকাই যথেষ্ট।

import pricing লিখলে গোটা মডিউলটা আসে, আর ভিতরের জিনিসে পৌঁছাতে হয় বিন্দু দিয়ে: pricing.TAX_RATE, pricing.line_total(...)।

বিন্দুটা বাড়তি টাইপিং মনে হতে পারে, কিন্তু সে একটা কাজ করে — পড়ার সময় বলে দেয় জিনিসটা কোথা থেকে এসেছে। বড় ফাইলে সেটা অনেক।

শুধু যেটা দরকার সেটাই আনা

python
from pricing import line_total

print(line_total(15.0, 3))
print(TAX_RATE)
text
51.75
NameError: name 'TAX_RATE' is not defined

from ... import ... কেবল যে নামটা চাওয়া হয়েছে সেটাই নিয়ে আসে, আর সেটাকে সরাসরি ব্যবহার করা যায় — বিন্দু লাগে না।

কিন্তু বাকি নামগুলো আসে না। TAX_RATE চাওয়া হয়নি, তাই সে নেই — যদিও একই ফাইলে সে বসে আছে।

কোনটা কখন? মোটামুটি নিয়ম: দুই-তিনটা নাম দরকার হলে from, আর অনেক কিছু দরকার হলে গোটা মডিউল। আর একটা জিনিস কখনো করবেন না — from pricing import *। ওটা সব নাম নিঃশব্দে টেনে আনে, তারপর line_total কোথা থেকে এলো তা খুঁজে বের করার আর কোনো উপায় থাকে না।

নাম বদলে নেওয়া

python
import pricing as p

print(p.line_total(15.0, 3))
text
51.75

as মডিউলটাকে এই ফাইলের ভিতরে একটা ছোট নাম দেয়।

এটা তখনই লিখবেন যখন নামটা সত্যিই লম্বা, অথবা প্রথা এমনই। অলসতার জন্য এক অক্ষরের নাম দিলে পড়ার সুবিধাটাই নষ্ট হয় — p.line_total(...) দেখে কেউ বলতে পারবে না জিনিসটা কোথাকার।

ইমপোর্ট করার মানে হলো ফাইলটা চালানো

এই জায়গাটা বেশিরভাগ মানুষ পরে গিয়ে আবিষ্কার করে, প্রায়ই খারাপ সময়ে।

tools.py:

python
print("tools is being set up")


def ping():
    return "pong"

main.py:

python
print("main starts")
import tools
import tools

print(tools.ping())
print("main ends")
text
main starts
tools is being set up
pong
main ends

দুটো জিনিস লক্ষ্য করুন।

ইমপোর্ট করা মানে ফাইলটা উপর থেকে নিচ পর্যন্ত চালানো। def লাইনগুলো ফাংশন তৈরি করে, আর বাকি যা আছে — যেমন ওই print — সেগুলো সত্যিই ঘটে। মডিউলের ভিতরে টপ-লেভেলে print বা input রাখলে যে-ই তাকে ইমপোর্ট করবে তার ঘাড়ে সেটা এসে পড়বে।

দ্বিতীয় import tools কিছুই করেনি। একটা মডিউল প্রোগ্রামে একবারই চলে; এরপর থেকে পাইথন আগেরটাকেই ফিরিয়ে দেয়। তাই একই মডিউল দশ জায়গায় ইমপোর্ট করা নিরাপদ ও সস্তা।

__name__ আর সেই বিখ্যাত লাইনটা

প্রতিটা মডিউলের ভিতরে __name__ নামের একটা নাম থাকে, যেটা পাইথন নিজে বসায়।

tools.py:

python
def ping():
    return "pong"


print("module name is", __name__)

if __name__ == "__main__":
    print("running directly")

python tools.py চালালে:

text
module name is __main__
running directly

কিন্তু অন্য একটা ফাইল থেকে import tools করলে:

text
module name is tools
imported, name was tools

নিয়মটা সরল: যে ফাইলটাকে আপনি চালাচ্ছেন তার __name__ হয় "__main__"; যেগুলো ইমপোর্ট হচ্ছে তাদের __name__ হয় তাদের নিজের নাম।

তাই if __name__ == "__main__":-এর ভিতরের অংশটা চলে কেবল সরাসরি চালালে, ইমপোর্ট করলে নয়।

এটার দরকার আগের অংশ থেকেই বোঝা যায়। ইমপোর্ট মানে ফাইলটা চালানো — তাই ফাইলের শেষে main() লিখে রাখলে, যে-ই তাকে ইমপোর্ট করবে তার প্রোগ্রাম হঠাৎ আপনার প্রোগ্রামটাও চালিয়ে দেবে। ওই লাইনটা সেই দরজাটা বন্ধ করে।

স্ট্যান্ডার্ড লাইব্রেরি

পাইথনের সাথেই অনেকগুলো মডিউল আসে। কিছু ইনস্টল করতে হয় না।

python
import math
import statistics

print(math.sqrt(144))
print(math.floor(3.7), math.ceil(3.2))
print(statistics.mean([10, 20, 60]))
text
12.0
3 4
30

আরেকটা, যেটা প্রায়ই দরকার হয়:

python
import random

random.seed(7)
print(random.randint(1, 100))
print(random.choice(["pen", "bag", "ink"]))
text
42
pen

random.seed(7) না থাকলে প্রতিবার আলাদা সংখ্যা আসত — সেটাই তো উদ্দেশ্য। বীজ বসালে ক্রমটা নির্দিষ্ট হয়ে যায়, তাই উপরের আউটপুটটা আপনার মেশিনেও একই আসবে। পরীক্ষা লেখার সময় এটা অপরিহার্য: এলোমেলো কিছু পরীক্ষা করতে হলে আগে তাকে পুনরাবৃত্তিযোগ্য বানাতে হয়।

এটা "এলোমেলো" নয়, "এলোমেলোর মতো"। random একটা হিসাব থেকে সংখ্যা বানায়, তাই একই বীজ থেকে সবসময় একই ক্রম। পাসওয়ার্ড বা টোকেনের জন্য এটা ব্যবহার করবেন না — তার জন্য secrets মডিউল আছে।

পাইথন কোথায় খোঁজে

import pricing লিখলে পাইথন ক্রম ধরে খোঁজে: আগে ইন্টারপ্রেটারের ভিতরে গাঁথা মডিউলগুলো, তারপর যে স্ক্রিপ্টটা চলছে তার নিজের ফোল্ডার, তারপর স্ট্যান্ডার্ড লাইব্রেরি, তারপর ইনস্টল করা প্যাকেজ।

এই ক্রমটার একটা ফল আছে, আর সেটা মানুষকে ভোগায়।

আপনার ফোল্ডারে random.py নামের একটা ফাইল আছে, আর আপনি লিখলেন:

python
import random

print(random.choice([1, 2, 3]))
text
AttributeError: module 'random' has no attribute 'choice'

আপনার ফাইলটা আসল random-এর আগে পড়েছে, তাই সেটাই ইমপোর্ট হয়েছে — আর তার ভিতরে choice নেই।

বার্তাটা বিভ্রান্তিকর, কারণ সে বলছে random-এ choice নেই, যেটা শুনতে অসম্ভব। কিন্তু সে আপনার random-এর কথা বলছে।

নিয়ম: আপনার ফাইলের নাম কোনো মডিউলের নামের সাথে মেলানো যাবে না। random.py, string.py, json.py, csv.py — এগুলো নিরীহ নাম বলে মনে হয়, আর ঠিক সেজন্যই ফাঁদটা কাজ করে।

একটা ব্যতিক্রম, যা নিয়মটাকেই বোঝায়। math.py নামের ফাইল বানিয়ে import math করলে আসল math-ই আসবে, আপনারটা নয়। কারণ math ইন্টারপ্রেটারের ভিতরে গাঁথা, আর সেটা তালিকার সবার আগে। অর্থাৎ ফাঁদটা কিছু নামে কাজ করে, কিছুতে করে না — যা মনে রাখার চেয়ে নামটা বদলে ফেলাই সহজ।

ফোল্ডারে সাজানো

ফাইল বাড়তে থাকলে তাদের ফোল্ডারে ভাগ করা যায়। ফোল্ডারটার ভিতরে একটা __init__.py ফাইল রাখুন — খালি হলেও চলবে — তাহলে ফোল্ডারটা একটা প্যাকেজ হয়ে যায়।

text
shop/
    __init__.py
    pricing.py
main.py

তারপর বিন্দু দিয়ে পথটা লেখা যায়:

python
from shop.pricing import line_total

print(line_total(15.0, 3))
text
51.75

আপাতত এটুকুই যথেষ্ট। এই অধ্যায়ের বাকি অংশে আমরা সমতল ফাইলেই থাকব।


একটা সম্পূর্ণ উদাহরণ

তিনটে ফাইল, আর তাদের মধ্যে নির্ভরতা এক দিকেই।

pricing.py:

python
"""Prices and tax. Nothing here prints, and nothing here asks for input."""

TAX_RATE = 0.15


def line_total(price, quantity):
    """One line of an order, tax included."""
    return round(price * quantity * (1 + TAX_RATE), 2)


def order_total(lines):
    """`lines` is a list of (name, price, quantity) tuples."""
    return round(sum(line_total(price, qty) for _, price, qty in lines), 2)

report.py:

python
"""Turning numbers into lines of text. It imports pricing; pricing imports nothing."""

import pricing


def receipt(lines):
    rows = [f"{name:<8} {pricing.line_total(price, qty):>8.2f}"
            for name, price, qty in lines]
    rows.append("-" * 17)
    rows.append(f"{'total':<8} {pricing.order_total(lines):>8.2f}")
    return rows


if __name__ == "__main__":
    print("report.py has no data of its own to show")

main.py:

python
"""The only file that is meant to be run."""

import statistics

from report import receipt

ORDER = [
    ("pen", 15.0, 3),
    ("bag", 850.0, 1),
    ("ink", 120.0, 2),
]


def main():
    for row in receipt(ORDER):
        print(row)

    print()
    quantities = [qty for _, _, qty in ORDER]
    print("mean quantity:", statistics.mean(quantities))


if __name__ == "__main__":
    main()

python main.py চালালে:

text
pen         51.75
bag        977.50
ink        276.00
-----------------
total     1305.25

mean quantity: 2

চারটে জিনিস দেখার মতো।

নির্ভরতা এক দিকে যায়। main জানে report-কে, report জানে pricing-কে, আর pricing কাউকে জানে না। এই ক্রমটা একটা তালিকা বানায়, আর তালিকার শেষ মাথার ফাইলটা — pricing — একা একা পরীক্ষা করা যায়। pricing-কে যদি আবার report-কে ইমপোর্ট করতে হতো, দুজনের কেউই আর একা দাঁড়াতে পারত না।

pricing.py কিছু ছাপে না, কিছু জিজ্ঞেসও করে না। সে কেবল মান নেয় আর মান ফেরত দেয় — একবিংশ অধ্যায়ের সেই পিওর ফাংশনগুলো। ইমপোর্ট মানে ফাইলটা চালানো, তাই এই সংযমটা ভদ্রতা নয়, প্রয়োজন।

report.py-তেও if __name__ == "__main__": আছে, আর সেটা কোনো কাজের কিছু করে না — এই ফাইলের নিজের কোনো তথ্য নেই বলে তার দেখানোরও কিছু নেই। একে সরিয়ে ফেলা যেত। রেখে দেওয়া হয়েছে কারণ ফাইলটা যদি কোনোদিন সরাসরি চালানো হয়, তখন সে অন্তত কথা বলে — নীরবে কিছু না করে থেমে যাওয়ার চেয়ে ভালো।

main.py ছাড়া কোনো ফাইল নিজে থেকে কিছু করে না। পুরো প্রোগ্রামের কেবল একটাই প্রবেশপথ, আর সেটা পড়লেই বোঝা যায় কী ঘটবে।


কিছু ভাঙা অবস্থা ও তার সমাধান

ModuleNotFoundError: No module named 'priceing' বানান ভুল, নয়তো ফাইলটা অন্য ফোল্ডারে। যে স্ক্রিপ্টটা চালাচ্ছেন তার পাশেই ফাইলটা আছে কিনা দেখুন।

ImportError: cannot import name 'line_totl' from 'pricing' মডিউলটা পাওয়া গেছে, ভিতরের নামটা নয়। বার্তাটা প্রায়ই কাছের নামটা বলে দেয় — Did you mean: 'line_total'?

AttributeError: module 'random' has no attribute 'choice' প্রায় সবসময়ই আপনার নিজের একটা ফাইল আসল মডিউলটাকে ঢেকে দিয়েছে। ফোল্ডারে ওই নামের ফাইল খুঁজুন, আর তার নাম বদলান। __pycache__ ফোল্ডারটাও মুছে দিন।

ImportError: cannot import name 'A' from partially initialized module 'a' (most likely due to a circular import) দুটো ফাইল একে অপরকে ইমপোর্ট করছে। সাধারণ সমাধান হলো যে অংশটা দুজনেরই দরকার সেটাকে তৃতীয় একটা ফাইলে সরানো, যেটা কাউকে ইমপোর্ট করে না।

ইমপোর্ট করামাত্র কিছু একটা ছাপা হলো বা ইনপুট চাইল মডিউলের টপ-লেভেলে কোড আছে। সেটাকে একটা ফাংশনে ঢোকান, আর if __name__ == "__main__":-এর ভিতর থেকে ডাকুন।

সম্পাদনা করলাম, কিন্তু পুরোনো আচরণই দেখছি প্রোগ্রামটা আবার চালান — একটা চলতে থাকা প্রোগ্রাম মডিউলটাকে আর নতুন করে পড়ে না। নোটবুক ব্যবহার করলে কার্নেল রিস্টার্ট করুন।