درک ترشی برای اینکه دکوراتورها با چند پردازش کار کنند


نویسنده(های): هان چی

در ابتدا منتشر شد به سمت هوش مصنوعی.

عکس توسط هرت نیکس در پاشیدن
import os
from multiprocessing import Pool
import time
from functools import wraps
import heartrate

port_base = 10000

def initialize_worker():
# This function runs only in the worker processes
process_id = os.getpid()
port = port_base + process_id % 10000 # Unique port for each process
print(f"Tracing on port {port} for process {process_id}")
heartrate.trace(browser=True, port=port)

def track_execution_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
print(f"Starting task at {start_time}")
result = func(*args, **kwargs)
end_time = time.time()
print(f"Ending task at {end_time}")
print(f"Task duration: {end_time - start_time}")
return result

return wrapper

@track_execution_time
def meaningless_task(dummy_text_partial):
words = dummy_text_partial.split()
lorem_count = sum(1 for word in words if word.lower() == "lorem")

for i in range(5):
time.sleep(1)

return lorem_count

def main():
dummy_text = """
Lorem ipsum dolor sit amet, consectetur Lorem adipiscing Lorem elit
"""

with Pool(processes=2, initializer=initialize_worker) as pool:
results = pool.map(meaningless_task, dummy_text.split(","))

print("Word count results:", results)
pool.close()

if __name__ == "__main__":
main()

کد بالا از 2 کارگر در یک استخر چند پردازشی برای شمارش تعداد رشته استفاده می کند lorem در هر بند تولید شده با تقسیم یک جمله روی کاما ظاهر می شود.

منطق پردازش کارگر هدف این مقاله نیست، اما برمی گردد Word count results: [1, 2] چون Lorem ipsum dolor sit amet 1 دارد lorem و consectetur Lorem adipiscing Lorem elit 2 دارد.

