تطوير وحدة Python لجعل الإنتاج سعيدًا

مرحبا! أنا أمثل فريق التطوير في CyberDuckNinja غير الربحية. نقوم بإنشاء ودعم مجموعة كاملة من المنتجات التي تسهل تطوير التطبيقات الخلفية وخدمات التعلم الآلي.



اليوم أود أن أتطرق إلى موضوع دمج Python في C ++.







بدأ كل شيء بمكالمة من صديق في الساعة الثانية صباحًا ، والذي اشتكى: "لدينا إنتاج تحت الحمل ..." في المحادثة تبين أن كود الإنتاج تمت كتابته باستخدام ipyparallel (حزمة Python التي تسمح بالحوسبة المتوازية والموزعة) لحساب النموذج والحصول على النتائج عبر الإنترنت. قررنا أن نفهم بنية ipyparallel وأداء التنميط تحت الحمل.



أصبح من الواضح على الفور أن جميع الوحدات النمطية لهذه الحزمة مصممة بشكل مثالي ، ولكن يتم قضاء معظم الوقت في الشبكات ، وتحليل json والإجراءات الوسيطة الأخرى.

بناءً على دراسة مفصلة لـ ipyparallel ، اتضح أن المكتبة بأكملها تتكون من وحدتين متفاعلين:



  • Ipcontroler ، وهو المسؤول عن التحكم وجدولة المهام ،
  • المحرك ، وهو منفذ الشفرة.


تبين أن هناك ميزة رائعة تتمثل في أن هذه الوحدات تتفاعل من خلال pyzmq. بفضل بنية المحرك الجيدة ، تمكنا من استبدال تنفيذ الشبكات بحلنا المبني على cppzmq. يفتح هذا البديل مجال تطوير لا نهاية له: يمكن كتابة النظير في جزء C ++ من التطبيق.



أدى هذا إلى جعل تجمعات المحرك أسرع من الناحية النظرية ، لكنها لم تحل مشكلة دمج المكتبات في كود Python. إذا كان عليك فعل الكثير لدمج مكتبتك ، فلن يكون هذا الحل مطلوبًا وسيبقى على الرف. بقي السؤال حول كيفية دمج تطوراتنا بشكل أصلي في قاعدة بيانات المحرك الحالية.



لقد احتجنا إلى بعض المعايير المعقولة لفهم النهج الذي يجب اتباعه: سهولة التطوير ، أو إعلان API داخل C ++ فقط ، أو عدم وجود أغلفة إضافية داخل Python ، أو الاستخدام الأصلي للقوة الكاملة للمكتبات. ولكي لا يتم الخلط بين الطرق الأصلية (وليس كذلك) في السحب عبر كود C ++ في Python ، قمنا ببعض البحث. في بداية عام 2019 ، يمكن العثور على أربع طرق شائعة لتوسيع Python على الإنترنت:



  1. الأنواع
  2. CFFI
  3. سايثون
  4. CPython API


لقد نظرنا في جميع خيارات التكامل.



1. الأنواع



Ctypes هي واجهة دالة خارجية تسمح لك بتحميل مكتبات ديناميكية تقوم بتصدير واجهة C. باستخدامه ، يمكنك استخدام مكتبات C من Python ، على سبيل المثال ، libev ، libpq.



على سبيل المثال ، توجد مكتبة مكتوبة بلغة C ++ بواجهة:



extern "C"
{
    Foo* Foo_new();
    void Foo_bar(Foo* foo);
}


نكتب غلافًا لها:



import ctypes

lib = ctypes.cdll.LoadLibrary('./libfoo.so')

class Foo:
    def __init__(self) -> None:
        super().__init__()

        lib.Foo_new.argtypes = []
        lib.Foo_new.restype = ctypes.c_void_p
        lib.Foo_bar.argtypes = []
        lib.Foo_bar.restype = ctypes.c_void_p

        self.obj = lib.Foo_new()

    def bar(self) -> None:
        lib.Foo_bar(self.obj)


