পাইথনে ইন্টারেষ্টিং বাগ; একই মেথডে ইউনিট টেস্ট সেটআপ আর টিয়ার-ডাউন

পাইথনে ইন্টারেষ্টিং বাগ; একই মেথডে ইউনিট টেস্ট সেটআপ আর টিয়ার-ডাউন
২৯ নভেম্বর ২০২৩, বুধবার 


কাজের জায়গায় রিসেন্টলি একটা ইন্টারেষ্টিং বাগ ফেস করেছি, যেটা আমি নিজেই আসলে না জেনে তৈরী করেছিলাম। পাইথনে শেখা একটা নতুন জিনিস ব্যবহার করতে যেয়েই হয়তো (?) বাগটা কোডে ইন্ট্রোডিউসড হয়েছে। সেটাই প্রথমে লিখছি। তারপর আরেকটা নতুন জিনিস - যেটাও আসলে নতুন শিখলাম - কী করে একটা ইউনিট টেস্টের আগে সেটআপ (test setup) আর টেস্টের পরবর্তী টিয়ার-ডাউন (tear down) একই মেথডে করা যায়, সেটা লিখবো। খুব ছোট করেই লিখবো - কারণ এখন বেশ রাত, ঘুমানো দরকার। 

প্রথম বিষয়: বাগ 
ধরা যাক, নিচের কোডের মতো, আমাদের একটা সিম্পল {string: string } ডিকশেনারী আছে। আমাদের কাজ হচ্ছে, একটা key দেয়া হলে, তার করেস্পন্ডিং value প্রিন্ট করা - খুব সিম্পল ভাবে নিচের মতো করে কোডটা লেখা যায়: 

ছবি: ১

ছবি ১'র লাইন-১০-এর কোড, একটু pythonic করা যায়, যেখানে একলাইনেই কন্ডিশন চেক, আর ভ্যালু এসাইনমেন্ট করা যায়। যেমন নিচের কোডের মতো [এই ব্যাপারটাই আমি নতুন শিখে ভেবেছিলাম ব্যবহার করি!]:

ছবি: ২
val := myDict.get(user_input) আসলে একটা এসাইনমেন্ট স্টেটমেন্ট। প্রথমে ডিকশনারি থেকে ভ্যালু নিয়ে এসে ভ্যারিয়েবল val -এ রাখবে, তারপর if -কন্ডিশন চেক হবে। অর্থাৎ,  ছবি ১ আর ছবি ২ -এর কোড একই আউটপুট দিবে, কোনো সমস্যা নাই। পার্থক্য শুধু হচ্ছে:  ছবি-২'র কোডে ডিকশেনারীটা  দুইবারের বদলে একবার এক্সেস হচ্ছে। ডিকশনারিটা 'get' করে key'র করেস্পন্ডিং value  নিয়ে এসে ভ্যারিয়েবল val -এ রাখছে। যদি key না থাকে, তাহলে ডিফল্ট None ভ্যালু পাবে। আর if None  যেহেতু false তাই, key না থাকলে else ব্লক, অর্থাৎ লাইন ১২-১৩ এক্সেকিউট হবে। কোনো সমস্যা হওয়ার কথা না। তাই না?

কিন্তু এই দুই কোডেই একটা বাগ আছে। কি ধরতে পারছেন? খুব suttle বা সুক্ষ একটা বাগ। একটা হিন্ট দেই, value  যেহেতু string, কোনো একটা key'র রেস্পেক্টিভ value যদি empty string ("") হয়, তখন কি হবে? নিচের কোডে খেয়াল করুন:

ছবি: ৩
আমরা নতুন একটা key-value পেয়ার অ্যাড করেছি (লাইন ৫), যেখানে value আসলে empty string। যখন, key ("empty") দিয়ে ডিকশেনারী লুক-আপ করছি, করেস্পন্ডিং value ("") থাকার পরও বলছে "Key not found"! কারণ কী? কারণটা হচ্ছে, পাইথনে empty string, empty list কিংবা সংখ্যা শূন্য (0) if -কন্ডিশনে false ইভালুয়েট করে বা রিটার্ন করে! আর তাই,  একটা ভ্যালিড value থাকার পরও if কন্ডিশন true রিটার্ন করছে না।

