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

۱. دادهٔ پرس‌وجو را فهرستِ جفت‌ها نگه داریم

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

در قراردادِ ما، ورودی یک tuple دقیق از صفر تا شش جفت است؛ هر جفت هم tuple دوعضویِ نام و مقدارِ str است. نامِ تکراری مجاز است و ترتیبِ سراسری اهمیت دارد. برای نمونه، سه جفتِ a برابر یک، b برابر دو و دوباره a برابر سه باید به همان ترتیب بمانند. دیکشنریِ ساده برای a فقط مقدار آخر را نگه می‌دارد؛ نگاشتِ نام به فهرستِ مقدارها نیز به‌تنهایی جایگاهِ میان‌گذاریِ b را توصیف نمی‌کند.

مستندات urllib.parse، خروجی parse_qsl را فهرستِ جفت‌ها معرفی می‌کند و برای urlencode با دنبالهٔ جفت‌ها، حفظِ ترتیب را توضیح می‌دهد. بنابراین از مسیرِ دیکشنری استفاده نمی‌کنیم. گزینهٔ doseq هم خاموش می‌ماند، چون مقدارهای این قرارداد فقط رشته‌اند؛ پذیرشِ فهرست به‌عنوان مقدار و گسترشِ خودکارش، دامنهٔ دیگری می‌ساخت.

خالی بودن با غایب بودن فرق دارد. رشتهٔ پرس‌وجوی خالی، هیچ جفتی ندارد؛ x= یک جفت با نام x و مقدار خالی است. =x هم نام خالی و مقدار x دارد و = یک جفت با دو رشتهٔ خالی است. این چهار حالت در مثال پذیرفته و متمایزند. هنگام خواندن، keep_blank_values=True را صریح می‌گذاریم؛ پیش‌فرضِ کتابخانه برای نگهداریِ مقدارهای خالی، انتخابِ این قرارداد نیست.

۲. فاصله، علامتِ جمع و UTF-8 را فقط یک بار باز کنیم

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

در مسیرِ انتخاب‌شدهٔ quote_plus، فاصله به + تبدیل می‌شود، ولی علامتِ جمعِ موجود در خود داده به %2B می‌رسد. بنابراین x=+ مقدارِ یک فاصله می‌دهد و x=%2B مقدارِ علامت جمع. برای دادهٔ متنیِ %2B، باید درصد نیز کد شود: x=%252B پس از یک بار بازخوانی هنوز متنِ %2B را دارد. بازخوانیِ دوم داده را به + تغییر می‌دهد و دیگر همان قراردادِ یک‌مرحله‌ای نیست.

ابتدا مرزِ جفت‌ها و مرزِ نام و مقدار از & و = خام تشخیص داده می‌شود؛ سپس محتوای هر جزء باز می‌شود. اگر مقدار x&y باشد، شکلِ کدشدهٔ آن x%26y است و درصدِ ۲۶ نباید پیش از تشخیصِ مرزها به جداکننده تبدیل شود. در شاهدِ دستی، نام خالی و همین مقدار کنار چند جفت دیگر قرار دارند تا این تمایز، فقط یک توضیح لفظی نماند.

برای متن فارسی، بایت‌های UTF-8 درصدگذاری می‌شوند؛ حرف گ در خروجی %DA%AF است. خوانندهٔ نمونه errors برابر strict دارد تا توالیِ نامعتبر با نویسهٔ جایگزین پنهان نشود. در عین حال، هیچ نرمال‌سازیِ یونیکد انجام نمی‌دهیم. é و ترکیبِ e با علامتِ جداگانه، ممکن است شبیه دیده شوند، اما دو رشته و دو نمایشِ متفاوت‌اند. طولِ نویسه و طولِ بایت هم در اینجا یک واحد نیستند.

۳. نمایشِ پایتون با کدگذارِ WHATWG یکسان نیست

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

استاندارد زندهٔ URL در WHATWG، قالبِ فرم را به‌صورت فهرستی از نام و مقدار شرح می‌دهد. در مجموعهٔ نویسه‌های آزادِ کدگذارِ فرمِ آن، ستاره حاضر است و تیلدا درصدگذاری می‌شود. اما quote_plus پایتون با safe خالی، تیلدا را نگه می‌دارد و ستاره را درصدگذاری می‌کند. پس برای مقدارِ *~، خروجیِ پایتون x=%2A~ است و شاهدِ دستیِ قواعد فرمِ استاندارد x=*%7E.

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