نستخلص النتائج:



  1. عدم القدرة على التفاعل مع المترجم API. Ctypes هي طريقة للتفاعل مع مكتبات C على جانب Python ، لكنها لا توفر طريقة لتفاعل كود C / C ++ مع Python.
  2. تصدير واجهة على غرار C. يمكن أن تتفاعل الأنواع مع مكتبات ABI في هذا النمط ، ولكن يجب على أي لغة أخرى تصدير متغيراتها ووظائفها وطرقها من خلال غلاف C.
  3. الحاجة إلى كتابة أغلفة. يجب كتابتها على جانب C ++ من الكود لتوافق ABI مع C ، وعلى جانب Python لتقليل كمية الكود المعياري.


لا تناسبنا الأنواع ، فنحن نجرب الطريقة التالية - CFFI.



2. CFFI



CFFI مشابه لـ Ctypes ، لكن لديه بعض الميزات الإضافية. دعنا نوضح مثالاً بنفس المكتبة:



import cffi

ffi = cffi.FFI()

ffi.cdef("""
    Foo* Foo_new();
    void Foo_bar(Foo* foo);
""")

lib = ffi.dlopen("./libfoo.so")

class Foo:
    def __init__(self) -> None:
        super().__init__()

        self.obj = lib.Foo_new()

    def bar(self) -> None:
        lib.Foo_bar(self.obj)


نستخلص الاستنتاجات:



لا يزال CFFI له نفس العيوب ، باستثناء أن الأغلفة تصبح أكثر بدانة ، لأنك تحتاج إلى إخبار المكتبة بتعريف واجهتها. CFFI غير مناسب أيضًا ، دعنا ننتقل إلى الطريقة التالية - Cython.



3. سايثون



Cython هي لغة برمجة فرعية / وصفية تسمح لك بكتابة امتدادات في مزيج من C / C ++ و Python وتحميل النتيجة كمكتبة ديناميكية. هذه المرة توجد مكتبة مكتوبة بلغة C ++ ولها واجهة:



#ifndef RECTANGLE_H
#define RECTANGLE_H

namespace shapes {
    class Rectangle {
        public:
            int x0, y0, x1, y1;
            Rectangle();
            Rectangle(int x0, int y0, int x1, int y1);
            ~Rectangle();
            int getArea();
            void getSize(int* width, int* height);
            void move(int dx, int dy);
    };
}

#endif


ثم نحدد هذه الواجهة بلغة Cython:



cdef extern from "Rectangle.cpp":
    pass

# Declare the class with cdef
cdef extern from "Rectangle.h" namespace "shapes":
    cdef cppclass Rectangle:
        Rectangle() except +
        Rectangle(int, int, int, int) except +
        int x0, y0, x1, y1
        int getArea()
        void getSize(int* width, int* height)
        void move(int, int)


ونكتب لها غلافًا:



# distutils: language = c++

from Rectangle cimport Rectangle

cdef class PyRectangle:
    cdef Rectangle c_rect

    def __cinit__(self, int x0, int y0, int x1, int y1):
        self.c_rect = Rectangle(x0, y0, x1, y1)

    def get_area(self):
        return self.c_rect.getArea()

    def get_size(self):
        cdef int width, height
        self.c_rect.getSize(&width, &height)
        return width, height

    def move(self, dx, dy):
        self.c_rect.move(dx, dy)

    # Attribute access
    @property
    def x0(self):
        return self.c_rect.x0

    @x0.setter
    def x0(self, x0):
        self.c_rect.x0 = x0

    # Attribute access
    @property
    def x1(self):
        return self.c_rect.x1

    @x1.setter
    def x1(self, x1):
        self.c_rect.x1 = x1

    # Attribute access
    @property
    def y0(self):
        return self.c_rect.y0

    @y0.setter
    def y0(self, y0):
        self.c_rect.y0 = y0

    # Attribute access
    @property
    def y1(self):
        return self.c_rect.y1

    @y1.setter
    def y1(self, y1):
        self.c_rect.y1 = y1


الآن يمكننا استخدام هذه الفئة من كود Python العادي:



import rect
x0, y0, x1, y1 = 1, 2, 3, 4
rect_obj = rect.PyRectangle(x0, y0, x1, y1)
print(dir(rect_obj))


نستخلص النتائج:



  1. عند استخدام Cython ، لا يزال يتعين عليك كتابة تعليمات برمجية مجمعة على جانب C ++ ، ولكنك لم تعد بحاجة إلى تصدير واجهة نمط C.
  2. ما زلت لا تستطيع التفاعل مع المترجم.