ضربان قلب لازم است pip install heartrate (https://github.com/alexmojaki/heartrate) اگر می خواهید ردیابی اجرای فانتزی داشته باشید.
در غیر این صورت حذف کنید import heartrate ، کل را حذف کنید def initialize_worker عملکرد و حذف کنید initializer=initialize_worker در استخر

مشکل

اگر کامنت بگذارید خط را بنویسید @wraps(func) ، باید دریافت کنید
AttributeError: Can’t pickle local object ‘track_execution_time..wrapper’

چرا این یک مشکل است

  1. پردازش چندگانه نیاز به ترشی تابع کارگر دارد (meaningless_task در مثال بالا).
  2. ترشی کردن یک تابع مستلزم یافتن تابع در محدوده جهانی ماژول است
  3. دکوراتورها توابع را بسته بندی می کنند و تابع دیگری به همین نام را برمی گردانند (در صورت استفاده از نحو @). این توابع پیچیده شده (def wrapper) در یک تابع تزئینی تعریف می شوند (def track_execution_time). پس از بازگشت تابع تزئین، تابع پیچیده از محدوده خارج می شود و بنابراین نمی توان آن را در محدوده جهانی یافت.
عکس توسط وال وسا در پاشیدن

چگونه می کند functools.wraps مشکل را حل کند؟

wraps ویژگی ها را از تابع خام به تابع تزئین شده کپی می کند، بنابراین ترشی می تواند آنچه را که نیاز دارد به دست آورد.

از https://docs.python.org/3/library/functools.html#functools.update_wrapper، wraps ویژگی های تعریف شده در را کپی می کند WRAPPER_ASSIGNMENTS (__module__، __name__، __qualname__، __annotations__، __type_params__، و __doc__)

ترشی به کدام ویژگی نیاز دارد؟

از https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled:

توجه داشته باشید که توابع (توکار و تعریف شده توسط کاربر) به طور کامل ترشی می شوند نام واجد شرایط، نه از نظر ارزش. [2] این بدان معنی است که فقط نام تابع، همراه با نام ماژول و کلاس های حاوی ترشی است. نه کد تابع و نه هیچ یک از ویژگی های تابع آن ترشی نیست. بنابراین ماژول تعریف کننده باید در محیط unpickling قابل واردات باشد و ماژول باید حاوی شی نامگذاری شده باشد، در غیر این صورت یک استثنا ایجاد می شود.

ترشی نیاز دارد __qualname__ از تابعی که به عنوان یک نام قابل دسترسی جهانی انتخاب شده است. track_execution_time..wrapper در AttributeError در بالا مسیر را از محدوده جهانی ماژول توصیف می کند، اما پوشش دیگر قابل دسترسی نیست.

چرا از روکش ها استفاده کنیم

شما نیازی به این کار ندارید، اما خوب است که Wraps سایر ویژگی های مفید را کپی کنید، در صورتی که می خواهید از آنها استفاده کنید، مانند __docs__ برای نشان دادن مستندات

def track_execution_time(func):
# @wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
print(f"Starting task at {start_time}")
result = func(*args, **kwargs)
end_time = time.time()
print(f"Ending task at {end_time}")
print(f"Task duration: {end_time - start_time}")
return result

wrapper.__qualname__ = func.__qualname__

return wrapper

میتونستی حذف کنی @wraps(func) و انجام شد wrapper.__qualname__ = func.__qualname__ مانند بالا (هر دو دارای یک مقدار هستند meaningless_task ) پس از انجام تکلیف.

چرا ترشی به __qualname__ نیاز دارد

Pickle به نام شیء در حال ترشی نیاز دارد، بنابراین می تواند از آن نام برای پیدا کردن تعریف هنگام برداشتن ترشی استفاده کند.
وقت آن است که از سوراخ خرگوش به مثال دیگری بروید تا اصول ترشی را یاد بگیرید.

عکس توسط تینه ایوانیچ در پاشیدن
import pickle

def add(x, y):
return x + y

# with open("func.pkl", "wb") as f:
# pickle.dump(add, f)

pickled = pickle.dumps(add)

# del globals()["add"]

# globals()["add"] = lambda x, y: x * y

# with open("func.pkl", "rb") as f:
# loaded_add = pickle.load(f)

loaded_add = pickle.loads(pickled)

print(loaded_add(2, 3))

کد بالا باید ترشی شود و با موفقیت برداشته شود و بعد از افزودن 2+3 عدد 5 را چاپ کند.

حذف شیء ترشی بین ترشی و ترشی کردن

اگر کامنت نگذارید del globals()[“add”] ، باید AttributeError را ببینید: Can’t get attribute ‘add’ on

این بدان معناست که برداشتن ترشی انجام نشد.

ترشی نیاز به __qualname__ از شیئی که ترشی می شود تا در سطح جهانی قابل دسترسی باشد. با حذف مصنوعی آن، مرحله unpickling قادر به یافتن آن نیست.

این مشکل کمی متفاوت از قبل است. قبلا حتی نمی توانستیم ترشی کنیم. در اینجا می توانیم ترشی کنیم اما نمی توانیم ترشی کنیم.
با این حال، این شکست غیرمستقیم دلیل آن را توضیح می دهد __qualname__ باید در مثال ضربان قلب به درستی مشخص شود.

درج پیاده‌سازی جعلی برای رفع مشکل

اگر کامنت نگذارید globals()[“add”] = lambda x, y: x * y ، خروجی 6 را به جای 5 خواهید دید. (add = lambda x, y: x * y هم کار میکنه)
زیرا جمع به ضرب تغییر کرد (2 * 3 = 6).

این کد را بازنویسی می کند def add قبلا تعریف شده است.

این نشان می‌دهد که pickle اهمیتی نمی‌دهد که اجرای شی کدی که در ابتدا ترشی شده بود چیست.
هر کدی در زمان اجرا این فرصت را دارد که پیاده سازی بدون ترشی را تغییر دهد تا زمانی که به همان نامی که در هنگام ترشی مشاهده می شود اشاره کند.

ترشی استفاده می کند __qualname__ (در این مورد اضافه کنید) تا هر چیزی را جستجو کنید add در فرآیند OS که در حال حذف شدن است، مانند اجرای اشتباه ضرب به جای جمع، مقید است.

شما حتی می توانید ثابت های دلخواه مانند add = 2 قبل از ترشی کردن و گرفتن TypeError: ‘int’ object is not callable

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

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

عکس توسط گرگوار ژانو در پاشیدن

آیا با unpickling سعی نمی کنید عملکرد تزئین نشده را بارگیری کنید؟

از آنجایی که ما انجام می دهیم wrapper.__qualname__ = func.__qualname__ ، منطقی است که بپرسیم چگونه ترشی به عملکرد اصلی پیوند نمی خورد.

پاسخ این است که هنگام برداشتن ترشی، عملکرد قبلاً تزئین شده است. تکرار نکته کلیدی بالا:

pickle اهمیتی نمی دهد که اجرای شی کدی که در ابتدا ترشی شده بود چیست.

پیوند دادن به همان قسمت از اسناد (https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled) و نقل قول از همان بخش اما بزرگنمایی:

ماژول تعریف کننده باید در محیط unpickling قابل واردات باشد

ما فقط باید اطمینان حاصل کنیم که هر شیئی که به ترشی __qualname__ متصل است، شامل پیاده سازی مورد نظر ما باشد. اولین باری که فایل حاوی تابع اصلی اجرا شد، دکوراتور قبلا اجرا شده بود و تمام ارجاعات بیشتر به تابع اصلی در def main به نسخه تزئین شده اشاره خواهد شد.

تئوری چند پردازش

در طی چند پردازش، پایتون از 1 روش از 3 روش برای ایجاد فرآیندهای فرزند (fork/spawn/forkserver) استفاده می‌کند. https://stackoverflow.com/questions/64095876/multiprocessing-fork-vs-spawn

  • مجموعه با multiprocessing.set_start_method("spawn")
  • بررسی کنید multiprocessing.get_start_method()

در فورک (لینوکس یا ویندوز wsl، اما دیگر به صورت پیش‌فرض پایتون 3.14 را شروع نمی‌کند)، هر پردازش فرزند حافظه را از فرآیند والد به ارث می‌برد و از نقطه فورکی که وظایف در آن ارسال می‌شوند، شروع به اجرا می‌کند. Pool(processes=2, initializer=initialize_worker) (https://stackoverflow.com/a/60910365).

در spawn (لینوکس یا ویندوز بدون wsl)، هر پردازشگر فرزند، منبع را از بالای اسکریپت دوباره اجرا می کند و اشیاء مورد نیاز را دوباره وارد می کند.

forkserver ترکیبی است که ابتدا سرور دیگری ایجاد می کند، قبل از اینکه پردازش های فرزند از سرور ایجاد شود و حافظه را از آن به ارث ببرد.

در هر 3 مورد، هنگامی که یک فرآیند فرزند شروع می شود، عملکرد قبلا تزئین شده است، بنابراین ترشی کردن و برداشتن ترشی با نسخه تزئین شده کار می کند.

اینکه چگونه این واقعاً درست است بستگی به درک کد سطح پایین نشان داده شده در آن دارد https://stackoverflow.com/a/71690229 که از حوصله این مقاله خارج است.

چرا از دکوراتورها استفاده کنیم

چرا با اجرای تمام منطق اضافی در عملکرد کارگر از دکوراتورها اجتناب نکنید؟
این کار مستلزم انجام ویرایش هایی در تابع کارگر است که ممکن است مورد نظر نباشد.

دکوراتورها عملکرد کارگر را از منطق اضافی جدا می کنند.
چنین منطق اضافی نیز قابل استفاده مجدد و ترکیب است، مانند چیدن چند دکوراتور در فلاسک (https://explore-flask.readthedocs.io/en/latest/views.html#caching).

چرا ضربان قلب

زیرا این مورد استفاده ای بود که این مقاله از آن الهام گرفت.

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

در کدهای دیگر، چنین شمارش‌هایی می‌توانند استنباط کنند که اگر 1 تکرار حلقه یک کار را پردازش کند، استنباط کنیم که چند کار برای هر کارگر ارسال شده است، که به تصمیم‌گیری تخصیص کار در بین کارگران کمک می‌کند، مانند استفاده از فرآیند API سطح پایین‌تر به جای Pool.

می توانید انتظار داشته باشید 2 پنجره مرورگر باز شود که هر کدام نشان دهنده 1 فرآیند است.

شما می توانید تعداد به روز رسانی را از سایت مشاهده کنید time.sleep(1) . اگر مرورگر شما خیلی آهسته باز می شود و نمی توانید تغییرات را قبل از تکمیل کد مشاهده کنید، محدوده (5) را به محدوده (500) افزایش دهید.

برای جزئیات بیشتر یک stack trace در پایین وجود دارد.
می توانید اضافه کنید initialize_worker در فرآیند والد نیز برای اثبات عدم اجرای توابع کارگر.

در صورتی که در محدوده (500) مانند کد نادیده گرفتن Ctrl+C گیر کردید (من نمی دانم چرا در 70٪ مواقع غیرقابل پیش بینی این اتفاق می افتد)، می توانید با بستن پنجره ترمینال در IDE خود، فرآیند را از بین ببرید.

دستورات bash مفید (خط به خط به صورت تعاملی اجرا می شود) برای بازرسی روابط و پورت های والد-فرزند شما.

ps -eo pid,ppid,cmd | grep heartrate # find parent pid and child pid
pstree -p 126489 # assumes you know parent pid

نسل بندر

Heartrate نیاز به ایجاد یک پورت جدید برای هر فرآیند دارد و من در ابتدا سعی کردم از یک دکوراتور برای ایجاد پورت های افزایشی در هر فرآیند استفاده کنم. این شکست خورد زیرا دکوراتور فقط یک بار برای همه کارگران اجرا می شود، نه یک بار برای هر کارگر.

initializer از Pool همچنین در هر فرآیند با استفاده از همان عملکرد یکسان را اجرا می کند initargs (در بالا نشان داده نشده است)، که در صورت شروع بیش از 1 نمونه ضربان قلب منجر به برخورد پورت می شود.
در نهایت، ایده ای برای ایجاد پورت های منحصر به فرد تصادفی سازی است os.getpid() . دوست دارم در مورد این یا راه حل های دیگر برای مشکل پورت بازخورد یا پیشنهاداتی برای ابزارهای مشابه Heartrate داشته باشید (از آنجایی که 4 ساله است).

خلاصه

هم در حین ترشی کردن و هم در زمان ترشی کردن، شیء ترشی باید در سطح جهانی قابل دسترسی باشد.
اگر در دسترس نیست، ممکن است به این دلیل باشد که تعریف وجود دارد اما مسیر رسیدن به آن اشتباه است (مثال دکوراتور)، یا تعریف حذف شده است (مثال test_pickle).

این مقاله فقط دکوراتورهای مبتنی بر عملکرد و عملکردهای کارگر را توضیح می دهد. دکوراتورهای طبقاتی (مرجع 2) و کلاس های کارگری حتی پیچیده تر هستند.

مراجع

  1. توابع ترشی توسط __qualname__: https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled
  2. دکوراتورهای کلاسی: https://gael-varoquaux.info/programming/decoration-in-python-done-right-decorating-and-pickling.html
  3. دکوراتورهای انباشته: https://explore-flask.readthedocs.io/en/latest/views.html#caching
  4. روش های چند پردازشی: https://stackoverflow.com/questions/64095876/multiprocessing-fork-vs-spawn
  5. رفتار کپی حافظه بین فورک و اسپون: https://stackoverflow.com/a/60910365
  6. مشکلات چنگال کپی کردن برخی اما نه همه اشیا: https://pythonspeed.com/articles/python-multiprocessing/
  7. مدیریت چند پردازش آهسته: https://pythonspeed.com/articles/faster-multiprocessing-pickle
  8. منبع چند پردازشی: https://stackoverflow.com/a/71690229

منتشر شده از طریق به سمت هوش مصنوعی



منبع: https://towardsai.net/p/machine-learning/understanding-pickle-to-make-decorators-work-with-multiprocessing