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

نامهایی مانند Voltage و Current در این مثال، برچسب خانوادههای پردازشاند. عددِ نگهداریشده در فیلد value را ولت یا آمپر تفسیر نمیکنیم. این تفکیک برای کار مهندسی ضروری است: انتخاب تابعِ مناسب بر اساس کلاس، بهتنهایی درستبودن واحد، منشأ داده یا دامنهٔ فیزیکی آن را ثابت نمیکند. اگر چنین تضمینی لازم باشد، قرارداد مستقل و شاهد متناسب میخواهد.
تابع تألیفیِ route_samples دو ورودیِ ازپیشساخته میگیرد: نام کلاسهایی که پردازشگر اختصاصی دارند، و مشخصات نمونهها. هر مشخصه شامل نام کلاس، شمارش صحیح از منفی چهار تا مثبت چهار، و نوع یک آرگومان کمکی است. حداکثر هشت ثبت و هشت نمونه میپذیریم. این سقفها سیاست آموزشی خودماناند؛ محدودیت رسمی پایتون یا مشخصات دستگاه نیستند.
پیش از ساختِ هر نمونه و پردازشگر، تمام درخواست بررسی میشود. نام ناشناخته، ثبت تکراری، مقدار منطقی به جای عدد صحیح و ردیف نامعتبرِ انتهایی رد میشوند. این کار، بحثِ انتخابِ تابع را از پذیرش داده جدا نگه میدارد. برای مقایسه با مسئلهای متفاوت، مقالهٔ اعتبارسنجیِ زمان اجرای دادهٔ حسگر با dataclass مسئولیت سازندهٔ رکورد را بررسی میکند؛ اینجا محور درس، حلِ مسیر پردازشگر است.
۲. فقط نوعِ نخستین آرگومان، مسیر را انتخاب میکند

مطابق تعریف رسمی singledispatch، گزینش بر پایهٔ نوعِ نخستین آرگومان انجام میشود. تابع پایه، مسیر پیشفرضِ object را فراهم میکند و ثبت صریحِ register(cls, handler) پردازشگر کلاس مشخص را اضافه میکند. در برنامهٔ ما هر فراخوانی، ثبتخانهٔ تازهای میسازد؛ بنابراین تنظیمات یک درخواست قرار نیست به درخواست بعدی نشت کند.
آرگومان کمکی را یک بار رشته و بار دیگر عدد صحیح میگذاریم. پردازشگرِ انتخابشده این تفاوت را در خروجی گزارش میکند، ولی همان تفاوت، انتخاب را عوض نمیکند. این سازوکار، گزینش همزمان بر چند آرگومان نیست. همچنین نوشتن راهنمای نوع کنار پارامترهای یک تابع، نباید با اجرای اعتبارسنجیِ همهٔ ورودیها اشتباه گرفته شود؛ در این مثال، خودمان ثبت صریح و مرز ورودی روشن داریم.
دو ابزار بازبینی هم مفیدند: dispatch(cls) تابعِ انتخابی را برای یک کلاس نشان میدهد، و registry نمای فقطخواندنیِ ثبتهاست. پرسیدنِ مسیر، با اجرای پردازشگر برابر نیست. کد تعدادِ ورود به پردازشگرها را ثبت میکند تا این تفاوت دیده شود؛ نه اینکه از مشاهدهٔ ثبتخانه، موفقیتِ بدنهٔ دلخواه را نتیجه بگیریم.
۳. تبارِ کلاس و ترتیبِ پایهها را دقیق بخوانیم