سیاستِ ورودیِ خام نیز انتخابِ خودمان است: هر بخش باید دقیقاً یک = خام داشته باشد، بخشِ خالی میان دو & مجاز نیست و درصد باید دو رقمِ شانزده‌شانزدهی داشته باشد. دیگر نویسه‌های خام را به الفبای اعلام‌شده محدود می‌کنیم. این سخت‌گیری‌ها الزامِ عمومیِ WHATWG نیستند؛ الگوریتمِ استاندارد بخش‌های خالی را کنار می‌گذارد و نامِ بدون مساوی را با مقدار خالی می‌پذیرد. strict_parsing به‌تنهایی نیز جای بررسیِ درصدِ ناقص را نمی‌گیرد.

۴. کد کامل با مرزهای ورودی و مرجع مستقلِ بایتی

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

هر نام یا مقدار حداکثر دوازده نویسهٔ یونیکد و بیست‌وچهار بایتِ UTF-8 دارد. شش نویسهٔ ایموجیِ چهاربایتی دقیقاً به سقف بایت می‌رسند، اما هفتمی رد می‌شود؛ دوازده حرفِ دوبایتی به هر دو سقف می‌رسند. سقفِ متنِ پرس‌وجو ۸۷۵ نویسهٔ ASCII است: شش جفت، هر جزء حداکثر ۷۲ نویسهٔ درصدگذاری‌شده، شش مساوی و پنج جداکننده. این محاسبه، کرانِ همین مثال است.

نوع و تعدادِ جفت‌ها پیش از فراخوانیِ کدگذار بررسی می‌شوند؛ ورودیِ iterable دلخواه پذیرفته نیست که برای فهمیدنِ طولش مصرف شود. در خواندن هم نوع، طولِ خام و تعداد بخش‌ها پیش از parse_qsl کنترل می‌شوند. این سقف‌ها روی اشیای ازپیش‌دریافت‌شده اعمال می‌شوند و اندازه‌گیریِ حافظهٔ فرایند یا بودجهٔ دریافتِ شبکه نیستند. تابع، یک مؤلفهٔ پرس‌وجو می‌خواند؛ میزبان، مسیر و کلِ URL را تفسیر نمی‌کند.

"""A bounded Python query-pair codec, not a URL or OpenAPI validator."""
import json
import platform
import sys
from itertools import product
from urllib.parse import parse_qsl, quote_plus, urlencode

MAX_PAIRS = 6
MAX_COMPONENT_CHARS = 12
MAX_COMPONENT_UTF8_BYTES = 24
MAX_QUERY_ASCII_CHARS = MAX_PAIRS * (2 * 3 * MAX_COMPONENT_UTF8_BYTES + 1) + MAX_PAIRS - 1
PYTHON_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
HEX = "0123456789abcdefABCDEF"


def _component(value):
    if type(value) is not str:
        raise ValueError("component_type")
    if len(value) > MAX_COMPONENT_CHARS:
        raise ValueError("component_characters")
    try:
        raw = value.encode("utf-8", errors="strict")
    except UnicodeEncodeError as error:
        raise ValueError("utf8") from error
    if len(raw) > MAX_COMPONENT_UTF8_BYTES:
        raise ValueError("component_bytes")


def encode_pairs(pairs):
    # Exact tuple avoids consuming an arbitrary/unbounded iterable before the cap.
    if type(pairs) is not tuple:
        raise ValueError("pairs_type")
    if len(pairs) > MAX_PAIRS:
        raise ValueError("pair_count")
    for pair in pairs:
        if type(pair) is not tuple or len(pair) != 2:
            raise ValueError("pair_shape")
        _component(pair[0])
        _component(pair[1])
    return urlencode(pairs, doseq=False, safe="", encoding="utf-8",
                     errors="strict", quote_via=quote_plus)


def _encoded_component_syntax(text):
    # This is OUR accepted raw grammar, stricter than general form parsing.
    position = 0
    while position < len(text):
        symbol = text[position]
        if symbol == "%":
            if position + 2 >= len(text) or any(c not in HEX for c in text[position + 1:position + 3]):
                raise ValueError("query_syntax")
            position += 3
        elif symbol in PYTHON_SAFE or symbol in "*+":
            position += 1
        else:
            raise ValueError("query_syntax")


