پروتکل بافر

برخی از اشیاء موجود در پایتون، دسترسی به یک آرایه حافظه یا بافر زیرین را پوشش می‌دهند. چنین اشیایی شامل bytes و bytearray توکار و برخی نوع‌های توسعه‌ای مانند array.array هستند. کتابخانه‌های شخص ثالث ممکن است برای مقاصد خاص، مانند پردازش تصویر یا تحلیل عددی، نوع‌های خود را تعریف کنند.

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

پایتون چنین امکانی را در سطح C و پایتون به شکل پروتکل بافر فراهم می‌کند. این پروتکل دو سمت دارد:

  • در سمت تولید‌کننده، یک نوع می‌تواند «رابط بافر» را ارائه کند که به اشیاء آن نوع اجازه می‌دهد تا اطلاعاتی درباره‌ی بافر زیرین خود را آشکار کنند. این رابط در بخش ساختارهای شیء بافر شرح داده شده است؛ برای پایتون به شبیه‌سازی انواع بافر مراجعه کنید.

  • در سمت مصرف‌کننده، چندین روش برای به دست آوردن اشاره‌گر به داده‌های خام زیربنایی یک شیء (برای مثال، یک پارامتر متد) در دسترس است. برای پایتون به memoryview مراجعه کنید.

اشیاء ساده‌ای مانند bytes و bytearray بافر زیرین خود را به شکل بایت‌محور در دسترس قرار می‌دهند. شکل‌های دیگری نیز ممکن است؛ برای مثال، عناصری که یک array.array در دسترس قرار می‌دهد می‌توانند مقادیر چندبایتی باشند.

نمونه‌ای از مصرف‌کنندگان رابط بافر، متد write() در شیءهای پرونده است: هر شیئی که بتواند دنباله‌ای از بایت‌ها را از طریق رابط بافر اکسپورت کند، می‌تواند در پرونده‌ای نوشته شود. در حالی که write() تنها به دسترسی فقط‌خواندنی به محتوای درونی شیئی که به آن پاس داده شده نیاز دارد، متدهای دیگری مانند readinto() به دسترسی نوشتن به محتوای آرگومان خود نیاز دارند. رابط بافر به شیءها اجازه می‌دهد تا اکسپورت کردن بافرهای خواندنی-نوشتنی و فقط‌خواندنی را به‌صورت انتخابی بپذیرند یا رد کنند.

دو راه وجود دارد که مصرف‌کننده‌ی رابط بافر بتواند بافری را روی یک شیء هدف به دست آورد:

در هر دو مورد، PyBuffer_Release() باید زمانی که دیگر به بافر نیازی نیست فراخوانی شود. عدم انجام این کار می‌تواند به مشکلات مختلفی مانند نشت منابع منجر شود.

اضافه شده در نسخه‌ی 3.12: پروتکل بافر اکنون در پایتون دسترسی‌پذیر است؛ به شبیه‌سازی انواع بافر و memoryview مراجعه کنید.

ساختار بافر

ساختارهای بافر (یا به‌سادگی «بافرها») به‌عنوان راهی برای افشای داده‌های دودویی از یک شیء دیگر به برنامه‌نویس پایتون مفید هستند. همچنین می‌توان از آن‌ها به‌عنوان سازوکاری برای اسلایس‌کردن بدون کپی استفاده کرد. با استفاده از توانایی آن‌ها در ارجاع به یک بلوک حافظه، می‌توان هر داده‌ای را به‌سادگی به برنامه‌نویس پایتون افشا کرد. این حافظه می‌تواند یک آرایه‌ی بزرگ و ثابت در یک توسعه C باشد، می‌تواند یک بلوک خام حافظه برای دستکاری پیش از انتقال به یک کتابخانه‌ی سیستم‌عامل باشد، یا می‌تواند برای به‌گردش درآوردن داده‌های ساختاریافته در قالب بومی و درون‌حافظه‌ای خود استفاده شود.

