فهرست مقاله
۱. پیش از تفسیر حافظه، ارجاعِ نگهدارنده را پیدا کنیم

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

ارجاعِ معمولی میتواند شیء را زنده نگه دارد. در مقابل، weakref.ref امکانِ مشاهده را فراهم میکند: فراخوانیِ آن تا هنگامِ وجودِ مرجع، شیء را برمیگرداند و پس از پایانِ عمر، None میدهد. طبق قراردادِ WeakValueDictionary، حضورِ شیء بهعنوانِ مقدارِ این نگاشت، بهتنهایی مانعِ پایانِ عمرش نمیشود. کلیدِ نمونه ثابت است و خودِ نگاشتِ ضعیف در تمامِ آزمایش موجود میماند؛ ناپدیدشدنِ مقدار را با حذفِ خودِ نگاشت اشتباه نمیگیریم.
سه نگهدارندهٔ مستقل تعریف میکنیم: نامِ جایگزین، دیکشنریِ معمولی و نتیجهٔ ذخیرهشدهٔ جستوجو در کشِ ضعیف. موردِ سوم ظریف است: found = cache.get(…) خودِ شیء را برمیگرداند؛ نگهداشتنِ found یک ارجاعِ معمولی میسازد. ضعیفبودنِ مسیرِ دسترسی به معنیِ ضعیفبودنِ مقدارِ بازگشتی نیست. در شاهدِ اختصاصی، ابتدا found باقی میماند و نمونه قابلمشاهده است؛ سپس found حذف میشود و رخدادِ callback دقیقاً یکبار ثبت میشود.
مشاهده هم باید کوتاه و کنترلشده باشد. اگر نتیجهٔ probe() را برای بازرسی در متغیری ذخیره کنیم، همان متغیر میتواند عمرِ نمونه را تغییر دهد. کدِ جدول فقط عبارتِ بولیِ probe() is not None را نگه میدارد؛ آدرس یا نمایشِ شیء چاپ نمیشود. کلاسِ Sample معمولی و قابلِ ارجاعِ ضعیف است. دو نوعِ دقیقِ list و dict مستقیماً این قابلیت را ندارند؛ این مشاهده دربارهٔ همهٔ زیرکلاسها یا همهٔ اشیای پایتون نیست.
۳. finalize و دامِ روشِ وابسته به شیء

finalize تابعی را برای پایانِ عمرِ نمونه ثبت میکند، اما آرگومانهای آن باید با دقت انتخاب شوند. در شکلِ امنِ این آزمایش، تابعِ مستقلِ record_label فقط فهرستِ رخدادها و برچسبِ تغییرناپذیر را دریافت میکند. در شکلِ مسئلهدار، sample.record ثبت میشود. این روشِ وابسته، self را نگه میدارد؛ در نتیجه حذفِ نامِ اصلی کافی نیست. هشدارِ رسمیِ finalize نیز بر نبودِ ارجاعِ مستقیم یا غیرمستقیمِ callback و آرگومانهایش به شیءِ هدف تأکید دارد.
برای آشکارکردنِ علت، شاهدِ کد بررسی میکند که __self__ همان نمونه است. پس از حذفِ مالکِ برنامه، نمونه هنوز موجود میماند. سپس detach ثبت را لغو میکند و نتیجهٔ بازگشتی فوراً کنار گذاشته میشود؛ در اجرای ثبتشده نمونه پایان مییابد و callback لغوشده اجرا نمیشود. این یک دامِ نگهداریِ قابلِ توضیح است؛ از مشاهدهٔ آن نمیتوان افزایشِ حافظهٔ یک محصول یا وجودِ نشت در سرویسِ واقعی را نتیجه گرفت.
متدهای بازرسی نیز بیاثر نیستند. peek یک تاپلِ شاملِ شیء برمیگرداند و ثبت را زنده میگذارد. detach همان جنسِ اطلاعات را برمیگرداند، اما ثبت را غیرفعال میکند. نگهداشتنِ هرکدام از این تاپلها میتواند نمونه را نگه دارد. همچنین فراخوانیِ صریحِ finalizer، callback را حداکثر یکبار اجرا میکند؛ ممکن است پس از آن خودِ نمونه هنوز زنده باشد. پس alive وضعیتِ ثبت است و آزمونِ مستقلی برای زندهبودنِ نمونه محسوب نمیشود.
۴. کد کامل و مرجعِ مستقلِ نگهداری