def decode_query(query):
    if type(query) is not str:
        raise ValueError("query_type")
    if len(query) > MAX_QUERY_ASCII_CHARS:
        raise ValueError("query_length")
    if not query.isascii():
        raise ValueError("query_ascii")
    if query == "":
        return ()
    if query.count("&") + 1 > MAX_PAIRS:
        raise ValueError("pair_count")
    for field in query.split("&"):
        if field.count("=") != 1:
            raise ValueError("query_syntax")
        name, value = field.split("=")
        _encoded_component_syntax(name)
        _encoded_component_syntax(value)
    try:
        pairs = tuple(parse_qsl(query, keep_blank_values=True, strict_parsing=True,
                                encoding="utf-8", errors="strict",
                                max_num_fields=MAX_PAIRS, separator="&"))
    except UnicodeDecodeError as error:
        raise ValueError("utf8") from error
    for name, value in pairs:
        _component(name)
        _component(value)
    return pairs


def reference_encode(pairs):
    # Independent byte codec: no urllib, main codec or validation calls.
    result = []
    for name, value in pairs:
        encoded = []
        for text in (name, value):
            pieces = []
            for byte in text.encode("utf-8"):
                if byte == 32:
                    pieces.append("+")
                elif chr(byte) in PYTHON_SAFE:
                    pieces.append(chr(byte))
                else:
                    pieces.append("%" + format(byte, "02X"))
            encoded.append("".join(pieces))
        result.append(encoded[0] + "=" + encoded[1])
    return "&".join(result)


def reference_decode(query):
    # Oracle is used on finite known-valid profiles, not as a public validator.
    if query == "":
        return ()
    result = []
    for field in query.split("&"):
        components = []
        for text in field.split("=", 1):
            octets = bytearray()
            position = 0
            while position < len(text):
                symbol = text[position]
                if symbol == "%":
                    octets.append(int(text[position + 1:position + 3], 16))
                    position += 3
                else:
                    octets.append(32 if symbol == "+" else ord(symbol))
                    position += 1
            components.append(bytes(octets).decode("utf-8", errors="strict"))
        result.append(tuple(components))
    return tuple(result)


def error_code(operation, value):
    try:
        operation(value)
    except ValueError as error:
        return str(error)
    return None