وقتی نوع دقیق ثبت نشده باشد، جستوجوی پردازشگر در تبار کلاس ادامه پیدا میکند. اگر Sample و Voltage ثبت شده باشند، نمونهٔ FilteredVoltage به پردازشگر نزدیکترِ Voltage میرسد. افزودن ثبت اختصاصی برای خودِ زیرکلاس، همان مسیر نزدیکتر را جایگزین میکند. ترتیبِ زمانیِ ثبتِ دو کلاس، جای ترتیبِ تبارِ اسمی را نمیگیرد.
دو کلاس تشخیصی، پایههای یکسان را با ترتیب مخالف اعلام میکنند: یکی ابتدا Voltage و سپس Tagged، و دیگری برعکس. وقتی هر دو پایه ثبت شدهاند، این دو نمونه به دو پردازشگر متفاوت میرسند. این شاهد دربارهٔ ترتیبِ ارثبری است؛ معنایش قابلجایگزینیبودنِ دو کمیت فیزیکی نیست. برای نمونهٔ اصلی، انتظار دستی چنین است: زیرکلاس پالایششده و کلاسِ نخست به مسیر ولتاژ، کلاسِ معکوس به مسیر برچسب، و جریان به مسیر عمومی نمونه میروند.
کلاس Lookalike نیز فیلد همنام و مقدار مشابه دارد، اما از این خانواده ارث نمیبرد؛ بدون ثبت مستقل به مسیر پیشفرض میرسد. این پرسش با مرزِ Protocol در زمان اجرا فرق دارد: حضورِ ساختاریِ عضو، انتخابِ پردازشگر در یک ثبتخانهٔ اسمی نیست. کلاسهای پایهٔ انتزاعیِ مجازی و تبارهای سفارشی را وارد این آزمایش نمیکنیم.
۴. کدِ کامل را با جدولِ تبارِ مستقل بسنجیم

