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

مرجع رسمیِ sys.getsizeof میگوید عددِ برگشتی، اندازهٔ مستقیمِ همان شیء بر حسب بایت است و اندازهٔ اشیای موردِ ارجاع را شامل نمیشود. این تابع روشِ __sizeof__ شیء را به کار میگیرد و برای اشیای مدیریتشده، سربارِ مربوط به جمعآوریِ زباله را نیز اضافه میکند. رفتارِ افزونههای خارجی ممکن است متفاوت باشد؛ برای همین دامنهٔ ما فقط دو نوعِ داخلیِ دقیق است.
برای یک فهرست، اندازهٔ مستقیم را جدا ثبت میکنیم و سپس اندازهٔ اشیای مجازِ قابلدسترسی را فقط یک بار اضافه میکنیم. اندازهٔ اشارهگرها را دستی دوباره به این جمع نمیافزاییم. این حسابداری تعریفِ روشنی دارد، اما به معنیِ حافظهٔ منحصراً متعلق به ریشه نیست؛ همان برگ ممکن است از جایی بیرونِ گراف هم ارجاع داشته باشد. مبلغِ جمع نیز گواهِ آزادشدنِ همان تعداد بایت با حذفِ ریشه نیست.
آزمایشِ حافظه با tracemalloc پرسش دیگری داشت: تخصیصِ ردگیریشده در یک بازهٔ اجرا. اینجا رویدادهای تخصیص را دنبال نمیکنیم؛ روی اشیای زندهٔ موجود، یک جمعِ مشخص میسازیم. ابزارِ پیمایش نیز فهرست، دیکشنری و خروجیِ خودش را میسازد، ولی آنها جزو گرافِ ورودی شمرده نمیشوند. بنابراین نتیجه را کلِ حافظهٔ برنامه یا RSS نمینامیم.
۲. قراردادِ گرافِ محدودِ list و bytes

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

طبق بخشِ اشیا، مقدارها و نوعها، هویت با مقدار فرق دارد. دو فهرستِ خالی میتوانند مقدارِ برابر و هویتِ متفاوت داشته باشند؛ باید هر دو شمرده شوند. از طرفِ دیگر، عملیاتی روی نوعِ تغییرناپذیر ممکن است همان شیءِ موجود را برگرداند. پس برای bytes، برابر بودنِ مقدار یا نوشتنِ یک سازندهٔ دیگر را دلیلِ جدابودنِ تخصیص قرار نمیدهیم.
دیکشنریِ دیدهشدهها، هویت را به خودِ شیء نگاشت میکند. نگهداریِ ارجاعِ قوی مهم است: قراردادِ id یکتایی را فقط در طولِ عمرِ شیء تضمین میکند و اشیای با عمرِ غیرهمپوشان میتوانند همان شماره را بگیرند. ما شمارهها یا نشانیها را در خروجی منتشر نمیکنیم. این سازوکار، مالکیتِ حافظه یا مجوزِ دسترسی را تشخیص نمیدهد؛ فقط از تکرارِ شمارش در همین گراف جلوگیری میکند.
پنج ساختِ کوچک داریم: برگِ بایتی، فهرستِ خالی، گرافِ مشترکِ بدون چرخه، خودچرخه و چرخهٔ دو فهرستی با برگِ مشترک. در گرافِ سوم، ریشه دو بار به یک فرزند و یک بار مستقیم به محموله اشاره میکند؛ فرزند هم همان محموله را دارد. سه شیء و چهار ارجاع داریم. در اجرای ثبتشده جمعِ درست ۱۹۳ بایت و جمعِ نادرستِ مسیرهای تکراری ۳۵۵ بایت است.
خودچرخه فقط یک شیء و یک یال دارد؛ دیدنِ دوبارهٔ همان هویت، پیمایش را ادامه نمیدهد. چرخهٔ دو فهرستی، سه شیء و پنج یال دارد. اندازههای مستقیمِ این دو نمونه در همین اجرا ۸۸ و ۲۰۱ بایت جمع شدهاند. این عددها به ساخت و نسخهٔ مفسر وابستهاند؛ از تعدادِ خانهها یک فرمولِ جهانی برای سربار یا ظرفیتِ تخصیص نتیجه نمیگیریم.
۴. کد کامل و مرجعِ گرههای شمارهگذاریشده