ورودیِ عمومیِ آزمایش یک تاپلِ دقیقاً سهعضوی از boolهای دقیق و یک bool دقیق برای انتخابِ callback است. کنترلها پیش از ساختِ نمونه اعتبارسنجی میشوند؛ عددِ صفر بهجای False و زیرکلاسِ تاپل پذیرفته نمیشود. مرجعِ مستقل، هیچ weakref یا جمعآوریِ حافظهای اجرا نمیکند: کافی است یکی از سه نگهدارنده یا callback وابسته فعال باشد تا انتظارِ نگهداری درست باشد. تمامِ ۱۶ ترکیب با این جدولِ حقیقت مقایسه میشوند؛ این ۱۶ مورد یک خانوادهٔ آزموناند.
کد را در فایلِ مستقل اجرا کنید. اجرای ثبتشده با CPython 3.14.5 یکبار انجام شد: ۱۱ گروهِ معنادار موفق، کدِ خروج صفر و stderr خالی بود. نسخهٔ مستنداتِ خواندهشده 3.14.8 است و با نسخهٔ اجرا یکی معرفی نمیشود. خروجیِ کاملِ همان اجرا در ادامه آمده است؛ مقدارهای بولی دربارهٔ همین نمونههای محدودند. خروجی شاملِ شناسهٔ حافظه، اندازهٔ تخصیص یا نتیجهٔ بنچمارک نیست.
"""Finite CPython ownership observations, not memory size or cleanup guarantees."""
import gc
import itertools
import json
import platform
import weakref
class Sample:
def __init__(self):
self.label = "sample-A"
self.millivolts = (1200, 1300) # Synthetic payload, never real sensor data.
def record(self, events):
events.append(self.label)
return self.label
def record_label(events, label):
events.append(label)
return label
def finalize_sample(sample, events, bound):
if bound:
finalizer = weakref.finalize(sample, sample.record, events)
else:
finalizer = weakref.finalize(sample, record_label, events, sample.label)
finalizer.atexit = False
return finalizer
def observe_profile(holds, bound_callback):
"""Exact three flags: alias, strong dictionary, weak-cache lookup result."""
if type(holds) is not tuple:
raise TypeError("holds must be an exact tuple")
if len(holds) != 3:
raise ValueError("exactly three hold flags")
if any(type(x) is not bool for x in holds) or type(bound_callback) is not bool:
raise TypeError("all controls must be exact bool")
events = []
sample = Sample()
probe = weakref.ref(sample)
cache = weakref.WeakValueDictionary()
cache["sample"] = sample
finalizer = finalize_sample(sample, events, bound_callback)
owners = [sample if holds[0] else None,
{"sample": sample} if holds[1] else {},
cache.get("sample") if holds[2] else None]
try:
sample = None
gc.collect() # Actual local observation point, not a timing/RSS assertion.
result = {"holds": holds, "bound_callback": bound_callback,
"referent_alive": probe() is not None,
"weak_cache_has_value": "sample" in cache,
"finalizer_alive": finalizer.alive,
"events_before_cleanup": tuple(events)}
finally:
# Discard detach's returned tuple immediately; it would otherwise own obj.
finalizer.detach()
owners.clear()
sample = None
gc.collect()
result["owned_fixture_released"] = probe() is None and not cache and not finalizer.alive
return result
def lookup_witness():
events = []
sample = Sample()
probe = weakref.ref(sample)
cache = weakref.WeakValueDictionary({"sample": sample})
finalizer = finalize_sample(sample, events, False)
found = cache.get("sample")
sample = None
gc.collect()
held = probe() is found and finalizer.alive and "sample" in cache and events == []
found = None
gc.collect()
released = probe() is None and not cache and not finalizer.alive and events == ["sample-A"]
finalizer.detach()
return {"lookup_result_holds_referent": held, "after_lookup_release": released}
def returned_tuple_witness(method):
events = []
sample = Sample()
probe = weakref.ref(sample)
finalizer = finalize_sample(sample, events, False)
returned = finalizer.peek() if method == "peek" else finalizer.detach()
correct_identity = returned[0] is sample
sample = None
gc.collect()
held = probe() is not None and events == []
active_while_held = finalizer.alive
returned = None
gc.collect()
released = probe() is None and not finalizer.alive
finalizer.detach()
return {"tuple_contains_original_identity": correct_identity,
"tuple_keeps_referent_alive": held, "finalizer_alive_while_tuple_held": active_while_held,
"released_after_tuple_dropped": released, "events": tuple(events)}
def bound_method_witness():
events = []
sample = Sample()
probe = weakref.ref(sample)
finalizer = finalize_sample(sample, events, True)
bound_to_sample = finalizer.peek()[1].__self__ is sample
sample = None
gc.collect()
kept = probe() is not None and finalizer.alive and events == []
finalizer.detach()
gc.collect()
return {"registered_function_owns_self": bound_to_sample,
"still_alive_without_application_owner": kept,
"released_after_detach_result_discarded": probe() is None,
"cancelled_callback_not_invoked": events == []}
def call_once_witness():
events = []
sample = Sample()
probe = weakref.ref(sample)
finalizer = finalize_sample(sample, events, False)
first = finalizer()
dead_finalizer_live_object = not finalizer.alive and probe() is sample
second = finalizer()
no_state = finalizer.peek() is None and finalizer.detach() is None
sample = None
gc.collect()
return {"first_result": first, "second_result": second,
"dead_finalizer_with_live_object": dead_finalizer_live_object,
"peek_detach_after_dead_are_none": no_state,
"events": tuple(events), "object_later_released": probe() is None}
def rejects(kind, holds, bound=False):
try:
observe_profile(holds, bound)
except kind:
return True
return False
def run_example():
if platform.python_implementation() != "CPython":
raise RuntimeError("This observation profile is explicitly CPython-only")
settings_before = (gc.isenabled(), gc.get_debug())
table, oracle_ok = [], True
for holds in itertools.product((False, True), repeat=3):
for bound in (False, True):
observed = observe_profile(holds, bound)
# Independent root-union truth table, no weakref/GC/finalizer operation.
expected_retained = any(holds) or bound
expected_events = () if expected_retained else ("sample-A",)
oracle_ok &= (observed["referent_alive"] is expected_retained and
observed["weak_cache_has_value"] is expected_retained and
observed["finalizer_alive"] is expected_retained and
observed["events_before_cleanup"] == expected_events)
table.append(observed)
weak_only = table[0]
lookup = lookup_witness()
peek = returned_tuple_witness("peek")
detach = returned_tuple_witness("detach")
bound = bound_method_witness()
called = call_once_witness()
class TupleChild(tuple):
def __len__(self):
raise AssertionError("reject exact type before invoking hooks")
bad = [(TypeError, [], False), (TypeError, True, False),
(TypeError, TupleChild((False,) * 3), False),
(ValueError, (), False), (ValueError, (False,) * 2, False),
(ValueError, (False,) * 4, False), (TypeError, (0, False, False), False),
(TypeError, (False, 1, False), False), (TypeError, (False,) * 3, 0)]
unsupported = []
for value in ([], {}):
try:
weakref.ref(value)
except TypeError:
unsupported.append(True)
else:
unsupported.append(False)
checks = {
"independent_boolean_root_union_all16_profiles_one_family": bool(oracle_ok and len(table) == 16),
"weak_cache_alone_not_owner_and_safe_callback_observed_once": not weak_only["referent_alive"] and not weak_only["weak_cache_has_value"] and weak_only["events_before_cleanup"] == ("sample-A",),
"each_application_strong_root_retains_value": all(x["referent_alive"] for x in table if any(x["holds"])),
"weak_cache_lookup_result_itself_is_a_strong_owner": all(lookup.values()),
"bound_method_finalizer_retains_self_until_detached": all(bound.values()),
"peek_result_retains_object_while_finalizer_stays_alive": peek["tuple_contains_original_identity"] and peek["tuple_keeps_referent_alive"] and peek["finalizer_alive_while_tuple_held"] and peek["released_after_tuple_dropped"] and peek["events"] == ("sample-A",),
"detach_result_retains_object_even_with_dead_finalizer": detach["tuple_contains_original_identity"] and detach["tuple_keeps_referent_alive"] and not detach["finalizer_alive_while_tuple_held"] and detach["released_after_tuple_dropped"] and detach["events"] == (),
"explicit_finalizer_call_at_most_once_not_object_liveness": called["first_result"] == "sample-A" and called["second_result"] is None and called["dead_finalizer_with_live_object"] and called["peek_detach_after_dead_are_none"] and called["events"] == ("sample-A",) and called["object_later_released"],
"builtin_list_and_dict_do_not_directly_support_weakref": all(unsupported),
"strict_own_profile_controls_rejected_before_fixture": all(rejects(kind, holds, flag) for kind, holds, flag in bad),
"all_owned_profiles_released_no_GC_settings_changed": all(x["owned_fixture_released"] for x in table) and settings_before == (gc.isenabled(), gc.get_debug()),
}
assert all(type(value) is bool and value for value in checks.values()), checks
return {"runtime": {"python": platform.python_version(), "implementation": platform.python_implementation()},
"payload": {"origin": "synthetic only", "label": "sample-A", "millivolts": [1200, 1300]},
"root_truth_table": table, "lookup": lookup, "bound_method": bound,
"peek_tuple": peek, "detach_tuple": detach, "explicit_call": called,
"oracle_profiles_one_family": len(table), "meaningful_named_groups": len(checks), "checks": checks,
"scope": "Actual local CPython observations after explicit gc.collect; acyclic owned Sample fixtures. All finalizers atexitFalse, observed/cancelled before exit. No memory byte/RSS/GC latency/portable prompt finalization/resource cleanup/thread/cycle/interpreter-shutdown experiment."}
if __name__ == "__main__":
print(json.dumps(run_example(), ensure_ascii=True, indent=2))
{
"runtime": {
"python": "3.14.5",
"implementation": "CPython"
},
"payload": {
"origin": "synthetic only",
"label": "sample-A",
"millivolts": [
1200,
1300
]
},
"root_truth_table": [
{
"holds": [
false,
false,
false
],
"bound_callback": false,
"referent_alive": false,
"weak_cache_has_value": false,
"finalizer_alive": false,
"events_before_cleanup": [
"sample-A"
],
"owned_fixture_released": true
},
{
"holds": [
false,
false,
false
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
false,
false,
true
],
"bound_callback": false,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
false,
false,
true
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
false,
true,
false
],
"bound_callback": false,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
false,
true,
false
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
false,
true,
true
],
"bound_callback": false,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
false,
true,
true
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
false,
false
],
"bound_callback": false,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
false,
false
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
false,
true
],
"bound_callback": false,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
false,
true
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
true,
false
],
"bound_callback": false,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
true,
false
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
true,
true
],
"bound_callback": false,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
},
{
"holds": [
true,
true,
true
],
"bound_callback": true,
"referent_alive": true,
"weak_cache_has_value": true,
"finalizer_alive": true,
"events_before_cleanup": [],
"owned_fixture_released": true
}
],
"lookup": {
"lookup_result_holds_referent": true,
"after_lookup_release": true
},
"bound_method": {
"registered_function_owns_self": true,
"still_alive_without_application_owner": true,
"released_after_detach_result_discarded": true,
"cancelled_callback_not_invoked": true
},
"peek_tuple": {
"tuple_contains_original_identity": true,
"tuple_keeps_referent_alive": true,
"finalizer_alive_while_tuple_held": true,
"released_after_tuple_dropped": true,
"events": [
"sample-A"
]
},
"detach_tuple": {
"tuple_contains_original_identity": true,
"tuple_keeps_referent_alive": true,
"finalizer_alive_while_tuple_held": false,
"released_after_tuple_dropped": true,
"events": []
},
"explicit_call": {
"first_result": "sample-A",
"second_result": null,
"dead_finalizer_with_live_object": true,
"peek_detach_after_dead_are_none": true,
"events": [
"sample-A"
],
"object_later_released": true
},
"oracle_profiles_one_family": 16,
"meaningful_named_groups": 11,
"checks": {
"independent_boolean_root_union_all16_profiles_one_family": true,
"weak_cache_alone_not_owner_and_safe_callback_observed_once": true,
"each_application_strong_root_retains_value": true,
"weak_cache_lookup_result_itself_is_a_strong_owner": true,
"bound_method_finalizer_retains_self_until_detached": true,
"peek_result_retains_object_while_finalizer_stays_alive": true,
"detach_result_retains_object_even_with_dead_finalizer": true,
"explicit_finalizer_call_at_most_once_not_object_liveness": true,
"builtin_list_and_dict_do_not_directly_support_weakref": true,
"strict_own_profile_controls_rejected_before_fixture": true,
"all_owned_profiles_released_no_GC_settings_changed": true
},
"scope": "Actual local CPython observations after explicit gc.collect; acyclic owned Sample fixtures. All finalizers atexitFalse, observed/cancelled before exit. No memory byte/RSS/GC latency/portable prompt finalization/resource cleanup/thread/cycle/interpreter-shutdown experiment."
}
همهٔ finalizerها atexit=False دارند تا آزمایش به مرحلهٔ خروجِ مفسر واگذار نشود. بخشِ finally ثبت را جدا میکند، نگهدارندهها را کنار میگذارد و وضعیتِ رهاشدنِ نمونهٔ متعلق به آزمایش را میسنجد. تنظیمِ فعالبودن و پرچمِ اشکالزداییِ gc نیز تغییر نمیکند. مستنداتِ gc.collect تعدادِ اشیای جمعشده و جمعنشدنی را توضیح میدهد؛ کد عمداً این عدد را به بایت، کاهشِ RSS یا مدتِ آزادسازی تعبیر نمیکند.
۵. چهار پرسش دربارهٔ عمر، callback و مشاهده

