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中文網其他相關文章!