PHP迭代器与生成器:高效处理大型数据集的利器
数组以及迭代是任何应用程序的基石。随着我们获得新工具,我们使用数组的方式也应该随之改进。
例如,生成器就是一种新工具。起初我们只有数组,然后我们获得了定义自己的类数组结构(称为迭代器)的能力。但自PHP 5.5以来,我们可以快速创建称为生成器的类迭代器结构。
生成器看起来像函数,但我们可以将它们用作迭代器。它们为我们提供了一种简单的语法,用于创建本质上可中断、可重复的函数。它们非常棒!
我们将研究几个可以使用生成器的领域,并探讨使用生成器时需要注意的一些问题。最后,我们将学习一个由才华横溢的Nikita Popov创建的优秀库。
示例代码可在https://github.com/sitepoint-editors/generators-and-iter找到。
关键要点
array_filter
和array_map
不兼容,需要使用Nikic/Iter等其他工具来处理此类数据。问题
假设您有很多关系数据,并且想要进行一些预加载。也许数据是逗号分隔的,您需要加载每种数据类型并将它们组合在一起。
您可以从以下简单的代码开始:
<code class="language-php">function readCSV($file) { $rows = []; $handle = fopen($file, "r"); while (!feof($handle)) { $rows[] = fgetcsv($handle); } fclose($handle); return $rows; } $authors = array_filter( readCSV("authors.csv") ); $categories = array_filter( readCSV("categories.csv") ); $posts = array_filter( readCSV("posts.csv") );</code>
然后,您可能会尝试通过迭代或高阶函数来连接相关元素:
<code class="language-php">function filterByColumn($array, $column, $value) { return array_filter( $array, function($item) use ($column, $value) { return $item[$column] == $value; } ); } $authors = array_map(function($author) use ($posts) { $author["posts"] = filterByColumn( $posts, 1, $author[0] ); // 对 $author 进行其他更改 return $author; }, $authors); $categories = array_map(function($category) use ($posts) { $category["posts"] = filterByColumn( $posts, 2, $category[0] ); // 对 $category 进行其他更改 return $category; }, $categories); $posts = array_map(function($post) use ($authors, $categories) { foreach ($authors as $author) { if ($author[0] == $post[1]) { $post["author"] = $author; break; } } foreach ($categories as $category) { if ($category[0] == $post[1]) { $post["category"] = $category; break; } } // 对 $post 进行其他更改 return $post; }, $posts);</code>
看起来不错,对吧?那么,当我们有大量CSV文件要解析时会发生什么?让我们稍微分析一下内存使用情况……
<code class="language-php">function formatBytes($bytes, $precision = 2) { $kilobyte = 1024; $megabyte = 1024 * 1024; if ($bytes >= 0 && $bytes < $kilobyte) { return $bytes . " b"; } if ($bytes >= $kilobyte && $bytes < $megabyte) { return round($bytes / $kilobyte, $precision) . " kb"; } return round($bytes / $megabyte, $precision) . " mb"; } print "memory:" . formatBytes(memory_get_peak_usage());</code>
(示例代码包含generate.php
,您可以使用它来创建这些CSV文件……)
如果您有大型CSV文件,这段代码应该会显示将这些数组链接在一起需要多少内存。至少与您必须读取的文件大小相同,因为PHP必须将所有内容都保存在内存中。
生成器来救援!
改进此问题的一种方法是使用生成器。如果您不熟悉它们,现在是学习更多知识的好时机。
生成器允许您一次加载少量总数据。使用生成器您无需做太多事情:
<code class="language-php">function readCSV($file) { $rows = []; $handle = fopen($file, "r"); while (!feof($handle)) { $rows[] = fgetcsv($handle); } fclose($handle); return $rows; } $authors = array_filter( readCSV("authors.csv") ); $categories = array_filter( readCSV("categories.csv") ); $posts = array_filter( readCSV("posts.csv") );</code>
如果您遍历CSV数据,您会注意到所需的内存量会立即减少:
<code class="language-php">function filterByColumn($array, $column, $value) { return array_filter( $array, function($item) use ($column, $value) { return $item[$column] == $value; } ); } $authors = array_map(function($author) use ($posts) { $author["posts"] = filterByColumn( $posts, 1, $author[0] ); // 对 $author 进行其他更改 return $author; }, $authors); $categories = array_map(function($category) use ($posts) { $category["posts"] = filterByColumn( $posts, 2, $category[0] ); // 对 $category 进行其他更改 return $category; }, $categories); $posts = array_map(function($post) use ($authors, $categories) { foreach ($authors as $author) { if ($author[0] == $post[1]) { $post["author"] = $author; break; } } foreach ($categories as $category) { if ($category[0] == $post[1]) { $post["category"] = $category; break; } } // 对 $post 进行其他更改 return $post; }, $posts);</code>
如果您之前看到的是兆字节的内存使用量,现在您将看到千字节。这是一个巨大的改进,但它并非没有问题。
首先,array_filter
和array_map
不适用于生成器。您必须找到其他工具来处理此类数据。以下是一个您可以尝试的工具!
<code class="language-php">function formatBytes($bytes, $precision = 2) { $kilobyte = 1024; $megabyte = 1024 * 1024; if ($bytes >= 0 && $bytes < $kilobyte) { return $bytes . " b"; } if ($bytes >= $kilobyte && $bytes < $megabyte) { return round($bytes / $kilobyte, $precision) . " kb"; } return round($bytes / $megabyte, $precision) . " mb"; } print "memory:" . formatBytes(memory_get_peak_usage());</code>
此库引入了一些可与迭代器和生成器一起使用的函数。那么,您如何在不将任何数据保存在内存中的情况下仍然获得所有这些相关数据呢?
<code class="language-php">function readCSVGenerator($file) { $handle = fopen($file, "r"); while (!feof($handle)) { yield fgetcsv($handle); } fclose($handle); }</code>
这可以更简洁:
<code class="language-php">foreach (readCSVGenerator("posts.csv") as $post) { // 使用 $post 执行某些操作 } print "memory:" . formatBytes(memory_get_peak_usage());</code>
(重新读取每个数据源每次都效率低下。考虑将较小的相关数据(如作者和类别)保存在内存中……)
其他有趣的事情
对于Nikic的库来说,这只是冰山一角!曾经想要展平一个数组(或迭代器/生成器)吗?
<code class="language-bash">composer require nikic/iter</code>
您可以使用slice
和take
等函数返回可迭代变量的切片:
<code class="language-php">// ... (后续代码与原文类似,但使用iter库函数进行优化,此处省略以节省篇幅) ...</code>
当您更多地使用生成器时,您可能会发现您并不总是可以重用它们。考虑以下示例:
<code class="language-php">// ... (使用iter库函数简化代码,此处省略以节省篇幅) ...</code>
如果您尝试运行该代码,您将看到一个异常,提示:“无法遍历已关闭的生成器”。此库中的每个迭代器函数都有一个可回绕的对应函数:
<code class="language-php">// ... (使用iter\flatten和iter\toArray函数的示例代码,此处省略以节省篇幅) ...</code>
您可以多次使用此映射函数。您甚至可以使您自己的生成器可回绕:
<code class="language-php">// ... (使用iter\slice和iter\toArray函数的示例代码,此处省略以节省篇幅) ...</code>
您从中获得的是一个可重用的生成器!
结论
对于您需要考虑的每个循环操作,生成器可能都是一个选项。它们甚至对其他事情也很有用。在语言功能不足的地方,Nikic的库提供了大量的高阶函数。
您是否已经在使用生成器?您是否想查看更多关于如何在您自己的应用程序中实现它们以获得一些性能提升的示例?请告诉我们!
(FAQs部分与原文类似,此处省略以节省篇幅。 可以根据需要选择性保留或重新组织FAQs部分。)
以上是发电机和尼基/迭代的内存性能增强的详细内容。更多信息请关注PHP中文网其他相关文章!