برخلاف بیشتر نوع‌های داده‌ای که مفسر پایتون ارائه می‌کند، بافرها اشاره‌گرهای PyObject نیستند، بلکه ساختارهای ساده‌ی C هستند. این امر امکان می‌دهد که آن‌ها به‌سادگی ساخته و کپی شوند. وقتی به یک دربرگیرنده‌ی عام برای بافر نیاز باشد، می‌توان یک شیء memoryview ایجاد کرد.

برای دستورالعمل‌های کوتاه درباره‌ی نحوه‌ی نوشتن یک شیء اکسپورتکننده (exporting object)، به ساختارهای شیء بافر مراجعه کنید. برای به دست آوردن یک بافر، به PyObject_GetBuffer() مراجعه کنید.

type Py_buffer
قسمتی از ABI پایدار شامل تمام اعضا از نسخه‌ی 3.11.
void *buf

اشاره‌گری به ابتدای ساختار منطقی‌ای که توسط فیلدهای بافر توصیف می‌شود. این می‌تواند هر مکانی درون بلوک حافظه فیزیکی زیرین اکسپورتکننده باشد. برای مثال، با strides منفی، مقدار ممکن است به انتهای بلوک حافظه اشاره کند.

برای آرایه‌های پیوسته، مقدار به ابتدای بلوک حافظه اشاره می‌کند.

PyObject *obj

ارجاعی جدید به شیء اکسپورتکننده. این ارجاع در مالکیت مصرف‌کننده است و به‌طور خودکار توسط PyBuffer_Release() آزاد می‌شود (یعنی شمارش ارجاع کاهش می‌یابد) و به NULL تنظیم می‌شود. این فیلد معادل مقدار بازگشتی هر تابع استاندارد C-API است.

به‌عنوان یک مورد خاص، برای بافرهای موقتی که توسط PyMemoryView_FromBuffer() یا PyBuffer_FillInfo() پوشش داده می‌شوند، این فیلد NULL است. به‌طور کلی، اشیاء اکسپورتکننده به هیچ وجه نباید از این طرح استفاده کنند.

Py_ssize_t len

product(shape) * itemsize. برای آرایه‌های پیوسته، این مقدار طول بلوک حافظه‌ی زیرین است. برای آرایه‌های ناپیوسته، این مقدار طولی است که ساختار منطقی در صورت کپی شدن به یک نمایش پیوسته خواهد داشت.

دسترسی به ((char *)buf)[0] up to ((char *)buf)[len-1] تنها زمانی معتبر است که بافر از طریق درخواستی که پیوستگی را تضمین می‌کند به دست آمده باشد. در بیشتر موارد، چنین درخواستی PyBUF_SIMPLE یا PyBUF_WRITABLE خواهد بود.

int readonly

نشانگری که مشخص می‌کند بافر فقط‌خواندنی است یا خیر. این فیلد توسط پرچم PyBUF_WRITABLE کنترل می‌شود.

Py_ssize_t itemsize

اندازه‌ی آیتم به بایت برای یک عنصر منفرد. همانند مقدار struct.calcsize() است که روی مقادیر غیر NULL format فراخوانی می‌شود.

استثنای مهم: اگر مصرف‌کننده‌ای بافر را بدون پرچم PyBUF_FORMAT درخواست کند، format برابر NULL قرار می‌گیرد، اما itemsize همچنان مقدار مربوط به قالب اصلی را دارد.

اگر shape موجود باشد، برابری product(shape) * itemsize == len همچنان برقرار است و مصرف‌کننده می‌تواند از itemsize برای پیمایش بافر استفاده کند.

اگر shape در نتیجه‌ی درخواست PyBUF_SIMPLE یا PyBUF_WRITABLE برابر NULL باشد، مصرف‌کننده باید itemsize را نادیده بگیرد و فرض کند که itemsize == 1 است.

char *format

رشته‌ای خاتمه‌یافته با NULL به سبک سینتکس ماژول struct که محتویات یک آیتم منفرد را توصیف می‌کند. اگر این NULL باشد، "B" (بایت‌های بدون علامت) فرض می‌شود.

این فیلد توسط پرچم PyBUF_FORMAT کنترل می‌شود.