کدِ shared_object_size.py با CPython 3.14.5 و فقط کتابخانهٔ استاندارد اجرا شده است؛ مستنداتِ خواندهشده 3.14.8 هستند. پیمایشِ اصلی پشتهٔ صریح دارد و به بازگشتِ تابعی وابسته نیست. اندازهگیری فقط پس از اعتبارسنجیِ کامل انجام میشود؛ در شاهدِ نوعِ نامعتبر، روشِ سفارشیِ sizeof اصلاً فراخوانی نمیشود. این محدودسازی، تضمینِ امنیت برای اشیای دلخواه یا محیطِ دستکاریشده نیست.
"""Explicit bounded list/bytes reachable shallow-size accounting, not RSS.
The caller must keep the graph stable during this synchronous call. References
are kept strong while ids are used. Only list elements are graph edges; bytes
are leaves. No arbitrary objects, GC traversal or ownership model is supplied.
"""
import json
import platform
import sys
MAX_NODES = 16
MAX_LIST_ITEMS = 8
MAX_EDGES = 32
MAX_LEAF_BYTES = 64
def _collect(root):
"""Validate before getsizeof; retain strong objects, not only their ids."""
pending = [root]
objects = {}
edges = 0
while pending:
node = pending.pop()
if type(node) is not list and type(node) is not bytes:
raise TypeError('only exact list nodes and exact bytes leaves')
identity = id(node)
if identity in objects:
assert objects[identity] is node
continue
if len(objects) == MAX_NODES:
raise ValueError('too many unique reachable objects')
if type(node) is bytes:
if len(node) > MAX_LEAF_BYTES:
raise ValueError('bytes leaf too long')
else:
if len(node) > MAX_LIST_ITEMS:
raise ValueError('list has too many references')
edges += len(node)
if edges > MAX_EDGES:
raise ValueError('too many directed references')
objects[identity] = node # Keep this object alive until sizing finishes.
if type(node) is list:
pending.extend(node)
return objects, edges
def reachable_size(root):
"""Sum getsizeof ONCE per identity under the explicit stable-graph policy.
Direct list storage already includes its reference slots. Referent objects
are added once; pointer bytes are not separately added a second time.
The analyzer's own stack/dict/result and hidden type/allocator data are not
counted. The result is neither owned/reclaimable memory nor process RSS.
"""
objects, edges = _collect(root)
lists = [node for node in objects.values() if type(node) is list]
leaves = [node for node in objects.values() if type(node) is bytes]
list_bytes = sum(sys.getsizeof(node) for node in lists)
leaf_bytes = sum(sys.getsizeof(node) for node in leaves)
return {'total_shallow_bytes': list_bytes + leaf_bytes,
'list_shallow_bytes': list_bytes, 'bytes_shallow_bytes': leaf_bytes,
'unique_objects': len(objects), 'list_objects': len(lists),
'bytes_objects': len(leaves), 'directed_references': edges}
def indexed_oracle(objects, adjacency, root_index):
"""Trusted MANUAL object table and integer graph, no id/object traversal.
A finite bitmask closure over declared integer indices determines which
table entries count. The size primitive is shared, the reachability logic
is independent. Distinct table entries must denote distinct live objects.
"""
assert len(objects) == len(adjacency)
assert all(objects[i] is not objects[j]
for i in range(len(objects)) for j in range(i))
for i, node in enumerate(objects):
assert len(adjacency[i]) == (len(node) if type(node) is list else 0)
if type(node) is list:
assert all(node[j] is objects[target]
for j, target in enumerate(adjacency[i]))
reached = 1 << root_index
for step in range(len(objects)):
following = reached
for i, targets in enumerate(adjacency):
if reached & (1 << i):
for target in targets:
following |= 1 << target
reached = following
indices = [i for i in range(len(objects)) if reached & (1 << i)]
list_indices = [i for i in indices if type(objects[i]) is list]
leaf_indices = [i for i in indices if type(objects[i]) is bytes]
list_bytes = sum(sys.getsizeof(objects[i]) for i in list_indices)
leaf_bytes = sum(sys.getsizeof(objects[i]) for i in leaf_indices)
return {'total_shallow_bytes': list_bytes + leaf_bytes,
'list_shallow_bytes': list_bytes, 'bytes_shallow_bytes': leaf_bytes,
'unique_objects': len(indices), 'list_objects': len(list_indices),
'bytes_objects': len(leaf_indices),
'directed_references': sum(len(adjacency[i]) for i in list_indices)}
def five_profiles():
leaf = bytes(range(4))
empty = []
payload = bytes(range(16))
child = [payload]
shared = [child, child, payload]
self_loop = []
self_loop.append(self_loop)
left, right = [], []
cyclic_payload = bytes(range(4, 12))
left.extend((right, cyclic_payload, right))
right.extend((left, cyclic_payload))
return (
('bytes_leaf', leaf, (leaf,), ((),)),
('empty_list', empty, (empty,), ((),)),
('shared_dag', shared, (shared, child, payload), ((1, 1, 2), (2,), ())),
('self_cycle', self_loop, (self_loop,), ((0,),)),
('mutual_cycle', left, (left, right, cyclic_payload), ((1, 2, 1), (0, 2), ())),
)
def rejects(kind, root):
try:
reachable_size(root)
except kind:
return True
return False
def list_chain(n):
node = []
for i in range(n - 1):
node = [node]
return node
def main():
profiles = five_profiles()
snapshots = [(nodes, tuple(tuple(node) if type(node) is list else node for node in nodes))
for name, root, nodes, adjacency in profiles]
measured = {}
checks = {}
oracle_ok = True
for name, root, nodes, adjacency in profiles:
measured[name] = reachable_size(root)
oracle_ok &= measured[name] == indexed_oracle(nodes, adjacency, 0)
checks['five_profiles_match_independent_manually_indexed_bitmask_oracle'] = bool(oracle_ok)
shared = profiles[2][1]
child, payload = shared[0], shared[2]
per_occurrence = (sys.getsizeof(shared) + 2 * sys.getsizeof(child)
+ 3 * sys.getsizeof(payload))
checks['shared_dag_counts_identity_once_not_each_reference_path'] = (
measured['shared_dag']['unique_objects'] == 3
and measured['shared_dag']['directed_references'] == 4
and measured['shared_dag']['total_shallow_bytes']
== sys.getsizeof(shared) + sys.getsizeof(child) + sys.getsizeof(payload)
and per_occurrence > measured['shared_dag']['total_shallow_bytes'])
checks['self_and_mutual_cycles_terminate_without_repeated_nodes'] = (
measured['self_cycle']['unique_objects'] == 1
and measured['self_cycle']['directed_references'] == 1
and measured['self_cycle']['total_shallow_bytes'] == sys.getsizeof(profiles[3][1])
and measured['mutual_cycle']['unique_objects'] == 3
and measured['mutual_cycle']['list_objects'] == 2
and measured['mutual_cycle']['bytes_objects'] == 1
and measured['mutual_cycle']['directed_references'] == 5)
equal_left, equal_right = [], []
equal_root = [equal_left, equal_right]
checks['equal_mutable_values_do_not_merge_distinct_live_identities'] = (
equal_left == equal_right and equal_left is not equal_right
and reachable_size(equal_root) == indexed_oracle(
(equal_root, equal_left, equal_right), ((1, 2), (), ()), 0))
original_bytes = bytes(range(8))
converted_bytes = bytes(original_bytes)
pair = [original_bytes, converted_bytes]
same_bytes_identity = original_bytes is converted_bytes
manual_nodes = ((pair, original_bytes) if same_bytes_identity
else (pair, original_bytes, converted_bytes))
manual_edges = ((1, 1), ()) if same_bytes_identity else ((1, 2), (), ())
checks['immutable_factory_identity_observed_not_assumed_distinct'] = (
original_bytes == converted_bytes
and reachable_size(pair) == indexed_oracle(manual_nodes, manual_edges, 0))
checks['empty_bytes_and_empty_list_are_valid_single_object_roots'] = (
reachable_size(b'') == {'total_shallow_bytes': sys.getsizeof(b''),
'list_shallow_bytes': 0, 'bytes_shallow_bytes': sys.getsizeof(b''),
'unique_objects': 1, 'list_objects': 0, 'bytes_objects': 1, 'directed_references': 0}
and reachable_size([])['unique_objects'] == 1)
checks['unique_node_limit_accepts16_and_rejects17_without_recursion'] = (
reachable_size(list_chain(16))['unique_objects'] == 16
and rejects(ValueError, list_chain(17)))
one_leaf = b'X'
checks['per_list_reference_limit_counts_alias_slots'] = (
reachable_size([one_leaf] * 8)['directed_references'] == 8
and reachable_size([one_leaf] * 8)['unique_objects'] == 2
and rejects(ValueError, [one_leaf] * 9))
edge_root = [[one_leaf] * 7 for i in range(4)]
too_many_edges = [[one_leaf] * 8 for i in range(4)]
checks['edge_limit_accepts32_and_rejects36_distinct_parent_slots'] = (
reachable_size(edge_root)['directed_references'] == 32
and reachable_size(edge_root)['unique_objects'] == 6
and rejects(ValueError, too_many_edges))
checks['bytes_payload_limit_accepts64_and_rejects65'] = (
reachable_size(bytes(range(64)))['bytes_objects'] == 1
and rejects(ValueError, bytes(range(65))))
hook_calls = []
class Custom:
def __sizeof__(self):
hook_calls.append('called')
return 1
class ListChild(list):
pass
class BytesChild(bytes):
pass
invalid = (None, True, 0, 1.0, 'x', (), {}, bytearray(b'x'),
memoryview(b'x'), ListChild(), BytesChild(b'x'), Custom())
checks['exact_types_root_and_descendants_reject_before_custom_size_hooks'] = (
all(rejects(TypeError, node) and rejects(TypeError, [node]) for node in invalid)
and hook_calls == [])
stable = True
for nodes, before in snapshots:
for node, previous in zip(nodes, before, strict=True):
if type(node) is list:
stable &= len(node) == len(previous) and all(
child is old for child, old in zip(node, previous, strict=True))
else:
stable &= node is previous
checks['analysis_preserves_all_authored_graph_edges_and_leaves'] = bool(stable)
assert len(checks) == 12
assert all(type(value) is bool and value for value in checks.values()), checks
print(json.dumps({'python': platform.python_version(), 'implementation': platform.python_implementation(),
'size_unit': 'bytes reported by sys.getsizeof in this runtime',
'scope': 'Explicit list edges/bytes leaves only; sum once per live identity, not RSS, ownership, reclaim or all Python references.',
'profile_limits': {'unique_objects':16, 'items_per_list':8, 'directed_references':32, 'bytes_per_leaf':64},
'five_profiles': measured,
'shared_dag_wrong_reference_path_total_bytes': per_occurrence,
'equal_empty_lists_are_distinct_observed': equal_left is not equal_right,
'bytes_constructor_reused_identity_observed': same_bytes_identity,
'identity_numbers_or_addresses_published': False,
'oracle_scope': 'Manually indexed graph/bitmask reachability independent of id-based object traversal; same direct-size primitive, no physical-memory oracle.',
'strict_type_rejections': 2 * len(invalid),
'actual_memory_freeing_GC_RSS_or_speed_measured': False,
'checks': checks}, ensure_ascii=True, indent=2))
if __name__ == '__main__':
main()
مرجعِ مستقل، جدولِ گرههای همان نمونه و فهرستِ یالهای عددیِ دستی را دارد. با بستارِ ماسکِ بیتی، شمارههای قابلدسترسی را تعیین و اندازهٔ همان گرهها را جمع میکند؛ دیکشنریِ هویت یا پشتهٔ تابعِ اصلی را برای ساختِ انتظار تکرار نمیکند. با این حال، ابزارِ اندازهگیریِ مستقیم مشترک است. استقلالِ مرجع مربوط به انتخاب و شمارشِ گرههاست، نه سنجشِ مستقلِ حافظهٔ فیزیکی.
دوازده خانوادهٔ نامدار، پنج نمونه، بیششماریِ اشتراک، توقف در چرخه، هویتِ مقدارهای برابر، رفتارِ سازندهٔ بایتی، ریشهٔ تهی، مرزِ تعدادِ گره، خانه، یال و طولِ برگ، نوعِ نامعتبر و حفظِ گراف را میسنجند. در مرزِ یال، چهار فهرستِ هفتارجاعی زیرِ ریشهٔ چهارتایی، جمعِ سیودو میسازند؛ افزودنِ یک خانه به هرکدام، سیوشش میشود و رد میشود. تعدادِ حالتهای داخلی، گروهِ تازه نیست.
{
"python": "3.14.5",
"implementation": "CPython",
"size_unit": "bytes reported by sys.getsizeof in this runtime",
"scope": "Explicit list edges/bytes leaves only; sum once per live identity, not RSS, ownership, reclaim or all Python references.",
"profile_limits": {
"unique_objects": 16,
"items_per_list": 8,
"directed_references": 32,
"bytes_per_leaf": 64
},
"five_profiles": {
"bytes_leaf": {
"total_shallow_bytes": 37,
"list_shallow_bytes": 0,
"bytes_shallow_bytes": 37,
"unique_objects": 1,
"list_objects": 0,
"bytes_objects": 1,
"directed_references": 0
},
"empty_list": {
"total_shallow_bytes": 56,
"list_shallow_bytes": 56,
"bytes_shallow_bytes": 0,
"unique_objects": 1,
"list_objects": 1,
"bytes_objects": 0,
"directed_references": 0
},
"shared_dag": {
"total_shallow_bytes": 193,
"list_shallow_bytes": 144,
"bytes_shallow_bytes": 49,
"unique_objects": 3,
"list_objects": 2,
"bytes_objects": 1,
"directed_references": 4
},
"self_cycle": {
"total_shallow_bytes": 88,
"list_shallow_bytes": 88,
"bytes_shallow_bytes": 0,
"unique_objects": 1,
"list_objects": 1,
"bytes_objects": 0,
"directed_references": 1
},
"mutual_cycle": {
"total_shallow_bytes": 201,
"list_shallow_bytes": 160,
"bytes_shallow_bytes": 41,
"unique_objects": 3,
"list_objects": 2,
"bytes_objects": 1,
"directed_references": 5
}
},
"shared_dag_wrong_reference_path_total_bytes": 355,
"equal_empty_lists_are_distinct_observed": true,
"bytes_constructor_reused_identity_observed": true,
"identity_numbers_or_addresses_published": false,
"oracle_scope": "Manually indexed graph/bitmask reachability independent of id-based object traversal; same direct-size primitive, no physical-memory oracle.",
"strict_type_rejections": 24,
"actual_memory_freeing_GC_RSS_or_speed_measured": false,
"checks": {
"five_profiles_match_independent_manually_indexed_bitmask_oracle": true,
"shared_dag_counts_identity_once_not_each_reference_path": true,
"self_and_mutual_cycles_terminate_without_repeated_nodes": true,
"equal_mutable_values_do_not_merge_distinct_live_identities": true,
"immutable_factory_identity_observed_not_assumed_distinct": true,
"empty_bytes_and_empty_list_are_valid_single_object_roots": true,
"unique_node_limit_accepts16_and_rejects17_without_recursion": true,
"per_list_reference_limit_counts_alias_slots": true,
"edge_limit_accepts32_and_rejects36_distinct_parent_slots": true,
"bytes_payload_limit_accepts64_and_rejects65": true,
"exact_types_root_and_descendants_reject_before_custom_size_hooks": true,
"analysis_preserves_all_authored_graph_edges_and_leaves": true
}
}
۵. چهار پرسش دربارهٔ اشتراک و اندازه

