OverflowError در پایتون وقتی پرتاب می‌شود که یک عملیات ریاضی نتیجه‌ای بزرگ‌تر از محدودهٔ قابل‌نمایش در نوع داده‌ای فعلی تولید کند؛ مثلاً math.exp(1000) یا تبدیل یک عدد غول‌آسا به float. برخلاف زبان‌های C و Java، در پایتون ۳ عدد صحیح int ذاتاً نامحدود است و سرریز نمی‌کند، ولی همین سادگی باعث می‌شود توسعه‌دهندگان به‌اشتباه فرض کنند هیچ نوع داده‌ای سرریز نمی‌شود و در کد عددی و مالی، غافلگیر شوند. در این مقاله، تجربه‌ام از ده‌ها پروندهٔ واقعی این خطا را در پروژه‌های عددی، مالی و یادگیری ماشین با شما به اشتراک می‌گذارم.

OverflowError چیست و از کجا می‌آید؟

OverflowError در پایتون استثنایی است که وقتی پرتاب می‌شود که نتیجهٔ یک عملیات ریاضی، از محدودهٔ قابل‌نمایش در نوع دادهٔ فعلی بیرون بزند. این خطا از ریشهٔ ArithmeticError ارث می‌برد و در کنار ZeroDivisionError و FloatingPointError، خانوادهٔ خطاهای عددی را تشکیل می‌دهد.

ساختار ارث‌بری آن به این شکل است:

BaseException
 └── Exception
      └── ArithmeticError
           ├── OverflowError
           ├── ZeroDivisionError
           └── FloatingPointError

از منظر تاریخی، OverflowError یادگار دوران پایتون ۲ است؛ زمانی که اعداد صحیح پایتون دقیقاً مانند C و Java محدود به ۶۴ بیت بودند و از محدودهٔ مجاز خارج می‌شدند. در پایتون ۳، این محدودیت برای int برداشته شد ولی OverflowError برای انواع دیگر داده‌های عددی باقی ماند. مفهوم کلی «سرریز عددی» در علوم کامپیوتر به‌عنوان «Integer Overflow» شناخته می‌شود و در ویکی‌پدیا ذیل Integer overflow توضیح داده شده است.

یک نکتهٔ ظریف: OverflowError و FloatingPointError با هم متفاوتند. اولی وقتی رخ می‌دهد که نتیجهٔ عملیات از بازهٔ قابل‌نمایش بیرون بزند (مثلاً math.exp(1000)). دومی وقتی رخ می‌دهد که عملیات با یک نتیجهٔ غیرقابل‌تعریف مواجه شود (مثلاً 0.0 / 0.0) و به‌طور پیش‌فرض در پایتون خاموش است. برای درک چارچوب گسترده‌تر، مدیریت خطا در پایتون و آموزش پایتون از صفر را پیشنهاد می‌کنم.

OverflowError خطای «عدد بزرگ» نیست؛ خطای «عددی که در قالب فعلی جا نمی‌شود» است. تشخیص این تفاوت، کلید معماری درست کد عددی است.

در سطح فنی، این استثنا از لایهٔ C در مفسر CPython می‌آید. وقتی یک عملیات C-level (مثل PyLong_AsLong) نتیجه‌ای خارج از بازهٔ ۶۴ بیتی تولید کند، مفسر یک OverflowError با پیام توصیفی پرتاب می‌کند. پیام‌های رایجی که در این حالت دیده می‌شود عبارتند از Python int too large to convert to C long و math range error و int too large to convert to float.

چرا int در پایتون ۳ سرریز نمی‌کند؟

این یکی از مهم‌ترین پرسش‌هایی است که در جلسات مشاوره بارها شنیده‌ام. پاسخ در طراحی داخلی PyLongObject نهفته است: در پایتون ۳، اعداد صحیح به‌صورت arbitrary-precision ذخیره می‌شوند. یعنی هر عدد صحیح، به‌جای یک بازهٔ ثابت، به‌اندازهٔ لازم حافظه می‌گیرد.

>>> 2 ** 100
1267650600228229401496703205376
>>> 2 ** 10000  # 3011 رقم اعشاری
... (عدد بسیار بزرگ چاپ می‌شود)
>>> import sys
>>> sys.getsizeof(2 ** 100)
40
>>> sys.getsizeof(2 ** 10000)
1376

این طراحی، پایتون را از بسیاری از باگ‌های سرریز عددی در زبان‌های سطح پایین نجات می‌دهد. در C، عدد 2 ** 63 روی long long باعث wrap-around می‌شود و نتیجه به عدد منفی تبدیل می‌شود — یک فاجعهٔ امنیتی که در ادبیات امنیت نرم‌افزار با نام‌های CWE-190 و CWE-191 شناخته می‌شود.

اما این ویژگی، دو هزینهٔ پنهان دارد که در کد عددی مهم است:

  • هزینهٔ حافظه: یک int بزرگ‌تر از ۶۴ بیت، به‌اندازهٔ چندین کلمهٔ حافظه ذخیره می‌شود. در برنامه‌هایی که میلیون‌ها عدد صحیح بزرگ را در لیست نگه می‌دارند، این مصرف حافظه می‌تواند به‌سرعت به MemoryError منجر شود.
  • هزینهٔ پردازش: عملیات روی اعداد بزرگ، از نظر تعداد CPU cycle گران‌تر است. برای محاسبات عددی سنگین، استفاده از int پایتون به‌جای numpy.int64 می‌تواند ده‌ها برابر کندتر باشد.

