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


به عنوان یک برنامه نویس همیشه دلم می خواست برنامه ام قابلیت افزودن پلاگین رو داشته باشه. پس شروع کردم به تحقیق راجب سیستم های پلاگین. خیلی هم دلم می خواست بدونم که مثلا cms هایی مثل wordpress چه شکلی اینقدر پلاگین های قدرتمندی دارند که با استفاده از اونها به کلی میشه یه وبسایت ساده wordpress رو زیر و رو کرد.

پلاگین ها به ما این قابلیت رو میدن که بدون دست زدن به سورس کد، رفتارش رو موقع run time (تو پایتون که اینطوریه) تغییر بدیم.

ویژگی ها، قوانین و نحوه کار:

تو جریان تحقیقاتم با نحوه کار بعضی از سیستم های پلاگین آشنا شدم ولی هیچ کدومشون باب میل من نبود. اکثرا از event ها استفاده می کردند که فکر می کنم در نوع خودش میتونه خیلی قدرتمند باشه ولی چیزی که من از یک سیستم پلاگین می خواستم این بود.

ویژگی ها

  • ساده باشه (مهم ترین)
  • به من قابلیت override برخی از method های برخی از class های توی سورس کد برنامه رو موقع run time بده (همین و وااسلام). اینطوری اگه ساختار برنامه به صورت object oriented باشه و قسمت های مختلف برنامه رو به توابع کوچک تری تقسیم کنیم (مثل class based view ها در جنگو) میشه به پلاگین ها قابلیت شخصی سازی خیلی زیادی رو داد.

