#python
На днях я понял, что хоть мой канал и называется P(hD)ython, на нём всё ещё нет ни одного полноценного поста про
Python 😅.Исправим это! Запускаю серию постов, которые помогут Вам при прохождении Python-собеседований в 2026 году 😎.
Один из моих любимых вопросов на
Python-собесах звучит так: «если все ругают Python за скорость, то почему на нём пишут столько ML и математики?!». Удивительно, но часто на нём сыплются даже матёрые разработчики.Отвечаю! Сам по себе
Python и правда медленный (почему - отдельный вопрос), но когда дело доходит до тяжёлых вычислений, оказывается, что он вызывает функции, написанные на компилируемых языках вроде Rust, C\C++ или... Fortran!Так, например,
numpy по сути является обёрткой над С-массивами, а scipy.linalg.lapack обычно импортирует Fortran-библиотеку LAPACK! Даже pytorch, исполняясь на GPU, по факту делает это на C\C++ & CUDA ⚙️.Всё это достигается за счёт Foreign Function Interface (FFI) - механизма, позволяющего коду на одном языке вызывать функции, написанные на другом. FFI есть не только в
Python. Он имеется в Rust (extern "C" & unsafe), Java (JNI & JNA), Go (cgo) и т.д., так что идея этого поста легко обобщается и на другие языки.А теперь пример! Сегодня мы напишем свой собственный numpy с поэлементным умножением float-векторов!
Сперва реализуем функцию
fmul поэлементного умножения float-векторов на C в файле fmul.c:#include <stddef.h>
void fmul(const float* a, const float* b, float* out, size_t n) {
for (size_t i = 0; i < n; ++i) out[i] = a[i] * b[i];
}
Скомпилируем его как разделяемую библиотеку:
gcc -O3 -fPIC -shared -o libfmul.so fmul.c
Установим
cffi для поддержки FFI в Python:pip install cffi
Теперь напишем
Python-класс для работы с float-векторами и fmul в файле fmul.py:from array import array
from pathlib import Path
from cffi import FFI
# через FFI подгружаем разделяемую библиотеку в Python-код
ffi = FFI()
ffi.cdef("void fmul(const float* a,const float* b,float* out,size_t n);")
C = ffi.dlopen(str(Path(__file__).with_name("libfmul.so")))
class F32Vec:
# используем __slots__ для более компактного и производительного представления объектов класса
__slots__ = ("a",)
# требуем, чтобы наш класс конструировался из float-массива Python
def __init__(self, data):
self.a = (
data
if isinstance(data, array) and data.typecode == "f"
else array("f", data)
)
def __len__(self):
return len(self.a)
def _mul_arr(self, other):
if len(self.a) != len(other):
raise ValueError("len mismatch")
# готовим выходной массив
out = array("f", [0.0]) * len(self.a)
# вызываем через FFI C-функцию fmul
C.fmul(
ffi.from_buffer("float[]", self.a),
ffi.from_buffer("float[]", other),
ffi.from_buffer("float[]", out),
len(self.a),
)
return F32Vec(out)
# перегружаем операторы умножения, чтобы поэлементное произведение работало как в numpy
def __mul__(self, other):
if isinstance(other, F32Vec):
return self._mul_arr(other.a)
if isinstance(other, (int, float)):
return self._mul_arr(array("f", [float(other)]) * len(self.a))
return self._mul_arr(array("f", other))
__rmul__ = __mul__
def tolist(self):
return list(self.a)
def __repr__(self):
return f"F32Vec({list(self.a)})"
if __name__ == "__main__":
x = F32Vec([0.0, 0.5, 1.0, 1.5])
y = F32Vec([1.0, 2.0, 3.0, 4.0])
print(x._mul_arr(y.a))
print((x * y).tolist())
print((x * 2.0))
print((2.0 * x).tolist())
Наконец, запустим код:
python3 fmul.py
F32Vec([0.0, 1.0, 3.0, 6.0])
[0.0, 1.0, 3.0, 6.0]
F32Vec([0.0, 1.0, 2.0, 3.0])
[0.0, 1.0, 2.0, 3.0]
Сегодня мы узнали, почему медленный
Python способен быстро перемножать вектора, и как работает FFI! К сожалению, FFI громоздок и требует знания других языков. Как этого избежать - поговорим в следующем посте 🚀!С уважением,
Михаил Масягин