def run_example():
    checks = {}

    def check(name, condition):
        if type(condition) is not bool or not condition:
            raise AssertionError(name)
        checks[name] = condition

    fixture = (("mode", "slow fast"), ("tag", "A+B"), ("tag", ""),
               ("", "x&y"), ("q", "\u06af"))
    expected = "mode=slow+fast&tag=A%2BB&tag=&=x%26y&q=%DA%AF"
    check("manual_ordered_fixture_exact_text_and_pairs",
          encode_pairs(fixture) == expected and decode_query(expected) == fixture
          and reference_encode(fixture) == expected and reference_decode(expected) == fixture)

    interleaved = (("a", "1"), ("b", "2"), ("a", "3"))
    check("duplicate_names_preserve_global_order_and_dictionary_loses_data",
          encode_pairs(interleaved) == "a=1&b=2&a=3"
          and decode_query("a=1&b=2&a=3") == interleaved
          and tuple(dict(interleaved).items()) != interleaved)
    check("empty_collection_present_blank_and_blank_name_are_distinct",
          encode_pairs(()) == "" and decode_query("") == ()
          and decode_query("x=") == (("x", ""),)
          and decode_query("=x") == (("", "x"),)
          and decode_query("=") == (("", ""),))
    check("space_literal_plus_and_percent_text_decode_exactly_once",
          decode_query("x=+&x=%2B&x=%252B") == (("x", " "), ("x", "+"), ("x", "%2B"))
          and encode_pairs((("x", "%2B"),)) == "x=%252B")
    delimiters = (("&=", "/?#;"),)
    check("delimiters_are_data_only_after_component_encoding",
          encode_pairs(delimiters) == "%26%3D=%2F%3F%23%3B"
          and decode_query("%26%3D=%2F%3F%23%3B") == delimiters)
    unicode_pairs = (("q", "\u06af"), ("q", "\u00e9"), ("q", "e\u0301"))
    check("strict_utf8_roundtrip_does_not_apply_unicode_normalization",
          encode_pairs(unicode_pairs) == "q=%DA%AF&q=%C3%A9&q=e%CC%81"
          and decode_query(encode_pairs(unicode_pairs)) == unicode_pairs
          and unicode_pairs[1][1] != unicode_pairs[2][1])
    check("noncanonical_accepted_aliases_reserialize_to_declared_python_form",
          decode_query("x=%41%20b&y=%2b") == (("x", "A b"), ("y", "+"))
          and encode_pairs(decode_query("x=%41%20b&y=%2b")) == "x=A+b&y=%2B")
    check("python_star_tilde_output_differs_from_whatwg_form_safe_set",
          encode_pairs((("x", "*~"),)) == "x=%2A~"
          and "x=%2A~" != "x=*%7E"
          and decode_query("x=*%7E") == (("x", "*~"),))

    syntax_bad = ("x=%", "x=%0", "x=%GG", "x=1&&y=2", "&x=1", "x=1&",
                  "x", "x=a=b", "x=a;b", "x=raw space", "?x=1", "x=#")
    check("own_raw_syntax_rejects_bad_percent_and_ambiguous_fields",
          all(error_code(decode_query, query) == "query_syntax" for query in syntax_bad))
    utf8_bad = ("x=%FF", "x=%C0%AF", "x=%ED%A0%80", "x=%F4%90%80%80")
    check("invalid_utf8_is_rejected_instead_of_replaced",
          all(error_code(decode_query, query) == "utf8" for query in utf8_bad))

    class MustNotIterate:
        def __iter__(self):
            raise AssertionError("arbitrary iterable consumed")

    bad_pairs = ((None, "pairs_type"), ([], "pairs_type"), (MustNotIterate(), "pairs_type"),
                 ((("x",),), "pair_shape"), ((["x", "y"],), "pair_shape"),
                 (((1, "y"),), "component_type"), ((("x", True),), "component_type"),
                 ((("x", b"y"),), "component_type"), ((("x", "\ud800"),), "utf8"))
    check("exact_pair_shapes_types_and_preconsumption_boundary",
          all(error_code(encode_pairs, value) == expected_error for value, expected_error in bad_pairs))

    maximum = (("\u06af" * 12, "\u06af" * 12),) * MAX_PAIRS
    maximum_query = encode_pairs(maximum)
    limit_checks = (
        len(maximum_query) == MAX_QUERY_ASCII_CHARS == 875,
        decode_query(maximum_query) == maximum,
        encode_pairs((("x", "a" * 12),)) == "x=" + "a" * 12,
        decode_query("x=" + "a" * 12) == (("x", "a" * 12),),
        decode_query("x=" + "%F0%9F%98%80" * 6) == (("x", "\U0001f600" * 6),),
        error_code(encode_pairs, (("x", "a" * 13),)) == "component_characters",
        error_code(decode_query, "x=" + "a" * 13) == "component_characters",
        error_code(encode_pairs, (("x", "\U0001f600" * 7),)) == "component_bytes",
        error_code(decode_query, "x=" + "%F0%9F%98%80" * 7) == "component_bytes",
        error_code(decode_query, "x" * 876) == "query_length",
        error_code(encode_pairs, (("x", ""),) * 7) == "pair_count",
        error_code(decode_query, "&".join(["x="] * 7)) == "pair_count",
        error_code(decode_query, "x=\u06af") == "query_ascii",
        all(error_code(decode_query, value) == "query_type" for value in (None, b"x=1", False, 1)),
    )
    check("inclusive_character_byte_pair_and_raw_query_limits_have_distinct_units",
          all(limit_checks))

    pool = (("x", ""), ("x", "a b"), ("x", "+"),
            ("", "%2B"), ("\u06af", "&="), ("*~", "\u00e9"))
    family_cases = 0
    for length in range(4):
        for profile in product(pool, repeat=length):
            actual = encode_pairs(profile)
            if (actual != reference_encode(profile) or decode_query(actual) != profile
                    or reference_decode(actual) != profile):
                raise AssertionError("independent byte reference")
            family_cases += 1
    check("finite_259_profiles_match_independent_byte_codec_ONE_group", family_cases == 259)
    original_fixture = tuple(tuple(pair) for pair in fixture)
    first = decode_query(expected)
    second = decode_query(expected)
    check("inputs_remain_immutable_and_results_have_no_shared_mutable_state",
          fixture == original_fixture and first == second == fixture
          and type(first) is tuple and all(type(pair) is tuple for pair in first)
          and first is not second)
    assert len(checks) == 14 and all(type(value) is bool and value for value in checks.values())
    return {
        "python_version": platform.python_version(),
        "dependencies": "standard library only",
        "scope": "Bounded Python-specific ordered query component; no HTTP, full URL, OpenAPI validator, authentication, benchmark or universal WHATWG serializer identity",
        "limits": {"pairs": MAX_PAIRS, "component_unicode_characters": MAX_COMPONENT_CHARS,
                   "component_utf8_bytes": MAX_COMPONENT_UTF8_BYTES,
                   "raw_query_ascii_characters": MAX_QUERY_ASCII_CHARS},
        "manual_pairs": fixture,
        "manual_query": expected,
        "decoded_once_percent_text": decode_query("x=%252B"),
        "accepted_alias_reencoded": encode_pairs(decode_query("x=%41%20b&y=%2b")),
        "star_tilde_witness": {"Python_quote_plus_safe_empty": encode_pairs((("x", "*~"),)),
                               "WHATWG_form_safe_set_hand_expected": "x=*%7E",
                               "both_decode_to_same_pairs_here": decode_query("x=*%7E") == decode_query("x=%2A~")},
        "maximum_accepted_query_ascii_characters": len(maximum_query),
        "syntax_invalid_fixtures_ONE_group": len(syntax_bad),
        "invalid_utf8_fixtures_ONE_group": len(utf8_bad),
        "shape_type_fixtures_ONE_group": len(bad_pairs),
        "independent_reference_profiles_ONE_group": family_cases,
        "checks": checks,
    }