به همین دلیل، در پروژه‌های یادگیری ماشین و محاسبات علمی، معمولاً از کتابخانه‌هایی مثل numpy استفاده می‌شود که اعداد را در بازه‌های ثابت (۶۴ بیت یا کمتر) نگه می‌دارند. اما همین ویژگی، آن‌ها را به OverflowError و سرریز خاموش حساس می‌کند.

import numpy as np

# این عملیات در numpy باعث سرریز خاموش می‌شود
a = np.int64(2 ** 62)
print(a * 4)  # نتیجه منفی می‌شود، بدون خطا

# در پایتون خالص
b = 2 ** 62
print(b * 4)  # عدد صحیح بزرگ، درست

این تفاوت، یکی از مهم‌ترین دلایلی است که در پروژه‌های عددی باید هوشیار باشید. کد شما در پایتون خالص درست کار می‌کند ولی به‌محض استفاده از numpy یا pandas، منطق سرریز کاملاً متفاوت می‌شود.

کجا پایتون واقعاً سرریز می‌کند؟

پس از تأیید این‌که int در پایتون ۳ سرریز نمی‌کند، سؤال درست این است: «پس OverflowError کجا رخ می‌دهد؟». در عمل، این خطا در پنج بستر مشخص پایتون ظاهر می‌شود:

بستر اول: تبدیل int به float

هر int در پایتون، حتی اگر arbitrary-precision باشد، وقتی به float تبدیل شود، باید در قالب IEEE 754 جا بگیرد. این قالب، بازهٔ محدودی دارد (حداکثر حدود 1.8e308) و اعداد بزرگ‌تر، باعث OverflowError می‌شوند:

>>> float(10 ** 400)
Traceback (most recent call last):
    ...
OverflowError: int too large to convert to float

این الگو در کدهای علمی و یادگیری ماشین بسیار رایج است، چون بسیاری از کتابخانه‌ها ورودی‌های خود را به float64 تبدیل می‌کنند.

بستر دوم: توابع math

تابع‌های کتابخانهٔ math مثل exp، pow، factorial و lgamma، وقتی نتیجه از بازهٔ float بیرون بزند، OverflowError پرتاب می‌کنند:

>>> import math
>>> math.exp(1000)
Traceback (most recent call last):
    ...
OverflowError: math range error

>>> math.factorial(10 ** 10)  # روی float یا int با محدودیت
OverflowError: factorial() argument should not exceed 2147483647

پیام math range error یکی از پرتکرارترین پیام‌های OverflowError در کدهای علمی است.

بستر سوم: numpy و سرریز خاموش

کتابخانهٔ numpy به‌طور پیش‌فرض سرریز را به‌شکل wrap-around مدیریت می‌کند و پیام خطا نمی‌دهد — که این خودش از خود OverflowError خطرناک‌تر است. ولی در برخی توابع مثل np.exp، می‌توان با تنظیمات np.seterr رفتار را تغییر داد:

import numpy as np

np.seterr(over='raise')
try:
    np.exp(1000)
except FloatingPointError as e:
    print("overflow detected:", e)

بستر چهارم: Decimal و Fraction

کتابخانه‌های decimal و fractions با دقت بالاتر کار می‌کنند، ولی همچنان بازهٔ محدودی دارند. اگر تنظیمات پیش‌فرض را رد کنید، OverflowError یا decimal.Overflow می‌بینید:

from decimal import Decimal, getcontext, Overflow

getcontext().prec = 50
try:
    result = Decimal(10) ** Decimal(10 ** 6)
except Overflow:
    print("decimal overflow")

بستر پنجم: ctypes و C-extensions

هنگام اتصال به کد C با ctypes یا نوشتن extension، بازه‌های C اعمال می‌شوند. مثلاً c_int فقط ۳۲ بیت دارد و مقدار بزرگ‌تر باعث OverflowError می‌شود:

import ctypes

libc = ctypes.CDLL(None)
# اگر آرگومان بزرگ‌تر از بازه c_int باشد، خطا می‌دهد

این پنج بستر، تمام سناریوهایی هستند که OverflowError واقعاً در کد پایتون مدرن ظاهر می‌شود. تشخیص درست، بر اساس شناسایی این بسترها انجام می‌شود. برای تفکیک از خطاهای عددی مشابه، مقاله‌های خطای ValueError در پایتون و خطای TypeError در پایتون مرجع مکمل خوبی هستند.

ریشهٔ ریاضی: استاندارد IEEE 754

برای درک عمیق OverflowError، باید استاندارد IEEE 754 را بشناسید؛ چون همین استاندارد است که بازهٔ float در پایتون (و تقریباً هر زبان مدرن دیگر) را تعیین می‌کند. این استاندارد در ویکی‌پدیا ذیل IEEE 754 به‌تفصیل توضیح داده شده است.