int ndim

تعداد ابعادی که حافظه به‌صورت یک آرایه‌ی n-بعدی بازنمایی می‌کند. اگر 0 باشد، buf به یک آیتم واحد اشاره می‌کند که نمایانگر یک اسکالر است. در این حالت، shape، strides و suboffsets باید NULL باشند. حداکثر تعداد ابعاد توسط PyBUF_MAX_NDIM تعیین می‌شود.

Py_ssize_t *shape

آرایه‌ای از Py_ssize_t به طول ndim که شکل حافظه را به‌عنوان یک آرایه‌ی n-بعدی نشان می‌دهد. توجه داشته باشید که shape[0] * ... * shape[ndim-1] * itemsize حتماً باید با len برابر باشد.

مقادیر شکل به shape[n] >= 0 محدود هستند. مورد shape[n] == 0 نیاز به توجه ویژه دارد. برای اطلاعات بیشتر به complex arrays مراجعه کنید.

آرایه‌ی شکل برای مصرف‌کننده فقط‌خواندنی است.

Py_ssize_t *strides

آرایه‌ای از Py_ssize_t به طول ndim که مشخص می‌کند برای رسیدن به عنصر جدید در هر بُعد باید چند بایت را رد کرد.

مقادیر گام (stride) می‌توانند هر عدد صحیحی باشند. برای آرایه‌های معمولی، گام‌ها معمولاً مثبت هستند، اما مصرف‌کننده باید حتماً بتواند مورد strides[n] <= 0 را مدیریت کند. برای اطلاعات بیشتر به complex arrays مراجعه کنید.

آرایه‌ی strides برای مصرف‌کننده فقط‌خواندنی است.

Py_ssize_t *suboffsets

آرایه‌ای از Py_ssize_t به طول ndim. اگر suboffsets[n] >= 0 باشد، مقادیر ذخیره‌شده در راستای بعد n-اُم اشاره‌گر هستند و مقدار زیرآفست تعیین می‌کند که پس از ارجاع‌زدایی (de-referencing)، چند بایت باید به هر اشاره‌گر افزوده شود. مقدار منفیِ زیرآفست نشان می‌دهد که نباید ارجاع‌زدایی انجام شود (گام‌برداری (striding) در یک بلوک حافظه‌ی پیوسته).

اگر همه‌ی suboffsets منفی باشند (یعنی نیازی به ارجاع‌زدایی نیست)، آن‌گاه این فیلد باید NULL باشد (مقدار پیش‌فرض).

این نوع نمایش آرایه توسط Python Imaging Library (PIL) استفاده می‌شود. برای اطلاعات بیشتر درباره‌ی نحوه‌ی دسترسی به عناصر چنین آرایه‌ای، به complex arrays مراجعه کنید.

آرایه‌ی suboffsets برای مصرف‌کننده فقط‌خواندنی است.

void *internal

این مورد برای استفاده داخلی توسط شیء اکسپورتکننده است. برای مثال، اکسپورتکننده ممکن است آن را به یک عدد صحیح قالب‌ریزی مجدد کند و از آن برای ذخیره پرچم‌هایی استفاده کند که مشخص می‌کنند آرایه‌های shape، strides و suboffsets هنگام رها شدن بافر باید آزاد شوند یا خیر. مصرف‌کننده به هیچ وجه نباید این مقدار را تغییر دهد.

ثابت‌ها:

PyBUF_MAX_NDIM
قسمتی از ABI پایدار از نسخه‌ی 3.11.

حداکثر تعداد ابعادی که حافظه بازنمایی می‌کند. اکسپورتکنندگان باید (MUST) این محدودیت را رعایت کنند و بهتر است (SHOULD) مصرف‌کنندگان بافرهای چندبعدی بتوانند تا PyBUF_MAX_NDIM بعد را مدیریت کنند. در حال حاضر روی ۶۴ تنظیم شده است.

انواع درخواست بافر