অর্থাৎ, if []: অথবা  if 0:, কিংবা  if "": - সবগুলাই false রিটার্ন করবে 

আশা করি ব্যাগটা বুঝতে পেরেছেন। 

বাগটা সরাতে আমাদের এক্সপ্লিসিটলি বলে দিতে হবে ভ্যারিয়েবল val যেন None না হয়, যেমন নিচের কোডে:



ছবি:৪
ছবি ৪-এ লাইন ১০-এ ভ্যারিয়েবল val -তে ডিকশনারির ভ্যালু রাখার পর এক্সপ্লিসিটলি চেক করছি যে সেটা যেন not None হয়, তবেই প্রিন্ট করছি। এখন empty string ("") যেহেতু None না, তাই if -কন্ডিশন true হবে, আর প্রিন্ট স্টেটমেন্ট এক্সেকিউট হবে, আমরা "Key not found" -এর পরিবর্তে যা চাচ্ছিলাম, সেই এম্পটি স্ট্রিং আউটপুটে দেখতে পাবো [এম্পটি স্ট্রিং হওয়ায় দেখতে আসলে পারছি না :)] 

খেয়াল রাখতে হবে, ব্রাকেট বা পারেনথেসিস না দিলে কিন্তু আরেকটা বাগ তৈরী হবে; কী সেটা? সেটা না হয় নিজেরাই নিচের লিংকে গিয়ে কোড মোডিফাই করে, কোড রান করে দেখে নিন!

কোড: এই লিংকে 

দ্বিতীয় বিষয়: পাইথন ইউনিট টেস্ট: একই মেথডে সেটআপ আর টিয়ার-ডাউন: একটু জটিল 

কখনো কখনো ইউনিট টেস্ট কিছু সেটআপ করা লাগে। যেমন, ধরা যাক, আপনি একটা মেথড লিখেছেন, যেটা কিনা ডাটাবেস-র কোনো একটা টেবিলে একটা এন্ট্রি আপডেট করবে। এই মেথডের ইউনিট টেস্ট হিসাবে আপনি হয়তো চেক করবেন, মেথড কল করা হলে ডাটাবেসে একটা row updated হয় কিনা। ভালো কথা, কিন্তু ইউনিট টেস্টটা রান করার আগে আপনাকে তো নিশ্চিত করতে হবে যে ওই এন্ট্রিটা ডাটাবেসে আছে, ঠিক না? এন্ট্রি যদি না থাকে, তাহলে আপডেট করবেন কিভাবে? ইউনিট টেস্টটা তো ফেল করবে। আর, এই  এন্ট্রিটা যে আছে, সেটা নিশ্চিত করার কাজটাই আপনাকে শুরুর সেটআপ মেথডে করে নিতে হবে। 

আচ্ছা, যখন আপনার ইউনিট টেস্ট শেষ, এইবার সেটআপ-এ ইন্সার্ট করা ওই এন্ট্রি আপনাকে ডিলিট করতে হবে। এটাই টিয়ার-ডাউন (tear -down) প্রসেস। 

আর এক সেটআপ আর টিয়ার-ডাউন করার দুইটা প্রসেসই একটা মেথড দিয়ে করা যায়! পাইথনে yield কীওয়ার্ড দিয়ে সেটা করা যায়, উদাহরণ হিসাবে নিচের কোডের টেম্পলেট দিচ্ছি:

ছবি: ৫: ইউনিট টেস্ট টেম্পলেট 