در قالب binary64 که پایتون برای float استفاده می‌کند، هر عدد به‌صورت ۶۴ بیت ذخیره می‌شود: ۱ بیت علامت، ۱۱ بیت نمایندهٔ (exponent) و ۵۲ بیت مانتیس. بازهٔ قابل‌نمایش این قالب به این شکل است:

پارامترمقدار تقریبی
کوچک‌ترین عدد مثبت نرمال2.2 × 10^-308
بزرگ‌ترین عدد مثبت نرمال1.8 × 10^308
تعداد ارقام دقتحدود ۱۵ تا ۱۷ رقم اعشار
بازهٔ نمایندهٔ-1022 تا +1023

هر عدد بزرگ‌تر از 1.8 × 10^308 به‌عنوان «سرریز» در نظر گرفته می‌شود. در IEEE 754، این وضعیت به دو شکل مدیریت می‌شود: یا به بی‌نهایت (inf) تبدیل می‌شود، یا خطا گزارش می‌دهد. پایتون و numpy بسته به تنظیمات و عملیات، بین این دو رفتار سوئیچ می‌کنند:

import numpy as np

# در پایتون خالص: خطا
try:
    x = 10.0 ** 400
except OverflowError as e:
    print("python:", e)

# در numpy: بی‌نهایت
x = np.float64(10.0) ** 400
print("numpy:", x)  # inf
print("is inf:", np.isinf(x))  # True

این تفاوت رفتار، در پروژه‌های علمی و یادگیری ماشین بسیار مهم است؛ چون ورودی‌های نامعتبر می‌توانند در numpy به بی‌نهایت تبدیل شوند و سپس در لایه‌های بعدی، نتایج نامعقولی تولید کنند. یک قاعده‌ی مهم در پروژه‌های عددی: همیشه بعد از عملیات numpy، نتیجه را با np.isinf و np.isnan بررسی کنید.

در سطح پیاده‌سازی، پایتون این رفتار را از طریق بررسی‌های C-level مدیریت می‌کند. تابع PyOS_double_to_string و توابع مشابه در CPython، بازهٔ نتیجه را بررسی می‌کنند و در صورت خارج بودن، OverflowError پرتاب می‌کنند.

سناریوهای واقعی که این خطا را می‌سازند

در طول سال‌ها کار با پایتون، OverflowError را در این شش الگو دیده‌ام. شناختن هر الگو، تشخیص را چند برابر سریع‌تر می‌کند.

سناریوی اول: تابع سیگموئید بدون کلیپ

در یادگیری ماشین، تابع سیگموئید به‌صورت 1 / (1 + exp(-x)) محاسبه می‌شود. اگر x بسیار منفی باشد، exp(-x) سرریز می‌کند:

import math

def sigmoid_wrong(x):
    return 1 / (1 + math.exp(-x))

# خطا
sigmoid_wrong(-1000)
# OverflowError: math range error

راه‌حل درست، با پایداری عددی:

def sigmoid(x):
    if x >= 0:
        z = math.exp(-x)
        return 1 / (1 + z)
    else:
        z = math.exp(x)
        return z / (1 + z)

سناریوی دوم: تابع softmax بدون کاهش

تابع softmax نیز به همین مشکل حساس است. راه‌حل استاندارد، کاهش بیشینه:

import numpy as np

def softmax_wrong(x):
    return np.exp(x) / np.exp(x).sum()

def softmax(x):
    x = x - np.max(x)
    return np.exp(x) / np.exp(x).sum()

بدون کاهش بیشینه، حتی در numpy هم نتیجه به بی‌نهایت می‌رسد و نرمال‌سازی به NaN تبدیل می‌شود. این الگو در پروژه‌های یادگیری عمیق کلاسیک است.

سناریوی سوم: محاسبات سود مرکب

در کدهای مالی، محاسبهٔ سود مرکب برای بازه‌های طولانی، می‌تواند به سرریز منجر شود:

def compound_interest_wrong(principal, rate, years):
    return principal * (1 + rate) ** years  # ممکن است سرریز کند

# برای rate = 0.5 و years = 2000
compound_interest_wrong(1, 0.5, 2000)
# OverflowError

راه‌حل درست، استفاده از logarithm و مقایسه با آستانه یا استفاده از Decimal با دقت کنترل‌شده.

سناریوی چهارم: فاکتوریل در محاسبات ترکیبیاتی

محاسبهٔ فاکتوریل برای اعداد بزرگ، یکی از شایع‌ترین موقعیت‌های OverflowError است:

import math

math.factorial(1000)  # OK در پایتون ۳
math.factorial(10 ** 7)  # OverflowError در برخی نسخه‌ها

# در numpy
import numpy as np
np.math.factorial(1000)  # احتمالاً خطا یا سرریز خاموش

راه‌حل درست برای محاسبات ترکیبیاتی بزرگ، استفاده از لگاریتم فاکتوریل (math.lgamma) است:

import math

log_factorial = math.lgamma(1000 + 1)  # log(n!) به‌صورت پایدار
print(math.exp(log_factorial))  # خود n! (اگر در بازهٔ float جا بگیرد)

سناریوی پنجم: numpy بدون تنظیم سرریز

در numpy، سرریز به‌طور پیش‌فرض خاموش است و نتیجه wrap می‌شود. این رفتار می‌تواند منجر به باگ‌های بسیار خطرناک شود:

import numpy as np

a = np.int32(2 ** 30)
print(a + a)  # نتیجه منفی می‌شود، بدون هشدار

# فعال‌سازی بررسی سرریز
np.seterr(over='warn')

در پروژه‌های تولیدی، همیشه np.seterr را در ابتدای برنامه تنظیم کنید. این یک تکنیک کوچک است که از بسیاری از باگ‌های پنهان جلوگیری می‌کند.

سناریوی ششم: تبدیل داده در pandas

کتابخانهٔ pandas هنگام خواندن فایل‌های CSV، اعداد را به int64 یا float64 تبدیل می‌کند. اگر داده شامل اعداد بسیار بزرگ باشد، این تبدیل می‌تواند به OverflowError یا سرریز خاموش منجر شود:

import pandas as pd

# فرض کنید CSV شامل عدد 10 ** 30 باشد
df = pd.read_csv("big_numbers.csv")
# ممکن است خطا بدهد یا به float تبدیل شود و دقت را از دست بدهد

راه‌حل درست، خواندن ستون‌های عددی به‌صورت object و سپس تبدیل کنترل‌شده است:

df = pd.read_csv("big_numbers.csv", dtype={"amount": "object"})
df["amount"] = df["amount"].apply(int)  # تبدیل به int پایتون

این تکنیک را در پروژه‌های مالی که با اعداد بزرگ (مثل ریال یا ارزهای دیجیتال) سروکار دارند، زیاد استفاده کرده‌ام. سناریوهای مشابه در پروژه‌های پردازش داده انبوه با وب اسکرپینگ با پایتون هم رخ می‌دهند.

روش تشخیص در پنج گام

در برخورد با OverflowError، پروتکل زیر را در پروژه‌های خودم اجرا می‌کنم. این پروتکل، هم برای پرونده‌های کوچک و هم برای باگ‌های پیچیده در سیستم‌های بزرگ مفید است.

گام اول: خواندن دقیق پیام خطا

پیام‌های OverflowError معمولاً به سه دستهٔ اصلی تقسیم می‌شوند:

OverflowError: int too large to convert to float
OverflowError: math range error
OverflowError: Python int too large to convert to C long

هر پیام، مسیر تشخیص را روشن می‌کند. پیام اول یعنی تبدیل int به float ناموفق بوده؛ پیام دوم یعنی تابع math سرریز کرده؛ پیام سوم یعنی تبدیل به C-level با شکست مواجه شده. مستندات دقیق این پیام‌ها در مستندات رسمی پایتون موجود است.

گام دوم: بررسی traceback کامل

در traceback، نقطهٔ پرتاب استثنا در آخرین فریم نمایش داده می‌شود. اگر خطا از یک تابع math می‌آید، مسیر مشخص است؛ اگر از یک کتابخانهٔ ثالث می‌آید، بهتر است با chain=True لاگ کنید:

import traceback

try:
    risky_math()
except OverflowError:
    traceback.print_exc(limit=None, chain=True)

گام سوم: شناسایی نوع داده درگیر

اولین سؤال این است: کدام نوع داده سرریز کرده؟ int، float، Decimal یا numpy.int64؟ این تشخیص با کد زیر انجام می‌شود:

def diagnose_overflow(value):
    print(f"type: {type(value).__name__}")
    print(f"value: {value!r}")
    print(f"size: {getattr(value, 'nbytes', None)}")
    print(f"is finite: {getattr(value, 'is_finite', lambda: None)()}")

import numpy as np
diagnose_overflow(np.int32(2 ** 30))

این رویکرد، مخصوصاً در کدهایی که با ترکیبی از انواع داده کار می‌کنند، بسیار مفید است.

گام چهارم: بازتولید در محیط کنترل‌شده

پیش از هر تغییری، خطا را در حداقل کد ممکن بازتولید کنید. اگر خطا در pipeline داده رخ می‌دهد، آرایه را جدا کرده و به‌تنهایی تست کنید:

import numpy as np

arr = np.array([1e300, 1e300])
result = np.sum(arr)
print(result)  # احتمالاً inf

در پایتون خالص معادل این عملیات، خطا می‌دهد:

try:
    x = 1e300 + 1e300
except OverflowError as e:
    print(e)

گام پنجم: بررسی تنظیمات numpy و decimal

در محیط‌هایی که با numpy یا decimal کار می‌کنید، تنظیمات پیش‌فرض را بررسی کنید:

import numpy as np
print(np.geterr())

from decimal import getcontext
print(getcontext())

خروجی این دو دستور، معمولاً مقصر واقعی را نشان می‌دهد. برای مثال، اگر numpy.geterr()["over"] روی "ignore" باشد، سرریز بدون هشدار رخ می‌دهد و فقط با بررسی نتیجه قابل تشخیص است.

اگر خطا با خطاهای لایه‌های پایین‌تر سیستمعامل قاطی شود، مقاله‌های خطای OSError در پایتون و خطای MemoryError در پایتون تفکیک این لایه‌ها را روشن می‌کنند.

الگوهای درست مدیریت سرریز

