اگر دو تابع فقط عدد بگیرند، تقسیم پنج بر دو از نظر پایتون روشن است؛ اما معلوم نیست پنج ولت را بر دو اهم تقسیم کرده‌ایم یا بر دو کیلو‌اهم. نام متغیر و راهنمای نوع کمک می‌کنند، ولی واحد را خودکار همراه حساب نمی‌برند. این درس یک مدل کوچکِ کمیت بُعددار می‌سازد: مقدار گویا، بُعدهای صریح و تبدیل مقیاس محدود. هدف، آزمودن قرارداد عملیات است؛ ابزار بررسی ایستای نوع، حسگر واقعی یا یک کتابخانهٔ جامع واحدها اجرا نشده است.

۱. عددِ یکسان، کمیتِ متفاوت

دو کمیت را فقط از روی عدد یکی نگیریم
تصویر مفهومی: دو کمیت را فقط از روی عدد یکی نگیریم؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مثال تألیفی، پنج ولت روی مقاومت ایدئال دو کیلو‌اهم است. جریان دوونیم میلی‌آمپر، توان دوازده‌ونیم میلی‌وات و انرژیِ سه ثانیه، سه‌هشتادم ژول می‌شود. رابطه‌های مدار در اینجا مفروض‌اند؛ کلاس کمیت خودش کشف نمی‌کند که یک قطعه اهمی است یا توان در چه جهتی تعریف شده. ما می‌خواهیم اشتباهِ مقیاس و ترکیب واحد در همین محاسبه، پیش از تولید پاسخِ ظاهراً معتبر آشکار شود.

در تابع سادهٔ دارای راهنمای float، ورودی‌های پنج و دو نتیجهٔ ۲٫۵ می‌دهند. نتیجه هنوز برچسبی ندارد و خود تابع نمی‌فهمد عدد دوم نمایندهٔ دو کیلو‌اهم بوده است. مقدمهٔ رسمی typing می‌گوید پایتون راهنماهای نوعِ تابع و متغیر را خودکار تحمیل نمی‌کند. این درس فقط رفتار زمان اجرا را می‌سنجد؛ هیچ گزارش موفقیت یا شکستِ بررسی‌کنندهٔ ایستا ندارد.

مدل ما نام واحد ورودی را به مقدارِ مبنای SI تبدیل می‌کند و سپس بردار بُعد را نگه می‌دارد. بنابراین پنج ولت و پنج آمپر، هرچند عدد ظاهری برابر دارند، برای جمع سازگار نیستند. همچنین دو کیلو‌اهم و دوهزار اهم پس از تبدیل، نمایش داخلی یکسان دارند. همراه‌کردن این اطلاعات با خروجیِ تقسیم، تفاوت اصلی با فیلد عددیِ بدون واحد است.

۲. بُعدهای SI و تبدیلِ مقیاسِ دقیق

مقیاسِ واحد را صریح نگه داریم
تصویر مفهومی: مقیاسِ واحد را صریح نگه داریم؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

ترتیب مختصات در این برنامه کیلوگرم، متر، ثانیه، آمپر، کلوین، مول و کاندلاست. هر بُعد با هفت توان صحیح نمایش داده می‌شود. مطابق بخش‌های بُعد و واحد مشتق در NIST SP 330، توان‌های بُعدی می‌توانند مثبت، منفی یا صفر باشند. برای ولت، بردار ما یک، دو، منفی سه، منفی یک و سه صفر پایانی است؛ مقاومت در مختصهٔ آمپر، توان منفی دو دارد.

در ضرب، توان‌ها مؤلفه‌به‌مؤلفه جمع و در تقسیم کم می‌شوند. تقسیم ولت بر اهم، بُعد آمپر می‌دهد؛ ضرب آن در ولت، بُعد وات دارد. جمع و تفریق فقط وقتی مجازند که دو بردار برابر باشند. برای نسبت یک کمیت به خودش، همهٔ توان‌ها صفر می‌شوند؛ واحد قراردادیِ one چنین مقداری را نمایش می‌دهد. نتیجهٔ عددیِ نسبت به مقدارها نیز وابسته است و از صفر بودن بُعد به‌تنهایی به دست نمی‌آید.