পাইথনে @pytest.fixture ডেকরেট ব্যবহার করে আমরা কোনো টেস্ট মেথড রান করার আগে সেটআপ মেথড রান করাতে পারি। ছবি ৫-এ ইউনিট টেস্ট test_method রান করার আগে সবসময় setup_tear_down মেথড কল হবে। 
আর ওই মেথডে প্রথমে ডাটাবেসে এন্ট্রি দিতে হবে। তারপরেই yied কীওয়ার্ড ব্যবহার করে কোডের এক্সেকিউশন caller 'র কাছে ফেরত পাঠানো যায়। Caller অর্থাৎ test_method কাজ শেষ করার পর (টেস্ট মেথড row update count এসার্ট করার পর) আবারো setup_tear_down মেথডে ফেরত আসবে। আর তারপর ডাটাবেস থেকে ওই আপডেটেড এন্ট্রি ডিলিট করে দিলেই আমাদের কাজ শেষ!

আশাকরি  ব্যাপার দুইটা বোঝা গেছে! ধন্যবাদ! 

কাজের জায়গায় যখন আগে শেখা এলগোরিদম, ডাটা স্ট্রাকচার কাজে লাগে!

কাজের জায়গায় যখন আগে শেখা এলগোরিদম, ডাটা স্ট্রাকচার কাজে লাগে!

স্কুলে/কলেজে/ইউনিভার্সিটিতে যেইসব এলগোরিদম আর ডাটা স্ট্রাকচার আমরা শিখি, কিংবা ইন্টারভিউ'র প্রিপারেশন নিতে গিয়ে প্রব্লেম সলভিং প্রাকটিস করতে যেইসব জটিল সল্যুশন আমরা ব্যবহার করি - তার কতটুকু জবের দৈনন্দিন সত্যিকার কাজে গিয়ে আসলে লাগে? আমার ধারণা খুব একটা না। অন্তত আমার লাগে না। কিন্তু তাই বলে আমি বলছি না যে এইগুলো শেখার দরকার নাই, প্র্যাক্টিস করা বৃথা - মাথা কাজ করানোর জন্য এইসব খুবই জরুরি। আর কাজের জায়গায় যখন কোনো একটা প্রব্লেম সল্ভ করতে গিয়ে আগের শেখা কোনো এলগোরিদম, ডাটা স্ট্রাকচার কিংবা টেকনিক কাজে লাগে, তখন আসলেই খুব ভালো লাগে। এতো ভূমিকা দেয়ার কারণ হচ্ছে, কয়েকদিন আগে আমার তাই-ই হয়েছিল, জেনেরিক প্রব্লেমটা আর সল্যুশনটা শেয়ার করার লোভ সামলাতে পারছি না। নিচে দিচ্ছি:

প্রব্লেম: ধরা যাক, n সংখ্যক লিস্টে কিছু নম্বর আছে। আপনাকে সেগুলো (পাইথনে) list of lists আকারে ইনপুট দেয়া হবে। আরেকটা সংখ্যা, p দেয়া হবে। আপনার কাজ হবে, p লেংথ-এর একটা লিস্ট রিটার্ন করা,  যেখানে ইনপুটের n লিস্টের সবগুলো থেকে প্রায় সমান সংখক এলিমেন্ট নিতে হবে। আর ডুপ্লিকেট থাকতে পারবে না। দুইটা উদাহরণ দেই:

কেস ১:
list_of_lists = [[1, 2, 3], [3, 4, 5], [6, 7]], p = 6;
expected output = [1, 3, 6, 2, 4, 7] 

কেস ২
list_of_lists = [[1, 2, 3], [3, 4, 5], [6, 7]], p = 7;
expected output = [1, 3, 6, 2, 4, 7, 5]

প্রব্লেমটা আশাকরি বোঝা গেছে। কেস-১ সিম্পল। সবগুলা ইনপুট লিস্ট থেকে ২ টা করে নিয়ে ৬ সাইজের আউটপুট লিস্ট তৈরী করলেই হলো। কেস-২ একটু জটিল,  যেহেতু আউটপুট লিস্টে ডুপ্লিকেট থাকতে পারবে না, ইনপুটের দ্বিত্বীয় লিস্ট থেকে 5 নিয়ে ৭ সাইজের লিস্ট তৈরী করা লাগলো। 