بافرها معمولاً با ارسال درخواست بافر به یک شیء اکسپورتکننده از طریق PyObject_GetBuffer() به دست می‌آیند. از آنجا که پیچیدگی ساختار منطقی حافظه می‌تواند به‌شدت متفاوت باشد، مصرف‌کننده از آرگومان پرچم‌ها برای تعیین نوع دقیق بافری که می‌تواند آن را مدیریت کند استفاده می‌کند.

همه‌ی فیلدهای Py_buffer به‌طور بی‌ابهام توسط نوع درخواست تعریف می‌شوند.

فیلدهای مستقل از درخواست

فیلد‌های زیر تحت تأثیر پرچم‌ها قرار نمی‌گیرند و همیشه باید با مقادیر صحیح پر شوند: obj، buf، len، itemsize، ndim.

فقط‌خواندنی، قالب

PyBUF_WRITABLE
قسمتی از ABI پایدار از نسخه‌ی 3.11.

فیلد readonly را کنترل می‌کند. اگر تنظیم شده باشد، اکسپورتکننده باید یا بافر نوشتنی فراهم کند یا شکست را گزارش دهد. در غیر این صورت، اکسپورتکننده می‌تواند بافر فقط‌خواندنی یا نوشتنی فراهم کند، اما این انتخاب باید برای همه‌ی مصرف‌کنندگان یکسان باشد. برای مثال، می‌توان از PyBUF_SIMPLE | PyBUF_WRITABLE برای درخواست یک بافر نوشتنی ساده استفاده کرد.

PyBUF_WRITEABLE

این یک نام مستعار برای PyBUF_WRITABLE است.

PyBUF_FORMAT
قسمتی از ABI پایدار از نسخه‌ی 3.11.

فیلد format را کنترل می‌کند. اگر تنظیم شده باشد، این فیلد باید به‌درستی پر شود. در غیر این صورت، این فیلد باید NULL باشد.

می‌توان PyBUF_WRITABLE را با هر یک از پرچم‌های بخش بعدی از طریق عملگر | ترکیب کرد. از آنجا که PyBUF_SIMPLE به صورت 0 تعریف شده است، می‌توان از PyBUF_WRITABLE به عنوان یک پرچم مستقل برای درخواست یک بافر ساده‌ی نوشتنی استفاده کرد.

PyBUF_FORMAT باید با عملگر | به هر یک از پرچم‌ها به‌جز PyBUF_SIMPLE اضافه شود، زیرا مورد دوم از قبل قالب B (بایت‌های بدون علامت) را در بر می‌گیرد. PyBUF_FORMAT را نمی‌توان به‌تنهایی استفاده کرد.

شکل، گام‌ها (strides)، زیرآفست‌ها (suboffsets)

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

درخواست

شکل

گام‌ها

suboffsets

PyBUF_INDIRECT
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

در صورت نیاز

PyBUF_STRIDES
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

PyBUF_ND
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

NULL

NULL

PyBUF_SIMPLE
قسمتی از ABI پایدار از نسخه‌ی 3.11.

NULL

NULL

NULL

درخواست‌های پیوستگی

می‌توان پیوستگی به سبک C یا Fortran را به‌طور صریح درخواست کرد، هم به همراه اطلاعات گام (stride) و هم بدون آن. بدون اطلاعات گام، بافر باید پیوسته به سبک C (C-contiguous) باشد.

درخواست

شکل

گام‌ها

suboffsets

پیوسته

PyBUF_C_CONTIGUOUS
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

C

PyBUF_F_CONTIGUOUS
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

F

PyBUF_ANY_CONTIGUOUS
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

C یا F

PyBUF_ND

بله

NULL

NULL

C

درخواست‌های ترکیبی

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

در جدول زیر U نمایانگر پیوستگی تعریف‌نشده است. مصرف‌کننده برای تعیین پیوستگی باید PyBuffer_IsContiguous() را فراخوانی کند.

درخواست

شکل

گام‌ها

suboffsets

پیوسته

فقط‌خواندنی

قالب

PyBUF_FULL
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

در صورت نیاز

U

0

بله

PyBUF_FULL_RO
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

در صورت نیاز

