فهرست مقاله
فرض کنید برای نمونههای موقتِ یک آزمایش، برچسبِ بازبینی نگه میداریم. دو نمونه میتوانند شناسهٔ منطقی یکسان داشته باشند، اما دو شیء جدا باشند. هنگام بازسازیِ نمونهٔ دوم، فراداده را دوباره ثبت میکنیم و انتظار داریم تا وقتی نمونهٔ تازه زنده است، برچسب هم باقی بماند. این انتظار در WeakKeyDictionary الزاماً درست نیست: برابر بودنِ کلیدِ تازه، هویتِ کلیدی را که عمرِ ورودی به آن بسته شده عوض نمیکند. این نوشته با نمونههای کاملاً ساختگی، مرزِ این دو مفهوم را قابل مشاهده میکند.
۱. فرادادهٔ نمونه را با هویتِ مالک اشتباه نگیریم

در یک دیکشنری معمولی، نگهداریِ کلید خودش یک مرجعِ قوی به آن شیء میسازد. نگاشتِ کلیدِ ضعیف برای موقعیتی مفید است که جدول نباید بهتنهایی شیء را زنده نگه دارد. بااینحال، «ضعیف» به معنیِ نادیدهگرفتنِ برابری نیست. کلید هنوز قراردادِ هش و برابری دارد و چند شیء متمایز ممکن است برای جستوجو یک کلید محسوب شوند. بنابراین باید جداگانه بپرسیم مقدارِ کدام ورودی عوض میشود و عمرِ آن ورودی به کدام شیء وابسته است.
مدلِ ما برای هر موقعیتِ ورودی، یک نمونهٔ خصوصی میسازد. شمارهٔ موقعیت، هویتِ آموزشیِ آن است؛ شمارهٔ گروه فقط برابری را تعیین میکند. همهٔ نمونهها عمداً هشِ صفر دارند تا روشن شود هشِ یکسان، بهتنهایی برابری نمیسازد. برچسبِ فراداده عددی بیواحد از صفر تا نه است. این عدد نتیجهٔ اندازهگیری، نسخهٔ واقعیِ دستگاه یا اعتبارسنجیِ کالیبراسیون نیست؛ صرفاً کمک میکند بازنویسیِ مقدار را ببینیم.
تابع observe_registry فقط یک تا سه نمونه، گروههای صفر تا دو و حداکثر شش رویداد میپذیرد. رویدادها ثبتِ برچسب، حذفِ ورودی و کنارگذاشتنِ مالک هستند. این محدودیتها سیاستِ آموزشیِ خودِ برنامهاند؛ محدودیتِ عمومیِ کتابخانه محسوب نمیشوند. هیچ کلیدِ دلخواهِ کاربر، چرخهٔ ارجاع یا روشِ جمعآوریِ سفارشی وارد آزمایش نمیشود.
در درسِ مالکیتِ مرجع در weakref.finalize، نگهداشتنِ متدِ وابسته میتوانست خودِ شیء را زنده نگه دارد. مسئلهٔ حاضر به وابستگیِ متد نمیپردازد؛ تفاوتِ برابریِ دو کلید با هویتِ مالکِ ورودی را بررسی میکند.
۲. کلیدِ برابر، مقدار را عوض میکند؛ مالک همان قبلی است