چرا با پاککردنِ نامِ اصلی، مقدار هنوز در کش دیده میشود؟ یک مسیرِ قویِ دیگر کافی است: نامِ جایگزین، دیکشنری، نتیجهٔ ذخیرهشدهٔ get یا روشِ وابستهٔ callback. در جدول، ترکیبِ بدونِ این چهار عامل را با حالتی که فقط یکی فعال است مقایسه کنید. باقیماندنِ مقدار بهتنهایی ثابت نمیکند کشِ ضعیف آن را نگه داشته است.
چرا detach انجام شده ولی شیء هنوز زنده است؟ اگر تاپلِ بازگشتی را ذخیره کرده باشید، عضوِ نخستِ آن خودِ شیء است. شاهدِ detach دقیقاً همین وضعیت را میسازد: finalizer غیرفعال است، نمونه زنده است و callback اجرا نشده است. با کنارگذاشتنِ تاپل، آن نگهدارنده هم حذف میشود؛ این با صداکردنِ callback تفاوت دارد.
آیا alive=False یعنی حافظهٔ نمونه آزاد شده است؟ خیر. شاهدِ فراخوانیِ صریح، finalizer را غیرفعال میکند درحالیکه Sample هنوز از نامِ محلی قابلدسترسی است. فراخوانیِ دوم None میدهد و رخدادِ دیگری نمیافزاید. حتی پایانِ عمرِ شیء نیز در این آزمایش، شاهدِ بازگشتِ صفحههای حافظه به سیستمعامل نیست.
آیا این روش جای بستنِ صریحِ فایل یا اتصال را میگیرد؟ خیر؛ آزمایش هیچ منبعِ بیرونی ندارد. برای چنین منابعی قراردادِ close و مدیریتِ صریحِ عمر را جدا طراحی کنید. کاربردِ این تمرین در عیبیابی آن است که پیش از تغییرِ الگوریتم یا تفسیرِ نمودارِ حافظه، نگهدارندهٔ ناخواسته را با شاهدی کوچک و قابلِ تکرار پیدا کنیم؛ تضمینِ زمانبندیِ جمعآوری از آن به دست نمیآید.
منابع و مطالعهٔ بیشتر
- Python3.14.8؛ weakref و قراردادِ finalize
- Python3.14.8؛ جمعآوریِ صریح و محدودیتهای آن
- Python3.14.8؛ عمر، ارجاع و مدلِ داده
بررسی منابع: ۲۸ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

