سال ۲۰۱۴، وقتی روی یک پروژه‌ی تحلیل داده کار می‌کردم، با کدی روبرو شدم که نیمی‌اش در پایتون ۲ نوشته شده بود و نیمه‌ی دیگر در پایتون ۳. توسعه‌دهنده‌ی قبلی، بدون این‌که متوجه شود، از کتابخانه‌ای استفاده کرده بود که فقط در پایتون ۲ کار می‌کرد و از کتابخانه‌ی دیگری که فقط در پایتون ۳ در دسترس بود. نتیجه، یک آشفتگی بود که سه روز دیباگ طول کشید تا بفهمم مشکل نه در کد من، که در فهم نادرست تفاوت پایتون ۲ و ۳ نهفته است. آن پروژه به من یاد داد که تفاوت پایتون ۲ و ۳ فقط یک بحث تاریخی نیست؛ تا سال‌ها، تصمیمی زنده برای هر توسعه‌دهنده‌ای بود که با کد قدیمی سر و کار داشت. در این مقاله، همان تفاوت‌ها را از منظر یک توسعه‌دهنده‌ی عملی مرور می‌کنم: نه فقط فهرست تغییرات، بلکه دلیل هر تغییر و مسیر واقعی مهاجرت.

چرا پایتون ۳ آمد؟

برای درک تفاوت‌ها، باید اول بدانیم چرا پایتون ۳ ساخته شد. پایتون ۲ در سال ۲۰۰۰ منتشر شد و پایتون ۳ در سال ۲۰۰۸. اگر بخواهیم ساده بگوییم: پایتون ۲ چند تصمیم طراحی داشت که با گذر زمان، به گلوگاه تبدیل شده بودند. تیم توسعه‌ی پایتون تصمیم گرفت به‌جای حل تدریجی این مشکلات، یک نسخه‌ی جدید بسازد که سازگار با گذشته نباشد — تصمیمی که جسورانه بود ولی پیامدهای طولانی داشت.

این تصمیم، درس مهمی برای هر توسعه‌دهنده‌ای دارد: پایتون ۳ نمونه‌ای نادر از این است که یک زبان محبوب، عمداً سازگاری عقب‌رو را قربانی بهبود طراحی می‌کند. اگر با مفاهیم پایه‌ی پایتون آشنا نیستید، آموزش پایتون از صفر نقطه‌ی شروع خوبی است. اما حتی برای کسی که امروز پایتون ۳ را یاد می‌گیرد، دانستن این تاریخچه مفید است — چون در پروژه‌های واقعی، دیر یا زود با کد قدیمی روبرو می‌شوید.

اولین تفاوت مشهود: print

معروف‌ترین تفاوت، که هر تازه‌کاری با آن روبرو می‌شود:

# Python 2
print "Hello, world!"

# Python 3
print("Hello, world!")

در پایتون ۲، print یک دستور (statement) بود، مثل if یا while. در پایتون ۳، print یک تابع شد. این تغییر، در نگاه اول ساده به نظر می‌رسد ولی تأثیرات عمیقی دارد: می‌توانید print را به‌عنوان پارامتر به تابع دیگر پاس دهید، در comprehension استفاده کنید، یا نامش را موقتاً تغییر دهید. این انعطاف، نشانه‌ی مهمی از فلسفه‌ی پایتون ۳ است: هر چیزی که می‌تواند تابع باشد، بهتر است تابع باشد.

نکته‌ی مهمی که در پروژه‌های واقعی دیدم: بعضی توسعه‌دهنده‌ها برای سازگاری با هر دو نسخه، از from __future__ import print_function استفاده می‌کردند. این راه‌حل موقت، در دهه‌ی ۲۰۱۰ رایج بود ولی امروز، دیگر معنی ندارد — پایتون ۲ در سال ۲۰۲۰ به پایان عمر خود رسید.