فهرست واحدهای پذیرفته‌شده فقط دوازده نام دارد: one، ولت و میلی‌ولت، آمپر و میلی‌آمپر، اهم و کیلو‌اهم، وات و میلی‌وات، ژول، ثانیه و میلی‌ثانیه. شناسه‌های ASCII در کد ثابت‌اند. از جدول پیشوندهای NIST فقط ضریب دقیق یک‌هزارمِ milli و هزارِ kilo را استفاده می‌کنیم. این برنامه هر ترکیب دلخواهِ پیشوند و نام واحد را تجزیه نمی‌کند.

مقدار ورودی باید int یا Fraction دقیق باشد؛ float، رشته، bool و نوع مختلط رد می‌شوند. مستندات Fraction فرق مقدار گویا با تبدیل عدد دودویی را روشن می‌کند. ما مقیاس‌ها را از ابتدا به صورت کسر می‌نویسیم، نه اینکه یک عدد گرد‌شده را بعداً اصلاح کنیم. دقت این حساب دربارهٔ مقدارِ تعریف‌شده است؛ عدم قطعیت ابزار یا دقت واقعیِ پنج ولت را برآورد نمی‌کند.

۳. کد کاملِ کمیت و عملگرهایِ بررسی‌شده

قرارداد در خودِ عملیات بررسی می‌شود
تصویر مفهومی: قرارداد در خودِ عملیات بررسی می‌شود؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

هر شیء، یک Fraction و tuple دقیقِ هفت‌عضوی دارد. توان‌ها باید int دقیق و بین منفی شانزده تا مثبت شانزده باشند. طول بیتی صورت و مخرجِ کسرِ کاهش‌یافته حداکثر ۲۵۶ است. این کران‌ها برای محدودکردن نمایش آموزشی‌اند؛ قانون SI یا محدودیت عمومیِ Fraction نیستند. ساخت ورودی و ساخت نتیجهٔ هر عمل هر دو از همین بررسی‌ها می‌گذرند.

در نتیجه، فضای ورودی زیر همهٔ عملیات بسته نیست. ممکن است دو مقدار مجاز، حاصل‌ضربی با صورت بزرگ‌تر بسازند؛ آن عملیات OverflowError می‌دهد و نتیجه را موفق اعلام نمی‌کند. این بودجه، وعدهٔ مصرف ثابت پردازنده یا مقاومت در برابر ورودی خصمانه نیست؛ حتی ساخت یک عدد صحیح بسیار بزرگ پیش از فراخوانی، هزینهٔ بیرون از این تابع دارد. تقسیم بر کمیتِ صفر نیز خطای تقسیم را حفظ می‌کند.

"""Small runtime dimensional algebra, exact rational scales, no static checker."""
from dataclasses import dataclass, FrozenInstanceError
from fractions import Fraction
import itertools
import json
import platform
from types import MappingProxyType


# Coordinate order: kilogram, metre, second, ampere, kelvin, mole, candela.
ZERO = (0, 0, 0, 0, 0, 0, 0)
VOLT = (1, 2, -3, -1, 0, 0, 0)
AMPERE = (0, 0, 0, 1, 0, 0, 0)
OHM = (1, 2, -3, -2, 0, 0, 0)
WATT = (1, 2, -3, 0, 0, 0, 0)
JOULE = (1, 2, -2, 0, 0, 0, 0)
SECOND = (0, 0, 1, 0, 0, 0, 0)
UNITS = MappingProxyType({
    "one": (Fraction(1), ZERO),
    "V": (Fraction(1), VOLT), "mV": (Fraction(1, 1000), VOLT),
    "A": (Fraction(1), AMPERE), "mA": (Fraction(1, 1000), AMPERE),
    "ohm": (Fraction(1), OHM), "kohm": (Fraction(1000), OHM),
    "W": (Fraction(1), WATT), "mW": (Fraction(1, 1000), WATT),
    "J": (Fraction(1), JOULE),
    "s": (Fraction(1), SECOND), "ms": (Fraction(1, 1000), SECOND),
})


