ساخت سازوکار پلاگین در پایتون

اشتراک‌گذاری

حتماً تا به حال با برنامه هایی سروکار داشتید که امکان نصب افزونه داشتند. در این پست قرار هست بهتون بگم چطور میشه توی پایتون برنامه‌ای بنویسیم که بشه بهش افزونه اضافه کرد.

معماری افزونه‌ای (plugin architecture) یک شیوه توسعه نرم‌افزار است که به برنامه‌نویس این امکان رو میده که بدون نیاز به ویرایش کد های اصلی برنامه‌ش (هسته/main) قابلیت ها رو با ارائه بسته‌هایی به نام پلاگین در اختیار کاربران قرار بده و کاربران هم می‌تونن به سادگی با توجه به نیازشون از بین این افزونه ها، اون هایی که می‌خوان رو به برنامه‌شون اضافه کنن.

از طرفی میشه از این معماری به عنوان یک پلن تجاری نیز استفاده کرد. مثلا شما می‌تونید برنامه خودتون رو به صورت رایگان و حتی متن‌باز عرضه کنید اما با فروش پلاگین ها (و یا ارائه خدمات) کسب درآمدکنید. اگر شما هم یک کاربر وردپرس باشید حتما با این پلن آشنا هستید! 😄

پ.ن: پروژه telegram-post-bot من در گیت‌هاب از همین ساختار پیروی می‌کنه و توش امکان اضافه کردن پلاگین هست و درواقع همین پروژه بهانه‌ای شد که من پیاده‌سازی این معماری رو توی پایتون یاد بگیرم و امتحان کنم.

خب وقتشه که دیگه مقدمه رو بذاریم کنار و بریم سراغ اصل مطلب!

پلاگین ها در قالب ماژول ها

ایده کلی برای پیاده‌سازی این ساز و کار این هست که برنامه ما بتونه به نحوی پلاگین ها که در فایل های پایتونی جدا هستن وارد (import) کنه و از محتویات اون ها استفاده کنه.

طرح یک مدل برای پلاگین ها

برای راحتی کار شما و بهبود کد هاتون پیشنهاد می‌کنم یک مدل طراحی کنید، هرچند که این مورد اختیاری است. در واقع شما با طراحی یک کلاس مجازی abstract می‌تونید توابع و خواصی که از پلاگین ها در نظر دارید رو تعیین کنید. با استفاده از ماژول ABC پایتون می‌تونید به سادگی یک کلاس مجازی بسازید؛ با این کار می‌تونید مطمئن باشید که تمامی توابع و خواص مورد انتضار شما در پلاگین ها موجود هستند و قابل استفاده‌اند. درواقع اگر در پلاگین توابع تعریف نشوند پایتون یک استثنا ایجاد خواهد کرد. نمونه زیر یک مثال از یک مدل برای پلاگین هاست است (همون‌طور که مشخصه این کد‌ها مربوط به رباتی هست که دارم می‌نویسم! البته با کمی تغییر):

from abc import ABC, abstractmethod, abstractproperty

class ParserModel(ABC):
    @abstractmethod
    def __init__(self, config):
        self.config = config

    @abstractmethod
    def new_posts(self) -> Iterable[MessageModel]:
        """Bot core will call this method after each interval to get a list of new posts"""
        pass

    @abstractmethod
    def last_post(self) -> Iterable[MessageModel]:
        """The method that will be call when user sends `/last` command"""
        pass

    @abstractproperty
    def properties(self) -> dict:
        pass

    @properties.setter
    def properties(self, value:dict):
        pass

برای این که بتونیم توابع و ویژگی ها رو بصورت مجازی در این کلاس بسازیم باید از دو دکوریتور (decorator) @abstractmethod برای توابع و @abstractproperty برای ویژگی ها (property) استفاده کنیم. کمی پایین‌تر درمورد property در پایتون توضیح می‌دم ولی فعلا همینقدر بدونید که تو مثال بالا وقتی کارمون تموم بشه با property میشه یه همچین کارایی کرد:

x.properties = 4     # call setter
a = x.properties     # call getter

حالا که مدل رو ساختیم، کلاس هایی که از این مدل ارث بری می‌کنن حتما باید دارای این متد ها و ویژگی ها باشن وگرنه پایتون بهشون گیر میده! خط ۴ باعث میشه که خود کلاس ParserModel رو نشه مستقیم استفاده کرد (یعنی نمیشه اون رو init کرد)، در واقع دیگه هیچ جایی از کد نمیشه کد زیر رو نوشت. به این کار میگن نمونه سازی یا ساختن instance.

x = ParserModel(config)

با نوشتن خط ۴، نمونه‌سازی از کلاس مجازی باعث ایجاد یک استثنا می‌شود. (و حذف خط ۴ باعث می‌شود که بتونید نمونه‌سازی کنید) با این حال کلاس های فرزند همیشه می‌تونن کلاس والد خودشون رو init کنن. اون ها همیشه با تابع super() به کلاس والد خودشون دسترسی دارن:

class Parser(ParserModel):
    def __init__(config):
        super().__init__(config)
...

این خطوط در کلاس فرزند در مثال ما باعث می‌شود تا کلاس فرزند دارای خاصیت config شود، یعنی در ادامه کد بالا میشه نوشت:

    @property
    def properties(self):
        return self.config.get('properties')