if __name__ == "__main__":
    output = json.dumps(run_example(), ensure_ascii=True, indent=2, allow_nan=False) + "\n"
    sys.stdout.buffer.write(output.encode("utf-8"))

مرجعِ مستقل، بایت‌های UTF-8 را یکی‌یکی با جدولِ صریحِ نویسه‌های آزاد به متن تبدیل می‌کند؛ خوانندهٔ مرجع نیز درصدها را مستقیم به بایت برمی‌گرداند. این دو تابع از urllib و از توابع اصلیِ کدگذاری و اعتبارسنجی استفاده نمی‌کنند. خانوادهٔ متناهیِ ۲۵۹ نمایه، شامل طولِ صفر تا سه از شش جفتِ ثابت، فقط یک گروهِ بررسی است؛ تعدادِ نمایه‌ها را تعدادِ آزمون‌های نام‌دار نمی‌نامیم.

{
  "python_version": "3.14.5",
  "dependencies": "standard library only",
  "scope": "Bounded Python-specific ordered query component; no HTTP, full URL, OpenAPI validator, authentication, benchmark or universal WHATWG serializer identity",
  "limits": {
    "pairs": 6,
    "component_unicode_characters": 12,
    "component_utf8_bytes": 24,
    "raw_query_ascii_characters": 875
  },
  "manual_pairs": [
    [
      "mode",
      "slow fast"
    ],
    [
      "tag",
      "A+B"
    ],
    [
      "tag",
      ""
    ],
    [
      "",
      "x&y"
    ],
    [
      "q",
      "گ"
    ]
  ],
  "manual_query": "mode=slow+fast&tag=A%2BB&tag=&=x%26y&q=%DA%AF",
  "decoded_once_percent_text": [
    [
      "x",
      "%2B"
    ]
  ],
  "accepted_alias_reencoded": "x=A+b&y=%2B",
  "star_tilde_witness": {
    "Python_quote_plus_safe_empty": "x=%2A~",
    "WHATWG_form_safe_set_hand_expected": "x=*%7E",
    "both_decode_to_same_pairs_here": true
  },
  "maximum_accepted_query_ascii_characters": 875,
  "syntax_invalid_fixtures_ONE_group": 12,
  "invalid_utf8_fixtures_ONE_group": 4,
  "shape_type_fixtures_ONE_group": 9,
  "independent_reference_profiles_ONE_group": 259,
  "checks": {
    "manual_ordered_fixture_exact_text_and_pairs": true,
    "duplicate_names_preserve_global_order_and_dictionary_loses_data": true,
    "empty_collection_present_blank_and_blank_name_are_distinct": true,
    "space_literal_plus_and_percent_text_decode_exactly_once": true,
    "delimiters_are_data_only_after_component_encoding": true,
    "strict_utf8_roundtrip_does_not_apply_unicode_normalization": true,
    "noncanonical_accepted_aliases_reserialize_to_declared_python_form": true,
    "python_star_tilde_output_differs_from_whatwg_form_safe_set": true,
    "own_raw_syntax_rejects_bad_percent_and_ambiguous_fields": true,
    "invalid_utf8_is_rejected_instead_of_replaced": true,
    "exact_pair_shapes_types_and_preconsumption_boundary": true,
    "inclusive_character_byte_pair_and_raw_query_limits_have_distinct_units": true,
    "finite_259_profiles_match_independent_byte_codec_ONE_group": true,
    "inputs_remain_immutable_and_results_have_no_shared_mutable_state": true
  }
}

