Rio: برنامه های وب در پایتون خالص – توضیحات فنی

ما اخیراً ریو (https://github.com/rio-labs/rio) را راهاندازی کردهایم، چارچوب جدید ما که برای کمک به ایجاد برنامههای وب و محلی با استفاده از پایتون خالص طراحی شده است. پاسخ جامعه ما برای ما بسیار مثبت و فوق العاده انگیزه بخش بوده است.
با ستایش موجی از کنجکاوی به راه افتاده است. رایج ترین سوالی که با آن مواجه شده ایم این است که “ریو واقعا چگونه کار می کند؟” اگر شما هم همین فکر را کرده اید، در جای درستی هستید! در اینجا ما عملکرد درونی ریو را بررسی می کنیم و آنچه را که آن را بسیار قدرتمند می کند کشف خواهیم کرد.
موضوعات زیر را بررسی خواهیم کرد:
کامپوننت ها Dataclass هستند
در میان کلاس ها و عملکردهای متعدد در ریو، یکی از آنها ضروری است: rio.Component
. این کلاس پایه در سراسر کد وجود دارد و پایه و اساس هر مؤلفه در یک برنامه را تشکیل می دهد، از جمله مؤلفه های تعریف شده توسط کاربر و اجزای داخلی.
اجزای موجود در ریو چندین مسئولیت کلیدی دارند. در درجه اول، آنها وضعیت برنامه شما را مدیریت می کنند. به عنوان مثال، زمانی که کاربر متنی را در a وارد می کند rio.TextInput
، باید آن مقدار را ذخیره کنید. صرف درخواست ورودی کاربر بدون ذخیره آن، تجربه کاربری رضایت بخشی را ایجاد نمی کند. 🙂
برای سادهسازی ذخیرهسازی ارزش، تمام اجزای Rio به گونهای طراحی شدهاند که بهطور خودکار به عنوان کارکرد عمل کنند dataclass
es کلاسهای داده در پایتون به کاهش کد دیگ برای بسیاری از کلاسهای ساده کمک میکنند. به عنوان مثال، اگر شما نیاز به ذخیره اطلاعات در مورد یک مشتری، بدون استفاده از کلاس داده دارید، ممکن است کد شما به شکل زیر باشد:
class Cat:
def __init__(self, name: str, age: int, loves_catnip: bool) -> None:
self.name = name
self.age = age
self.loves_catnip = loves_catnip
این رویکرد کاربردی است اما کاملاً پرمخاطب است. توجه کنید که چگونه نام هر ویژگی باید سه بار تکرار شود: یک بار به عنوان پارامتر تابع، یک بار به عنوان ویژگی کلاس و یک بار برای اختصاص مقدار پارامتر به ویژگی. وقتی وراثت معرفی می شود، افزونگی حتی دست و پاگیرتر می شود:
class Animal:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
class Cat(Animal):
def __init__(self, name: str, age: int, loves_catnip: bool) -> None:
super().__init__(name, age)
self.loves_catnip = loves_catnip
class Dog(Animal):
def __init__(self, name: str, age: int, is_good_boy: bool) -> None:
super().__init__(name, age)
self.is_good_boy = is_good_boy
توجه داشته باشید که هر چند وقت یکبار مجبور شدیم نام ویژگی ها را یکسان بنویسیم. این تکرار آنقدر رایج است که توسعه دهندگان پایتون کلاس های داده را به عنوان جایگزین ساده تری معرفی کردند. در اینجا نحوه نمایش همان سناریو با استفاده از کلاس های داده آمده است:
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Cat(Animal):
loves_catnip: bool
@dataclass
class Dog(Animal):
is_good_boy: bool
این رویکرد بسیار سادهتر است و در حین دستیابی به همان نتیجه، به تلاش کمتری نیاز دارد. بهعلاوه، کلاسهای داده مزایای بیشتری مانند عملکرد مقایسه خودکار (در میان سایر موارد) ارائه میکنند که به ما امکان میدهد به راحتی بررسی کنیم که آیا دو نمونه یکسان هستند یا خیر.
از آنجایی که اجزای Rio باید مقادیر را ذخیره کنند و همه از آنها ارث ببرند rio.Component
، کاملاً منطقی است که همه آنها را به کلاس داده تبدیل کنیم. به همین دلیل است که اجزای Rio همیشه با ساختار آشنا و مشابهی همراه هستند:
class CatInput(rio.Component):
name: str
age: int
loves_catnip: bool
def build(self) -> rio.Component:
return ...
این ساختار باید آشنا به نظر برسد: ویژگیها در بالا تعریف شدهاند، وجود ندارد __init__
عملکرد، و الف build
روش برای ایجاد رابط کاربری کامپوننت استفاده می شود. هر زمان که هر یک از ویژگی ها تغییر کند، build دوباره فراخوانی می شود و کامپوننت متناسب با آن به روز می شود.
این سؤال را ایجاد می کند: ریو چگونه تشخیص می دهد که یک جزء تغییر می کند؟
مشاهده ویژگی ها (معروف به من، احساس می کنم تحت نظر هستم!)
همانطور که قبلاً دیدیم، یکی از دلایلی که کامپوننت های Rio کلاس داده هستند، راحتی است. توجه کنید که چگونه در dataclass
نسخهی Animal
، ما به صراحت به پایتون اطلاع می دهیم که کلاس دارای کدام فیلدها و انواع آنها است. مثلاً داریم می نویسیم name: str
. به همین دلیل، پایتون دقیقاً می داند که هر حیوان دارای یک فیلد به نام خواهد بود name
و مقدار آن فیلد همیشه یک رشته خواهد بود.
آن را با کلاس معمولی مقایسه کنید. ما به پایتون می گوییم که __init__
تابع یک پارامتر فراخوانی می پذیرد name
، و ما آن مقدار را در کلاس کپی می کنیم، اما هرگز در مورد آن به پایتون نمی گوییم. مفسر هیچ سرنخی ندارد که کلاس با کدام فیلدها می آید و نوع داده آنها چیست.
فیلدهای واضح نه تنها ایده خوبی هستند، زیرا به توسعه دهندگان دیگر در خواندن کد شما کمک می کنند، بلکه به این دلیل که کتابخانه هایی مانند Rio نیز می توانند آنها را بخوانند. از آنجایی که ما به صراحت فیلدهای خود را فهرست کردهایم، ریو اکنون میداند که کلاس دارای چه مقادیری است و آنها را برای تغییرات برای ما مشاهده میکند. چگونه؟ ساده، با استفاده __setattr__
.
کلاس های پایتون می توانند متدهای جادویی داشته باشند. قبلاً اینها را دیدهاید، آنها روشهایی هستند که با دو خط زیر شروع و پایان مییابند. __init__
یکی از آن روش ها است.
روش دیگر این است __setattr__
. پایتون این تابع را هر بار که مقداری به ویژگی کلاس اختصاص می دهید فراخوانی می کند. میتوانید از این برای ذخیره مقادیر در یک مقدار JSON، هر زمان که به آنها اختصاص داده شد، استفاده کنید. یا شاید شما در حال اشکال زدایی کد هستید و می خواهید print
ارزش های جدید وقتی تغییر می کنند. یا، البته، شاید بخواهید هر زمان که یک ویژگی تغییر کرد، مطلع شوید تا بتوانید UI را بازسازی کنید. این دقیقاً همان کاری است که ریو انجام می دهد.
class MyClass:
def __setattr__(self, name, value):
print(f"Setting {name} to {value}")
self.__dict__[name] = value
به همین دلیل بسیار مهم است که هنگام تغییر ویژگی یک جزء، مقدار جدیدی را به آن اختصاص دهید. اگر یک ویژگی را بدون تخصیص تغییر دهید (مثلاً با اضافه کردن به یک لیست)، پایتون راهاندازی نمیکند. __setattr__
، و ریو نمی داند که مؤلفه شما نیاز به به روز رسانی دارد. اگر کامپوننت شما پاسخگو نیست، کد خود را بررسی کنید تا مطمئن شوید که مقادیر جدیدی به ویژگی ها اختصاص می دهید.
تفاوت و آشتی
بنابراین، ریو تشخیص داده است که مؤلفه شما تغییر کرده است، build
فراخوانی شده است و مجموعه جدیدی از اجزا ایجاد شده است. بعد چه اتفاقی می افتد؟ آیا ریو باید به سادگی قطعات قدیمی را کنار بگذارد و آنها را با قطعات جدید جایگزین کند؟ نه! اجزای قبلی ممکن است حاوی بسیاری از حالت های مهم باشد که باید حفظ شود. این سناریو را تصور کنید: یک کاربر نام کامل گربه را در برنامه شما وارد کرده است، برخی از سوئیچ ها را تغییر داده و یک مورد را از منوی کشویی انتخاب کرده است. سپس، ممکن است با فشار دادن یک دکمه یا تغییر اندازه پنجره، یک رویداد را راه اندازی کنند. کنترل کننده رویداد شما فراخوانی می شود و یک جزء را به روز می کند. اگر قرار بود ریو اجزای قدیمی را با قطعات جدید جایگزین کند، آن نام گربه گرانبها برای همیشه گم می شد.
در عوض، ریو از مجموعه ای از تکنیک ها به نام استفاده می کند تفاوت و آشتی. اولین قدم یافتن تطابق بین اجزای قدیمی و جدید است. به عنوان مثال، اگر build
تابعی که قبلاً برگردانده شده است TextInput
و a را برمی گرداند TextInput
دوباره، احتمالاً همان مؤلفه با وضعیت به روز شده است. این فرآیند یافتن جفتهای تطبیق به عنوان diffing شناخته میشود. ایده اصلی ساده است: ریو به صورت بازگشتی خروجی جدید و قدیمی تابع ساخت را پیاده می کند. اگر با دو مؤلفه از یک نوع مواجه شود، مطابقت محسوب می شوند. سپس ریو به جستجوی فرزندان این کامپوننت ادامه می دهد و در آنجا نیز به دنبال مسابقات می گردد. اگر دو مؤلفه مطابقت نداشته باشند، جستجوی بازگشتی متوقف می شود و جزء جدید در نظر گرفته می شود. این تطابق “ساختاری” در بیشتر موارد به خوبی کار می کند و اکثر موارد مشابه را شناسایی می کند.
با این حال، شرایطی وجود دارد که کافی نیست. برای مثال، فهرستی از اجزای متعدد را تصور کنید. بسته به اقدامات کاربر، ممکن است اجزای جدیدی اضافه یا حذف شوند که ترتیب ورودی های موجود را تغییر می دهد. اگر بخواهیم به سادگی اولین مؤلفه در لیست قدیمی را با اولین مؤلفه در لیست جدید مطابقت دهیم، به سرعت با مشکلاتی مواجه خواهیم شد. برای جلوگیری از این امر، ریو از تکنیک دوم مبتنی بر کلیدهای واضح استفاده می کند. شاید متوجه شده باشید a key
پارامتر در یک جزء ریو قبل از. این پارامتر اختیاری برای همه اجزاء مشترک است و برخی حتی از شما می خواهند که یکی را تنظیم کنید.
اگر یک جزء در build
خروجی همان کلید یک مؤلفه در خروجی قبلی را دارد، آنها بدون توجه به تغییرات در موقعیت اصلی یا کلی آنها همیشه مطابقت در نظر گرفته می شوند. این به Rio اجازه میدهد تا اجزایی را که جابجا شدهاند ردیابی کند و آنها را بر اساس آن بهروزرسانی کند.
اکنون، با شناسایی جفتهای منطبق از اجزا، مرحله آشتی آغاز میشود. ریو اجزای جفت شده را مقایسه می کند و تعیین می کند که کدام ویژگی ها را از هر نسخه حفظ کند:
مشخصه هایی که به صراحت روی مؤلفه جدید تنظیم کرده اید همیشه اولویت دارند
ویژگی هایی که پس از مقداردهی اولیه در مولفه قبلی تغییر کرده اند، حفظ می شوند
در نهایت، هر ویژگی دیگر از جزء جدید گرفته می شود
این قوانین تعادل خوبی برقرار میکنند و همیشه ارزشهای جدید را رعایت میکنند و در عین حال وضعیت قبلی را حفظ میکنند، اگر احتمالاً عمداً تنظیم شده باشد.
این اولین قسمت از سری شیرجه عمیق ما به پایان می رسد. قسمت بعدی در هفته های آینده در دسترس خواهد بود. در ضمن، به سرور Discord ما بپیوندید! اگر هنوز در ابتدای سفر هستید، میتوانید برنامههای جالبی را که ساختهاید به نمایش بگذارید یا کمک بگیرید.
منبع: https://github.com/rio-labs/rio