pythonでは一定の回数ループさせたり、一定の範囲の数列を扱うために、range()関数があります。
pythonのプログラムでは頻繁に使われる便利な関数なのですが、実はPHPにもpythonと同様、range関数が標準で用意されています。
今回はrange関数を使ったコードの例と、大量回数ループ時のメモリ消費に関する問題( Allowed memory xx bytes exhausted問題)、および問題点の解決方法について説明します。
一定の回数ループさせる
まずは、ループカウンタ代わりにrange関数を使用する方法です。
foreach (range(1,5) as $i) {
echo $i . ", ";
}
# 実行結果
# 1, 2, 3, 4, 5,
これでfor($i = 1; $i < 6; $i++)
と同様のふるまいになります。この程度ならforで書いてしまっても良いですが1~5まで出力されるという範囲が、より分かりやすいです。
逆順にループさせる
第二引数より第一引数の数を大きくすると逆順で値を取得できます。
foreach (range(5,1) as $i) {
echo $i . ", ";
}
# 実行結果
# 5, 4, 3, 2, 1,
逆順のループは、終了条件の不等号を逆向きに間違えたりすることも多いので、こちらの方が直感的にわかりやすいです。
5づつ加算する
range()関数は通常数値を1づつ加減算しますが、第3引数に増加減の刻み値を指定できます。
下記のコードで5の倍数のみを出力できます
// 5の倍数だけ表示
foreach (range(0, 100, 5) as $i) {
echo $i . ", ";
}
# 実行結果
# 0, 5, 10, ... 100
初期値を端数にしておくと、nの倍数+m
の数列も作ることができます
foreach (range(3, 100, 5) as $i) {
echo $i . ", ";
}
# 3, 8, 13, 18, 23, ... 93, 98,
第3引数をマイナスにしても意味はありません。
関数仕様上は「step は正の数でなければなりません。」とあるので、これは使い方として間違ってます。
(将来のバージョンで例外が投げられるなど、振る舞いが変わるリスクもあります)
# この使い方は望ましくない!!
foreach (range(3, 100, -5) as $i) {
echo $i . ", ";
}
# 3, 8, 13, 18, 23, ... 93, 98,
逆順に取得したい場合は、やはり、第一引数の方を大きな数字になるようにします。
# 逆順に表示させるときはこちら
foreach (range(98, 0, 5) as $i) {
echo $i . ", ";
}
# 98, 93, 88, 83, 78, ... 8, 3,
時刻のセレクトボックス用の候補を作る
以下のようにrangeで2重ループを作ると、htmlのselect要素で時刻選択をするための候補を簡単に作れます。
内側のループを15刻みにしていますが、rangeの第3引数を30にすれば30分刻みの候補が作成できます
foreach (range(0,23) as $h) {
foreach (range(0, 59, 15) as $m) {
printf("%02d:%02d\n", $h, $m);
}
}
# 出力
# 00:00
# 00:15
# 00:30
# 00:45
# 01:00
# 01:15
# ...
# 23:15
# 23:30
# 23:45
アルファベットのシーケンスを作る
rangeにアスキー文字を指定することで、アルファベットのシーケンスを作る事もできます。
この並びは、厳密にはアルファベット順ではなく文字コード順ですが、ASCIIコードのアルファベットは文字コード順に並んでいるのでこの処理で取得できます。
foreach (range('A','F') as $i) {
echo $i . ", ";
}
# A,B,C,D,E,F
16進文字列を作る
以下のように2つのrangeを組み合わせると、16進数の文字列を組み立てられます。
$hexStr = array_merge(range('0','9'), range('A','F'));
foreach ($hexStr as $i) {
echo $i . ", ";
}
# 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F,
Excelの列名を返す
Excelの列名は数字ではなくアルファベットの表記(A, B, C... Z, AA, AB...)で、プログラムからの扱いが少々面倒なのですが、下記ののようにすると、Excelの列名を簡単に生成できます。
この例では、列名が1文字の時と2文字の時でそれぞれループさせているので、関数化したうえでyieldによるジェネレータ機能を利用しています。
function excelColName() {
foreach (range('A','Z') as $i) {
yield $i;
}
foreach (range('A','Z') as $i) {
foreach (range('A','Z') as $j) {
yield $i . $j;
}
}
}
foreach( excelColName() as $colName ) {
echo $colName . PHP_EOL;
}
# A, B, C, D, ... Y, Z, AA, AB, ... ZY, ZZ,
大量ループによるメモリ不足に対処する
このようにいろいろと便利なrange関数ですが、欠点もあります。
以下のように大量の値を指定すると、メモリ不足エラーが発生してしまいます。
foreach (range(1,1000000000) as $i) {
}
# PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 34359738376 bytes)
これは、range関数が指定された数分の配列を、内部で作ってしまっているためです。
※ちなみにpythonのrange()関数も同様のふるまいです。
pythonでは、このような場合のためにxrange()というものがあります。pythonのxrange関数は、内部で配列を作らずに数列を都度呼び元に返していくので、大量のメモリを消費することはありません。
PHPではxrange()関数は無いのですが、幸い、ジェネレータの機能を使うと簡単に自作することができます。
<?php
function xrange($start, $limit, $step = 1)
{
if ($start < $limit) {
if ($step <= 0) {
throw new LogicException('Step must be +ve');
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i;
}
} else {
if ($step >= 0) {
throw new LogicException('Step must be -ve');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
上記のコードは実は、PHPのジェネレータ機能に関するドキュメントのサンプルコードです。
http://php.net/manual/ja/language.generators.overview.php
このxrangeを利用することで、件数がどんなに大きくなっても消費するメモリは1KByte以内に収まるので、大量回数のループもこれで安心です。