راه‌حل هر OverflowError به بستر آن بستگی دارد، ولی الگوهای کلی زیر در بیشتر پرونده‌ها مفیدند.

الگوی اول: پایداری عددی در توابع نمایی

هرجا از exp استفاده می‌کنید، احتمال سرریز وجود دارد. راه‌حل استاندارد، کاهش بیشینه یا استفاده از حالت‌های جایگزین:

import math

def safe_exp(x, limit=709):
    # exp(709) نزدیک بزرگ‌ترین مقدار قابل‌نمایش در float است
    if x > limit:
        return float("inf")
    return math.exp(x)

عدد ۷۰۹ از این واقعیت می‌آید که exp(709) ≈ 8.2e307 و exp(710) از بازهٔ float64 خارج می‌شود. این الگو در تابع sigmoid، softmax و log-sum-exp بسیار کاربرد دارد.

الگوی دوم: استفاده از Decimal برای دقت کنترل‌شده

در کدهای مالی، استفاده از float هم دقت را از دست می‌دهد و هم می‌تواند سرریز کند. راه‌حل درست، Decimal با دقت مشخص است:

from decimal import Decimal, getcontext, Overflow, InvalidOperation

getcontext().prec = 50
getcontext().Emax = 10 ** 6

def compound(principal, rate, years):
    try:
        p = Decimal(str(principal))
        r = Decimal(str(rate))
        return p * (1 + r) ** int(years)
    except Overflow:
        raise ValueError("result exceeds representable range")
    except InvalidOperation as e:
        raise ValueError(f"invalid decimal operation: {e}")

این الگو، مخصوصاً در سیستم‌های بانکی و پرداخت الکترونیکی بسیار مهم است. یکی از پرونده‌هایی که در مشاوره‌ها دیدم، یک سیستم محاسبهٔ سود در یک صندوق سرمایه‌گذاری بود که به‌طور تصادفی با اعداد بزرگ سرریز می‌کرد و به‌جای خطا، نتیجهٔ اشتباه به کاربر نشان می‌داد.

الگوی سوم: تنظیم صریح numpy

در پروژه‌های عددی، تنظیمات numpy را در ابتدای برنامه صریح تعیین کنید:

import numpy as np

np.seterr(
    over="warn",
    under="ignore",
    divide="warn",
    invalid="warn",
)

# در تابع‌های حساس، به‌صورت موقت به raise تغییر دهید
with np.errstate(over="raise"):
    result = np.exp(large_array)

این الگو، انعطاف‌پذیری بالایی می‌دهد: در سراسر برنامه هشدار می‌گیرید و در بخش‌های حساس، به خطای سخت تبدیل می‌کنید.

الگوی چهارم: کاهش مقیاس با لگاریتم

در محاسبات آماری و یادگیری ماشین، معمولاً بهتر است محاسبات را در فضای لگاریتمی انجام دهید:

import math

def log_sum_exp(values):
    m = max(values)
    return m + math.log(sum(math.exp(v - m) for v in values))

این الگو، پایهٔ محاسبات پایدار در مدل‌های احتمالاتی است و از سرریز جلوگیری می‌کند. در پروژه‌های ساخت API با پایتون که مدل‌های عددی دارند، این تکنیک بسیار رایج است.

الگوی پنجم: استفاده از int پایتون به‌جای numpy.int64

در محاسباتی که احتمال سرریز وجود دارد و کارایی حیاتی نیست، از int پایتون استفاده کنید:

# اشتباه در حضور اعداد بزرگ
import numpy as np
total = np.int64(0)
for x in huge_list:
    total += x  # احتمال سرریز

# درست
total = 0
for x in huge_list:
    total += int(x)

هزینهٔ کارایی این تغییر در بسیاری از پروژه‌ها قابل‌قبول است و از یک دسته از باگ‌های پنهان جلوگیری می‌کند.

الگوی ششم: fail-fast در برابر ورودی غیرمعقول

در مرزهای ورودی برنامه، بازه‌های عددی را بررسی کنید:

MAX_REASONABLE = 10 ** 15

def process_amount(amount):
    if abs(amount) > MAX_REASONABLE:
        raise ValueError(f"amount out of range: {amount}")
    # ادامهٔ پردازش

این الگو از «سرریز غیرمنتظره در لایه‌های پایین‌تر» جلوگیری می‌کند. تجربه‌ام این است که هر بار اجازه داده‌ام یک مقدار غیرمعقول وارد لایه‌های عددی شود، در نهایت یکی از آن لایه‌ها با خطا شکست خورده و دیباگ سخت شده است.

برای عمیق‌تر شدن در پردازش داده‌های ورودی، به‌ویژه از منابع وب، اتصال پایتون به MySQL نمونه‌های عملی خوبی برای طراحی این مرزها را نشان می‌دهد.

سرریز در کد مالی و علمی

کدهای مالی و علمی، حساس‌ترین حوزه‌ها برای OverflowError هستند. در این حوزه‌ها، حتی یک بیت اختلاف در عدد، می‌تواند به تصمیم‌گیری اشتباه یا زیان مالی منجر شود. سه تجربهٔ مشخص از این حوزه را با شما به اشتراک می‌گذارم.

تجربهٔ اول: عدد مطلق در محاسبهٔ ارزش فعلی

