فهرست مقاله
فرض کنید دو کانالِ یک فیکسچرِ آزمون، عمداً به همان بلوکِ تنظیمات اشاره میکنند. پیش از آزمونِ بعدی نسخهای از فیکسچر میخواهیم تا تغییرِ دادهٔ آزمایشی به نمونهٔ قبلی نشت نکند. آیا deepcopy علاوه بر جداکردن نسخه، آن دو مسیرِ داخلی را هم مستقل میکند؟ این درس با گرافِ کوچکِ فهرستها، تغییرِ صریحِ یک مقدار و مرجعِ شمارهگذاریشده نشان میدهد چرا پاسخِ این دو پرسش یکسان نیست.
۱. فیکسچر را از نو بسازیم؛ انتساب کپی نیست

مرجعِ رسمی copy میانِ ایجادِ یک پیوندِ تازه و کپیِ شیء تفاوت میگذارد. نوشتنِ نامِ دوم برای فهرست، خودِ فهرست را تکثیر نمیکند. اگر از نامِ دوم به فرزندِ قابلتغییر برسیم و مقدارش را عوض کنیم، نامِ نخست همان تغییر را میبیند. برای آزمونِ مستقل، صرفاً متفاوتبودنِ نامِ متغیرها شاهدِ کافی نیست.
دادهٔ این آزمایش، شمارشِ صحیحِ ساختگی در محدودهٔ صفر تا ۴۰۹۵ است. مقدارِ ۲۰۴۸ را عمداً به ۲۰۵۶ تبدیل میکنیم؛ هیچ مبدل، ولتاژ، واحدِ میلیولت یا کالیبراسیونِ واقعی اجرا نشده است. نامِ شمارش کمک میکند این اعداد را با بایتِ حافظه یا اندازهگیریِ ابزار اشتباه نگیریم. فیکسچر فقط یک نمونهٔ کوچک برای پیادهسازیِ آزمونِ تکرارپذیر است، نه اجرای خطِ CI یا اتصال به سرویس.
ورودی، توصیفِ تغییرناپذیرِ یک تا چهار گره است. هر گره یک مقدار و تاپلِ شمارههای فرزند دارد؛ خودِ گره در آزمایش به فهرستی با مقدار در خانهٔ نخست و ارجاعهای فرزند در خانههای بعدی تبدیل میشود. شمارهٔ فرزند در مسیر، از صفر شروع میشود و خانهٔ مقدار را شامل نمیشود. شمارههای توصیف، نشانیِ حافظه یا id اشیای واقعی نیستند.
سه حالت داریم: alias همان ریشه را نگه میدارد، shallow یک کپیِ سطحی و deep یک deepcopy با memo پیشفرضِ تازه میسازد. پس از ساختِ نسخه، یک مسیرِ معتبر را دنبال و فقط مقدارِ گرهٔ مقصد را جایگزین میکنیم. مسیرِ تهی خودِ ریشه را انتخاب میکند. هر فراخوانی فیکسچرِ متعلق به خودش را میسازد و فهرستِ تغییرپذیرِ دلخواه از بیرون نمیگیرد.
۲. کپیِ سطحی، فرزندِ مشترک را جدا نمیکند

در شاهدِ اصلی، ریشه دو بار به یک فرزند اشاره میکند و مقدارِ فرزند ۲۰۴۸ است. پس از کپیِ سطحی، ریشهٔ جدید هویتِ جدا دارد، اما هر دو خانهٔ آن هنوز همان فرزندِ اصلی را نگه میدارند. تغییرِ مقدار از مسیرِ یکی از این خانهها، در ریشهٔ قبلی نیز دیده میشود. عبارتِ «فهرستِ بیرونی تازه است» دربارهٔ تمامِ دادهٔ زیرِ آن حکمی نمیدهد.
این رفتار را از تغییرِ خودِ ریشه جدا میسنجیم. اگر گراف فقط یک گرهٔ بدون فرزند داشته باشد و مقدارِ ریشه را جایگزین کنیم، alias نمونهٔ اصلی را تغییر میدهد؛ کپیِ سطحی و عمیق چنین نمیکنند. در نتیجه کپیِ سطحی همیشه بیفایده یا همیشه کافی نیست. باید بدانیم آزمون کدام لایه و کدام مسیرِ قابلتغییر را ویرایش خواهد کرد.
همچنین برابریِ مقدار، شاهدِ برابریِ هویت نیست. دو فرزندِ جدا میتوانند هر دو مقدارِ هفت داشته باشند؛ تغییرِ یکی نباید دیگری را عوض کند. در مقابل، دو خانهٔ اشارهکننده به یک فرزند، دو گرهٔ مستقل نمیسازند. این تمایز در حسابداریِ اشیای مشترک نیز مهم بود، اما آن درس اندازهٔ مستقیم را جمع میکرد و برنامهٔ حاضر هیچ getsizeof یا اندازهگیریِ حافظهای ندارد.
تعدادِ خانههای تکراری هم حذف نمیشود. اگر ریشه سه بار به یک فرزند اشاره کند، خروجی هنوز سه خانه و یک فرزندِ مشترک دارد. گزارش هم ترتیبِ ارجاعها و هم هویتهای یکتا را نگه میدارد. تبدیلِ فهرستِ فرزندان به مجموعه، قراردادِ داده را تغییر میدهد و راهِ درستی برای «اصلاح» اشتراک نیست.
۳. deepcopy اشتراک و چرخهٔ درون نسخه را حفظ میکند