def bounded_fraction(value):
    if type(value) not in (int, Fraction):
        raise TypeError("exact int or Fraction required; no bool, float or text")
    result = Fraction(value)
    if result.numerator.bit_length() > 256 or result.denominator.bit_length() > 256:
        raise OverflowError("rational numerator/denominator exceeds 256-bit budget")
    return result


def unit_definition(unit):
    if type(unit) is not str:
        raise TypeError("unit name must be exact str")
    if unit not in UNITS:
        raise ValueError("unknown unit name")
    return UNITS[unit]


@dataclass(frozen=True, slots=True)
class Quantity:
    si_value: Fraction
    dimensions: tuple[int, ...]

    def __post_init__(self):
        if type(self) is not Quantity or type(self.si_value) is not Fraction:
            raise TypeError("use an exact Quantity with a Fraction value")
        bounded_fraction(self.si_value)
        if type(self.dimensions) is not tuple or len(self.dimensions) != 7:
            raise TypeError("seven dimension coordinates in an exact tuple required")
        if any(type(x) is not int for x in self.dimensions):
            raise TypeError("dimension coordinates must be exact int")
        if any(not -16 <= x <= 16 for x in self.dimensions):
            raise OverflowError("dimension coordinate outside -16..16")

    def __add__(self, other):
        if type(other) is not Quantity:
            return NotImplemented
        if self.dimensions != other.dimensions:
            raise ValueError("addition requires equal dimensions")
        return Quantity(self.si_value + other.si_value, self.dimensions)

    def __sub__(self, other):
        if type(other) is not Quantity:
            return NotImplemented
        if self.dimensions != other.dimensions:
            raise ValueError("subtraction requires equal dimensions")
        return Quantity(self.si_value - other.si_value, self.dimensions)

    def __mul__(self, other):
        if type(other) is not Quantity:
            return NotImplemented
        return Quantity(self.si_value * other.si_value,
                        tuple(a + b for a, b in zip(self.dimensions, other.dimensions)))

    def __truediv__(self, other):
        if type(other) is not Quantity:
            return NotImplemented
        return Quantity(self.si_value / other.si_value,
                        tuple(a - b for a, b in zip(self.dimensions, other.dimensions)))

    def in_unit(self, unit):
        scale, dimensions = unit_definition(unit)
        if dimensions != self.dimensions:
            raise ValueError("requested unit has different dimensions")
        return bounded_fraction(self.si_value / scale)


def quantity(value, unit):
    scale, dimensions = unit_definition(unit)
    return Quantity(bounded_fraction(value) * scale, dimensions)


def prime_dimension_code(dimensions):
    # Independent representation: prime factor products, not coordinate addition.
    result = Fraction(1)
    for prime, exponent in zip((2, 3, 5, 7, 11, 13, 17), dimensions):
        result *= Fraction(prime) ** exponent
    return result


def rejects(exception, function, *args):
    try:
        function(*args)
    except exception:
        return True
    return False


def unchecked_current(voltage: float, resistance: float) -> float:
    # Annotations alone cannot tell whether these two plain numbers carry volts.
    return voltage / resistance


