تغییر پیش‌فرض چه چیزی را عوض می‌کند

Python 3.15 در ۹ اکتبر ۲۰۲۶ منتشر شد و تغییر پیش‌فرض UTF-8 از موضوع‌های مهم مهاجرت در این شاخه است. این تغییر برای تیمی که فایل فارسی میان ویندوز، لینوکس و محیط‌های کانتینری جابه‌جا می‌کند اهمیت دارد. بااین‌حال، ارتقای مفسر محتوای فایل قدیمی را تبدیل نمی‌کند. ابتدا قرارداد فعلی فایل‌ها را شناسایی کنید: چه کسی می‌نویسد، چه کسی می‌خواند و encoding مورد انتظار هر مرز چیست.

ابزار encoding_probe نسخهٔ پایتون، وضعیت UTF-8 mode، encoding محلی و encoding فایل‌سیستم را گزارش می‌کند. تنظیم محیط می‌تواند روی رفتار واقعی اثر داشته باشد؛ بنابراین صرف دیدن شمارهٔ نسخه برای تشخیص همهٔ پیش‌فرض‌ها کافی نیست. این ابزار را در همان محیط اجرای سرویس نیز اجرا کنید. نتیجهٔ ترمینال توسعه، نتیجهٔ فرایند داخل کانتینر یا سرویس ویندوز را تضمین نمی‌کند و باید جداگانه ثبت شود.

قرارداد روشن برای فایل جدید و قدیمی

برای فایل‌های جدید، encoding را در نقطهٔ خواندن و نوشتن صریح تعیین کنید. نمونهٔ گزارش JSON با UTF-8 و حالت ایجاد انحصاری ذخیره می‌شود تا فایل موجود ناخواسته بازنویسی نشود. استفاده از ensure_ascii=False شکل قابل خواندن متن فارسی را در فایل حفظ می‌کند. آزمون، داده را از همان فایل دوباره می‌خواند و ساختار اصلی را مقایسه می‌کند؛ تکیه بر ظاهر متن در یک ویرایشگر به‌تنهایی برای این قرارداد کافی نیست.

فایل قدیمی باید با encoding شناخته‌شدهٔ منبع خوانده شود، نه با حدس خودکار یا جایگزینی ساکت نویسه‌های خراب. convert_legacy محدودیت اندازه دارد، مسیر منبع و مقصد یکسان را رد می‌کند و با errors='strict' شکست را قابل مشاهده نگه می‌دارد. مقصد UTF-8 جداگانه ساخته می‌شود. این روش برای ورودی‌های کوچک و کنترل‌شده طراحی شده و برای فایل بزرگ باید نسخهٔ جریانی با قرارداد مشابه تعریف شود.

آزمون‌ها متن فارسی را رفت‌وبرگشت می‌دهند، یک ورودی CP1256 معلوم را تبدیل می‌کنند و خطای decoding را پیش از ایجاد مقصد بررسی می‌کنند. محدودیت اندازه و حفظ فایل گزارش موجود نیز آزموده می‌شود. نمونه عمداً از errors='replace' استفاده نمی‌کند، چون تبدیل نامعتبر به ظاهر موفق می‌تواند داده را بی‌صدا تغییر دهد. اگر دادهٔ واقعی چند encoding دارد، آن را در قرارداد ورودی تفکیک کنید و فایل مبهم را برای بررسی کنار بگذارید.

فایل‌های کامل، روش اجرا و آزمون

منطق فایل‌ها: Python 3.11+؛ بررسی تغییر پیش‌فرض: مفسر Python 3.15 با تنظیم محیط ثبت‌شده. چهار فایل زیر را با همین نام‌ها در یک پوشه ذخیره کنید؛ هیچ بخش کد با سه‌نقطه جایگزین نشده است.

1. تشخیص نسخه و encoding واقعی محیط

نام فایل: encoding_probe.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

import json
import locale
import sys


report = {
    'python': sys.version.split()[0],
    'utf8_mode': bool(sys.flags.utf8_mode),
    'locale_encoding': locale.getencoding(),
    'filesystem_encoding': sys.getfilesystemencoding(),
    'target_default_changed_in': '3.15',
}
print(json.dumps(report, ensure_ascii=True))

python encoding_probe.py اطلاعات محیط جاری را به‌صورت JSON چاپ می‌کند. برای اثبات پیش‌فرض 3.15 آن را با همان مفسر و تنظیم‌های واقعی سرویس اجرا کنید.

2. خواندن، گزارش و تبدیل صریح فایل

نام فایل: text_files.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

import json
from pathlib import Path


def read_utf8(path):
    return Path(path).read_text(encoding='utf-8', errors='strict')


def write_report(path, value):
    with Path(path).open('x', encoding='utf-8', newline='\n') as output:
        json.dump(value, output, ensure_ascii=False, indent=2)
        output.write('\n')


