2025, Nov 01 22:31

NaN के साथ सूची तुलना क्यों टूटती है और unittest में सही टेस्ट कैसे लिखें

NaN IEEE-754 में बराबर नहीं होता, इसलिए सूची तुलना विफल होती है. Python unittest में pandas रोलिंग मीन हेतु NaN मैचर, math.isnan और assertEqual से टेस्ट लिखें.

ऐसा कोड, जो NaN पैदा करता है, का परीक्षण अक्सर जाल बन जाता है: NaN का मतलब “not a number” होता है, और IEEE-754 के नियमों के अनुसार NaN के साथ कोई भी तुलना false होती है — यहां तक कि NaN == NaN भी. जब आपकी फ़ंक्शन वैध रूप से NaN लौटाती है, तो अनुक्रमों पर सीधी समानता-जांच विफल हो जाती है, भले दोनों तरफ़ से मान एक जैसे दिखें. यहां बताया गया है कि ऐसे परीक्षणों को कैसे मज़बूत रखें, वह भी उपयोगी unittest डायग्नोस्टिक्स खोए बिना.

विफलता को दोहराना

परिदृश्य: तीन तत्वों की विंडो वाला रोलिंग मीन. शुरुआती दो स्थानों की गणना नहीं हो सकती और वे NaN होते हैं, इसलिए अपेक्षित सूची की शुरुआत में NaN आते हैं. नीचे दिया गया परीक्षण दो सूचियों की सीधे तुलना करता है और असफल हो जाता है, जबकि संख्याएँ एक जैसी दिखती हैं.

from math import nan
import unittest
import pandas as pd
class DataBin:
    def __init__(self, seed: list) -> None:
        self.seed = seed
        self.values = []
    def compute_window_mean(self):
        self.values = pd.Series(self.seed).rolling(3).mean().to_list()
class RollingMeanSpec(unittest.TestCase):
    def setUp(self):
        self.bin = DataBin(seed=[10, 20, 30, 40])
    def test_three_point_window_on_list(self):
        expected = [nan, nan, 20.0, 30.0]
        self.bin.compute_window_mean()
        self.assertSequenceEqual(self.bin.values, expected)
unittest.main()

असर्शन त्रुटि ठीक पहले ही तत्व पर असमानता दिखाती है:

AssertionError: Sequences differ: [nan, nan, 20.0, 30.0] != [nan, nan, 20.0, 30.0]
First differing element 0:
nan
nan
  [nan, nan, 20.0, 30.0]

असल में टूटता क्या है

सभी मान NaN ही दर्शाते हैं, लेकिन वे समान ऑब्जेक्ट नहीं हैं और बराबर भी नहीं ठहरते. एक न्यूनतम उदाहरण जड़ कारण दिखाता है:

import math
import unittest
class CheckNaNEquality(unittest.TestCase):
    def test_single_nan_item(self):
        self.assertEqual([float("nan")], [math.nan])
if __name__ == "__main__":
    unittest.main()
AssertionError: Lists differ: [nan] != [nan]
First differing element 0:
nan
nan
  [nan]

रोलिंग‑मीन वाले मामले में दिक्कत इस वजह से आती है कि pd.Series.to_list math.nan नहीं, बल्कि नए float("nan") मान लौटाता है. दोनों ही NaN हैं, लेकिन एक‑दूसरे के बराबर नहीं होते. इसलिए सूची की प्रत्यक्ष समानता जाँच विफल होती है.

टिप्पणी: सामान्यतः आप self.assertEqual कॉल कर सकते हैं—यह उपयुक्त अंतर्निहित इम्प्लीमेंटेशन को स्वयं चुन लेगा.

मैचर के साथ सटीक समाधान

“इस स्थान पर NaN होना चाहिए” की तुलना का साफ़ तरीका है पहचान या कड़ी समानता से नहीं, बल्कि “is NaN” गुण से मिलान करना. इसे केवल परीक्षणों में काम आने वाले एक छोटे सहायक के रूप में लिखना आसान है.

import math
import typing as tp
class NaNToken:
    def __eq__(self, other: tp.Any) -> bool:
        return math.isnan(other)
    def __repr__(self) -> str:
        return "nan"

इसके बाद, अपेक्षित सूची NaN स्थानों को स्पष्ट रूप से बता सकती है, और विफलता पर प्रति‑तत्व विस्तृत अंतर बना रहता है:

import unittest
import pandas as pd
class DataBin:
    def __init__(self, seed: list) -> None:
        self.seed = seed
        self.values = []
    def compute_window_mean(self):
        self.values = pd.Series(self.seed).rolling(3).mean().to_list()
class RollingMeanSpec(unittest.TestCase):
    def setUp(self):
        self.bin = DataBin(seed=[10, 20, 30, 40])
    def test_three_point_window_on_list(self):
        expected = [NaNToken(), NaNToken(), 20.0, 30.0]
        self.bin.compute_window_mean()
        self.assertEqual(self.bin.values, expected)
if __name__ == "__main__":
    unittest.main()

अगर कोई non‑NaN मान भटकता है, तो unittest सब कुछ एक मात्र बूलियन में समेटने के बजाय सटीक अंतर दिखाता है:

AssertionError: Lists differ: [nan, nan, 20.0, 30.0] != [nan, nan, 20.0, 35.0]
First differing element 3:
30.0
35.0
- [nan, nan, 20.0, 30.0]
?                   ^
+ [nan, nan, 20.0, 35.0]
?    

इसे उस तरीके से तुलना करें जो पूरी जाँच को एक ही बूलियन में समेट देता है—और सारी डायग्नोस्टिक्स गायब हो जाती हैं:

AssertionError: False is not true

यह जानना क्यों ज़रूरी है

जब कोई फ़ंक्शन वैध रूप से NaN देता है, तो सतही समानता-जांच या तो नाज़ुक परीक्षणों की ओर धकेलती है या अपारदर्शी वर्कअराउंड्स की. “is NaN” गुण पर मिलान करना आशय को सुरक्षित रखता है, अलग‑अलग NaN इंस्टैंस के कारण होने वाले false negatives से बचाता है, और unittest की तत्व‑दर‑तत्व विस्तृत रिपोर्टिंग को बरक़रार रखता है. इससे आपकी अपेक्षित संरचनाएँ पठनीय रहती हैं और जिस डोमेन परिणाम की आप जाँच कर रहे हैं, उसके क़रीब रहती हैं.

मुख्य बातें

यदि कोई परीक्षण ऐसे अनुक्रमों की तुलना करता है जिनमें NaN हो सकता है, तो समानता की बजाय “is NaN” प्रेडिकेट से तुलना करें. pd.Series.to_list नए float("nan") मान लौटाता है, जो न तो math.nan के बराबर होते हैं और न ही एक‑दूसरे के, इसलिए प्रत्यक्ष सूची‑समानता टूटती है. math.isnan का उपयोग करने वाला एक छोटा मैचर तुलना को ठीक कर देता है और साथ ही समृद्ध डिफ़्स भी बनाए रखता है; आप self.assertEqual का प्रयोग करें ताकि unittest उचित तुलना‑तर्क स्वयं चुन सके.

यह लेख StackOverflow के एक प्रश्न (लेखक: Charizard_knows_to_code) और jonrsharpe के उत्तर पर आधारित है।