خب با کد بالا یک getter یا گیرنده برای ویژگی (property) مون ایجاد کردیم. این تابع زمانی که کاربر می‌خواد مقدار properties رو بگیره فراخونی میشه. موقع استفاده از ویژگی انگار داریم از یه متغیر مربوط به نمونه استفاده می‌کنیم، یعنی بعد از نمونه سازی از کلاسمون میشه نوشت a = x.property این کد باعث فراخونی این تابع میشه. چون کلاس مدل برای ویژگی properties تنظیم کننده یا setter هم تعیین کرده پس تو کلاس فرزند هم باید این کار رو انجام داد:

    @properties.setter
    def properties(self, value):
        self.config['properties'] = value

تابع بالا هم موقع ست کردن فراخونی میشه. یعنی بعد از نمونه‌سازی میشه نوشت x.property = a . بعد از این که باقی متد ها رو تو کلاس فرزند هم نوشتید حالا میشه کلاس فرزند رو بدون هیچ خطایی initiate کرد. البته کلاس های فرزند می‌تونن شامل توابع و ویژگی های دیگری هم باشن، اما شما دیگه با آن ها کاری ندارید! تمامی این کار ها باعث میشه تا شما از بابت کلاس های فرزند خیالتان راحت باشه و همچنین نوشتن کد های هسته براتون راحت تر بشه، چون دیگه می‌تونید از suggestion های ویرایشگرتون هم استفاده کنید!

البته یک کار هنوز باقی مونده، اون هم این که توی کد اصلی مطمئن بشید کلاس پلاگینی که دارید استفاده می‌کنید از مدل ما ارث بری کرده یا نه. این رو بعد از این که گفتم چطور import رو انجام بدید می‌گم.

وارد کردن پلاگین ها

خوشبختانه پایتون یک ماژول داره به نام importlib این ماژول به شما اجازه میده که توی کدتون و در زمان اجرا ماژول رو وارد کنید و به کلاس ها (و محتویات) داخل اون دسترسی داشته باشید. کد زیر برای وارد کردن پلاگین با توجه به کانفیگ هست. توی پروژه من اینطوریه که کاربر توی کانفیگ می‌گه کدوم پلاگین لود بشه، این مورد دست شماست که چطوری پلاگین یا ماژول رو مشخص کنید؛ مثلا می‌تونید کل دایرکتوری رو اسکن کنید و هر ماژولی که بود رو به یه لیست اضافه کنید و همه‌شون رو init کنید و …

در پروژه‌ی من آدرس هر پلاگین parser به شکل plugins/parser/[plugin-name]/plugin.py هست. پس برای وارد کردن اون ها باید علامت / رو با نقطه جایگزین و py رو هم از آخرش برداشت که میشه

plugins.parser.[name].plugin

که خب مشخصا name باید با اسم پوشه پلاگینه عوض بشه

import importlib
...
parser_name = config['parser']
parser_config = config['parser-config']
parser_module = importlib.import_module('.'.join(['plugins','parser', parser_name, 'plugin']))
parser:ParserModel = parser_module.Parser(parser_config)

پ.ن: البته اگه کد‌های پروژه من رو ببینید کمی با کد بالا فرق داره چون اونجا دارم از peewee برای کار با دیتابیس استفاده می‌کنم و یکسری کد برای رد و بدل دیتابیس ها و .. هست

کنترل کلاس پلاگین

اگر توی کد بالا دقت کنید راحت تر هستید که مشخص کنید اسم کلاسِ پلاگین چی باشه، این رو می‌تونید توی مستندات بگید. هر چند که در غیر این صورت هم راه های دیگه‌ای هست، ولی پیچیدگی کار بیشتر میشه.

اما مورد مهمی که قرار بود بگم اینه که از کجا مطمئن بشیم کلاس پلاگین از مدل ما ارث‌بری کرده. برای چک کردن این موضوع می‌تونید از تابع issubclass() استفاده کنید. مختارید که به کاربر یک هشدار ساده نمایش بدید، یک استثنا ایجاد کنید یا برنامه را ببندید؛ ریش و قیچی دست شماست!

if not issubclass(type(parser),ParserModel):
    print('ERROR: Imported NON-STANDARD plugin, this may cause critical issues')

حالا به سادگی می‌تونید از توابع و ویژگی های پلاگین استفاده کنید. مثلا:

posts = parser.new_posts()

Mission accomplished

امیدوارم این مطلب براتون مفید باشه، اگر سوالی داشتید در نظرات همین مطلب از ما بپرسید و یا تو گروه های ما در تلگرام و ماتریکس مطرح کنید. ممنون میشم اگه از پروژه telegram-post-bot من هم حمایت کنید و بهش ستاره بدید 😉. شاد و موفق باشد 👋

اشتراک‌گذاری

2 دیدگاه برای “ساخت سازوکار پلاگین در پایتون”

  1. avatar

    جالبه!
    توی پایتون بر خلاف سی‌شارپ و سی‌پلاس‌پلاس متد virtual وجود نداره
    مثل اینکه اینجوری با decorator میشه اضافه‌اش کرد

    1. avatar

      درواقع وقتی دیدن نیستش اومدن و با دکوریتور یه چیزی شبیه به اون رو پیاده کردن، منم چون قبلا C# گشتم که چطور میشه چنین کاری رو توی python انجامداد که این رو دیدم