دو نمونهٔ نخست را در گروهِ یک و نمونهٔ سوم را در گروهِ دو قرار دهید. ابتدا با نمونهٔ صفر، برچسبِ چهار را ثبت میکنیم؛ سپس با نمونهٔ یک، برچسبِ نه را مینویسیم. نمونههای صفر و یک برابرند، بنابراین دو ورودی نداریم. در گزارش، یک ورودی با هویتِ صفر، گروهِ یک و برچسبِ نه دیده میشود. مقدار تازه است، اما کلیدِ واقعیِ نگهداریشده همان نمونهٔ نخست است.
حالا مرجعِ قوی به نمونهٔ صفر را کنار میگذاریم. در مشاهدهٔ کنترلشدهٔ این برنامه، ورودی ناپدید میشود، درحالیکه نمونهٔ یک هنوز زنده است. زندهبودنِ نمونهٔ برابرِ دوم، مالکیتِ ورودی را منتقل نمیکند. این رفتار صریحاً در مستنداتِ کلاس توضیح داده شده است. منبعِ پیادهسازی نیز نشان میدهد انتساب با یک ارجاعِ ضعیف انجام میشود و حذفِ کلید به بازخوانِ همان ارجاع متصل است.
مقایسهٔ دو کنترل ضروری است. اگر گروهها متفاوت باشند، باوجودِ هشِ مشترک دو ورودی داریم. اگر بهجای نمونهٔ اصلی، نمونهٔ برابرِ دوم را کنار بگذاریم، ورودیِ نمونهٔ اصلی باقی میماند. این دو شاهد مانع میشوند نتیجه را به «هر برخوردِ هش» یا «مرگِ هر شیء برابر» نسبت بدهیم. شمارهٔ هویت در خروجی هم آدرسِ حافظه نیست؛ فقط اندیسِ ثابتِ نمونه در همین آزمایش است.
۳. حذفِ صریح پیش از ثبت، عمرِ ورودی را دوباره تعیین میکند

اگر سیاستِ برنامه این باشد که نمونهٔ تازه صاحبِ ورودی شود، میتوان ابتدا ورودیِ برابر را صریحاً حذف کرد و سپس با نمونهٔ انتخابشده نوشت. در شاهدِ دوم، پس از ثبتِ اولیه با نمونهٔ صفر، از طریق نمونهٔ یک حذف میکنیم و بعد برچسبِ نه را با همان نمونهٔ یک ثبت میکنیم. این بار کنارگذاشتنِ نمونهٔ صفر ورودی را حذف نمیکند؛ کنارگذاشتنِ نمونهٔ یک آن را حذف میکند.
این راهحل انتخابِ سیاست است، نه توصیهٔ همگانی برای هر کش. گاهی وابستگی به نخستین نمونه درست است و گاهی باید هویتِ تازه تعیینکننده باشد. همچنین حذف با یک کلیدِ برابر میتواند ورودیِ متعلق به شیء دیگری را بردارد. پیش از استفاده باید معلوم باشد گروهِ برابری دقیقاً چه معنایی دارد و کدام بخشِ برنامه مجاز است نمایندهٔ آن را عوض کند.
خودِ مشاهدهگر هم ممکن است عمر را تغییر دهد. فهرستی که از کلیدهای واقعی میسازیم، مرجعِ قوی نگه میدارد. در شاهدِ جداگانه، تا وقتی این فهرست باقی است، نمونهٔ اصلی زنده میماند؛ با خالیکردنِ آن، این نگهداری پایان مییابد. در مقابل، خروجیِ keyrefs() ارجاعهای ضعیف است و زندهبودنِ آیندهٔ کلید را تضمین نمیکند. به همین دلیل گزارشِ اصلی فقط بولینها و اعداد را برمیگرداند.
درسِ عمرِ بافر و memoryview نیز اهمیتِ مالکِ زنده را نشان میداد، اما محدودیتِ تغییرِ اندازه و آزادکردنِ نماها داشت. اینجا محدودیتِ بافر یا snapshotِ داده نداریم؛ مرزِ مسئله، عضویتِ نگاشت و کلیدِ نماینده است.
۴. ردگیریِ محدود و مرجعِ مستقلِ بازههای عمر