در محاسبهٔ ارزش فعلی (Present Value)، از فرمول PV = FV / (1+r)^n استفاده می‌شود. اگر نرخ r نزدیک صفر باشد و n بزرگ، (1+r)^n می‌تواند سرریز کند:

import math

def present_value_wrong(fv, r, n):
    return fv / (1 + r) ** n

# با r کوچک و n بزرگ
present_value_wrong(1000, 0.001, 10 ** 6)  # ممکن است خطا بدهد

راه‌حل درست، استفاده از exp و log:

def present_value(fv, r, n):
    log_discount = n * math.log1p(r)
    return fv * math.exp(-log_discount)

تابع math.log1p برای r کوچک، دقیق‌تر از log(1+r) است و سرریز نمی‌کند.

تجربهٔ دوم: محاسبهٔ انتروپی در متن‌کاوی

انتروپی اطلاعاتی برای متن‌های بزرگ، می‌تواند به OverflowError منجر شود:

import math

def entropy_wrong(probabilities):
    return -sum(p * math.log(p) for p in probabilities if p > 0)

# اگر probabilities کوچک یا خیلی کوچک باشند، لگاریتم بی‌نهایت منفی می‌دهد
# و multiplication می‌تواند سرریز کند

راه‌حل درست با آستانه:

def entropy(probabilities, threshold=1e-300):
    return -sum(
        p * math.log(p)
        for p in probabilities
        if p > threshold
    )

تجربهٔ سوم: ضرب ماتریس‌های بزرگ

در جبر خطی عددی، ضرب ماتریس‌های بزرگ با ورودی‌های بزرگ، می‌تواند به سرریز منجر شود. numpy به‌طور پیش‌فرض نتیجه را wrap می‌کند که خطرناک‌تر از خطاست:

import numpy as np

a = np.array([[1e150, 1e150], [1e150, 1e150]])
b = np.array([[1e150, 1e150], [1e150, 1e150]])
result = a @ b
print(result)  # inf

در این حالت، نتیجه به بی‌نهایت تبدیل می‌شود و اگر در محاسبات بعدی استفاده شود، همه چیز خراب می‌شود. راه‌حل: قبل از ضرب، مقیاس ماتریس‌ها را کاهش دهید یا از np.float128 استفاده کنید (در سیستم‌هایی که پشتیبانی می‌شود):

a = a.astype(np.float128)
b = b.astype(np.float128)
result = a @ b

این الگو، در پروژه‌های یادگیری ماشین که با تنسورهای بزرگ کار می‌کنند، بسیار رایج است. تجربهٔ مشابه در پردازش داده‌های انبوه با وب اسکرپینگ با پایتون هم دیده می‌شود.

OverflowError در numpy، pandas و torch

هر کتابخانهٔ عددی، رفتار اختصاصی با سرریز دارد. شناخت این رفتارها، در محیط‌های تولیدی حیاتی است.

numpy: سرریز خاموش با تنظیمات

numpy به‌طور پیش‌فرض سرریز integer را به‌شکل wrap-around مدیریت می‌کند (مثل C) و سرریز float را به inf. این رفتار از نظر کارایی خوب است ولی از نظر ایمنی خطرناک. راه‌حل: تنظیم صریح با np.seterr یا استفاده از np.errstate در بخش‌های حساس:

with np.errstate(over="raise", invalid="raise"):
    result = compute_something()

در کتابخانه‌های جانبی مثل کتابخانه pandas در پایتون، همین رفتار از طریق np.errstate قابل کنترل است.

pandas: سرریز در تبدیل نوع

pandas هنگام خواندن CSV یا Excel، اعداد را به نوع مناسب تبدیل می‌کند. اگر عدد بزرگ باشد و نوع پیش‌فرض با آن سازگار نباشد، سرریز رخ می‌دهد:

import pandas as pd

df = pd.read_csv("big_numbers.csv")
# ممکن است عدد به float تبدیل شود و دقت را از دست بدهد

راه‌حل: تعیین صریح نوع ستون‌های عددی:

df = pd.read_csv("big_numbers.csv", dtype={"amount": "Int64"})
# یا
df = pd.read_csv("big_numbers.csv", dtype={"amount": "object"})

نوع Int64 (با حرف بزرگ) در pandas، از مقادیر NA پشتیبانی می‌کند و در عین حال، بازهٔ int64 را حفظ می‌کند.

PyTorch: سرریز در محاسبات تنسور

در PyTorch، سرریز بسته به نوع تنسور (float32, float64, int32, int64) رفتار متفاوتی دارد. راه‌حل استاندارد:

import torch

tensor = torch.tensor([1e30, 1e30], dtype=torch.float32)
result = tensor * tensor  # inf

# با float64 دقیق‌تر
tensor = tensor.to(torch.float64)
result = tensor * tensor

# بررسی نتیجه
if torch.isinf(result).any():
    raise RuntimeError("overflow in tensor computation")

در مدل‌های یادگیری عمیق، همیشه بعد از forward pass، نتیجه را با torch.isnan و torch.isinf بررسی کنید. این کار از loss propagation نامعتبر جلوگیری می‌کند.

TensorFlow: سرریز در گراف