U

۱ یا ۰

بله

PyBUF_RECORDS
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

U

0

بله

PyBUF_RECORDS_RO
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

U

۱ یا ۰

بله

PyBUF_STRIDED
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

U

0

NULL

PyBUF_STRIDED_RO
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

بله

NULL

U

۱ یا ۰

NULL

PyBUF_CONTIG
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

NULL

NULL

C

0

NULL

PyBUF_CONTIG_RO
قسمتی از ABI پایدار از نسخه‌ی 3.11.

بله

NULL

NULL

C

۱ یا ۰

NULL

آرایه‌های پیچیده

به سبک NumPy: شکل و گام‌ها

ساختار منطقی آرایه‌های به سبک NumPy توسط itemsize، ndim، shape و strides تعریف می‌شود.

اگر ndim == 0 باشد، مکان حافظه‌ای که buf به آن اشاره می‌کند، به‌عنوان یک اسکالر با اندازه‌ی itemsize تفسیر می‌شود. در این حالت، هر دو shape و strides برابر NULL هستند.

اگر strides برابر NULL باشد، آرایه به‌عنوان یک آرایه‌ی C استاندارد n-بعدی تفسیر می‌شود. در غیر این صورت، مصرف‌کننده باید به یک آرایه‌ی n-بعدی به شرح زیر دسترسی داشته باشد:

ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1];
item = *((typeof(item) *)ptr);

همان‌طور که در بالا ذکر شد، buf می‌تواند به هر مکانی درون بلوک حافظه‌ی واقعی اشاره کند. یک اکسپورتکننده (exporter) می‌تواند اعتبار یک بافر را با این تابع بررسی کند:

def verify_structure(memlen, itemsize, ndim, shape, strides, offset):
    """Verify that the parameters represent a valid array within
       the bounds of the allocated memory:
           char *mem: start of the physical memory block
           memlen: length of the physical memory block
           offset: (char *)buf - mem
    """
    if offset % itemsize:
        return False
    if offset < 0 or offset+itemsize > memlen:
        return False
    if any(v % itemsize for v in strides):
        return False

    if ndim <= 0:
        return ndim == 0 and not shape and not strides
    if 0 in shape:
        return True

    imin = sum(strides[j]*(shape[j]-1) for j in range(ndim)
               if strides[j] <= 0)
    imax = sum(strides[j]*(shape[j]-1) for j in range(ndim)
               if strides[j] > 0)

    return 0 <= offset+imin and offset+imax+itemsize <= memlen

به سبک PIL: shape، strides و suboffsets

علاوه بر آیتم‌های معمولی، آرایه‌های به سبک PIL می‌توانند شامل اشاره‌گرهایی باشند که برای رسیدن به عنصر بعدی در یک بعد باید دنبال شوند. برای مثال، آرایه سه‌بعدی معمولی C یعنی char v[2][2][3] را می‌توان به‌صورت آرایه‌ای از ۲ اشاره‌گر به ۲ آرایه دوبعدی نیز در نظر گرفت: char (*v[2])[2][3]. در نمایش زیرآفست‌ها (suboffsets)، این دو اشاره‌گر می‌توانند در ابتدای buf تعبیه شوند و به دو آرایه char x[2][3] اشاره کنند که می‌توانند در هر جای حافظه قرار داشته باشند.

تابع زیر وقتی که هم گام‌ها و هم زیرآفست‌ها (suboffsets) غیر NULL باشند، اشاره‌گر به عنصری در آرایه‌ی N-بعدی را که یک اندیس N-بعدی به آن اشاره می‌کند برمی‌گرداند:

void *get_item_pointer(int ndim, void *buf, Py_ssize_t *strides,
                       Py_ssize_t *suboffsets, Py_ssize_t *indices) {
    char *pointer = (char*)buf;
    int i;
    for (i = 0; i < ndim; i++) {
        pointer += strides[i] * indices[i];
        if (suboffsets[i] >=0 ) {
            pointer = *((char**)pointer) + suboffsets[i];
        }
    }
    return (void*)pointer;
}