کدی که برای سازگاری با هر دو نسخه نوشته می‌شود، معمولاً در هر دو نسخه، نیمی از ظرفیتش را از دست می‌دهد؛ سازگاری کامل با گذشته، دشمن پیشرفت است.

تقسیم اعداد و تفاوت‌های عددی

این تفاوت، دام‌های خطرناکی در پروژه‌های علمی می‌سازد. در پایتون ۲:

# Python 2
>>> 5 / 2
2
>>> 5.0 / 2
2.5
>>> 5 // 2
2

در پایتون ۳:

# Python 3
>>> 5 / 2
2.5
>>> 5 // 2
2

تفاوت اینجاست که در پایتون ۲، تقسیم دو عدد صحیح، همیشه به عدد صحیح گرد می‌شد — نه گرد کردن معمولی، بلکه برش به سمت منفی بی‌نهایت. این رفتار، منشأ خطاهای نامرئی در محاسبات مالی و علمی بود. تصور کنید برنامه‌ای برای محاسبه‌ی مالیات نوشته‌اید و ناگهان همه‌ی اعداد اعشاری به عدد صحیح تبدیل می‌شوند.

در پایتون ۳، تقسیم با / همیشه نتیجه‌ی اعشاری برمی‌گرداند و برای تقسیم صحیح باید از // استفاده کنید. این تغییر، خوانایی کد را بالا برد ولی مهاجرت پروژه‌های علمی را دشوار کرد — چون در بعضی جاها، رفتار قبلی استفاده می‌شد و کسی متوجه نمی‌شد.

یونیکد و رشته‌ها

این بخش، شاید مهم‌ترین تفاوت پایتون ۲ و ۳ باشد. در پایتون ۲، دو نوع رشته داشتیم:

  • str: رشته‌ی بایتی (با کاراکترهای ASCII یا هر انکودینگ دلخواه)
  • unicode: رشته‌ی یونیکد (با پشتیبانی از همه‌ی زبان‌ها و ایموجی‌ها)

و تبدیل بین این دو، منبع بی‌پایان باگ بود. اگر یک رشته‌ی بایتی حاوی متن فارسی را چاپ می‌کردید و انکودینگ سیستم درست نبود، یا خطا می‌گرفتید یا خروجی نامفهوم. اگر با PHP کار کرده‌اید، این دردی را که در مدیریت کاراکترهای چندبایتی وجود دارد، در کار با آرایه‌ها در PHP تا حدی معادلش را دیده‌اید — ولی در پایتون ۲، این درد خیلی جدی‌تر بود.

در پایتون ۳، همه‌چیز یونیکد شد:

# Python 3
>>> text = "سلام"
>>> type(text)
<class "str">
>>> raw = b"hello"
>>> type(raw)
<class "bytes">

یعنی str در پایتون ۳، معادل unicode در پایتون ۲ است و برای داده‌های بایتی، نوع جداگانه‌ی bytes وجود دارد. این تفکیک صریح، در نگاه اول آزاردهنده به نظر می‌رسد ولی در عمل، جلوی خطاهای پنهان را می‌گیرد. در پروژه‌ای که داده‌ی متن فارسی را پردازش می‌کرد، همین تغییر باعث شد کد در پایتون ۳ درست کار کند و در پایتون ۲ بی‌دلیل خطا بدهد.

تغییرات در iteratorها و تابع range

یکی از تغییراتی که کد را کند یا سریع می‌کند، اما کمتر به چشم می‌آید: در پایتون ۲، range() یک لیست کامل می‌ساخت:

# Python 2
>>> range(10)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> type(range(10))
<type "list">

در پایتون ۳، range() یک iterator برمی‌گرداند که به‌صورت تنبل تولید می‌کند:

# Python 3
>>> range(10)
range(0, 10)
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

