Я бы хотел представить мой первый open source проект, размещённый на github. Это библиотека предоставляющая удобный и совершенно новый опыт работы с числовыми массивами в php. Вдохновением для создания послужила библиотека numoy на языке Python, которая включает широкий спектр возможностей для манипулирования данными и инструментами линейной алгебры.
Честно говоря, я был удивлён, когда обнаружил что на php до сих пор нет достойного аналога numpy. Конечно, некоторые попытки реализации можно найти на гитхабе, но они все крайне далеки от оригинала и не разделяют ту же идеологию. Моей целью было и есть создание похожей библиотеки, как минимум в отношении удобства использования и краткости синтаксиса. В дальнейших планах будет улучшение производительности.
В общем, приступим. Представьте, что у вас есть массив чисел, к примеру, представляющих собой значения температуры за какой-то период времени.
$list = [16, 22, -6, 23, -1, 13, 24, -23, 22];
И вам для дальнейшей работы нужно из этого массива выбрать только те значения, когда температура была выше нуля. В классическом случае вы бы сделали что-то вроде такого:
$result = [];
for($i=0; $i<count($list); $i++)
if ($list[$i] >= 0)
$result[] = $list[$i];
В результате получим массив из 6 элементов. Но, для такой простой и тривиальной операции мы напечатали слишком много кода. Конечно, можно использовать краткие версии вроде array_walk или аналогичные, но, всё равно будет много явной логики.
Библиотека numphp же предоставляет простой, но, в то же время, богатый функционал по выборке данных из числового массива по условию. Для решение той же задачи, вы можете просто написать:
// cast our array to the numphp array
$list = new np_array($list);
$result = $list[$list['> 0']];
Вот так просто!
Более того, это только лишь начало. Вы можете манипулировать данным объектом как захотите и даже устанавливать значения по определённому условию.
$list[$list['< 0']] = 0;
//result
[16, 22, 0, 23, 0, 13, 24, 0, 22]
Но постойте. До сих пор мы говорили только про выборку и изменение данных. А как насчёт математических операций? Хороший вопрос.
Давайте представим, вы создаёте RPG игру и у вашего героя есть какие-то способности, представленные в таком формате:
$powers = [62, 88, 34, 29];
Теперь, во время повышения уровня, вы хотите увеличить каждую способность на определённый показатель. Опять же, как бы вы поступили без numphp?
for($i=0; $i<count($powers); $i++)
$powers[$i]++;
Этот код работает, но разве он выглядит восхитительно? Вам нужно писать эти циклы раз за разом. Сравните это с элегантным решением библиотеки numphp:
$powers = $powers->add(1);
Более того, вы можете объединить предыдущие две возможности и, скажем, увеличить только те способности, которые на данный момент имеют значение меньше 30:
$powers = $powers[$powers['< 30']]->add(1);
Другая крутая возможность состоит в том, что вы можете выполнять математические операции и операции сравнения между двумя массивами всё в том же простом и понятном синтаксисе:
$powers = [62, 88, 34, 29];
$intensify = [2, 5, 4, 1];
$result = $powers->add($intensify);
//result
[64, 93, 38, 30]
Как вы можете видеть, numphp предоставляет элегантный синтаксический сахар для рутинных операций с числовыми массивами. Я описал лишь главную идею и возможности библиотеки на данный момент. Дополнительно уже реализованы удобные хелперы вроде генераторов (массив нулей, единиц, диапазона и даже ряда Фибоначчи), random модуль и так далее.
Обратите внимание, библиотека находится только в самом начале своего развития и я планирую добавлять новые возможности. Самая главная из них на данный момент — возможность работы с n-мерными массивами (матрицами, к примеру) и выполнять все основные виды операций из линейной алгебры.
Дополнительные возможности и документацию вы можете найти здесь.
Также, если вы хотите внести свой вклад в развитие — буду рад обсудить любые вопросы.
Комментарии (56)
Gemorroj
24.01.2018 22:55обратите внимание на http://www.php-fig.org/psr/psr-1/ и http://www.php-fig.org/psr/psr-2/
apollonin Автор
24.01.2018 22:57да, спасибо большое. Это имеет место быть. В последнее время пишу на python и там другие соглашения и глаз уже радуется python-style, а php-style не нравится. НО раз это php либа — нужно следовать psr )
Lure_of_Chaos
24.01.2018 23:36> $list[$list['> 0']];
о нет, только не вуду-магия (по всей видимости, с парсингом), уж лучше filter по предикатуapollonin Автор
25.01.2018 00:29если настолько сильно не нравится вуду-магия, можете использовать явный синтаксис =)
$result = $list[$list->gt(25)];
Но вообще это синтаксический сахар для того, что бы подобные конструкции занимали меньше места и были читаемыми.masterx
25.01.2018 01:22Если мы о «читаемом», тогда уж сделайте
$result = $list->gt(25); // [26, 78, 99]
apollonin Автор
25.01.2018 01:47Конечно, такой вариант я рассматривал.но:
1. это противоречит подходу, реализованному в numpy библиотеки и этому есть веская причина:
2. $list->gt(25) вернёт массив, называемый булевой маской: [false, false, true, true, false, true], где false — элемент в этой позиции НЕ удовлетворяет условию, true — удовлетворяет.
Преимущество такого подхода заключается в том, что такие булевые маски позволяют очень гибко выбрать элементы из массив и в дополнение их можно комбинировать и делать их логическое объединение:
$result = $list[operator::b_and($list->gte(5), $list->lt(8))]; // or $result = $list[operator::b_and($list['>= 5'], $list['< 8'])];
к моему сожалению, в php нельзя переопределить операторы, иначе синтаксис получился бы значительно красивее:
$list[$list > 25]; // and $result = $list[($list >= 5) & ($list < 8)];
Fesor
25.01.2018 13:05не достигается ли этот же вариант битовых масок обычной композицией функций?
function gt($b) { return function ($a) use ($b) { return $a > $b; }; } function lt($b) { return function ($a) use ($b) { return $a < $b; }; } function all(\Closure ...$filters) { return function (...$args) use ($filters) { foreach ($filters as $filter) { if (!$filter(...$args)) { return false; } } return true; }; } function any(\Closure ...$filters) { return function (...$args) use ($filters) { foreach ($filters as $filter) { if ($filter(...$args)) { return true; } } return false; }; } $arr = range(0, 40); array_filter($arr, any(lt(2), all(gt(25), lt(30)))); // 0, 1, 26, 27, 28, 29
может быть я не учитываю каких-то юзкейсов конечно… ну и да, понятно что php для подобного пока не самый удобный язык.
apollonin Автор
25.01.2018 13:35по сути, часть библиотеки и представляет собой отдельные методы-хелперы, которые потом можно компоновать как угодно, так как они возвращают одинаковые типы данных: np_array в данным случае.
методы all, any, кстати, в ближайших планах тоже ;)
apollonin Автор
25.01.2018 13:49да, ещё. булевые маски полезны в случае если нужно выбрать более гибко по индексам. К примеру из массива из 10 элементов выбрать только 1ый, 3-ий, 6-ой и 9-ый элементы.
$list[[false, true, false, true, false, false, true, false, false, true, false]];Fesor
25.01.2018 14:59К примеру из массива из 10 элементов выбрать только 1ый, 3-ий, 6-ой и 9-ый элементы.
в php это ограничение array_filter которое не предоставляет доступа к ключам. В JS том же я вполне могу сделать так:
arr.filter(byKey(in(1, 3, 6, 9))
SerafimArts
25.01.2018 15:33в php это ограничение array_filter которое не предоставляет доступа к ключам.
Чуток не понял. Коллбек array_filter принимает как ключи, так и значения. О каких ограничениях речь?
Fesor
25.01.2018 21:01да но согласись, с флагом это будет не так удобно, хотя думаю всеравно нужна будет свое обертка.
zzzmmtt
25.01.2018 09:28$resultArrray = $sourceArray[$something];
С точки зрения читаемости кода и синтаксиса php — это обращение к конкретному элементу массива по индексу. Вы же вместо индекса используете условие или результат выполнения метода. Это конечно не верх нечитаемости, но близко к тому. Хуже может быть только нагромождение тернарщины, которая тоже по сути своей является синтаксическим сахаром, но её применять тоже нужно с умом.
Ваше решение имеет право на жизнь конечно, и вполне допускаю, что кому-либо оно будет удобно и читаемо, но далеко не всем.apollonin Автор
25.01.2018 13:46Здесь нужно понимать, что это — отдельный тип данных, не классический массив.
И если немного расширить понятие индексации, то тут — обращение к конкретным элементам. Если так воспринимать, тогда не возникает недоразуменийoxidmod
25.01.2018 13:56Но синтаксис обманывает, что не очень хорошо. Встретив такое в чужом проекте и не зная особенностей либы можно угробить тучу времени.
Fesor
25.01.2018 12:55эх был бы пайп оператор в пыхе...
use function myCooolLib\Comparsion\gt; use function \array_filter as filter $arr = range(10, 30) |> filter($$, gt(25)); // 26, 27, 28, 29, 30
а вообще есть интересная RFC на эту тему: operator functions. Но вот только я бы для начала пофиксил способ задания ссылок на функции.
apollonin Автор
25.01.2018 13:40мне больше бы импонировао, если бы приняли вот это: wiki.php.net/rfc/operator-overloading
SerafimArts
25.01.2018 14:51PIPE оператор можно реализовать через HOM прокси (типа такого) с доступом к глобальным функциям. Получаем:
use function \array_filter as filter wrapper(range(10, 30)) ->filter(_, gt(25));
genew
25.01.2018 07:23Вместо кода
$result = []; for($i=0; $i<count($list); $i++) if ($list[$i] >= 0) $result[] = $list[$i];
можно написать так:
$result = array_filter($list, function($v) { return $v >= 0; });
apollonin Автор
25.01.2018 13:41можно. но при работе в области data science, machine learning такие операции нужно делать по 100 раз в день с разными условиями и так далее. Массажирование данных, так сказать.
oxidmod
25.01.2018 09:33Слишком уж непривычный синтаксис как по мне.
Пришлось бы долго тупить над вот этим куском
$result = $list[$list['> 0']];
если бы объявление было где — то выше.apollonin Автор
25.01.2018 13:43Могу согласиться с тем, что такой синтаксис непривычен. Но тут, как всегда, дело контекста.
если вы работаете в проекте где все знают что используется либа numphp и мы с ней знакомы, то проблем не будет. Более того, IDE подсветит что это объект.
Опять же, если кому крайне непривычно, можно использовать явный синтаксис: $list->gt(0);oxidmod
25.01.2018 13:58Каждый раз нужно выходить из потока, чтобы понять перед тобой обычный массив или объект либы. Особенно, когда '> 0' приходит в виде аргумента. Да, это проще когда постоянно работаешь с проектом, но в целом все-же не ок.
apollonin Автор
25.01.2018 14:05В любом случае, $list['>0'] — малая часть возможностей библиотеки =)
Математические операции над векторами — частая задача.
В ближайших планах — быстрый и удобный способ получить среднее значение, медиану или сумму всех элементов. $list->mean() и погнал дальше.
AlexLeonov
25.01.2018 11:20for($i=0; $i<count($list); $i++)
Месье не слышал о цикле foreach?
Я за такое студентам сразу «кол» ставлю. Рекомендую повторять вслух, как мантру, до полного просветления: «индексы массивов в PHP не обязаны быть плотны, монотонны и вообще не обязаны быть числами»zzzmmtt
25.01.2018 14:01foreach, равно как и прочие циклы не всегда хороши при обходе массива. Есть же array_walk, array_filter и иже с ними.
AlexLeonov
25.01.2018 14:30Если сравнивать foreach и for в PHP — вы что предпочтете?
zzzmmtt
25.01.2018 14:42Зависит от исходных данных и условий. Для простого прямого обхода массива foreach однозначно проще и удобнее. Для индексированного числами в строгом порядке массива, для выборки элементов с определенным шагом — почему бы не использовать for?
AlexLeonov
25.01.2018 14:59Почему? Потому что в языке нет инструмента, гарантирующего, что ваш массив так и останется «индексированным числами в строгом порядке»
for для обхода массивов — логическая бомба, которую вы подкладываете в свой кодSerafimArts
25.01.2018 15:27Потому что в языке нет инструмента, гарантирующего, что ваш массив так и останется «индексированным числами в строгом порядке»
Почему же? Для массивов в пыхе есть массивы, а не только хеш-мапы.AlexLeonov
25.01.2018 17:53SplFixedArray не гарантирует плотность индексов.
SerafimArts
25.01.2018 18:18Почему?
Заголовок спойлера$s = new \SplFixedArray(10); $s[0] = 23; $s[2] = 42; for ($i = 0, $len = \count($s); $i < $len; ++$i) { echo \gettype($s[$i]) . "\n"; } /** integer NULL integer NULL NULL NULL NULL NULL NULL NULL **/
Fortop
25.01.2018 23:39Потому.
<?php $splf = SplFixedArray::fromArray([3 => 33,22,4 => 44]); var_dump($splf);
zzzmmtt
25.01.2018 14:54Каждой задаче — свой инструмент. Ставить «кол» студентам за использование for — не логично. Логично уточнить почему был выбран for|foreach|while|array_walk|etc.
AlexLeonov
25.01.2018 15:00Отчего же. Совершенно логично.
Цикл for не проходится намеренно. Если он встречается в ДЗ, это значит, что ДЗ было списано, причем было списано совершенно бездумно, поскольку на лекции несколько раз повторяется, почему нужно использовать именно foreachoxidmod
25.01.2018 15:09Как использовать foreach для выбора каждого сотого элемента из массива в несколько сотен тысяч элементов?
AlexLeonov
25.01.2018 15:27Вопрос не имеет отношения к теме ветки дискуссии.
Ровно также «никак», как и цикл for.
SerafimArts
25.01.2018 15:28Имеется ввиду это?
foreach($items as $i => $value) { if ($i % 100 === 0) { yield $value; } }
apollonin Автор
25.01.2018 15:35Это похоже не дело принципа =) обход массива с заданным шагом — классическая задача for массива. это значительно более читабельно чем $i % 100. более того, foreach будет проходить и сравнивать каждый элемент, хотя это не оптимально, а for не тратит время на это, более того, не создаёт $value переменную каждый раз, тратя на это ресурсы.
oxidmod
25.01.2018 15:42Именно. Целью моего коммента было оспорить безапелляционность заявления
Если он встречается в ДЗ, это значит, что ДЗ было списано, причем было списано совершенно бездумно, поскольку на лекции несколько раз повторяется, почему нужно использовать именно foreach
AlexLeonov
25.01.2018 17:25А с чем тут спорить-то? Я совершенно точно знаю, что если встретил for для обхода массива — это говнокод, который подсказали студенту где-нибудь на «Ответах Mail.ru», поскольку оператор for принципиально не озвучивается на лекциях.
В чем предмет спора?oxidmod
25.01.2018 17:29В том, что вы неправы. Ниже человек даже бенчмарк провел.
for это такой же оператор как и другие. Каждому есть свое применение (даже eval-у и goto). Просто нужно понимать, что ты делаешь, вот и все.
То, что вы ставите свои принципы выше объективной истины, не делает вам чести, как преподавателю.
зы. Если студент использовал что-то, что не входило программу и может обосновать почему он это сделал, то это бонус студенту.AlexLeonov
25.01.2018 17:41В чем я неправ? В том, что в общем случае цикл for неприменим для перебора элементов массивов в PHP?
Вы это серьезно?
$days = [
'jan' => 31,
'feb' => 28,
'mar' => 31,
];
— примените здесь цикл for, пожалуйста. Хотя бы для того, чтобы подсчитать сумму количества дней во всех месяцах.
И тогда я немедленно признаю, что был неправ.oxidmod
25.01.2018 18:02Вы глупый или притворяетесь?
Еще раз процитирую свой коммент:
for это такой же оператор как и другие. Каждому есть свое применение (даже eval-у и goto).
Чтобы посчитать сумму элементов массива не нужен цикл совсем. Если понимать, что делаешь, то напишешь просто
$sum = array_sum($days);
for лучше подходит для перебора элементов массива с шагом. Выбор каждого k-того элемента, к примеру. И чем больше длина массива и шаг, тем for эффективней.
for лучше подходит для перебора массива в обратном порядке.
Да, for требует целочисленных индексов без пропусков. Если ты это понимаешь и учитываешь, то глупо отбрасывать возможность языка.
michael_vostrikov
25.01.2018 19:16примените здесь цикл for, пожалуйста
Пожалуйста, цикл for:
for ($sum = 0, $i = 0, $values = array_values($days), $cnt = count($values); $i < $cnt; $i++) $sum += $values[$i];
michael_vostrikov
25.01.2018 19:09По-вашему, студент не может нигде получить информацию кроме ваших лекций? Или может вы считаете, что до ваших лекций ни один студент не мог изучать другой язык. где есть for?
SerafimArts
25.01.2018 17:091) $value не создаётся каждый раз. Напомню, что выделение zval структуры происходит единожды и при следующем «тике» итерации, если этот контейнер не был задействован, то он переиспользуется. За счёт этого оптимизируются тяжёлые операции аллоцирования памяти. Помимо этого использование корутинки однозначно указывает на то, что в памяти может находиться только один контейнер для $value в один момент времени.
2) Получение элемента по индексу хеш-мапы — это, если ничего не путаю, операция O(n), а вызов next() итератора — это O(1). По-этому, в сумме, цикл for будет жрать O(log n), а foreach по всем элементам O(n). Кажется, я ничего не перепутал =)
3) Ну и тривиальный косяк с тем, кто в хеш-мапе (массиве пыха) индексы не упорядочены, а значит надо либо итерироваться по внутреннему итератору «массива» (функции next), либо делать slice, либо затратить тот же самый O(n) для сброса ключей через array_value
Кажется всё логично? Написал свои пунктики выше, проверил специально, и… Нет, нифига, for всё равно быстрее. Расскажете где я ошибся в своих выводах? А то, признаться, хз.
Заголовок спойлера// foreach function a(iterable $items): iterable { foreach ($items as $i => $value) { if ($i % 100 === 0) { yield $value; } } } // for function b(iterable $items): iterable { $array = \array_values(\is_array($items) ? $items : \iterator_to_array($items)); for ($i = 0, $len = \count($array); $i < $len; $i += 100) { yield $array[$i]; } } $items = \range(10, 100000); // foreach tests (~0.050s) $before = \microtime(true); foreach (a($items) as $i) { \ob_start(); \var_dump($i); \ob_end_clean(); } echo \number_format(\microtime(true) - $before, 5) . 's' . "\n"; // for tests (~0.005s) $before = \microtime(true); foreach (b($items) as $i) { \ob_start(); \var_dump($i); \ob_end_clean(); } echo \number_format(\microtime(true) - $before, 5) . 's' . "\n";
P.S. буферизацию добил, дабы случайно на DCE оптимизацию не нарваться.oxidmod
25.01.2018 17:26Доступ по индексу всегда O(1)
PHP arrays can contain integer and string keys at the same time as PHP does not distinguish between indexed and associative arrays.
Но да, индексы нужно держать в умеoxidmod
25.01.2018 17:32Не успел дописать. Количество итераций разное. И чем больше шаг и длинна исходного массива, тем больше разница в количестве итераций и сравнений.
SerafimArts
25.01.2018 18:15Ну так в вашей цитате как раз и написано, что это всё хешмапы, а к ним доступ O(n). Откуда O(1)?
oxidmod
25.01.2018 19:14Оттуда, что это хешмапа.
Сложность всегда одинакова, независимо от размера (опустим колизии хеш-функции)
Алгоритм всегда один и тот же:
1. Посчитали хеш от индекса
2. Обратились по полученному хешу.
O(n) было бы, если бы мы перебирали все елементы пока не найдем нужныйSerafimArts
26.01.2018 02:37Я изначально не правильно посчитал алгоритм получения элемента из хешмапы. Нашёл статью, просвятился, понял где косяк, спасибо )
dom1n1k
25.01.2018 13:52Ну как экспериментально-учебно-пет-проект пойдёт.
Но на практике — не взлетит. Не видно ни одного преимущества перед стандартными filter/map/etc — ни по удобству синтаксиса, ни по быстродействию.
masterx
Перефразируя классика, можно сказать, что все php–библиотеки нужны, все php–библиотеки важны. А если серьезно, то библиотека наверняка может быть полезна для повторяющихся специфичных задач с массивами чисел. Для единичных же случаев чаще всего можно обойтись без циклов, но с map, filter, reduce.
apollonin Автор
Совершенно верно. Особенно важно учитывать что в задачах связанных с data mining, machine learning, deep learning, такие вещи повторяюсь из раза в раз и тратить время рутинные циклические операции не стоит вообще