در همان شاهدِ دوکاناله، deepcopy ریشه و فرزندِ قابلدسترسی را از اشیای اصلی جدا میکند. با این حال، دو خانهٔ نسخه به همان یک فرزندِ کپیشده اشاره میکنند. تغییر از مسیرِ نخست، از مسیرِ دومِ نسخه نیز دیده میشود؛ فرزندِ اصلی مقدارِ ۲۰۴۸ را نگه میدارد. استقلال از نسخهٔ قبلی با مستقلکردنِ همهٔ مسیرهای داخلی برابر نیست.
مرجعِ رسمی، memo را نگاشتِ اشیای کپیشده در همان گذر معرفی میکند. در پیادهسازیِ دقیقِ نسخهٔ 3.14.5 نیز فهرستِ تازه پیش از پیمایشِ فرزندان در memo ثبت میشود. همین ترتیب اجازه میدهد ارجاعِ تکراری به نسخهٔ موجود برگردد و یک خودچرخه بینهایت باز نشود. memo جزئی از پروتکلِ کتابخانه است؛ این درس محتوایش را تغییر نمیدهد.
خودچرخه شاهدِ روشنی است: فهرست، فرزندِ خودش است. در کپیِ سطحی، فرزند هنوز به ریشهٔ اصلی اشاره میکند؛ بنابراین رفتن از آن مسیر و تغییرِ مقدار، نمونهٔ اصلی را عوض میکند. در کپیِ عمیق، فرزند به ریشهٔ کپیشده برمیگردد. چرخه از بین نرفته؛ در دامنهٔ هویتهای تازه بازساخته شده است. چرخهٔ دوگرهی و فرزندِ مشترک نیز جدا بررسی میشوند.
دو فراخوانیِ جداگانهٔ deepcopy با memoهای پیشفرضِ تازه، مرزِ دیگری هستند. شاهدِ اختصاصی، دو نسخه را از یک ریشه میسازد و تغییرِ فرزندِ نسخهٔ چپ را در نسخهٔ راست نمیبیند. این مشاهده برای فهرستهای داخلیِ کنترلشده است. مستندات اجازهٔ تعریفِ روشهای سفارشی را میدهند؛ از این نمونه برای همهٔ کلاسها، فایلها، اتصالها یا callbackها تضمین نمیسازیم.
کپیِ عمیق، داده را منجمد هم نمیکند. فهرستهای نسخه همچنان قابلتغییرند. موضوع با مشاهدهٔ عمر و نگهدارندهٔ شیء فرق دارد: اینجا weakref، finalize یا gc.collect نداریم و زمانِ نابودی یا بایتِ آزادشده را از هیچ نتیجهای استنتاج نمیکنیم.
۴. گرافِ مشاهدهشده و مرجعِ شمارهگذاریشدهٔ مستقل