def convert_legacy(source, destination, *, source_encoding, max_bytes=4 * 1024 * 1024):
    if max_bytes < 1:
        raise ValueError('max_bytes must be positive')
    source, destination = Path(source).resolve(), Path(destination).resolve()
    if source == destination:
        raise ValueError('Preserve the original source file')
    with source.open('rb') as original:
        raw = original.read(max_bytes + 1)
    if len(raw) > max_bytes:
        raise ValueError('Input exceeds the configured size limit')
    text = raw.decode(source_encoding, errors='strict')
    with destination.open('x', encoding='utf-8', newline='') as output:
        output.write(text)
    return destination

حد ورودی چهار مگابایت برای این نمونه انتخاب شده است. منبع حفظ می‌شود، encoding ورودی اجباری است و فایل مقصد با حالت x ساخته می‌شود.

3. آزمون فارسی و مسیرهای خطا

نام فایل: test_text_files.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

from pathlib import Path
import json
import tempfile
import unittest
from text_files import convert_legacy, read_utf8, write_report


class EncodingTests(unittest.TestCase):
    def test_persian_json_roundtrip(self):
        with tempfile.TemporaryDirectory(prefix='liyan-utf8-json-') as folder:
            path = Path(folder) / 'report.json'
            expected = {'name': 'لیان', 'message': 'سلام دنیا'}
            write_report(path, expected)
            self.assertEqual(json.loads(read_utf8(path)), expected)
            with self.assertRaises(FileExistsError):
                write_report(path, {'name': 'overwrite'})
            self.assertEqual(json.loads(read_utf8(path)), expected)

    def test_explicit_legacy_conversion(self):
        with tempfile.TemporaryDirectory(prefix='liyan-utf8-legacy-') as folder:
            old, new = Path(folder) / 'legacy.txt', Path(folder) / 'utf8.txt'
            old.write_bytes('سلام'.encode('cp1256'))
            convert_legacy(old, new, source_encoding='cp1256')
            self.assertEqual(read_utf8(new), 'سلام')
            self.assertNotEqual(old.read_bytes(), new.read_bytes())

    def test_decode_error_and_size_limit_do_not_create_output(self):
        with tempfile.TemporaryDirectory(prefix='liyan-utf8-failure-') as folder:
            old, new = Path(folder) / 'bad.txt', Path(folder) / 'new.txt'
            old.write_bytes(bytes([255]))
            with self.assertRaises(UnicodeDecodeError):
                convert_legacy(old, new, source_encoding='utf-8')
            self.assertFalse(new.exists())
            old.write_bytes(b'12345')
            with self.assertRaises(ValueError):
                convert_legacy(old, new, source_encoding='ascii', max_bytes=4)
            self.assertFalse(new.exists())


if __name__ == '__main__':
    unittest.main()

python -m unittest test_text_files.py را اجرا کنید. خطای decoding و ورودی بزرگ نباید فایل مقصد بسازند؛ گزارش موجود نیز باید دست‌نخورده بماند.

4. سناریوی کامل مهاجرت نمونه

نام فایل: migrate_demo.py؛ این فایل را کامل و بدون حذف سطرها ذخیره کنید.

import json
from pathlib import Path
import tempfile
from text_files import convert_legacy, read_utf8, write_report


with tempfile.TemporaryDirectory(prefix='liyan-encoding-demo-') as folder:
    source = Path(folder) / 'legacy.txt'
    destination = Path(folder) / 'utf8.txt'
    source.write_bytes('سلام'.encode('cp1256'))
    convert_legacy(source, destination, source_encoding='cp1256')
    report = {'converted_text': read_utf8(destination), 'original_preserved': source.exists()}
    report_path = Path(folder) / 'report.json'
    write_report(report_path, report)
    assert json.loads(read_utf8(report_path)) == report
    print(json.dumps(report, ensure_ascii=True))

python migrate_demo.py یک فایل CP1256 کنترل‌شده را به UTF-8 تبدیل می‌کند و گزارش را دوباره می‌خواند. خروجی کنسول ASCII است تا نمایش ترمینال قدیمی با صحت فایل اشتباه نشود.

آزمون دادهٔ فارسی پیش از ارتقای محیط

چهار فایل را کنار هم ذخیره کنید و python -m unittest test_text_files.py را اجرا کنید. این منطق صریح روی Python 3.11+ نیز قابل اجراست؛ تغییر پیش‌فرض شاخهٔ 3.15 باید جداگانه با probe همان مفسر بررسی شود. migrate_demo.py یک تبدیل نمونه و گزارش JSON را بررسی می‌کند. پیش از ارتقای سرویس، نمونهٔ نماینده‌ای از دادهٔ قدیمی، نام فایل فارسی، خروجی CSV و ارتباط بین فرایندها را در آزمون پذیرش نگه دارید.

چک‌لیست اجرای عملی

  • encoding را در مرز فایل صریح بنویسید.
  • probe را با مفسر واقعی سرویس اجرا کنید.
  • فایل اصلی و خروجی موجود را حفظ کنید.
  • خطای strict، محدودیت اندازه و رفت‌وبرگشت فارسی را تست کنید.

منابع

تدوین و راهنمای عملی: دانشنامه لیان.