فایل را با نام singledispatch_mro_resolution.py ذخیره کنید. کد فقط به کتابخانهٔ استاندارد نیاز دارد و خروجی کاملِ JSON تولید میکند. مسیر واقعی از خودِ functools.singledispatch استفاده میکند؛ انتظار مستقل، یک جدول صریحِ تبار برای همین هشت کلاس است. این جدول نه تابعِ گزینش را صدا میزند، نه تبارِ واقعیِ شیء را میخواند و نه ادعا دارد الگوریتم عمومیِ ترتیب کلاسها را دوباره پیاده کرده است.
"""Fixed nominal telemetry fixtures; dispatch chooses a handler, not valid units."""
from dataclasses import dataclass, FrozenInstanceError
from functools import singledispatch
from itertools import combinations
import json
import platform
import sys
CLASS_CODES = ('sample', 'voltage', 'current', 'tagged', 'filtered_voltage',
'voltage_first', 'tagged_first', 'lookalike')
MAX_ITEMS = 8
LOW_COUNT, HIGH_COUNT = -4, 4
class Sample:
__slots__ = ('value',)
def __init__(self, value):
self.value = value
class Voltage(Sample):
__slots__ = ()
class Current(Sample):
__slots__ = ()
class Tagged(Sample):
__slots__ = ()
class FilteredVoltage(Voltage):
__slots__ = ()
class VoltageFirst(Voltage, Tagged):
__slots__ = ()
class TaggedFirst(Tagged, Voltage):
__slots__ = ()
class Lookalike:
__slots__ = ('value',)
def __init__(self, value):
self.value = value
CLASSES = dict(zip(CLASS_CODES, (Sample, Voltage, Current, Tagged,
FilteredVoltage, VoltageFirst, TaggedFirst,
Lookalike)))
@dataclass(frozen=True)
class RouteReport:
# Rows: input class code, chosen handler tag, raw count, auxiliary type.
rows: tuple
callback_trace: tuple
registry: tuple
lookups: tuple
def _validate_request(registrations, sample_specs):
if type(registrations) is not tuple or type(sample_specs) is not tuple:
raise TypeError('registrations and sample_specs must be exact tuples')
if len(registrations) > MAX_ITEMS or len(sample_specs) > MAX_ITEMS:
raise ValueError('authored request size exceeds eight')
seen = set()
for code in registrations:
if type(code) is not str:
raise TypeError('registration code must be an exact str')
if code not in CLASSES:
raise ValueError('unknown registration code')
if code in seen:
raise ValueError('duplicate registration code')
seen.add(code)
for spec in sample_specs:
if type(spec) is not tuple:
raise TypeError('sample specification must be an exact tuple')
if len(spec) != 3:
raise ValueError('sample specification needs three fields')
code, count, auxiliary_kind = spec
if type(code) is not str:
raise TypeError('sample class code must be an exact str')
if code not in CLASSES:
raise ValueError('unknown sample class code')
if type(count) is not int or type(auxiliary_kind) is not int:
raise TypeError('count and auxiliary kind must be exact ints')
if not LOW_COUNT <= count <= HIGH_COUNT:
raise ValueError('raw count outside the authored interval')
if auxiliary_kind not in (0, 1):
raise ValueError('auxiliary kind must be zero or one')
def _make_dispatcher(registrations):
trace = []
def route_object(sample, auxiliary):
kind = type(auxiliary).__name__
trace.append(('object', sample.value, kind))
return 'object', sample.value, kind
generic = singledispatch(route_object)
def make_handler(tag):
def handler(sample, auxiliary):
kind = type(auxiliary).__name__
trace.append((tag, sample.value, kind))
return tag, sample.value, kind
handler.__name__ = 'route_' + tag
return handler
for code in registrations:
generic.register(CLASSES[code], make_handler(code))
return generic, trace
def _construct_sample(code, count):
return CLASSES[code](count)
def route_samples(registrations, sample_specs):
"""Only eight declared ordinary classes and already materialized fixtures."""
# No dispatcher, sample or formatter is constructed before whole admission.
_validate_request(registrations, sample_specs)
generic, trace = _make_dispatcher(registrations)
rows = []
for code, count, auxiliary_kind in sample_specs:
sample = _construct_sample(code, count)
auxiliary = 'diagnostic' if auxiliary_kind == 0 else 7
tag, preserved, kind = generic(sample, auxiliary)
rows.append((code, tag, preserved, kind))
names = {object: 'object', **{cls: code for code, cls in CLASSES.items()}}
registry = tuple(sorted(names[cls] for cls in generic.registry))
lookups = tuple((code, generic.dispatch(CLASSES[code]).__name__[6:])
for code in CLASS_CODES)
return RouteReport(tuple(rows), tuple(trace), registry, lookups)
# Independently declared nominal precedence, confined to this fixed universe.
# No actual __mro__, functools or production dispatch helper in expected path.
DECLARED_ANCESTRY = {
'sample': ('sample',),
'voltage': ('voltage', 'sample'),
'current': ('current', 'sample'),
'tagged': ('tagged', 'sample'),
'filtered_voltage': ('filtered_voltage', 'voltage', 'sample'),
'voltage_first': ('voltage_first', 'voltage', 'tagged', 'sample'),
'tagged_first': ('tagged_first', 'tagged', 'voltage', 'sample'),
'lookalike': ('lookalike',),
}
def declared_reference(registrations, sample_specs):
registered = frozenset(registrations)
chosen = {}
for code, ancestors in DECLARED_ANCESTRY.items():
chosen[code] = next((name for name in ancestors if name in registered),
'object')
rows = tuple((code, chosen[code], count, ('str', 'int')[kind])
for code, count, kind in sample_specs)
return RouteReport(rows, tuple(row[1:] for row in rows),
tuple(sorted(('object',) + registrations)),
tuple((code, chosen[code]) for code in CLASS_CODES))
def rejects(call, error_type):
try:
call()
except error_type:
return True
return False
def invalid_admission_observations():
class StrChild(str):
pass
class IntChild(int):
pass
class TupleChild(tuple):
pass
consumed = []
def lazy_specs():
consumed.append(True)
yield ('sample', 0, 0)
lazy = lazy_specs()
valid = ('voltage', 0, 0)
cases = [
([], (), TypeError), ((), [], TypeError),
(TupleChild(), (), TypeError), ((), TupleChild(), TypeError),
((True,), (), TypeError), ((StrChild('sample'),), (), TypeError),
(('unknown',), (), ValueError),
(('sample', 'sample'), (), ValueError),
(CLASS_CODES + ('sample',), (), ValueError),
((), (valid,) * 9, ValueError),
((), (list(valid),), TypeError),
((), (TupleChild(valid),), TypeError),
((), (('sample', 0),), ValueError),
((), (('sample', 0, 0, 0),), ValueError),
((), ((False, 0, 0),), TypeError),
((), ((StrChild('sample'), 0, 0),), TypeError),
((), (('unknown', 0, 0),), ValueError),
((), (('sample', True, 0),), TypeError),
((), (('sample', IntChild(0), 0),), TypeError),
((), (('sample', 0.0, 0),), TypeError),
((), (('sample', -5, 0),), ValueError),
((), (('sample', 5, 0),), ValueError),
((), (('sample', 0, False),), TypeError),
((), (('sample', 0, IntChild(0)),), TypeError),
((), (('sample', 0, -1),), ValueError),
((), (('sample', 0, 2),), ValueError),
((), (valid, ('sample', 0, 2)), ValueError),
(('voltage', 'missing'), (valid,), ValueError),
((), lazy, TypeError),
]
old_dispatcher, old_constructor = _make_dispatcher, _construct_sample
events = []
def forbidden(*args):
events.append('construction')
raise AssertionError('whole invalid request reached construction')
try:
globals()['_make_dispatcher'] = forbidden
globals()['_construct_sample'] = forbidden
passed = all(rejects(lambda r=r, s=s: route_samples(r, s), error)
for r, s, error in cases)
finally:
globals()['_make_dispatcher'] = old_dispatcher
globals()['_construct_sample'] = old_constructor
lazy.close()
return {'passed': passed and not events and not consumed,
'invalid_profiles': len(cases), 'construction_events': len(events),
'generator_consumed': bool(consumed)}
def run_example():
registrations = ('sample', 'voltage', 'tagged')
specs = (('filtered_voltage', -4, 0), ('voltage_first', 0, 1),
('tagged_first', 4, 0), ('current', 2, 1),
('lookalike', -1, 0))
manual = route_samples(registrations, specs)
manual_rows = (
('filtered_voltage', 'voltage', -4, 'str'),
('voltage_first', 'voltage', 0, 'int'),
('tagged_first', 'tagged', 4, 'str'),
('current', 'sample', 2, 'int'),
('lookalike', 'object', -1, 'str'))
all_specs = tuple((code, 0, 0) for code in CLASS_CODES)
fallback = route_samples((), all_specs)
inherited = route_samples(('sample', 'voltage'),
(('filtered_voltage', 1, 0),))
exact = route_samples(('sample', 'voltage', 'filtered_voltage'),
(('filtered_voltage', 1, 0),))
opposite = route_samples(('voltage', 'tagged'),
(('voltage_first', 0, 0),
('tagged_first', 0, 0)))
forward = route_samples(registrations, all_specs)
reverse = route_samples(tuple(reversed(registrations)), all_specs)
auxiliary = route_samples(('voltage',),
(('voltage', 3, 0), ('voltage', 3, 1)))
lookup_generic, lookup_trace = _make_dispatcher(('sample', 'voltage'))
lookup_names = tuple(lookup_generic.dispatch(CLASSES[code]).__name__
for code in ('current', 'filtered_voltage'))
registry_before = tuple(lookup_generic.registry)
readonly = rejects(lambda: lookup_generic.registry.__setitem__(
Lookalike, lambda sample, auxiliary: None), AttributeError)
# Assignment syntax exercises mappingproxy's mutation refusal directly.
try:
lookup_generic.registry[Lookalike] = lambda sample, auxiliary: None
direct_assignment_refused = False
except TypeError:
direct_assignment_refused = True
repeats = route_samples(('sample',),
(('current', 2, 1), ('sample', -2, 0),
('current', 2, 1)))
family_ok, profiles, queries = True, 0, 0
for size in range(len(CLASS_CODES) + 1):
for chosen in combinations(CLASS_CODES, size):
profiles += 1
for count in (-4, 0, 4):
for kind in (0, 1):
batch = tuple((code, count, kind) for code in CLASS_CODES)
actual = route_samples(chosen, batch)
expected = declared_reference(chosen, batch)
family_ok = family_ok and actual == expected
queries += len(batch)
admission = invalid_admission_observations()
boundary = route_samples(CLASS_CODES,
tuple((code, (-4, 4)[i % 2], i % 2)
for i, code in enumerate(CLASS_CODES)))
before = route_samples(('sample',), all_specs)
changed = route_samples(('tagged',), all_specs)
again = route_samples(('sample',), all_specs)
frozen = rejects(lambda: setattr(before, 'rows', ()), FrozenInstanceError)
checks = {
'manual_mixed_routes_match_five_literal_expectations': (
manual.rows == manual_rows
and manual.callback_trace == tuple(row[1:] for row in manual_rows)),
'no_registered_ancestor_uses_object_fallback_for_all_eight_classes': (
all(row[1] == 'object' for row in fallback.rows)),
'nearest_registered_nominal_ancestor_beats_more_distant_sample': (
inherited.rows == (('filtered_voltage', 'voltage', 1, 'str'),)),
'exact_registered_subclass_overrides_inherited_handler': (
exact.rows == (('filtered_voltage', 'filtered_voltage', 1, 'str'),)),
'opposite_declared_base_orders_choose_different_registered_handlers': (
tuple(row[1] for row in opposite.rows) == ('voltage', 'tagged')),
'registration_order_is_not_nominal_ancestor_precedence': forward == reverse,
'second_argument_type_changes_payload_metadata_but_not_selected_handler': (
auxiliary.rows == (('voltage', 'voltage', 3, 'str'),
('voltage', 'voltage', 3, 'int'))),
'same_value_attribute_on_unrelated_class_does_not_imply_registration': (
manual.rows[-1][1] == 'object' and Lookalike(-1).value == -1),
'dispatch_lookup_and_readonly_registry_do_not_run_formatters': (
lookup_names == ('route_sample', 'route_voltage') and not lookup_trace
and readonly and direct_assignment_refused
and tuple(lookup_generic.registry) == registry_before),
'input_order_duplicates_counts_and_callback_order_are_preserved': (
repeats.rows == (('current', 'sample', 2, 'int'),
('sample', 'sample', -2, 'str'),
('current', 'sample', 2, 'int'))
and repeats.callback_trace == tuple(row[1:] for row in repeats.rows)),
'all_fixed_universe_registration_subsets_match_declared_table_ONE_family': (
family_ok and profiles == 256 and queries == 12288),
'whole_late_invalid_requests_rejected_before_construction_or_callbacks': (
admission['passed']),
'authored_caps_inclusive_boundaries_and_empty_batch_are_supported': (
len(boundary.rows) == 8 and len(boundary.registry) == 9
and all(row[0] == row[1] for row in boundary.rows)
and route_samples((), ()).rows == ()),
'reports_are_frozen_and_interleaved_registrations_do_not_leak': (
before == again and before is not again and before != changed
and frozen and type(before.rows) is tuple
and all(type(row) is tuple for row in before.rows)),
}
assert len(checks) == 14 and all(type(v) is bool and v for v in checks.values()), checks
return {
'python': platform.python_version(),
'implementation': platform.python_implementation(),
'mechanism': 'actual_functools_singledispatch_explicit_nominal_registration',
'scope': 'eight authored ordinary classes; raw counts, not calibrated units',
'class_codes': CLASS_CODES,
'manual_rows': manual.rows,
'manual_callback_trace': manual.callback_trace,
'manual_registry': manual.registry,
'manual_dispatch_lookups': manual.lookups,
'lookup_observation': {'callbacks': len(lookup_trace),
'registry_assignment_refused': direct_assignment_refused},
'independent_reference': 'literal declared ancestry table; no actual mro or dispatch',
'case_counts_not_extra_groups': {'registration_subsets': profiles,
'sample_queries': queries,
'finite_family_groups': 1},
'invalid_admission': admission,
'authored_limits': {'registrations': MAX_ITEMS, 'samples': MAX_ITEMS,
'raw_count_closed_interval': [LOW_COUNT, HIGH_COUNT]},
'ABC_or_static_typechecker_or_device_or_network_executed': False,
'checks': checks,
}
if __name__ == '__main__':
sys.stdout.reconfigure(encoding='utf-8', newline='\n')
print(json.dumps(run_example(), ensure_ascii=True, indent=2, allow_nan=False))
چهارده گروهِ نامدار، شاهدهای دستی، مسیر پیشفرض، ثبت دقیق، ترتیب مخالفِ پایهها، نوع آرگومان دوم، بازبینی بدون فراخوانی، حفظ ترتیب و تکرار ورودی، اعتبارسنجی کامل، سقفها و استقلال درخواستها را در بر میگیرند. خانوادهٔ متناهی، تمام ۲۵۶ زیرمجموعهٔ ثبت را برای هشت کلاس، سه شمارش و دو نوع آرگومان کمکی بررسی میکند: ۱۲۲۸۸ پرسوجو، همگی در یک گروه. تعدادِ حالتها را به تعدادِ گروههای آزمون تبدیل نمیکنیم.
{
"python": "3.14.5",
"implementation": "CPython",
"mechanism": "actual_functools_singledispatch_explicit_nominal_registration",
"scope": "eight authored ordinary classes; raw counts, not calibrated units",
"class_codes": [
"sample",
"voltage",
"current",
"tagged",
"filtered_voltage",
"voltage_first",
"tagged_first",
"lookalike"
],
"manual_rows": [
[
"filtered_voltage",
"voltage",
-4,
"str"
],
[
"voltage_first",
"voltage",
0,
"int"
],
[
"tagged_first",
"tagged",
4,
"str"
],
[
"current",
"sample",
2,
"int"
],
[
"lookalike",
"object",
-1,
"str"
]
],
"manual_callback_trace": [
[
"voltage",
-4,
"str"
],
[
"voltage",
0,
"int"
],
[
"tagged",
4,
"str"
],
[
"sample",
2,
"int"
],
[
"object",
-1,
"str"
]
],
"manual_registry": [
"object",
"sample",
"tagged",
"voltage"
],
"manual_dispatch_lookups": [
[
"sample",
"sample"
],
[
"voltage",
"voltage"
],
[
"current",
"sample"
],
[
"tagged",
"tagged"
],
[
"filtered_voltage",
"voltage"
],
[
"voltage_first",
"voltage"
],
[
"tagged_first",
"tagged"
],
[
"lookalike",
"object"
]
],
"lookup_observation": {
"callbacks": 0,
"registry_assignment_refused": true
},
"independent_reference": "literal declared ancestry table; no actual mro or dispatch",
"case_counts_not_extra_groups": {
"registration_subsets": 256,
"sample_queries": 12288,
"finite_family_groups": 1
},
"invalid_admission": {
"passed": true,
"invalid_profiles": 29,
"construction_events": 0,
"generator_consumed": false
},
"authored_limits": {
"registrations": 8,
"samples": 8,
"raw_count_closed_interval": [
-4,
4
]
},
"ABC_or_static_typechecker_or_device_or_network_executed": false,
"checks": {
"manual_mixed_routes_match_five_literal_expectations": true,
"no_registered_ancestor_uses_object_fallback_for_all_eight_classes": true,
"nearest_registered_nominal_ancestor_beats_more_distant_sample": true,
"exact_registered_subclass_overrides_inherited_handler": true,
"opposite_declared_base_orders_choose_different_registered_handlers": true,
"registration_order_is_not_nominal_ancestor_precedence": true,
"second_argument_type_changes_payload_metadata_but_not_selected_handler": true,
"same_value_attribute_on_unrelated_class_does_not_imply_registration": true,
"dispatch_lookup_and_readonly_registry_do_not_run_formatters": true,
"input_order_duplicates_counts_and_callback_order_are_preserved": true,
"all_fixed_universe_registration_subsets_match_declared_table_ONE_family": true,
"whole_late_invalid_requests_rejected_before_construction_or_callbacks": true,
"authored_caps_inclusive_boundaries_and_empty_batch_are_supported": true,
"reports_are_frozen_and_interleaved_registrations_do_not_leak": true
}
}
در جدول دستی، هر ردیف هم نام کلاس ورودی و هم برچسب پردازشگر را دارد؛ شمارش خام نیز بدون تبدیل حفظ میشود. ردّ فراخوانیها باید با همان ترتیب ردیفها سازگار باشد. موفقیت این بررسیها شاهدِ همین جهان کوچک است، نه اثباتِ همهٔ رفتارهای پایتون. مستندات خواندهشده نسخهٔ ۳٫۱۴٫۹ و نسخهٔ اجرای این برنامه CPython ۳٫۱۴٫۵ است؛ زمان خواندنِ منبع و نسخهٔ اجرا را یکی نمیگیریم.
۵. چهار پرسش دربارهٔ ثبتِ نوع و مرزِ نتیجه