def run():
    checks = {}
    voltage = quantity(5, "V")
    resistance = quantity(2, "kohm")
    current = voltage / resistance
    power = voltage * current
    energy = power * quantity(3, "s")
    checks["hand_ohm_power_energy"] = (current.in_unit("mA") == Fraction(5, 2)
        and power.in_unit("mW") == Fraction(25, 2) and energy.in_unit("J") == Fraction(3, 80))
    checks["exact_prefix_conversion_and_addition"] = (quantity(250, "mV") + quantity(Fraction(3, 4), "V") == quantity(1, "V")
        and quantity(2, "kohm").in_unit("ohm") == 2000
        and quantity(Fraction(1, 3), "mA").in_unit("A") == Fraction(1, 3000))
    checks["incompatible_add_subtract_and_output_unit"] = (rejects(ValueError, lambda: voltage + quantity(5, "A"))
        and rejects(ValueError, lambda: voltage - quantity(5, "s")) and rejects(ValueError, current.in_unit, "V"))
    # Literal independent unit table: (canonical scale, prime dimension code).
    reference = {"one": (Fraction(1), Fraction(1)),
        "V": (Fraction(1), Fraction(18, 875)), "mV": (Fraction(1, 1000), Fraction(18, 875)),
        "A": (Fraction(1), Fraction(7)), "mA": (Fraction(1, 1000), Fraction(7)),
        "ohm": (Fraction(1), Fraction(18, 6125)), "kohm": (Fraction(1000), Fraction(18, 6125)),
        "W": (Fraction(1), Fraction(18, 125)), "mW": (Fraction(1, 1000), Fraction(18, 125)),
        "J": (Fraction(1), Fraction(18, 25)), "s": (Fraction(1), Fraction(5)),
        "ms": (Fraction(1, 1000), Fraction(5))}
    for left, right in itertools.product(reference, repeat=2):
        x, y = quantity(Fraction(2, 3), left), quantity(Fraction(-7, 5), right)
        sx, dx = reference[left]
        sy, dy = reference[right]
        product, quotient = x * y, x / y
        assert product.si_value == Fraction(-14, 15) * sx * sy
        assert quotient.si_value == Fraction(-10, 21) * sx / sy
        assert prime_dimension_code(product.dimensions) == dx * dy
        assert prime_dimension_code(quotient.dimensions) == dx / dy
    checks["144_unit_pairs_prime_factor_and_rational_oracle"] = True
    checks["power_two_independent_constitutive_forms"] = (power == current * current * resistance
        and power == voltage * voltage / resistance)
    checks["dimensionless_cancellation_and_sign"] = (voltage / voltage == quantity(1, "one")
        and quantity(-5, "V") / resistance == quantity(Fraction(-5, 2), "mA")
        and quantity(0, "W") == voltage * quantity(0, "A"))
    checks["division_by_zero_keeps_failure"] = rejects(ZeroDivisionError, lambda: voltage / quantity(0, "ohm"))
    checks["numeric_type_rejection"] = all(rejects(TypeError, quantity, value, "V") for value in [True, 1.0, "1", 1j, float("nan"), float("inf")])
    checks["unit_name_and_plain_scalar_rejection"] = (all(rejects(ValueError, quantity, 1, name) for name in ["MV", "C", " V", "V/A"])
        and rejects(TypeError, quantity, 1, 3) and rejects(TypeError, lambda: voltage + 1)
        and rejects(TypeError, lambda: voltage * 2))
    checks["direct_constructor_dimension_contract"] = (rejects(TypeError, Quantity, Fraction(1), [0] * 7)
        and rejects(TypeError, Quantity, Fraction(1), (0,) * 6)
        and rejects(TypeError, Quantity, Fraction(1), (True,) + (0,) * 6)
        and rejects(TypeError, Quantity, 1, ZERO)
        and rejects(OverflowError, Quantity, Fraction(1), (17,) + (0,) * 6))
    large = quantity(2 ** 255, "one")
    checks["rational_budget_and_result_overflow"] = (rejects(OverflowError, quantity, 2 ** 256, "one")
        and rejects(OverflowError, quantity, Fraction(1, 2 ** 256), "one")
        and rejects(OverflowError, lambda: large * quantity(2, "one")))
    checks["dimension_result_budget"] = rejects(OverflowError, lambda: Quantity(Fraction(1), (16,) + (0,) * 6) * Quantity(Fraction(1), (1,) + (0,) * 6))
    checks["ordinary_immutability_and_input_values"] = (rejects(FrozenInstanceError, setattr, voltage, "si_value", Fraction(9))
        and voltage == quantity(5, "V") and resistance == quantity(2000, "ohm"))
    checks["annotations_and_dimension_semantics_limits"] = (unchecked_current(5, 2) == 2.5
        and quantity(1, "J").dimensions == (quantity(1, "W") * quantity(1, "s")).dimensions)
    assert len(checks) == 14 and all(checks.values())
    return {"python": platform.python_version(), "checks": checks,
        "own_example": {"voltage_V": str(voltage.in_unit("V")), "resistance_ohm": str(resistance.in_unit("ohm")),
                        "current_mA": str(current.in_unit("mA")), "power_mW": str(power.in_unit("mW")),
                        "energy_J": str(energy.in_unit("J")), "power_dimensions_kg_m_s_A_K_mol_cd": power.dimensions},
        "independent_unit_pairs_one_group": len(reference) ** 2,
        "annotations_only_plain_numbers": unchecked_current(5, 2),
        "scope": "Runtime checks and exact rational arithmetic for a restricted multiplicative unit registry. No static checker, affine temperature, uncertainty, safety or complete physical-kind semantics."}