تفاوت مصرف حافظه در حلقه‌های بزرگ، چشمگیر است. اگر ۱۰ میلیون عدد را با range پیمایش کنید، در پایتون ۲ حدود ۸۰ مگابایت حافظه لازم است، در پایتون ۳ تقریباً هیچ. این تغییر به همین شکل در map، filter و zip هم اعمال شد: در پایتون ۲ لیست برمی‌گرداندند، در پایتون ۳ iterator.

اگر با بهینه‌سازی در PHP آشنا هستید، این مفهوم مشابه تبدیل حلقه‌ی آرایه‌ی بزرگ به Generator است — همان درسی که در بهینه‌سازی کدهای PHP روی آن تأکید کرده‌ام. تفاوت این‌جاست که در پایتون ۳، این رفتار پیش‌فرض شد و توسعه‌دهنده‌ها به‌طور خودکار از آن بهره می‌برند.

سینتکس استثناها

یکی دیگر از تفاوت‌های ظاهری ولی مهم، در نحوه‌ی نوشتن except است:

# Python 2
try:
    risky()
except ValueError, e:
    print e

# Python 3
try:
    risky()
except ValueError as e:
    print(e)

در پایتون ۲، از کاما برای اتصال نوع استثنا به متغیر استفاده می‌شد؛ در پایتون ۳، از as. این تغییر، هم خواناتر است و هم ابهام کمتری دارد. مسئله‌ی جالب این‌که در پایتون ۲، سینتکس کاما در بعضی موارد ابهام‌آمیز بود — وقتی چند نوع استثنا را می‌گرفتید، مرزها گم می‌شد. پایتون ۳ با پرانتز اجباری، این ابهام را حل کرد:

# Python 3
except (ValueError, TypeError) as e:
    handle(e)

اگر با خطاهای پایتون درگیرید، دو مقاله‌ی رفع خطای TypeError در پایتون و رفع خطای FileNotFoundError نمونه‌های عملی از مدیریت استثنا در پایتون ۳ را نشان می‌دهند. شکل درست مدیریت خطا، همان چیزی است که در مدیریت خطا در PHP هم در بستر دیگری دیده‌ایم.

تفاوت‌های عمیق‌تر: metaclass و نوع‌ها

برای توسعه‌دهندگانی که با مباحث پیشرفته‌ی پایتون کار می‌کنند، تفاوت‌های ظریف‌تری وجود دارد:

  • Metaclass: سینتکس تعریف metaclass تغییر کرد. در پایتون ۲ از __metaclass__ = Meta استفاده می‌شد، در پایتون ۳ از metaclass=Meta در امضای کلاس.
  • ترتیب MRO: الگوریتم Resolution Order در پایتون ۳ به C3 تغییر کرد که پیش‌بینی‌پذیرتر است. در بعضی پروژه‌های چندگانه‌ارث‌بری، این تغییر رفتار کلاس را عوض می‌کند.
  • __bool__ به‌جای __nonzero__: در پایتون ۳، متد تبدیل به bool تغییر نام داد. برای توسعه‌دهنده‌ی کتابخانه‌ها، این نوع تغییرات باید رعایت شود.
  • Annotations: پایتون ۳ پشتیبانی از Type Hints را در نسخه‌های بعدی اضافه کرد که در پایتون ۲ وجود نداشت.

اگر با شی‌گرایی در پایتون راحت نیستید، آموزش شی‌گرایی در PHP مفاهیم پایه را می‌رساند که با کمی تطبیق، در پایتون هم مستقیم استفاده می‌شود. تفاوت اصلی این‌جاست که پایتون از ابتدا طراحی‌اش شیءگرا بود ولی به‌شکل پویا، در حالی که PHP مدل متفاوتی داشت.

تفاوت‌های ظاهری پایتون ۲ و ۳، در چند ساعت یاد گرفته می‌شوند؛ تفاوت‌های عمیق آن، چند ماه برای تیمی که کد قدیمی دارد زمان می‌برد.

کتابخانه‌ها: بزرگ‌ترین مانع مهاجرت