در TensorFlow، عملیات روی گراف محاسباتی انجام می‌شود و سرریز در طول graph ممکن است رخ دهد. راه‌حل: استفاده از tf.debugging.check_numerics برای شناسایی زودهنگام:

import tensorflow as tf

result = tf.matmul(a, b)
result = tf.debugging.check_numerics(result, "overflow detected")

این تکنیک، در محیط تولید به‌طور پیش‌فرض خاموش است ولی می‌توانید در فاز دیباگ فعال کنید.

Decimal: سرریز قابل کنترل

کتابخانهٔ decimal، دقیق‌ترین کنترل را روی سرریز می‌دهد چون می‌توانید حداکثر نمایندهٔ مجاز را تعیین کنید:

from decimal import Decimal, getcontext

getcontext().prec = 100
getcontext().Emax = 10 ** 9
getcontext().Emin = -10 ** 9

این تنظیمات، مخصوصاً در سیستم‌های مالی که پیش‌بینی بازهٔ اعداد مهم است، حیاتی هستند.

برای مطالعهٔ تفصیلی دربارهٔ رفتار کتابخانه‌ها در دیگر خطاهای عددی، خطای RuntimeError در پایتون نمونه‌های مکمل را ارائه می‌دهد.

پرسش‌های پرتکرار درباره OverflowError

این بخش، پرسش‌هایی را پوشش می‌دهد که در جلسات مشاوره و انجمن‌های فنی بیشترین تکرار را داشته‌اند. پاسخ‌ها به‌شکلی نوشته شده‌اند که برای جستجوهای مستقیم و دستیارهای هوش مصنوعی به‌عنوان پاسخ معتبر قابل استخراج باشند.

چرا int در پایتون ۳ سرریز نمی‌کند ولی float می‌کند؟

در پایتون ۳، int به‌صورت arbitrary-precision ذخیره می‌شود؛ یعنی هر عدد صحیح به‌اندازهٔ لازم حافظه می‌گیرد و از پیش بازهٔ محدودی ندارد. در مقابل، float از استاندارد IEEE 754 (قالب binary64) استفاده می‌کند و بازهٔ محدودی در حدود ±1.8 × 10^308 دارد. همین بازهٔ محدود، منشأ OverflowError است.

OverflowError با ValueError و TypeError چه تفاوتی دارد؟

OverflowError از خانوادهٔ ArithmeticError است و وقتی رخ می‌دهد که نتیجهٔ یک عملیات ریاضی از بازهٔ قابل‌نمایش بیرون بزند. ValueError وقتی رخ می‌دهد که مقدار از نظر محتوا نامعتبر باشد (مثلاً int("abc")). TypeError وقتی رخ می‌دهد که نوع داده اشتباه باشد (مثلاً 1 + "1"). برای درک تفصیلی، خطای ValueError در پایتون را ببینید.

چرا math.exp گاهی OverflowError می‌دهد؟

تابع math.exp(x) عدد e^x را محاسبه می‌کند. برای x بزرگ‌تر از حدود ۷۰۹، این عدد از بازهٔ float64 خارج می‌شود و پایتون OverflowError: math range error پرتاب می‌کند. راه‌حل: قبل از فراخوانی، مقدار x را بررسی کنید یا از math.log1p استفاده کنید.

چرا numpy سرریز نمی‌دهد و به‌جایش inf می‌دهد؟

numpy از مدل «سرریز خاموش» پیروی می‌کند: نتیجهٔ عملیات خارج از بازه، به inf (برای float) یا wrap-around (برای int) تبدیل می‌شود. دلیل این طراحی، کارایی است؛ چرا که بررسی هر عملیات برای سرریز، هزینهٔ محاسباتی قابل‌توجهی دارد. برای فعال‌سازی خطا، از np.seterr(over="raise") استفاده کنید.

آیا Decimal هم سرریز می‌کند؟

بله، ولی بازهٔ قابل‌تنظیم دارد. با getcontext().Emax می‌توانید حداکثر نمایندهٔ مجاز را تعیین کنید. پیش‌فرض Emax در decimal بسیار بزرگ است (حدود 10^9)، ولی همچنان محدود است. برای اعداد بزرگ‌تر، باید Emax را افزایش دهید یا از fractions.Fraction استفاده کنید که arbitrary-precision است.

چطور می‌توان بدون تغییر کد، OverflowError را لاگ کرد؟

می‌توانید یک sys.excepthook تنظیم کنید که تمام استثناها را لاگ کند:

import sys
import logging

logging.basicConfig(level=logging.INFO)

def hook(exc_type, exc_value, exc_traceback):
    if issubclass(exc_type, OverflowError):
        logging.exception("overflow caught", exc_info=(exc_type, exc_value, exc_traceback))
    sys.__excepthook__(exc_type, exc_value, exc_traceback)

sys.excepthook = hook

آیا OverflowError در پایتون ۲ هم وجود دارد؟

بله، ولی در پایتون ۲، خود int هم می‌توانست سرریز کند (به‌جز زمانی که long استفاده می‌شد). در پایتون ۳، یکسان‌سازی int و long باعث شد اعداد صحیح نامحدود شوند و OverflowError فقط در تبدیل به انواع محدود مثل float باقی بماند.