if __name__ == "__main__":
    print(json.dumps(run(), ensure_ascii=True, indent=2))

فایل کامل با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. برای ضرب عددِ ساده در کمیت، تبدیل پنهان نداریم؛ باید عددِ بی‌بُعد را با one بسازیم. واحد ناشناخته، فاصلهٔ اضافه و نام دارای حروف بزرگِ متفاوت پذیرفته نیست. تغییر عادیِ فیلد در شیء frozen نیز خطا می‌دهد، اما این ویژگی مرز امنیتی یا تضمین در برابر دورزدن عمدیِ سازوکار پایتون نیست.

۴. مرجعِ مستقلِ بُعد و مثالِ توان

بُعد و مقدار را جداگانه بازبینی کنیم
تصویر مفهومی: بُعد و مقدار را جداگانه بازبینی کنیم؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

خروجی مثال، جریان را به صورت کسر پنج‌دوم میلی‌آمپر و توان را بیست‌وپنج‌دوم میلی‌وات ثبت می‌کند. مقدار داخلی مقاومت دوهزار است؛ بنابراین اشتباهِ استفاده از عدد دو اهم رخ نمی‌دهد. محاسبهٔ توان از سه مسیر ولتاژ ضرب‌در جریان، مربع جریان ضرب‌در مقاومت و مربع ولتاژ تقسیم بر مقاومت، همان کمیت را می‌دهد. این هم‌ارزی برای مقاومت ایدئالِ همین مثال بررسی شده است.

مرجع مستقلِ بُعد، به‌جای جمع بردارها، به هفت مختصه هفت عدد اولِ متفاوت نسبت می‌دهد. توان‌های منفی به مخرج می‌روند؛ سپس ضرب یا تقسیمِ دو کدِ کسری، مرجع ترکیب بُعد می‌شود. جدول کدِ واحدها و مقیاس‌ها جدا و صریح نوشته شده و از جدول اصلی تولید نمی‌شود. یکتایی تجزیهٔ عدد صحیح به عوامل اول، علت مناسب‌بودن این نمایش برای توان‌های صحیح است؛ آن را به بُعدهای کسری تعمیم نداده‌ایم.

{
  "python": "3.14.5",
  "checks": {
    "hand_ohm_power_energy": true,
    "exact_prefix_conversion_and_addition": true,
    "incompatible_add_subtract_and_output_unit": true,
    "144_unit_pairs_prime_factor_and_rational_oracle": true,
    "power_two_independent_constitutive_forms": true,
    "dimensionless_cancellation_and_sign": true,
    "division_by_zero_keeps_failure": true,
    "numeric_type_rejection": true,
    "unit_name_and_plain_scalar_rejection": true,
    "direct_constructor_dimension_contract": true,
    "rational_budget_and_result_overflow": true,
    "dimension_result_budget": true,
    "ordinary_immutability_and_input_values": true,
    "annotations_and_dimension_semantics_limits": true
  },
  "own_example": {
    "voltage_V": "5",
    "resistance_ohm": "2000",
    "current_mA": "5/2",
    "power_mW": "25/2",
    "energy_J": "3/80",
    "power_dimensions_kg_m_s_A_K_mol_cd": [
      1,
      2,
      -3,
      0,
      0,
      0,
      0
    ]
  },
  "independent_unit_pairs_one_group": 144,
  "annotations_only_plain_numbers": 2.5,
  "scope": "Runtime checks and exact rational arithmetic for a restricted multiplicative unit registry. No static checker, affine temperature, uncertainty, safety or complete physical-kind semantics."
}