در مهاجرت واقعی پروژه‌ها، بزرگ‌ترین درد، خودِ زبان نبود؛ کتابخانه‌ها بودند. در دهه‌ی ۲۰۱۰، بخش بزرگی از کتابخانه‌های مهم پایتون، فقط در پایتون ۲ کار می‌کردند. حتی وقتی نسخه‌ی پایتون ۳ منتشر شد، بعضی کتابخانه‌ها سال‌ها طول کشید تا پورت شوند.

سه دسته از کتابخانه‌ها بودند:

  • کتابخانه‌هایی که سریع پورت شدند: مثل Django، که از نسخه‌ی ۱.۵ پشتیبانی از پایتون ۳ را اضافه کرد.
  • کتابخانه‌هایی که کند پورت شدند: مثل SciPy و NumPy، که چند سال پورتشان طول کشید. در این مدت، پروژه‌های علمی در دوراهی انتخاب بودند.
  • کتابخانه‌هایی که هرگز پورت نشدند: پروژه‌هایی که توسعه‌دهنده‌شان رهایشان کرده بود یا خودشان خیلی خاص بودند.

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

اگر امروز بخواهید کتابخانه‌های پایتون را مدیریت کنید، ابزار استاندارد pip است. برای پروژه‌های جدی، ترکیب venv و requirements.txt — یا ابزارهای مدرن‌تر مثل Poetry — اجتناب‌ناپذیر است. اگر با مفهوم مدیریت وابستگی در PHP آشنا هستید، آموزش Composer در PHP همان مفاهیم را با سینتکس متفاوتی توضیح می‌دهد.

نقشه‌ی مهاجرت عملی در پروژه‌های واقعی

اگر پروژه‌ای با کد پایتون ۲ دارید، مهاجرت به پایتون ۳، یک پروژه‌ی چندروزه تا چندماهه است. نقشه‌ای که در پروژه‌های واقعی جواب داده، به این شکل است:

گام اول: ممیزی کد

ابزار 2to3 در پایتون تعبیه شده و می‌تواند بیشتر تغییرات سینتکسی را خودکار انجام دهد. ولی هرگز به‌عنوان راه‌حل نهایی به آن نگاه نکنید؛ بلکه به‌عنوان ابزار ممیزی استفاده کنید: خروجی‌اش لیست بلندی از تغییرات احتمالی به شما می‌دهد که می‌توانید یکی‌یکی بازبینی کنید.

2to3 -W -n -f all my_project/

گام دوم: بررسی وابستگی‌ها

فایل requirements.txt یا Pipfile را باز کنید و برای هر کتابخانه بررسی کنید که آیا نسخه‌ی سازگار با پایتون ۳ دارد. سه حالت ممکن است:

  1. نسخه‌ی سازگار موجود است: به‌روزرسانی و تست.
  2. جایگزین مدرن‌تری وجود دارد: مهاجرت به آن.
  3. هیچ‌کدام از دو مورد بالا نیست: کتابخانه را خودتان پورت کنید یا آن بخش را بازنویسی کنید.

گام سوم: تست‌های خودکار

قبل از هر تغییری، مطمئن شوید که تست‌های خودکار دارید. اگر ندارید، اول تست‌ها را بنویسید. در پروژه‌ای که تست نداشت، مهاجرت به پایتون ۳ به یک کابوس تبدیل شد چون در هر تغییر، نمی‌دانستیم چه چیزی خراب شده.

گام چهارم: مهاجرت تدریجی

به‌جای یک تغییر بزرگ، بخش‌به‌بخش مهاجرت کنید. یک ماژول را به پایتون ۳ ببرید، تست کنید، بعد سراغ بعدی بروید. در پروژه‌های بزرگ، این استراتژی زمان کل را بیشتر می‌کند ولی ریسک را بسیار پایین می‌آورد.

گام پنجم: پاک‌سازی