پرسش اول: چرا زیرکلاس پالایششده، مسیر عمومی نمونه را انتخاب نمیکند؟ پاسخ: در ثبتِ شاهد، پردازشگرِ نزدیکتر برای Voltage وجود دارد. اگر ثبتِ آن حذف شود، جستوجو به Sample میرسد؛ اگر خودِ زیرکلاس ثبت شود، پردازشگر دقیق مقدم میشود. پس پیش از تغییر کد، نوعِ واقعی و ثبتهای موجود را جدا بررسی میکنیم.
پرسش دوم: آیا تغییر آرگومان کمکی از متن به عدد، مسیر دیگری میسازد؟ پاسخ: خیر؛ انتخاب فقط به نوعِ نخستین آرگومان وابسته است. پردازشگر همچنان میتواند آرگومان دوم را بخواند یا طبق قراردادی جدا رد کند، اما این با تصمیمِ خودکارِ ثبتخانه بر چند نوع تفاوت دارد. مثالِ حاضر فقط حفظ نوعِ کمکی در گزارش را نشان میدهد.
پرسش سوم: چرا شیءِ همشکل به مسیر پیشفرض میرود؟ پاسخ: داشتن فیلدِ value، نسبتِ ارثبری یا ثبت صریح ایجاد نمیکند. میتوان برای همان کلاس، پردازشگرِ جدا ثبت کرد؛ اما این تصمیم همچنان گواهِ اعتبارِ مقدار، واحد یا رفتارِ تمامِ روشهای آن نیست. معیارِ انتخاب را با معیارِ پذیرش داده جایگزین نمیکنیم.
پرسش چهارم: آیا جدول مستقل، ابهامِ کلاسهای انتزاعی را هم حل کرده است؟ پاسخ: خیر؛ جدول فقط تبارهای عادیِ اعلامشدهٔ این آزمایش را پوشش میدهد. PEP443 بحثِ گستردهتری دربارهٔ کلاسهای انتزاعی دارد. همچنین ردِ ثبت مبتنی بر راهنمای نوع در متنِ مهِ ۲۰۱۳، با افزودهشدنِ پشتیبانیِ آن در نسخهٔ ۳٫۷ که مرجع جاری ذکر میکند، دو مقطع تاریخیاند. این دو سندِ رسمی از یک تولیدکنندهاند؛ برای ادعاهای فراتر از این دامنه، شاهد مستقلِ متناسب لازم است.
منابع و مطالعهٔ بیشتر
- Python3.14.9؛ تعریف کامل singledispatch، ثبت صریح و تبار کلاس
- PEP443؛ طراحی توابع عمومی با گزینش بر نخستین آرگومان
بررسی منابع: ۳۰ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