تابع، نوع و کرانِ تمامِ گرههای اعلامشده را پیش از ساختِ فیکسچر کنترل میکند؛ حتی گرهای که از ریشه قابلدسترسی نیست. حداکثر سه فرزند برای هر گره، هشت ارجاع در کل و چهار گام در مسیر میپذیریم. bool، زیرکلاس، ورودیِ تنبل، شمارهٔ ناشناخته و مسیرِ ناموجود رد میشوند. این سقفها سیاستِ آموزشیِ خودِ فایلاند و قراردادِ عمومیِ copy یا تضمینِ امنیت نیستند.
برای گزارشِ چرخه، خودِ فهرستها را با == مقایسه نمیکنیم. پیمایشِ اصلی به هر هویتِ زنده یک شمارهٔ محلی میدهد و هر گره را فقط یکبار ثبت میکند. خروجی شاملِ ریشهها و ردیفهای تغییرناپذیرِ مقدار و شمارهٔ فرزندان است. ارجاعهای قوی تا پایانِ مشاهده نگه داشته میشوند و id واقعی چاپ نمیشود. مقایسهٔ قبل و بعد روی این گزارشِ محدود انجام میشود.
مرجعِ مستقل فقط با جدولِ عددی کار میکند: برای deep دامنهٔ شمارههای تازه و برای shallow یک ریشهٔ تازه با ارجاعهای قبلی تعریف میکند. سپس مسیر را در همان جدول دنبال و مقدار را عوض میکند. ترتیبِ مشاهده را از نخستین راههای کوتاهِ معتبر به گرهها به دست میآورد؛ کپیکردنِ اشیای واقعی، id، تابعِ اصلی و سریالسازِ آن را برای تولیدِ انتظار فراخوانی نمیکند.
"""Controlled built-in list fixture graphs: copying is not alias elimination."""
from copy import copy, deepcopy
from itertools import product
import json
import platform
import sys
MAX_NODES = 4
MAX_CHILDREN = 3
MAX_EDGES = 8
MAX_PATH = 4
MAX_COUNTS = 4095
def _validate(graph_spec, root, mode, mutation_path, replacement_counts):
if type(graph_spec) is not tuple:
raise TypeError('exact tuple graph specification required')
if not 1 <= len(graph_spec) <= MAX_NODES:
raise ValueError('declared node count outside1..4')
edges = 0
for row in graph_spec:
if type(row) is not tuple or len(row) != 2:
raise TypeError('each node is an exact(payload,targets) tuple')
payload, targets = row
if type(payload) is not int:
raise TypeError('exact integer payload counts required')
if not 0 <= payload <= MAX_COUNTS:
raise ValueError('payload counts outside0..4095')
if type(targets) is not tuple:
raise TypeError('exact tuple child targets required')
if len(targets) > MAX_CHILDREN:
raise ValueError('too many child slots')
edges += len(targets)
for target in targets:
if type(target) is not int:
raise TypeError('exact integer target required')
if not 0 <= target < len(graph_spec):
raise ValueError('unknown declared target')
if edges > MAX_EDGES:
raise ValueError('too many declared child slots')
if type(root) is not int:
raise TypeError('exact integer root required')
if not 0 <= root < len(graph_spec):
raise ValueError('unknown root')
if type(mode) is not str:
raise TypeError('exact string copy mode required')
if mode not in ('alias', 'shallow', 'deep'):
raise ValueError('unknown copy mode')
if type(mutation_path) is not tuple:
raise TypeError('exact tuple child path required')
if len(mutation_path) > MAX_PATH:
raise ValueError('child path too long')
node = root
for child_slot in mutation_path:
if type(child_slot) is not int:
raise TypeError('exact integer child slot required')
targets = graph_spec[node][1]
if not 0 <= child_slot < len(targets):
raise ValueError('child path does not exist')
node = targets[child_slot]
if type(replacement_counts) is not int:
raise TypeError('exact integer replacement required')
if not 0 <= replacement_counts <= MAX_COUNTS:
raise ValueError('replacement outside0..4095')
def _build_graph(graph_spec):
nodes = [[payload] for payload, targets in graph_spec]
for node, row in zip(nodes, graph_spec, strict=True):
node.extend(nodes[target] for target in row[1])
return nodes
def _collect(roots):
# Keep actual objects strongly alive while using their identities.
nodes = []
numbers = {}
for root in roots:
if id(root) not in numbers:
numbers[id(root)] = len(nodes)
nodes.append(root)
cursor = 0
while cursor < len(nodes):
for child in nodes[cursor][1:]:
if id(child) not in numbers:
numbers[id(child)] = len(nodes)
nodes.append(child)
cursor += 1
return nodes, numbers
def _snapshot(*roots):
nodes, numbers = _collect(roots)
return {'roots': tuple(numbers[id(root)] for root in roots),
'nodes': tuple((node[0], tuple(numbers[id(child)] for child in node[1:]))
for node in nodes)}
def observe_fixture_copy(graph_spec, root, mode, mutation_path, replacement_counts):
"""Build an owned graph, copy its root, then replace one payload count.
Child slots are numbered from zero in the declaration; actual list slot0
is the payload, so following a child uses slot+1. Graph construction/copying
occur only after validation of the entire immutable argument envelope.
No arbitrary caller-owned mutable graph or user-defined copying hook enters.
"""
_validate(graph_spec, root, mode, mutation_path, replacement_counts)
owned = _build_graph(graph_spec)
original = owned[root]
if mode == 'alias':
copied = original
elif mode == 'shallow':
copied = copy(original)
else:
copied = deepcopy(original) # Fresh default memo for this one pass.
before = _snapshot(original, copied)
original_before = _snapshot(original)
selected = copied
for child_slot in mutation_path:
selected = selected[child_slot + 1]
selected[0] = replacement_counts
original_nodes, _ = _collect((original,))
copied_nodes, _ = _collect((copied,))
shared = len({id(node) for node in original_nodes}
& {id(node) for node in copied_nodes})
return {'before': before, 'after': _snapshot(original, copied),
'source_changed': _snapshot(original) != original_before,
'copied_root_is_original': copied is original,
'shared_mutable_nodes': shared}
def indexed_reference(graph_spec, root, mode, mutation_path, replacement_counts):
"""Expected result from integer tables and complete bounded short walks.
No actual list graph, id(), copy/deepcopy, target API, collector or serializer
call. Deep mode introduces an abstract second index domain; unused abstract
rows are not a claim about allocations made by the real copying operation.
Canonical identities are ranked by first shortest lexicographic root/path,
independently of the main queue that inspects actual object identities.
"""
count = len(graph_spec)
table = tuple(graph_spec)
if mode == 'alias':
copied_root = root
elif mode == 'shallow':
copied_root = count
table += (graph_spec[root],)
else:
copied_root = count + root
table += tuple((value, tuple(count + target for target in targets))
for value, targets in graph_spec)
def order_from_walks(roots, rows):
maximum_arity = max(len(targets) for value, targets in rows)
ordered = []
# A shortest path to any reachable vertex is simple and has <V edges.
for depth in range(len(rows)):
for start in roots:
for word in product(range(maximum_arity), repeat=depth):
node = start
for child_slot in word:
targets = rows[node][1]
if child_slot >= len(targets):
break
node = targets[child_slot]
else:
if node not in ordered:
ordered.append(node)
return tuple(ordered)
def table_snapshot(roots, rows):
ordered = order_from_walks(roots, rows)
return {'roots': tuple(ordered.index(start) for start in roots),
'nodes': tuple((rows[node][0], tuple(ordered.index(child)
for child in rows[node][1])) for node in ordered)}
before = table_snapshot((root, copied_root), table)
original_before = table_snapshot((root,), table)
target = copied_root
for child_slot in mutation_path:
target = table[target][1][child_slot]
table = tuple((replacement_counts if index == target else value, children)
for index, (value, children) in enumerate(table))
original_ids = set(order_from_walks((root,), table))
copied_ids = set(order_from_walks((copied_root,), table))
return {'before': before, 'after': table_snapshot((root, copied_root), table),
'source_changed': table_snapshot((root,), table) != original_before,
'copied_root_is_original': root == copied_root,
'shared_mutable_nodes': len(original_ids & copied_ids)}
def rejected(arguments):
try:
observe_fixture_copy(*arguments)
except (TypeError, ValueError):
return True
return False
def run_example():
checks = {}
shared_spec = ((0, (1, 1)), (2048, ()))
deep = observe_fixture_copy(shared_spec, 0, 'deep', (0,), 2056)
expected_before = {'roots': (0, 1),
'nodes': ((0, (2, 2)), (0, (3, 3)), (2048, ()), (2048, ()))}
expected_after = {'roots': (0, 1),
'nodes': ((0, (2, 2)), (0, (3, 3)), (2048, ()), (2056, ()))}
checks['manual_deep_clone_isolates_original_but_retains_one_shared_child'] = (
deep == {'before': expected_before, 'after': expected_after,
'source_changed': False, 'copied_root_is_original': False,
'shared_mutable_nodes': 0})
shallow = observe_fixture_copy(shared_spec, 0, 'shallow', (0,), 2056)
alias = observe_fixture_copy(shared_spec, 0, 'alias', (0,), 2056)
checks['shallow_descendant_update_reaches_original_shared_child'] = (
shallow['source_changed'] and not shallow['copied_root_is_original']
and shallow['shared_mutable_nodes'] == 1
and shallow['after'] == {'roots': (0, 1),
'nodes': ((0, (2, 2)), (0, (2, 2)), (2056, ()))}
and alias['copied_root_is_original'] and alias['shared_mutable_nodes'] == 2)
root_updates = {mode: observe_fixture_copy(((2048, ()),), 0, mode, (), 2056)
for mode in ('alias', 'shallow', 'deep')}
checks['direct_root_update_distinguishes_assignment_from_new_outer_lists'] = (
root_updates['alias']['source_changed']
and all(not root_updates[mode]['source_changed'] for mode in ('shallow', 'deep'))
and root_updates['alias']['after']['roots'] == (0, 0)
and root_updates['shallow']['after']['nodes'] == ((2048, ()), (2056, ())))
distinct = observe_fixture_copy(((0, (1, 2)), (7, ()), (7, ())), 0, 'deep', (0,), 8)
checks['equal_values_do_not_coalesce_distinct_mutable_children'] = (
len(distinct['before']['nodes']) == 6
and distinct['after']['nodes'][4:] == ((8, ()), (7, ()))
and not distinct['source_changed'])
cycle_spec = ((2048, (0,)),)
shallow_cycle = observe_fixture_copy(cycle_spec, 0, 'shallow', (0,), 2056)
deep_cycle = observe_fixture_copy(cycle_spec, 0, 'deep', (0,), 2056)
checks['self_cycle_shallow_backlink_and_deep_cycle_closure_differ'] = (
shallow_cycle['after'] == {'roots': (0, 1), 'nodes': ((2056, (0,)), (2048, (0,)))}
and deep_cycle['after'] == {'roots': (0, 1), 'nodes': ((2048, (0,)), (2056, (1,)))}
and shallow_cycle['source_changed'] and not deep_cycle['source_changed'])
mutual_spec = ((0, (1, 2)), (1, (0, 2)), (2, ()))
mutual = observe_fixture_copy(mutual_spec, 0, 'deep', (0, 1), 77)
checks['mutual_cycle_and_shared_descendant_keep_internal_identity_relation'] = (
mutual == indexed_reference(mutual_spec, 0, 'deep', (0, 1), 77)
and len(mutual['after']['nodes']) == 6 and not mutual['source_changed']
and mutual['after']['nodes'][4][1] == (1, 5)
and mutual['after']['nodes'][5] == (77, ()))
repeated = observe_fixture_copy(((0, (1, 1, 1)), (1, ())), 0, 'deep', (2,), 9)
checks['ordered_repeated_slots_are_preserved_not_deduplicated'] = (
repeated['after']['nodes'][1][1] == (3, 3, 3)
and repeated['after']['nodes'][3] == (9, ()) and not repeated['source_changed'])
graph_profiles = query_cases = 0
for count in range(1, 4):
for choices in product(range(count + 1), repeat=count):
for payloads in product((0, 1), repeat=count):
spec = tuple((value, () if choice == 0 else (choice - 1,))
for value, choice in zip(payloads, choices, strict=True))
graph_profiles += 1
for path in ((), (0,), (0, 0)):
node = 0
for slot in path:
if not spec[node][1]:
break
node = spec[node][1][slot]
else:
for mode in ('alias', 'shallow', 'deep'):
for replacement in (0, 1):
actual = observe_fixture_copy(spec, 0, mode, path, replacement)
expected = indexed_reference(spec, 0, mode, path, replacement)
assert actual == expected, (spec, mode, path, replacement)
query_cases += 1
checks['bounded_functional_graph_family_matches_independent_indexed_walk_oracle_ONE_group'] = (
graph_profiles > 0 and query_cases > graph_profiles)
build_attempts = []
previous_builder = globals()['_build_graph']
def forbidden_build(spec):
build_attempts.append(True)
raise AssertionError('Invalid declaration reached fixture allocation')
globals()['_build_graph'] = forbidden_build
unused_bad = (
(((0, ()), (True, ())), 0, 'deep', (), 1),
(((0, ()), (1, (2,))), 0, 'deep', (), 1),
(((0, ()), (1, (0, 0, 0, 0))), 0, 'deep', (), 1),
)
try:
rejected_unused = all(rejected(arguments) for arguments in unused_bad)
finally:
globals()['_build_graph'] = previous_builder
checks['entire_unreachable_declaration_rejects_before_owned_fixture_allocation'] = (
rejected_unused and build_attempts == [])
invalid_paths = ((True,), (-1,), (2,), (0, 0), (0.0,), (0,) * 5, [], '0')
checks['mutation_path_is_validated_as_exact_existing_child_slots'] = (
all(rejected((shared_spec, 0, 'deep', path, 1)) for path in invalid_paths)
and not observe_fixture_copy(shared_spec, 0, 'deep', (), 8)['source_changed'])
class TupleChild(tuple):
pass
class IntChild(int):
pass
class StrChild(str):
pass
consumed = []
def lazy_spec():
consumed.append(True)
yield (0, ())
invalid_types = (
([], 0, 'deep', (), 1), (TupleChild(shared_spec), 0, 'deep', (), 1),
(lazy_spec(), 0, 'deep', (), 1), (((IntChild(0), ()),), 0, 'deep', (), 1),
(((0, []),), 0, 'deep', (), 1), (((0, (True,)),), 0, 'deep', (), 1),
(shared_spec, True, 'deep', (), 1), (shared_spec, 0, StrChild('deep'), (), 1),
(shared_spec, 0, 'deep', TupleChild(()), 1), (shared_spec, 0, 'deep', (), True),
(shared_spec, 0, 'deep', (), 1.0), (shared_spec, 0, 'unknown', (), 1),
)
checks['strict_envelope_subclasses_no_coercion_and_generator_nonconsumption'] = (
all(rejected(arguments) for arguments in invalid_types) and consumed == [])
maximum = ((4095, (1, 1, 1)), (0, (2, 2, 2)), (1, (3, 0)), (2, ()))
maximum_result = observe_fixture_copy(maximum, 0, 'deep', (0, 0, 1, 0), 4095)
invalid_caps = (
((), 0, 'deep', (), 0), (tuple((0, ()) for _ in range(5)), 0, 'deep', (), 0),
(((0, (0, 0, 0, 0)),), 0, 'deep', (), 0),
(((0, (1, 1, 1)), (0, (2, 2, 2)), (0, (0, 0, 0))), 0, 'deep', (), 0),
(((4096, ()),), 0, 'deep', (), 0), (((-1, ()),), 0, 'deep', (), 0),
(shared_spec, 2, 'deep', (), 0), (shared_spec, 0, 'deep', (), 4096),
)
checks['inclusive_owned_resource_bounds_and_each_overflow_rejected'] = (
maximum_result == indexed_reference(maximum, 0, 'deep', (0, 0, 1, 0), 4095)
and all(rejected(arguments) for arguments in invalid_caps))
frozen_spec = tuple((value, tuple(children)) for value, children in shared_spec)
first = observe_fixture_copy(shared_spec, 0, 'deep', (0,), 1)
second = observe_fixture_copy(shared_spec, 0, 'deep', (0,), 1)
first['after']['nodes'] = ()
checks['fresh_owned_reports_immutable_node_rows_and_input_spec_unchanged'] = (
first is not second and first['after'] is not second['after']
and type(second['after']['nodes']) is tuple and type(second['after']['nodes'][0]) is tuple
and second['after']['nodes'] != () and shared_spec == frozen_spec)
owned = _build_graph(shared_spec)
source = owned[0]
left, right = deepcopy(source), deepcopy(source)
left[1][0] = 2056
source_nodes, _ = _collect((source,))
left_nodes, _ = _collect((left,))
right_nodes, _ = _collect((right,))
checks['fresh_deepcopy_passes_do_not_share_mutable_nodes_and_copy_is_not_frozen'] = (
not ({id(x) for x in left_nodes} & {id(x) for x in right_nodes})
and not ({id(x) for x in source_nodes} & {id(x) for x in left_nodes + right_nodes})
and source[1][0] == right[1][0] == 2048 and left[2][0] == 2056)
assert len(checks) == 14 and all(type(value) is bool and value for value in checks.values()), checks
return {'runtime': {'python': platform.python_version(), 'implementation': platform.python_implementation()},
'data_origin': 'Synthetic ADC count payloads, not voltage acquisition or calibration',
'manual_shared_child': {'declaration': shared_spec, 'mutation_path': (0,),
'replacement_counts': 2056, 'alias': alias, 'shallow': shallow, 'deep': deep},
'manual_self_cycle': {'shallow': shallow_cycle, 'deep': deep_cycle},
'independent_oracle': 'Abstract indexed copy domains plus complete bounded short-walk identity canonicalization; no copy/deepcopy/id/main serializer in expected path',
'functional_graph_profiles': graph_profiles, 'family_query_cases_ONE_group': query_cases,
'invalid_profiles': len(unused_bad) + len(invalid_paths) + len(invalid_types) + len(invalid_caps),
'groups_total': len(checks), 'checks': checks,
'limits': 'Exact owned list graphs and integer payloads only; no custom copy hooks/resources/concurrent mutation/GC timing/RSS/speed/CI server/security-completeness experiment'}
if __name__ == '__main__':
sys.stdout.buffer.write((json.dumps(run_example(), ensure_ascii=True, indent=2) + '\n').encode('ascii'))
یک خانواده، گرافهای تابعیِ کوچک با صفر یا یک فرزند در هر گره و مقدارهای صفر و یک را تولید میکند. مسیرهای معتبر، سه حالت و جایگزینیهای محدود با مرجع سنجیده میشوند. این خانواده دقیقاً یک گروه از چهارده گروهِ معنادار است؛ شاهدهای دستی، چرخه، برابریِ مقدار، ورودیِ نامعتبر، کرانها و حفظِ ورودی، گروههای دیگرند. تعدادِ واقعیِ حالتها در خروجیِ کامل ثبت میشود و برهانِ همهٔ اشیای پایتون نیست.
اجرای مستقلِ ثبتشده با CPython 3.14.5 و کتابخانهٔ استاندارد انجام میشود؛ مرجعِ خواندهشدهٔ مستندات 3.14.9 است و کدِ منبع از برچسبِ 3.14.5 آمده است. این دو سند ناشرِ مشترک دارند و دو اعتبارسنجیِ بیرونیِ مستقل معرفی نمیشوند. هیچ کدِ دانلودشدهای اجرا نشده است. تاریخِ دسترسی ۳۰ مهر ۱۴۰۵ است؛ تاریخِ نخستین انتشارِ صفحهها معلوم نیست.
{
"runtime": {
"python": "3.14.5",
"implementation": "CPython"
},
"data_origin": "Synthetic ADC count payloads, not voltage acquisition or calibration",
"manual_shared_child": {
"declaration": [
[
0,
[
1,
1
]
],
[
2048,
[]
]
],
"mutation_path": [
0
],
"replacement_counts": 2056,
"alias": {
"before": {
"roots": [
0,
0
],
"nodes": [
[
0,
[
1,
1
]
],
[
2048,
[]
]
]
},
"after": {
"roots": [
0,
0
],
"nodes": [
[
0,
[
1,
1
]
],
[
2056,
[]
]
]
},
"source_changed": true,
"copied_root_is_original": true,
"shared_mutable_nodes": 2
},
"shallow": {
"before": {
"roots": [
0,
1
],
"nodes": [
[
0,
[
2,
2
]
],
[
0,
[
2,
2
]
],
[
2048,
[]
]
]
},
"after": {
"roots": [
0,
1
],
"nodes": [
[
0,
[
2,
2
]
],
[
0,
[
2,
2
]
],
[
2056,
[]
]
]
},
"source_changed": true,
"copied_root_is_original": false,
"shared_mutable_nodes": 1
},
"deep": {
"before": {
"roots": [
0,
1
],
"nodes": [
[
0,
[
2,
2
]
],
[
0,
[
3,
3
]
],
[
2048,
[]
],
[
2048,
[]
]
]
},
"after": {
"roots": [
0,
1
],
"nodes": [
[
0,
[
2,
2
]
],
[
0,
[
3,
3
]
],
[
2048,
[]
],
[
2056,
[]
]
]
},
"source_changed": false,
"copied_root_is_original": false,
"shared_mutable_nodes": 0
}
},
"manual_self_cycle": {
"shallow": {
"before": {
"roots": [
0,
1
],
"nodes": [
[
2048,
[
0
]
],
[
2048,
[
0
]
]
]
},
"after": {
"roots": [
0,
1
],
"nodes": [
[
2056,
[
0
]
],
[
2048,
[
0
]
]
]
},
"source_changed": true,
"copied_root_is_original": false,
"shared_mutable_nodes": 1
},
"deep": {
"before": {
"roots": [
0,
1
],
"nodes": [
[
2048,
[
0
]
],
[
2048,
[
1
]
]
]
},
"after": {
"roots": [
0,
1
],
"nodes": [
[
2048,
[
0
]
],
[
2056,
[
1
]
]
]
},
"source_changed": false,
"copied_root_is_original": false,
"shared_mutable_nodes": 0
}
},
"independent_oracle": "Abstract indexed copy domains plus complete bounded short-walk identity canonicalization; no copy/deepcopy/id/main serializer in expected path",
"functional_graph_profiles": 552,
"family_query_cases_ONE_group": 7824,
"invalid_profiles": 31,
"groups_total": 14,
"checks": {
"manual_deep_clone_isolates_original_but_retains_one_shared_child": true,
"shallow_descendant_update_reaches_original_shared_child": true,
"direct_root_update_distinguishes_assignment_from_new_outer_lists": true,
"equal_values_do_not_coalesce_distinct_mutable_children": true,
"self_cycle_shallow_backlink_and_deep_cycle_closure_differ": true,
"mutual_cycle_and_shared_descendant_keep_internal_identity_relation": true,
"ordered_repeated_slots_are_preserved_not_deduplicated": true,
"bounded_functional_graph_family_matches_independent_indexed_walk_oracle_ONE_group": true,
"entire_unreachable_declaration_rejects_before_owned_fixture_allocation": true,
"mutation_path_is_validated_as_exact_existing_child_slots": true,
"strict_envelope_subclasses_no_coercion_and_generator_nonconsumption": true,
"inclusive_owned_resource_bounds_and_each_overflow_rejected": true,
"fresh_owned_reports_immutable_node_rows_and_input_spec_unchanged": true,
"fresh_deepcopy_passes_do_not_share_mutable_nodes_and_copy_is_not_frozen": true
},
"limits": "Exact owned list graphs and integer payloads only; no custom copy hooks/resources/concurrent mutation/GC timing/RSS/speed/CI server/security-completeness experiment"
}
۵. چهار پرسش دربارهٔ استقلال و تغییرپذیری فیکسچر