সল্যুশনটা কী হবে? আমার সলুশনটা বলার আগে বলে নেই: সেটাতে আমি ধাপে ধাপে পৌছাইসি। আমার  প্রথমদিনের সলুশনটা জটিল ছিল, পরে একটা ইমপ্রুভমেন্ট মাথায় আসলে ইমপ্লিমেন্ট করি। পরের দিন আরেকটু ভালো সল্যুশন মাথায় আসলে সেটা ইমপ্লিমেন্ট করে আলটিমেট সলুশনে পৌছাইসি। যদিও আগের সল্যুশন গুলো ঠিকই ছিল।  

সল্যুশন: প্রথমেই খেয়াল করে দেখি যে একটা পরিচিত আলগোরিদমের সাথে প্রব্লেমটার মিল আছে। merge sort আলগোরিথমের মার্জ অংশে আমরা দুইটা সর্টেড লিস্ট থেকে একটা আউটপুট লিস্ট বানাই। আর এখানেও অনেকটা তাই, শুধু দুইটার বদলে n-সংখ্যাক লিস্ট, আর সর্টেডর বদলে শর্ত হচ্ছে  আউটপুট লিস্ট ইউনিক হতে হবে। অর্থাৎ, যতক্ষণ p সাইজের লিস্ট হচ্ছে না, ততক্ষন এক এক করে প্রতিটা n -লিস্টের এলিমেন্ট নিবো, একটা set ব্যবহার করে ইউনিক কিনা টেস্ট করে আউটপুট লিস্টে এপেন্ড করতে থাকবো। আর যদি ইউনিক না হয়, তাহলে লিস্টের পরের এলিমেন্টটা নিবো, যতক্ষণ লিস্টে এলিমেন্ট থাকে। কিছু কর্নার কেস বাদে, এইটাই বেসিক সল্যুশন।

কর্নার কেস গুলো কি? কয়েকটা হতে পারে, যেমন: হাতের লিস্টে যদি এলিমেন্ট না থাকে? যদি আউটপুট p -র থেকে ইনপুট n লিস্টের সাইজ কম: (n < p) হয়।  যদি লিস্ট গুলোতে ডুপ্লিকেট সংখ্যা বেশি থাকে  - মূলত এই কয়টাই। কিছু চেক অ্যাড করে সেটাও হ্যান্ডেল করা যাবে। 

বাকিটা না পড়ে, আপনি কি একটু কোডটা ইমপ্লিমেন্ট করার চেষ্টা করবেন? আমার ধারণা করলেই কিছু ইম্প্রোভমেন্ট আপনার মাথায় আসবে। যেটা আমি নিচে দিচ্ছি।

প্রতিটা লিস্টের কোন ইনডেক্স-এ আছি, সেটা ম্যানেজ করা একটু ঝামেলা হবে। একটু চিন্তা করতেই  মাথায় আসলো যে আমি পাইথনের generator ব্যবহার করে সেটা সল্ভ করতে পারি। ইনপুট n -লিস্টের প্রতিটাকে যদি আমি শুরুতেই  generator -এ কনভার্ট করি, আর next() কল করে এলিমেন্ট বের করে আনি, তাহলে পাইথনই ইনডেক্স ম্যানেজ করবে, আমার আর সেটা হ্যান্ডেল করতে হবে না। একটা ঝামেলা হয়তো হবে, যদি জেনারেটরে এলিমেন্ট না থাকে, তাহলে exception থ্রো করবে। ভাবলাম, সেটা নাহয় try-except এ ঢুকিয়ে হ্যান্ডেল করবো। কোডের চেহারা অনেকটা এইরকম হলো:



কোডের লিংক : https://replit.com/join/vhqitodokp-ishtiaquehussai