هر صد‌وچهل‌وچهار زوج از دوازده واحد، با دو مقدار کسریِ معلوم بررسی شده‌اند؛ تمام آن‌ها فقط یک گروه از چهارده گروه‌اند. کنترل مقدار و کنترل بُعد جداست: مقیاس درست با بُعد غلط، یا بُعد درست با ضریب غلط، هر دو باید آشکار شوند. جواب دستی، تبدیل دقیق، سه فرم توان، علامت، صفر، خطای نوع و واحد، مرز نمایش و ثابت‌ماندن مقدارِ ورودی شاهدهای مکمل‌اند. موفقیت آن‌ها اثبات تمام برنامه‌های ممکن نیست.

این درس ادامهٔ مسئلهٔ اعتبارسنجی رکورد حسگر است، اما برچسب واحد را در عملیات ترکیب می‌کند. در مقابل، تبدیل مقدار و اختلاف دما مبدأ جابه‌جاشونده دارد؛ این کلاس فقط مقیاس ضربی را می‌فهمد و سلسیوس را نمی‌پذیرد. همچنین دو کمیت فیزیکی متفاوت ممکن است بُعد یکسان داشته باشند؛ نگهداری بردار، همهٔ معنای فیزیکی را حمل نمی‌کند.

۵. چهار پرسش دربارهٔ نوع و معنایِ فیزیکی

بُعدِ سازگار، مدلِ فیزیکیِ کامل نیست
تصویر مفهومی: بُعدِ سازگار، مدلِ فیزیکیِ کامل نیست؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: چرا جمع پنج ولت و پنج آمپر رد می‌شود؟ پاسخ: شرط جمع، برابری بُعدهاست، نه برابری مقدار عددی. بردار ولت با آمپر متفاوت است؛ برنامه ValueError می‌دهد و عدد ده را با یک برچسب دلخواه برنمی‌گرداند.

پرسش دوم: چرا دو کیلو‌اهم به جریان دوونیم آمپر نمی‌رسد؟ پاسخ: ورودی مقاومت ابتدا به دوهزار اهمِ مبنا تبدیل می‌شود. پنج تقسیم بر دوهزار آمپر، یک‌چهارصدم آمپر یا پنج‌دوم میلی‌آمپر است؛ پیشوند در مقدار وارد شده، نه در بُعد.

پرسش سوم: اگر بُعد نتیجه ژول باشد، انرژیِ فیزیکی تأیید شده است؟ پاسخ: خیر؛ گشتاور هم بُعد انرژی دارد و این کلاس نوع فیزیکیِ جداگانه‌ای برای آن ندارد. انتخاب قانون، معنای داده و اعتبار اندازه‌گیری همچنان مسئولیت مدل و فراخواننده است.

پرسش چهارم: آیا مقدارِ معتبر همیشه در محاسبهٔ بعدی پذیرفته می‌شود؟ پاسخ: خیر؛ نتیجه ممکن است از بودجهٔ کسر یا توان بُعد بیرون برود و خطا بدهد. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدند؛ تاریخ ایجاد صفحه‌های NIST سال‌های ۲۰۱۶ و ۲۰۱۹ و به‌روزرسانی هر دو ۱۸ اوت ۲۰۲۵ است. مستندات 3.14.8 از اجرای واقعی 3.14.5 جداست؛ نتیجهٔ بررسی ایستای نوع گزارش نشده است.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.