پیش از ساختنِ هر نمونه، تمامِ توالی اعتبارسنجی میشود. نوعها باید دقیق باشند؛ مثلاً بولین بهجای عدد و مولد بهجای تاپل پذیرفته نمیشود. هیچ رویدادی نمیتواند به نمونهای اشاره کند که قبلاً کنار گذاشته شده است. ردکردنِ یک رویدادِ نامعتبر در انتهای بسته، نباید باعث ساختنِ نمونههای ابتدای آن شود. این ویژگیِ رابطِ نوشتهشدهٔ ماست و دربارهٔ رفتارِ هر نگاشتِ پایتون ادعایی ندارد.
مسیرِ اصلی واقعاً از نگاشتِ استاندارد و مرجعهای ضعیف استفاده میکند. مرجعِ مستقل، برای هر پیشوندِ رویدادها بازههای عمر میسازد: نخستین ثبت، نماینده را تعیین میکند؛ نخستین حذفِ برابر یا کنارگذاشتنِ همان نماینده، بازه را میبندد. آخرین ثبتِ داخلِ بازه، برچسب را تعیین میکند. در این مسیر نه نگاشتِ ضعیف وجود دارد، نه بازخوانِ جمعآوری و نه بهروزرسانیِ همان جدولِ مسیرِ اصلی.
خانوادهٔ متناهی، گروههای صفر و یک و واژههای کوتاهِ رویداد را میپیماید؛ برای یک یا دو نمونه طول تا سه و برای سه نمونه طول تا دو است. تعدادِ واقعیِ ردگیریها و مشاهدهها در خروجی ثبت میشود. این خانواده فقط یکی از چهارده گروهِ آزمون است؛ شاهدهای دستی، ورودیِ نامعتبر، مرزِ اندازه و اثرِ مشاهدهگر هم در همان چهارده گروه قرار دارند. برابریِ این موارد اثباتی برای همهٔ برنامهها و همهٔ جمعآورندهها نیست.
"""Controlled CPython WeakKeyDictionary identity/lifetime experiment.
Values are unitless review tags. This is not a memory or speed benchmark.
"""
import gc
import itertools
import json
import platform
import sys
import weakref
class _FixtureKey:
__slots__ = ("group", "serial", "__weakref__")
def __init__(self, group, serial):
self.group = group
self.serial = serial
def __hash__(self):
return 0
def __eq__(self, other):
return type(other) is _FixtureKey and self.group == other.group
def _validate(groups, events):
if type(groups) is not tuple or type(events) is not tuple:
raise TypeError("groups and events must be exact tuples")
if not 1 <= len(groups) <= 3 or len(events) > 6:
raise ValueError("fixture size or event count exceeds the contract")
for label in groups:
if type(label) is not int:
raise TypeError("group labels must be exact integers")
if not 0 <= label <= 2:
raise ValueError("group label outside 0..2")
available = [True] * len(groups)
for event in events:
if type(event) is not tuple or len(event) != 3:
raise TypeError("every event must be an exact three-tuple")
action, index, value = event
if type(action) is not str or type(index) is not int:
raise TypeError("action/index types are not exact")
if action not in ("put", "erase", "drop"):
raise ValueError("unknown action")
if not 0 <= index < len(groups):
raise ValueError("identity index outside fixture")
if action == "put":
if type(value) is not int:
raise TypeError("review tag must be an exact integer")
if not 0 <= value <= 9:
raise ValueError("review tag outside 0..9")
elif value is not None:
raise ValueError("erase/drop must carry None")
if not available[index]:
raise ValueError("an event addresses an already dropped identity")
if action == "drop":
available[index] = False
def _snapshot(table, probes):
# This frame and its temporary strong key references end before any drop.
alive = tuple(probe() is not None for probe in probes)
entries = tuple(sorted((key.serial, key.group, tag)
for key, tag in table.items()))
return alive, entries
def observe_registry(groups, events):
"""Return initial/after-event immutable observations for controlled keys.
Entire sequential input validation precedes all fixture/map construction.
Explicit collection points are observations on this CPython fixture only.
"""
_validate(groups, events)
before = gc.isenabled(), gc.get_debug(), gc.get_threshold()
owners = [_FixtureKey(group, index) for index, group in enumerate(groups)]
probes = tuple(weakref.ref(key) for key in owners)
table = weakref.WeakKeyDictionary()
try:
snapshots = [_snapshot(table, probes)]
for action, index, value in events:
if action == "put":
table[owners[index]] = value
elif action == "erase":
table.pop(owners[index], None)
else:
owners[index] = None
gc.collect(0)
snapshots.append(_snapshot(table, probes))
return tuple(snapshots)
finally:
table.clear()
owners.clear()
gc.collect(0)
if any(probe() is not None for probe in probes):
raise AssertionError("an owned fixture key escaped cleanup")
if before != (gc.isenabled(), gc.get_debug(), gc.get_threshold()):
raise AssertionError("GC configuration changed")
def history_reference(groups, events):
"""Independent valid-input oracle: epochs from whole event-history intervals.
No weak references, map updates, GC, or production validator are used.
The first put starts an ownership interval. An equal erase or that original
identity's drop ends it; later equal puts change only the interval's tag.
"""
reports = []
for stop in range(len(events) + 1):
prefix = events[:stop]
alive = tuple(not any(action == "drop" and index == identity
for action, index, _ in prefix)
for identity in range(len(groups)))
entries = []
for group in range(3):
start_after = -1
while True:
starts = [pos for pos, (action, index, _) in enumerate(prefix)
if pos > start_after and action == "put"
and groups[index] == group]
if not starts:
break
start = starts[0]
original = prefix[start][1]
ends = [pos for pos, (action, index, _) in enumerate(prefix)
if pos > start and
((action == "erase" and groups[index] == group)
or (action == "drop" and index == original))]
end = ends[0] if ends else stop
if end == stop:
tags = [value for pos, (action, index, value)
in enumerate(prefix)
if start <= pos < end and action == "put"
and groups[index] == group]
entries.append((original, group, tags[-1]))
break
start_after = end
reports.append((alive, tuple(sorted(entries))))
return tuple(reports)
def _must_raise(kind, groups, events):
try:
observe_registry(groups, events)
except kind:
return
raise AssertionError("expected input rejection")
def _valid_word_without_validator(size, word):
# Family generation only: each identity may be dropped at most once and
# cannot subsequently be addressed. It does not predict registry contents.
for position, (_, index, _) in enumerate(word):
if not 0 <= index < size:
return False
if any(a == "drop" and i == index for a, i, _ in word[:position]):
return False
return True
def _family():
profiles = 0
observations = 0
for size in (1, 2, 3):
alphabet = tuple(event for identity in range(size)
for event in (("put", identity, 0),
("put", identity, 9),
("erase", identity, None),
("drop", identity, None)))
max_length = 3 if size <= 2 else 2
for groups in itertools.product((0, 1), repeat=size):
for length in range(max_length + 1):
for word in itertools.product(alphabet, repeat=length):
if not _valid_word_without_validator(size, word):
continue
actual = observe_registry(groups, word)
expected = history_reference(groups, word)
assert actual == expected, (groups, word, actual, expected)
profiles += 1
observations += len(actual)
return profiles, observations
def _observation_retention_witness():
# A saved weak key reference does not own a key. A list of actual keys does.
owners = [_FixtureKey(1, 0), _FixtureKey(1, 1)]
probes = tuple(weakref.ref(key) for key in owners)
table = weakref.WeakKeyDictionary()
table[owners[0]] = 4
table[owners[1]] = 9
weak_observations = table.keyrefs()
retained_keys = list(table.keys())
try:
owners[0] = None
gc.collect(0)
held = _snapshot(table, probes)
assert held == ((True, True), ((0, 1, 9),))
retained_keys.clear()
gc.collect(0)
released = _snapshot(table, probes)
assert released == ((False, True), ())
assert len(weak_observations) == 1 and weak_observations[0]() is None
return {"while_key_list_retained": held,
"after_key_list_released": released,
"saved_weak_key_reference_is_dead": True}
finally:
retained_keys.clear()
table.clear()
owners.clear()
gc.collect(0)
assert all(probe() is None for probe in probes)
def self_check():
checks = {}
before = gc.isenabled(), gc.get_debug(), gc.get_threshold()
equal = observe_registry((1, 1, 2),
(("put", 0, 4), ("put", 1, 9), ("drop", 0, None)))
assert equal[2] == ((True, True, True), ((0, 1, 9),))
assert equal[3] == ((False, True, True), ())
checks["equal_update_keeps_first_actual_key_lifetime"] = True
transferred = observe_registry((1, 1),
(("put", 0, 4), ("erase", 1, None),
("put", 1, 9), ("drop", 0, None),
("drop", 1, None)))
assert transferred[4] == ((False, True), ((1, 1, 9),))
assert transferred[5] == ((False, False), ())
checks["explicit_erase_then_put_selects_new_representative"] = True
different = observe_registry((0, 2), (("put", 0, 4), ("put", 1, 9)))
assert different[-1] == ((True, True), ((0, 0, 4), (1, 2, 9)))
checks["same_hash_does_not_make_unequal_groups_one_key"] = True
later = observe_registry((1, 1), (("put", 0, 4), ("put", 1, 9),
("drop", 1, None)))
assert later[-1] == ((True, False), ((0, 1, 9),))
checks["dropping_nonrepresentative_equal_identity_keeps_entry"] = True
erased = observe_registry((1, 1), (("put", 0, 4), ("erase", 1, None)))
assert erased[-1] == ((True, True), ())
checks["erase_through_equal_identity_removes_original_entry"] = True
restarted = observe_registry((1, 1), (("put", 0, 4), ("drop", 0, None),
("put", 1, 9)))
assert restarted[-1] == ((False, True), ((1, 1, 9),))
checks["put_after_original_death_starts_fresh_interval"] = True
assert observe_registry((2,), ()) == (((True,), ()),)
assert observe_registry((2,), (("drop", 0, None),))[-1] == ((False,), ())
assert observe_registry((2,), (("erase", 0, None),))[-1] == ((True,), ())
checks["initial_empty_unregistered_drop_and_absent_erase"] = True
global _FixtureKey
original_class = _FixtureKey
constructed = []
def forbidden_fixture(*args):
constructed.append(args)
raise AssertionError("late validation allocated a fixture")
try:
_FixtureKey = forbidden_fixture
_must_raise(ValueError, (1, 1), (("put", 0, 4), ("drop", 0, None),
("put", 0, 9)))
_must_raise(ValueError, (1,), (("put", 0, 4), ("put", 0, 10)))
assert constructed == []
finally:
_FixtureKey = original_class
checks["whole_late_invalid_batch_precedes_fixture_construction"] = True
class IntChild(int):
pass
class TupleChild(tuple):
pass
class StrChild(str):
pass
bad = [
(TypeError, [1], ()), (TypeError, (True,), ()),
(TypeError, (IntChild(1),), ()),
(TypeError, TupleChild((1,)), ()),
(TypeError, (1,), []),
(TypeError, (1,), (["put", 0, 4],)),
(TypeError, (1,), (("put", 0),)),
(TypeError, (1,), ((StrChild("put"), 0, 4),)),
(TypeError, (1,), (("put", True, 4),)),
(TypeError, (1,), (("put", 0, True),)),
(TypeError, (1,), (("put", 0, IntChild(4)),)),
(ValueError, (1,), (("PUT", 0, 4),)),
(ValueError, (1,), (("erase", 0, 0),)),
(ValueError, (1,), (("drop", 0, False),)),
(ValueError, (1,), (("drop", 0, None), ("drop", 0, None))),
(ValueError, (1,), (("drop", 0, None), ("erase", 0, None))),
]
for kind, groups, events in bad:
_must_raise(kind, groups, events)
consumed = []
def lazy():
consumed.append(True)
yield ("put", 0, 4)
iterator = lazy()
_must_raise(TypeError, (1,), iterator)
assert consumed == []
iterator.close()
checks["exact_types_shapes_action_names_and_no_generator_consumption"] = True
bounds = [
((), ()), ((0, 1, 2, 0), ()), ((-1,), ()), ((3,), ()),
((1,), (("put", -1, 0),)), ((1,), (("put", 1, 0),)),
((1,), (("put", 0, -1),)), ((1,), (("put", 0, 10),)),
((1,), (("erase", 0, None),) * 7),
]
for groups, events in bounds:
_must_raise(ValueError, groups, events)
assert len(observe_registry((0, 1, 2), (("put", 2, 9),) * 6)) == 7
checks["declared_resource_and_numeric_bounds_are_inclusive"] = True
family_profiles, family_observations = _family()
assert family_profiles > 0
checks["finite_history_interval_oracle_family_one_group"] = True
retention = _observation_retention_witness()
checks["weak_keyrefs_versus_strong_key_list_observer_effect"] = True
saved = observe_registry((1, 1), (("put", 1, 4),))
assert type(saved) is tuple and all(type(item) is tuple for item in saved)
assert all(type(alive) is tuple and type(entries) is tuple
for alive, entries in saved)
assert observe_registry((1, 1), (("put", 0, 9),))[-1][1] == ((0, 1, 9),)
assert saved == (((True, True), ()), ((True, True), ((1, 1, 4),)))
checks["detached_immutable_observations_and_independent_calls"] = True
assert before == (gc.isenabled(), gc.get_debug(), gc.get_threshold())
checks["owned_fixture_cleanup_and_gc_configuration_preserved"] = True
assert len(checks) == 14 and all(type(value) is bool and value
for value in checks.values())
return checks, family_profiles, family_observations, len(bad) + len(bounds) + 3, retention
def run_example():
checks, profiles, observations, invalids, retention = self_check()
return {
"runtime": {"implementation": platform.python_implementation(),
"python": platform.python_version()},
"contract": {"group_labels": [0, 2], "identities": [1, 3],
"events": [0, 6], "review_tag_unit": "unitless",
"review_tags": [0, 9], "all_fixture_hashes": 0,
"collection_generation": 0,
"no_memory_or_speed_measurement": True},
"equal_update_then_drop_original": observe_registry(
(1, 1, 2), (("put", 0, 4), ("put", 1, 9), ("drop", 0, None))),
"explicit_erase_and_reinsert": observe_registry(
(1, 1), (("put", 0, 4), ("erase", 1, None), ("put", 1, 9),
("drop", 0, None), ("drop", 1, None))),
"observer_retention_witness": retention,
"finite_family_ONE_group": {"profiles": profiles,
"initial_and_after_event_observations": observations,
"labels": [0, 1],
"word_max_length_for_one_or_two_identities": 3,
"word_max_length_for_three_identities": 2,
"counts_as_check_groups": 1},
"invalid_profiles_checked": invalids,
"groups_total": len(checks),
"checks": checks,
"passed": True,
}
if __name__ == "__main__":
if platform.python_implementation() != "CPython" or sys.version_info[:3] != (3, 14, 5):
raise RuntimeError("this recorded observation profile requires CPython 3.14.5")
sys.stdout.reconfigure(encoding="utf-8", newline="\n")
print(json.dumps(run_example(), ensure_ascii=True, indent=2))
خروجیِ زیر، تمامِ JSON چاپشدهٔ اجرای ثبتشده است. برچسبها عمداً ASCII هستند. نقاطِ صریحِ جمعآوری در نسلِ صفر و پاکسازیِ نهایی، مشاهداتِ همین نمونههای بیچرخه در CPython هستند؛ زمانبندیِ قابلحمل برای آزادسازی تضمین نمیکنند. برنامه تنظیمِ فعالبودن، آستانهها و حالتِ اشکالزداییِ جمعآورنده را تغییر نمیدهد.
{
"runtime": {
"implementation": "CPython",
"python": "3.14.5"
},
"contract": {
"group_labels": [
0,
2
],
"identities": [
1,
3
],
"events": [
0,
6
],
"review_tag_unit": "unitless",
"review_tags": [
0,
9
],
"all_fixture_hashes": 0,
"collection_generation": 0,
"no_memory_or_speed_measurement": true
},
"equal_update_then_drop_original": [
[
[
true,
true,
true
],
[]
],
[
[
true,
true,
true
],
[
[
0,
1,
4
]
]
],
[
[
true,
true,
true
],
[
[
0,
1,
9
]
]
],
[
[
false,
true,
true
],
[]
]
],
"explicit_erase_and_reinsert": [
[
[
true,
true
],
[]
],
[
[
true,
true
],
[
[
0,
1,
4
]
]
],
[
[
true,
true
],
[]
],
[
[
true,
true
],
[
[
1,
1,
9
]
]
],
[
[
false,
true
],
[
[
1,
1,
9
]
]
],
[
[
false,
false
],
[]
]
],
"observer_retention_witness": {
"while_key_list_retained": [
[
true,
true
],
[
[
0,
1,
9
]
]
],
"after_key_list_released": [
[
false,
true
],
[]
],
"saved_weak_key_reference_is_dead": true
},
"finite_family_ONE_group": {
"profiles": 2966,
"initial_and_after_event_observations": 10246,
"labels": [
0,
1
],
"word_max_length_for_one_or_two_identities": 3,
"word_max_length_for_three_identities": 2,
"counts_as_check_groups": 1
},
"invalid_profiles_checked": 28,
"groups_total": 14,
"checks": {
"equal_update_keeps_first_actual_key_lifetime": true,
"explicit_erase_then_put_selects_new_representative": true,
"same_hash_does_not_make_unequal_groups_one_key": true,
"dropping_nonrepresentative_equal_identity_keeps_entry": true,
"erase_through_equal_identity_removes_original_entry": true,
"put_after_original_death_starts_fresh_interval": true,
"initial_empty_unregistered_drop_and_absent_erase": true,
"whole_late_invalid_batch_precedes_fixture_construction": true,
"exact_types_shapes_action_names_and_no_generator_consumption": true,
"declared_resource_and_numeric_bounds_are_inclusive": true,
"finite_history_interval_oracle_family_one_group": true,
"weak_keyrefs_versus_strong_key_list_observer_effect": true,
"detached_immutable_observations_and_independent_calls": true,
"owned_fixture_cleanup_and_gc_configuration_preserved": true
},
"passed": true
}
۵. چهار پرسش دربارهٔ مشاهده، جمعآوری و مرزِ ادعا