قوانین

  • تموم پلاگین ها باید در پوشه plugin باشند (مثل wordpress)
  • همه باید ماژول های پایتون باشند.
  • نباید با "_" شروع بشن. چون در غیر این صورت به عنوان یک فایل private که پلاگین نیست محسوب میشن و اصلا لود نمیشن. گفتم این ویژگی رو هم اضافه کنم چون شاید کسی بخواد یک سری فایل توی این پوشه قرار بده ولی ازشون به عنوان پلاگین استفاده نکنه
  • همه این ماژول های پلاگین باید یک class به نام Plugin داشته باشند. خود این کلاس plugin هم باید از کلاسی به نام BasePlugin ارث بری کنه و دو متد به نام های load و unload رو داشته باشه حتی اگه خالی باشند. در اینجا BasePlugin تقریبا شبیه به یک interface عمل می کنه (تو پایتون interface نداریم اما توی زبان های خانواده c، مثل جاوا و c# وجود دارند). دلیل اینکه می خوام از interface استفاده کنم اینه که مطمئن بشم که حتما دو متد load و unload وجود خارجی دارند. چون همونطور که می دونید تو پایتون میشه توابعی رو که اصلا معلوم نیست از کجا اومدن رو صدا زد و این در run time موجب ارور میشه.

نحوه کار:

  • برنامه اصلی شروع به کار می کنه و در اول برنامه تمام ماژول های پلاگین رو پیدا و load میکنه.
  • پلاگین ها برنامه رو دستکاری می کنن
  • برنامه به روال عادیش ادامه میابه
  • در نهایت همه پلاگین ها unload میشن

حرف زدن بسه، بریم سراغ کد نویسی

خوب میدونم که تا اینجا رو اسکرول کردید ':).

اول از همه این ساختار پروژه ماست:

PyPlugin (project root)
| - - - UTILS (utility modules)
        | - - - plugin (plugins are inside this directory)
                | - - - __init__.py
                | - - - samplePlugin.py (self-explanatory)
        | - - - __init__.py
        | - - - pluginSystem.py (to work with plugins)
| - - - main.py (program's entry point)

ما برنامه اصلی (که بعدا قراره رفتارش رو توسط پلاگین ها تغییر بدیم) رو در فایل main.py پیاده سازی می کنیم. برای اینکه تمرکز روی پیاده سازی سیستم پلاگین باشه؛ کد های خیلی ابتدایی و ساده ای رو برای برنامه در نظر گرفتم.

حالا فایل main.py رو باز کرده و کد های زیر رو در آن وارد کنید.

class Program:
    def welcomeUser(self): return &quotHello user&quot
    def main(self):
         print (self.welcomeUser())

if __name__ == &quot__main__&quot:
    Program().main()

خوب الان اگه این برنامه رو اجرا کنید تنها چیزی که انجام میشه اینه که Hello user توی کنسول چاپ میشه. جالبه کل کد بالا فعلا برابر است با کد زیر :-()

print (&quotHello user&quot)

ولی خوب قابلیت شخصی سازی خیلی خوبی داره. مثلا فرض کنید که می خوایم به جای کلمه user از ما اسممون رو بپرسه و بعد با همون بهمون خوش آمد بگه. تنها کاری که باید کنیم اینه که تابع welcomeUser رو توسط یک پلاگین تغییر بدیم. اما چطور باید این کار رو بکنیم؟

خوب اول از همه باید یه سیستم پلاگین داشته باشیم که با استفاده از اون بتونیم پلاگین ها رو لود کنیم. خوب حالا وارد فایل pluginSystem.py در پوشه اصلی پروژه بشید و این کد ها رو وارد کنید:

https://gist.github.com/Amohammadi2/540a9bbe9d3ced244e3deca6504ebd35

کتابخانه های استفاده شده:

  • ماژول os. برای لیست کردن فایل های فولدر plugin و همچنین حذف کردن پسوند فایل ها
  • ماژول ABC، برای ساختن کلاس های abstract. در اصل تو پایتون همچین چیزی نداریم و این ماژول فقط شبیه سازی کلاس های abstract رو انجام میده. وقتی یک کلاس abstract باشه از اون فقط میشه به عنوان بیس استفاده کرد یعنی نمی تونید از روش instance به طور مستقیم بسازید. برای استفاده از این کلاس شما باید یک کلاس دیگه درست کرده و ازش ارث بری کنید. در ضمن تمام متد های abstract اون کلاس رو هم باید پیاده سازی کنید وگرنه پایتون بهتون ارور میده. توجه کنید که کلاس های abstract باید حداقل یک عضو abstract داشته باشند. دلیلش اینه که مکانیزم کلاس های abstract پایتون طوریه که پایتون به دنبال عضو های abstract میگرده و اگر هیچ عضو abstractای در اون کلاس نباشه، به این معنیه که هیچ چیزی نیاز به پیاده سازی نداره و موقع ساختن instance هم هیچ اروری بابتش نمیگیرید و با این حساب اون کلاس اصلا abstract نیست. (نکته: وقتی از کلاس abstract ارث میبرید، و اعضای abstract اون رو پیاده سازی می کنید در اصل یک override انجام میشه. اگه توجه کنید اعضای abstract با دکوریتور abstractmethod مشخص میشوند. وقتی که شما اون عضو رو override می کنید اون دکوریتور هم از بین میره و وقتی این اتفاق بیفته مثل اینه که دیگه هیچ عضو abstractای وجود نداره و می تونید به طور مستقیم از اون کلاس استفاده کنید)

توضیحات

تابع getPlugins لیستی از پلاگین هایی که در پوشه plugin هستند رو بر میگردونه. اینم مراحل کارشه

  • متغیری تعریف کردیم به نام plugins_path که به ما مسیر رسیدن به فایل های پلاگین رو نسبت به main.py نشون میده (دلیلش اینه که این تابع در اصل قراره از فایل main.py صدا زده بشه پس ما فرض رو بر این گذاشتیم که فولدر فعلی ما، همون فولدری است که فایل main.py در اون قرار گرفته (PyPlugin).
  • با استفاده از os.listdir لیستی از تمام فایل های داخل فولدر plugin رو میگیره.
  • از اونجایی که این لیست میتونه شامل فایل های غیر پایتون باشه سه تا شرط رو برسی می کنیم. 1-نباید با "_" شروع بشه 2- باید آخرش پسوند .py داشته باشه 3- باید فایل باشه (نه فولدر)
  • حالا که مطمئن شدیم یک ماژول پایتون است، با استفاده از importlib فایل رو import می کنیم.
  • بعد از روی کلاس Plugin (این کلاس حتما باید در همه پلاگین ها تعریف شده باشه) یک instance میسازیم و اون instance در لیست قرار خواهد گرفت

در ضمن در این کد از list comprehension استفاده شده که می تونید راجبش در مطلب زیر مطالعه کنید

https://realpython.com/list-comprehension-python/

در قدم بعدی میریم سراغ کلاس BasePlugin.

  • تمام پلاگین ها از این کلاس باید ارث بری کنند (هیچ اجباری (حداقل در پایتون) نیست. می تونن ارث بری نکنند. ولی باید حتما دو متد load و unload رو داشته باشند)
  • این کلاس برای این که abstract شناخته بشه از ABC ارث بری کرده
  • همچنین دو متد به نام های load و unload داره که برای اینکه نشون بدیم abstract با دکوریتور abstractmethod اونها رو دکوریت کرده ام

خوب تا به اینجا حدود 1/2 کار ها رو انجام داده ایم.

  • 1/4 کار: گرفتن پلاگین ها و صدا زدن متد load و unload هر کدومشون در جاهای مناسب در فایل main.py
  • 1/4 کار: نوشتن اولین پلاگین

خوب وارد فایل main.py بشید کمی تغییرش بدید

https://gist.github.com/d343c35a47e98839ced9163aab35ec38.git


توضیحات:

  • یک class property به نام plugins در کلاس Program ایجاد کرده ایم که لیستی از plugin های ما را شامل میشود.
  • در خط 12 همه پلاگین ها لود میشن و یک instance از روی کلاس Program به عنوان پارامتر اول به تابع load همه پلاگین ها داده میشه. پلاگین ها اینجا می تونن تغییرات رو اعمال کنند (نکته: بنابر این تموم پلاگین ها باید تابع load رو طوری پیاده سازی کنند که یک آرگومان قبول کنه)
  • در ادامه تابع main رو صدا میزنیم تا برنامه شروع به کار کنه
  • در نهایت تابع unload همه پلاگین ها فرخوانی میشه تا کار های نهایی رو قبل از پایان برنامه انجام بدهند.

نوشتن پلاگین

وارد پوشه plugin بشوید و کد زیر را در آن وارد کنید.

https://gist.github.com/Amohammadi2/ec5aaf4d320163cb0ecc5ab78e37c6d4

توضیحات: وقتی که پلاگین لود میشه، ما یک instance از روی کلاس Program که در فایل main.py تعریف کرده ایم می میگیریم. همونطور که می دونید روی این instance متدی به نام welcomUser وجود داره. ما اون متد رو با یه تابع دیگه عوضش می کنیم و بدین صورت رفتارش رو به دلخواه خودمون تغییر میدیم

برای تغییر دادن welcomUser از تابع overrideWelcomeUser استفاده کرده ام. تابع overrideWelcomeUser، آرگومانی به نام original_function وجود داره که در اصل نسخه اصلی تابع wlecomeUser است. داخل overrideWelcomeUser، یک تابع داخلی به نام innerFunction می بینیم. در اصل این تابع قراره با تابع اصلی عوض بشه.

حالا شاید براتون این سوال پیش بیاد که خوب چرا من مستقیما از overrideWelcomeUser استفاده نکرده ام؟ دلیلش اینه که اگه لازم شد بتونم از نسخه اصلی تابع استفاده کنم وگرنه فرقی نداره، شما می تونید کل welcomeUser رو از اول بنویسید و از نسخه اصلی استفاده نکنید

نکته: اگر overrideWelcomeUser رو مستقیما به instance کلاس Program بدهیم. در عملکرد کلیدواژه self تاثیری نمیگذارد. یعنی self به یک instance از کلاس Plugin اشاره خواهد کرد نه به یک instance از نوع Program

اجرای برنامه و دیدن نتایج:

کاری نداره که، فایل main.py رو اجرا کنید تا نتیجه رو مشاهده کنید.

خواهید دید که برنامه ازتون اسمتون رو میپرسه و خوش آمد میگه.

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