پرسش اول: چرا دو نام برای شروعِ دو آزمون کافی نیست؟ چون هر دو میتوانند همان ریشه را نگه دارند. در حالتِ alias، حتی جایگزینیِ مقدارِ ریشه به نمونهٔ قبلی میرسد. برای تعیینِ مرزِ آزمون باید هویتِ اشیای قابلتغییر را بشناسیم، نه فقط تعدادِ متغیرها را.
پرسش دوم: چرا با ریشهٔ تازه هنوز دادهٔ قبلی تغییر میکند؟ کپیِ سطحی ارجاعِ فرزند را دوباره استفاده میکند. اگر ویرایش از آن مسیر باشد، هویتِ مشترک هنوز وجود دارد. شاهدِ خودچرخه نشان میدهد حتی فرزندِ بهظاهر برگشتی هم ممکن است به ریشهٔ قدیمی برسد، نه پوستهٔ تازه.
پرسش سوم: چرا دو کانالِ نسخهٔ عمیق همچنان به هم وابستهاند؟ در ورودی هر دو به یک فرزند اشاره میکردند و گذرِ کپی یک فرزندِ تازه برای همان هویت میسازد. برای استقلالِ هر کانال باید مدلِ داده یا سیاستِ تکثیر را جدا طراحی کرد؛ حذفِ خودکارِ اشتراک، وعدهٔ این API نیست.
پرسش چهارم: این آزمون چه چیزی را برای CI تضمین میکند؟ فقط رفتارِ محدودِ فیکسچرِ داخلی را قابلِ بررسی میکند. تغییرِ همزمانِ منبع، روشِ سفارشی، منبعِ خارجی، مصرفِ حافظه، سرعت و استقرارِ خطِ اجرا بیرونِ دامنهاند. پیش از افزودنِ نوع یا عملیاتِ تازه، همان مرزِ مالکیت و مرجعِ انتظار باید دوباره تعریف شود.
منابع و مطالعهٔ بیشتر
بررسی منابع: ۳۰ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