پرسش اول: چرا فرزندِ تکرارشده را یک بار حساب میکنیم ولی خانههای ریشه حذف نمیشوند؟ پاسخ: خانهها بخشی از همان فهرستِ موجودند و اندازهٔ مستقیمِ آن باقی میماند. ارجاعِ دوم شیءِ فرزندِ تازهای نساخته است؛ فقط از اضافهکردنِ دوبارهٔ اندازهٔ خودِ فرزند و برگش جلوگیری میکنیم.
پرسش دوم: دو مقدارِ برابر همیشه یک گرهاند؟ پاسخ: خیر؛ دو فهرستِ خالیِ جدا، در مثال برابر ولی مستقلاند. برای bytes، کد هویتِ نتیجهٔ سازنده را مشاهده میکند و مرجع را مطابقِ همان مشاهده میسازد؛ نه استفادهٔ مجدد و نه ساختِ شیءِ تازه را از برابریِ محتوا استنتاج نمیکند.
پرسش سوم: حذفِ ریشه دقیقاً ۱۹۳ بایت آزاد میکند؟ پاسخ: چنین نتیجهای نداریم. ممکن است اشیا از بیرون ارجاع داشته باشند، نمونهٔ چرخهدار باقی بماند یا مدیریتِ تخصیص حافظه را فوراً به سیستمعامل برنگرداند. جدولِ مرجع و ابزارِ بررسیِ ما هم ارجاعهای خود را دارند؛ این برنامه زمان یا مقدارِ آزادسازی را نمیسنجد.
پرسش چهارم: همین تابع برای ndarray مناسب است؟ پاسخ: این قرارداد آن را رد میکند. درسِ اشتراکِ پنجرهٔ NumPy نشان میدهد nbytes و بافرِ مشترک مسئلهٔ دیگری دارند. افزودنِ نوع تازه، تعریفِ یال و واحدِ حسابداریِ تازه میخواهد. منابع در ۲۷ مهر ۱۴۰۵ خوانده شدند؛ این تشخیصِ محدود، خبرِ تازه یا تضمینِ مصرفِ یک محصول نیست.
منابع و مطالعهٔ بیشتر
- Python3.14.8؛ اندازهٔ مستقیمِ getsizeof
- Python3.14.8؛ هویت، مقدار و ارجاعِ اشیا
- Python3.14.8؛ id و دامنهٔ یکتایی در طول عمر
بررسی منابع: ۲۷ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