پرسش یک: چرا زندهماندنِ نمونهٔ برابر، برچسب را نگه نداشت؟ چون ثبتِ دوم فقط مقدار را بازنویسی کرد. وابستگیِ عمر هنوز به شیءِ نمونهٔ نخست بود. در شاهدِ سهنمونهای، پس از دو ثبت، نماینده صفر و برچسب نه است؛ با کنارگذاشتنِ صفر، ورودی حذف میشود و زندهبودنِ نمونهٔ یک این وابستگی را جابهجا نمیکند.
پرسش دو: آیا هشِ یکسان برای یکیشدنِ ورودیها کافی است؟ خیر. در این کلاس همهٔ هشها صفرند، اما گروههای متفاوت برابر نیستند و جدا ثبت میشوند. برابری، هویت و هش سه مفهومِ مرتبط ولی متمایزند. تغییرندادنِ این ویژگیها در طولِ عمرِ نمونه هم جزو فرضهای آزمایش است.
پرسش سه: چگونه نمونهٔ تازه را نماینده کنیم؟ با حذفِ صریحِ ورودیِ برابر و ثبتِ دوباره با شیء موردنظر. شاهدِ مربوط، باقیماندنِ ورودی پس از کنارگذاشتنِ نمونهٔ قدیمی و حذفشدنش پس از کنارگذاشتنِ نمونهٔ تازه را نشان میدهد. این انتقال باید با سیاستِ مالکیتِ برنامه سازگار باشد؛ چندریسمانیبودن و همزمانی در این مدل بررسی نشدهاند.
پرسش چهار: آیا ناپدیدشدنِ ورودی یعنی حافظه به سیستمعامل برگشته است؟ خیر. خروجی فقط دسترسیپذیریِ نمونه و عضویتِ نگاشت را مشاهده میکند. اندازهٔ RSS، سرعت، رفتارِ چرخهها، قلابهای دلخواه و آزادسازیِ منابعِ خارجی سنجیده نشدهاند. نسخهٔ مستنداتِ خواندهشده ۳.۱۴.۹، برچسبِ منبعِ کلاس v3.14.5 و محیطِ اجرای ثبتشده CPython 3.14.5 است. دو مرجع از یک تولیدکنندهاند و تأییدِ مستقلِ بیرونی نیستند. تاریخِ دسترسی ۲۲ اکتبر ۲۰۲۶ است؛ تاریخِ نخستین انتشارِ این صفحهها معلوم نیست. پیوندهای داخلی از موجودیِ ذخیرهشدهٔ ۲۱ اکتبر، ساعتِ ۱۶:۳۸ تهران انتخاب شدهاند و دسترسیِ زندهٔ امروزِ آنها تأیید نشده است.
منابع و مطالعهٔ بیشتر
- Python3.14.9؛ ارجاعِ ضعیف و قراردادِ کامل WeakKeyDictionary
- CPythonv3.14.5؛ کلاس کامل WeakKeyDictionary در منبعِ weakref.py
بررسی منابع: ۳۰ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