بعد از مهاجرت کامل، تمام کدهای سازگاری با پایتون ۲ را حذف کنید. کدهایی مثل from __future__ import ... یا شرط‌های if sys.version_info[0] == 2 فقط سربار هستند و باعث سردرگمی می‌شوند. اگر با گیت در پروژه‌های وردپرسی آشنا هستید، از همان اصول نسخه‌بندی و شاخه‌بندی استفاده کنید تا مهاجرت را امن و قابل بازگشت نگه دارید.

چرا پایتون ۲ کنار گذاشته شد؟

پایتون ۲ در ۱ ژانویه ۲۰۲۰ به پایان عمر خود رسید. یعنی تیم توسعه‌ی پایتون اعلام کرد که از آن تاریخ به بعد، هیچ به‌روزرسانی امنیتی برای پایتون ۲ منتشر نمی‌شود. این تصمیم، جسورانه بود ولی چند سال طول کشید تا کامل اجرا شود. دلایل:

  • هزینه‌ی نگهداری: تیم پایتون باید برای هر ویژگی جدید، دو نسخه را نگه می‌داشت. این کار، سرعت پیشرفت زبان را کم می‌کرد.
  • مشکلات امنیتی: نسخه‌های قدیمی، آسیب‌پذیر می‌شوند و پچ کردنشان هزینه دارد.
  • جامعه‌ی آماده: در ۲۰۲۰، بیش از ۹۵٪ کتابخانه‌های مهم پورت شده بودند. یعنی دلایل مهاجرت کمتر از قبل بود.

با این حال، در پروژه‌های واقعی هنوز هم می‌بینم که بعضی سایت‌ها با پایتون ۲ کار می‌کنند، معمولاً به دلیل کد قدیمی که مهاجرت نکرده. اگر با پروژه‌ای روبرو شدید که روی پایتون ۲ کار می‌کند، مهاجرت را در اولویت اول بگذارید — چون نه امن است و نه در بلندمدت قابل نگهداری.

پروژه‌ای که روی پایتون ۲ مانده، مثل ماشینی است که بنزینش تمام شده ولی راننده امیدوار است تا مقصد برسد؛ هر روز تأخیر، هزینه‌ی قطع‌شدن را بیشتر می‌کند.

سخن آخر

تفاوت پایتون ۲ و ۳، فقط یک فهرست تغییرات نیست؛ داستان یک تصمیم طراحی جسورانه است که سال‌ها روی اکوسیستم اثر گذاشت. سه نکته‌ی مهم که در این مقاله به آن‌ها رسیدیم: اول، تفاوت‌های ظاهری — مثل print و تقسیم — در چند ساعت یاد گرفته می‌شوند ولی رفتار عمیق‌شان (مثل تفاوت مصرف حافظه در range) ممکن است ماه‌ها بعد خودش را نشان دهد؛ دوم، بزرگ‌ترین مانع مهاجرت در پروژه‌های واقعی، کتابخانه‌ها هستند، نه زبان — پس هر تصمیم مهاجرت، از ممیزی وابستگی‌ها شروع می‌شود؛ سوم، اگر امروز پروژه‌ای روی پایتون ۲ دارید، مهاجرت را به تأخیر نیندازید چون هزینه‌ی هر ماه تأخیر، بیشتر از ماه قبل است.

اگر امروز فقط یک کار بکنید، پیشنهاد می‌کنم نسخه‌ی پایتون پروژه‌های فعلی‌تان را با python --version چک کنید و اگر پروژه‌ای روی ۲.۷ است، یک شاخه‌ی گیت برای مهاجرت باز کنید. تجربه‌ی خودتان از مهاجرت — مخصوصاً اگر با کتابخانه‌ای روبرو شده‌اید که پورت نشد — در دیدگاه‌ها بنویسید؛ همین تجربه‌های میدانی، برای خواننده‌ی بعدی از هر مستند رسمی ارزشمندتر است. 🐍