چگونه در pytest، OverflowError را تست کنیم؟

با استفاده از pytest.raises:

import pytest
import math

def test_exp_overflow():
    with pytest.raises(OverflowError, match="math range error"):
        math.exp(1000)

این الگو، دقیقاً نوع استثنا و پیام آن را بررسی می‌کند و در تمام حالت‌های اجرا (حتی با -O) فعال است.

آیا OverflowError در asyncio هم وجود دارد؟

بله، ولی رفتار آن تحت تأثیر حلقهٔ رویداد نیست. OverflowError در asyncio دقیقاً همان OverflowError همزمان است. تنها نکتهٔ خاص این است که اگر خطا در یک Task رخ دهد و await نشود، ممکن است در لاگ asyncio ظاهر شود. برای مطالعهٔ بیشتر در این حوزه، خطای RuntimeError در پایتون نکات مکمل را ارائه می‌دهد.

چرا OverflowError در محاسبهٔ سود مرکب اتفاق می‌افتد؟

فرمول سود مرکب P * (1+r)^n است. برای r مثبت و n بزرگ، (1+r)^n به‌صورت نمایی رشد می‌کند و از بازهٔ float64 بیرون می‌زند. حتی برای r = 0.05 و n = 5000، این مقدار از 10^100 عبور می‌کند. راه‌حل: محاسبه در فضای لگاریتمی یا استفاده از Decimal با Emax بزرگ.

آیا numpy.int64 هم می‌تواند OverflowError بدهد؟

نه دقیقاً OverflowError، بلکه سرریز خاموش می‌دهد. یعنی نتیجه wrap-around می‌شود و به عدد منفی یا کوچک تبدیل می‌شود. برای شناسایی این وضعیت، باید با np.seterr(over="raise") آن را به خطا تبدیل کنید یا نتیجه را با بازهٔ مورد انتظار مقایسه کنید.

چطور پیش از سرریز، آن را شناسایی کنیم؟

سه روش عملی: اول، قبل از عملیات، ورودی‌ها را با بازهٔ مورد انتظار بررسی کنید. دوم، از np.isfinite یا math.isfinite پس از عملیات استفاده کنید. سوم، محاسبات را در فضای لگاریتمی انجام دهید تا بازهٔ اعداد کوچک‌تر شود. این سه تکنیک را در پروژه‌های عددی خودم همیشه استفاده می‌کنم.

برای مطالعات مکمل در همین خانوادهٔ خطاهای پایتون، خطای PermissionError در پایتون و خطای NameError در پایتون نکات مهمی را در مدیریت خطاهای زمان اجرا پوشش می‌دهند.

آنچه سرریز عددی به معماری کد من آموخت

OverflowError بیش از آنکه یک خطای عددی باشد، یک «قرارداد» است دربارهٔ محدودیت‌های بازهٔ اعداد در انواع داده‌های مختلف. سه اصلی که پس از سال‌ها کار با آن، در معماری کد خودم رعایت می‌کنم:

نخست، بازهٔ اعداد را در طراحی مدل داده صریح کنید. هر فیلد عددی، یک بازه دارد: مبلغ یک تراکنش، تعداد یک رکورد، نرخ یک بهره. اگر این بازه را در زمان طراحی صریح کنید، هم انتخاب نوع داده آسان‌تر می‌شود و هم بررسی‌ها در مرزهای ورودی به‌طور طبیعی قرار می‌گیرند. تجربه‌ام این است که کدهایی که بازهٔ صریح دارند، کمتر به OverflowError می‌خورند.

دوم، در محاسبات عددی، از پایداری عددی غافل نشوید. بسیاری از الگوریتم‌های ریاضی (sigmoid، softmax، محاسبهٔ احتمال، ارزش فعلی) وقتی ساده نوشته شوند، در بازه‌های مرزی ناپایدارند. نسخهٔ پایدار این الگوریتم‌ها همیشه وجود دارد: کاهش بیشینه، لگاریتمی کردن، یا مقیاس‌بندی. انتخاب این نسخه‌ها از ابتدا، هزینه‌ای ندارد؛ ولی تغییر آن‌ها در محیط تولید، ممکن است هفته‌ها وقت بگیرد.

سوم، بین «کارایی» و «امنی» در تنظیمات کتابخانه‌ها آگاهانه انتخاب کنید. numpy به‌طور پیش‌فرض کارایی را انتخاب می‌کند، ولی در پروژه‌های تولیدی، امنیت عددی هم مهم است. یک خط np.seterr(over="warn") در ابتدای برنامه، تعادل درستی بین این دو برقرار می‌کند و به شما اجازه می‌دهد در بخش‌های حساس، به خطای سخت سوئیچ کنید.

در پایان، اگر در پروژه‌ای با حالت خاصی از OverflowError برخورد کردید که این‌جا پوشش داده نشده — مثلاً در ترکیب با numba، cython، یا درایورهای GPU با محدودیت‌های خاص — تجربه‌تان را در دیدگاه‌ها بنویسید. به‌ویژه اگر راه‌حلی پیدا کرده‌اید که با رویکردهای معمول متفاوت است و می‌تواند برای خوانندهٔ بعدی ارزشمند باشد. 🔢