تبقى الطريقة الأخيرة - CPython API. نحن نحاول ذلك.



4. CPython API



CPython API - API الذي يسمح لك بتطوير وحدات لمترجم Python في C ++. أفضل رهان لك هو pybind11 ، مكتبة C ++ عالية المستوى تجعل العمل مع CPython API مناسبًا. بمساعدتها ، يمكنك بسهولة تصدير الوظائف والفئات وتحويل البيانات بين ذاكرة Python والذاكرة الأصلية في C ++.



لذلك ، لنأخذ الكود من المثال السابق ونكتب غلافًا له:



PYBIND11_MODULE(rect, m) {
    py::class_<Rectangle>(m, "PyRectangle")
        .def(py::init<>())
        .def(py::init<int, int, int, int>())
        .def("getArea", &Rectangle::getArea)
        .def("getSize", [](Rectangle &rect) -> std::tuple<int, int> {
            int width, height;

            rect.getSize(&width, &height);

            return std::make_tuple(width, height);
        })
        .def("move", &Rectangle::move)
        .def_readwrite("x0", &Rectangle::x0)
        .def_readwrite("x1", &Rectangle::x1)
        .def_readwrite("y0", &Rectangle::y0)
        .def_readwrite("y1", &Rectangle::y1);
}


كتبنا الغلاف ، والآن يجب تجميعه في مكتبة ثنائية. نحتاج إلى شيئين: نظام بناء ومدير حزم. لنأخذ CMake و Conan لهذه الأغراض ، على التوالي.



لجعل البناء على Conan يعمل ، تحتاج إلى تثبيت Conan نفسه بطريقة مناسبة:



pip3 install conan cmake


وتسجيل مستودعات إضافية:



conan remote add bincrafters https://api.bintray.com/conan/bincrafters/public-conan
conan remote add cyberduckninja https://api.bintray.com/conan/cyberduckninja/conan


دعنا نصف تبعيات المشروع لمكتبة pybind في ملف conanfile.txt:



[requires]
pybind11/2.3.0@conan/stable

[generators]
cmake


دعونا نضيف ملف CMake. انتبه إلى التكامل المضمن مع Conan - عند تنفيذ CMake ، سيتم تشغيل أمر تثبيت conan ، والذي يثبت التبعيات ويولد متغيرات CMake بمعلومات حول التبعيات:



cmake_minimum_required(VERSION 3.17)

set(project rectangle)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS OFF)

	if (NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake")
    	message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan")
    	file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/v0.15/conan.cmake" "${CMAKE_BINARY_DIR}/conan.cmake")
	endif ()

	set(CONAN_SYSTEM_INCLUDES "On")

	include(${CMAKE_BINARY_DIR}/conan.cmake)

	conan_cmake_run(
        	CONANFILE conanfile.txt
        	BASIC_SETUP
        	BUILD missing
        	NO_OUTPUT_DIRS
	)

find_package(Python3 COMPONENTS Interpreter Development)
include_directories(${PYTHON_INCLUDE_DIRS})
include_directories(${Python3_INCLUDE_DIRS})
find_package(pybind11 REQUIRED)

pybind11_add_module(${PROJECT_NAME} main.cpp )

target_include_directories(
    	${PROJECT_NAME}
    	PRIVATE
    	${NUMPY_ROOT}/include
    	${PROJECT_SOURCE_DIR}/vendor/General_NetSDK_Eng_Linux64_IS_V3.051
    	${PROJECT_SOURCE_DIR}/vendor/ffmpeg4.2.1
)

target_link_libraries(
    	${PROJECT_NAME}
    	PRIVATE
    	${CONAN_LIBS}
)


جميع الاستعدادات كاملة ، دعنا نجمع:



cmake . -DCMAKE_BUILD_TYPE=Release 
cmake --build . --parallel 2


نستخلص النتائج:



  1. تلقينا المكتبة الثنائية المجمعة ، والتي يمكن تحميلها لاحقًا في مترجم بايثون بوسائلها.
  2. لقد أصبح تصدير الكود إلى Python أسهل بكثير مقارنة بالطرق المذكورة أعلاه ، وأصبح كود التغليف أكثر إحكاما ومكتوبًا بنفس اللغة.


تتمثل إحدى ميزات cpython / pybind11 في تحميل أو الحصول على أو تنفيذ وظيفة من وقت تشغيل Python أثناء تشغيل C ++ والعكس صحيح.



دعنا نلقي نظرة على مثال بسيط:



#include <pybind11/embed.h>  //     

namespace py = pybind11;

int main() {
    py::scoped_interpreter guard{}; //  python vm
    py::print("Hello, World!"); //     Hello, World!
}


من خلال الجمع بين القدرة على تضمين مترجم Python في تطبيق C ++ ومحرك وحدات Python ، توصلنا إلى نهج مثير للاهتمام حيث لا يشعر رمز محرك ipyparalles باستبدال المكونات. بالنسبة للتطبيقات ، اخترنا بنية تبدأ فيها دورات الحياة والأحداث برمز C ++ ، وعندها فقط يبدأ مترجم Python في نفس العملية.



لفهم ، دعونا نرى كيف يعمل نهجنا:



#include <pybind11/embed.h>

#include "pyrectangle.hpp" //  ++  rectangle

using namespace py::literals;
//            rectangle
constexpr static char init_script[] = R"__(
    import sys

    sys.modules['rect'] = rect
)__";
//             rectangle
constexpr static char load_script[] = R"__(
    import sys, os
    from importlib import import_module

    sys.path.insert(0, os.path.dirname(path))
    module_name, _ = os.path.splitext(path)
    import_module(os.path.basename(module_name))
)__";