چهارده گروه، شاهدِ دستی، تکرارِ نام، خالی، جمع، جداکننده، متن فارسی، نمایش‌های هم‌ارز، تفاوتِ دو قالب، درصدِ ناقص، UTF-8 نامعتبر، نوع و کران‌ها را پوشش می‌دهند. بازکدگذاریِ x=%41%20b&y=%2b در سیاستِ ما x=A+b&y=%2B می‌شود؛ تابع ورودیِ پذیرفته‌شده را الزاماً بایت‌به‌بایت حفظ نمی‌کند. معیارِ رفت‌وبرگشت، جفت‌های مرتبِ رمزگشایی‌شده است.

درسِ قرارداد Base64 دربارهٔ نمایشِ یکتای محمولهٔ دودویی است؛ اینجا تکرارِ نام و ترتیبِ جفت‌های متنی مهم‌اند. قرارداد JSON حسگر نیز نامِ تکراری را رد می‌کند، درحالی‌که این نمونه عمداً آن را نگه می‌دارد. تاریخِ دسترسیِ منابع ۲۹ مهر ۱۴۰۵ است؛ مستندات Python 3.14.8 و تاریخِ به‌روزرسانیِ کلِ سند WHATWG، ۱۹ اکتبر، با نسخهٔ اجرای ثبت‌شده و تاریخِ انتشارِ یک تغییرِ خاص یکی نیستند.

۵. چهار پرسش پاسخ‌دار برای طراحی قرارداد

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

پرسش اول: آیا a=1&b=2&a=3 را می‌توان بدون اطلاعات اضافه به دیکشنریِ ساده تبدیل کرد؟ پاسخ: خیر؛ مقدارِ نخستِ a حذف می‌شود. حتی گروه‌بندیِ مقدارها زیر نامِ a، به‌تنهایی جایِ b میان دو رخداد را نگه نمی‌دارد. برای قراردادِ این درس، دنبالهٔ مرتبِ جفت‌ها را حفظ می‌کنیم.

پرسش دوم: خروجیِ خواندنِ x=%252B علامتِ جمع است؟ پاسخ: خیر؛ بعد از یک بار خواندن، مقدارِ دقیق %2B است. خواندنِ دوباره، معنای دیگری می‌سازد. برای علامتِ جمعِ واقعی در داده، x=%2B لازم است؛ x=+ هم یک فاصله می‌دهد.

پرسش سوم: چرا x=*%7E پذیرفته می‌شود ولی بازکدگذاری همان متن نیست؟ پاسخ: جفتِ حاصل x و *~ است. کدگذارِ انتخاب‌شدهٔ پایتون آن را x=%2A~ می‌نویسد. قرارداد، بازگشتِ داده را می‌سنجد و ادعای یکسانیِ همهٔ کدگذارهای فرم یا حفظِ متنِ ورودی ندارد.

پرسش چهارم: strict_parsing=True به‌تنهایی برای رد کردنِ x=%GG کافی است؟ پاسخ: خیر؛ قواعدِ درصد را خودمان پیش از کتابخانه بررسی می‌کنیم. همچنین x بدون مساوی در سیاستِ ما رد می‌شود، هرچند الگوریتمِ فرمِ استاندارد آن را نامی با مقدارِ خالی می‌خواند. مرزِ قواعدِ کتابخانه، استاندارد و انتخابِ کاربرد باید صریح بماند.

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

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