এরপর আরেকটু ভাবার পর মনে হলো, একটা কিউ ব্যবহার করে তো কোডটা আরো ভালো করা যায়, count ব্যবহার করে এতো জটিল করার তো দরকার নাই। আগে সেই কোডটার স্ক্রিনশট দিচ্ছি, তারপর নিচে ব্যাখ্যা করছি:


 

আইডিয়াটা হচ্ছে, জেনেরেটরগুলোকে শুরুতে একটা কিউতে ঢুকিয়ে রাখলাম। এরপর যতক্ষণ output লিস্টের সাইজ p  না হয় এবং কিউয়ে এলিমেন্ট থাকে, ততক্ষন ডিকিউ করে কিউ থেকে জেনেরেটর বের করে নিলাম। এরপর জেনারেটর থেকে এলিমেন্ট next() কল করে নিলাম, এলিমেন্ট ইউনিক হলে output লিস্টে, set -এ এপেন্ড, অ্যাড করে নিলাম। তারপর হাতের জেনেরেটরকে আবারো কিউ-এ ঢুকিয়ে দিলাম। আর যদি, এলিমেন্ট ইউনিক না হয়, তাহলে জেনেরেটর থেকে next ()- কল করে আবারো একই কাজ করতে থাকলাম (while True) । যদি কখনো next() কল করে জেনারেটরে এলিমেন্ট না পাই, তখন জেনারেটর StopIteration এক্সেপশন থ্রো করবে। সেই এক্সেপশন হ্যান্ডেল করার জন্য কিছুই না, শুধু হাতের জেনেরেটরকে পুনরায় আর কিউতে অ্যাড না করলেই হবে - অর্থাৎ, এলিমেন্ট যখন আর নাই-ই তাহলে সেটা কিউ থেকে ফেলে দিতে হবে।    

আমরা চাইলে except Exception লিখেও কাজটা করতে পারতাম। তবে ভালো কোডিং প্রাকটিস হচ্ছে, too-broad-exception হ্যান্ডেল না করে স্পেসিফিক এক্সেপশন হ্যান্ডেল করা। জেনেরেটরে এলিমেন্ট না থাকলে next() যেহেতু StopIteration এক্সেপশন থ্রো করে, কাজেই সেটা হ্যান্ডেল করাই ভালো। 

সবশেষ: গতরাতে আমার বন্ধু মিথুনের সাথে প্রব্লেমটা নিয়ে আলাপ করছিলাম। মিথুন ইউনিভার্সিটি অফ মেমফিস, টেনেসিতে পিএইচডি করছে। আলাপ করতে গিয়ে আরেকটা আইডিয়া আমাদের মাথায় আসলো। জেনারেটার ছাড়াই শুধু কিউ দিয়েই তো প্রব্লেমটা আসলে সল্ভ করা যায়! ইনপুট list_of_lists 'র প্রতিটা লিস্টের এলিমেন্টগুলোকে কিউয়ে ঢুকিয়ে, পরে সেই কিউ গুলোকে আরেকটা কিউ-এ ঢুকিয়ে queue of queues বা nested queue  করেও তো প্রব্লেমটা সল্ভ করা যায়। চমৎকার আইডিয়া! কারো পাইথনে জেনেরেটর'র আইডিয়া না থাকলে, এভাবেও প্রব্লেমটা সল্ভ করা যাবে। নিচে কোডের স্ক্রিনশট দিচ্ছি:





আর সবগুলো ভার্শনের কোডের লিংক নিচের লিংকে পাওয়া যাবে:
https://replit.com/join/vhqitodokp-ishtiaquehussai

সবাইকে ধন্যবাদ!
--ইশতিয়াক, ৭ ব্যাচ, সিএসই, ডিইউ    

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম  ৩ ফেব্রুয়ারি, শনিবার, ২০২৪ রেগুলার এক্সপ্রেশন (Regular Exp...