int main() {
    py::scoped_interpreter guard; //  
    py::module pyrectangle("rect");    

    add_pyrectangle(pyrectangle); //  
    py::exec(init_script, py::globals(), py::dict("rect"_a = pyrectangle)); //        Python.
    py::exec(load_script, py::globals(), py::dict("path"_a = "main.py")); //  main.py

    return 0;
}


في المثال أعلاه ، تمت إعادة توجيه وحدة pyrectangle إلى مترجم Python وإتاحتها للاستيراد كمستقيم. دعنا نوضح بمثال أنه لم يتغير شيء في الكود "المخصص":



from pprint import pprint

from rect import PyRectangle

r = PyRectangle(0, 3, 5, 8)

pprint(r)

assert r.getArea() == 25

width, height = r.getSize()

assert width == 5 and height == 5


يتميز هذا النهج بالمرونة العالية والعديد من نقاط التخصيص ، فضلاً عن القدرة على إدارة ذاكرة Python بشكل قانوني. لكن هناك مشاكل - تكلفة الخطأ أعلى بكثير من الخيارات الأخرى ، ويجب أن تكون على دراية بهذه المخاطر.



وبالتالي ، فإن ctypes و CFFI غير مناسبين لنا بسبب الحاجة إلى تصدير واجهات مكتبة على غرار C ، وأيضًا بسبب الحاجة إلى كتابة أغلفة على جانب Python ، وفي النهاية ، استخدام CPython API إذا كان التضمين ضروريًا. Cython خالية من عيوب التصدير ، لكنها تحتفظ بجميع العيوب الأخرى. يدعم Pybind11 فقط أغلفة التضمين والكتابة على جانب C ++. كما أن لديها قدرات واسعة للتعامل مع هياكل البيانات واستدعاء وظائف وأساليب Python. نتيجة لذلك ، استقرنا على pybind11 باعتباره غلاف C ++ عالي المستوى لواجهة برمجة تطبيقات CPython.



من خلال الجمع بين استخدام python المضمن داخل تطبيق C ++ مع آلية الوحدة النمطية لإعادة توجيه البيانات بسرعة وإعادة استخدام قاعدة رمز المحرك المتوازي ipyparallel ، حصلنا على صاروخ صاروخ. إنه مطابق في الميكانيكا للأصل ويعمل بشكل أسرع عن طريق تقليل الطبقات لتفاعلات الشبكة ومعالجة json والإجراءات الوسيطة الأخرى. يتيح هذا الآن لصديقي الاحتفاظ بالأحمال في الإنتاج ، والتي تلقيت من أجلها النجمة الأولى في مشروع GitHub .



Conan, Russian Python Week C++, Python Conan .



Russian Python Week 4 — 14 17 . , Python: Python- . , Python.

.



All Articles