見出し画像

【Node.js】JavaScriptで使えるPythonライクなrange関数



時は2022年。4月1日……


…………タイトル詐欺じゃないよ、どうもけしたんです。
 
 今回はタイトルにある通り

「Pythonライクなrange関数」

を作ったのでご紹介します。
 
 range関数に関する記事は山ほどありますが、そんな中、自分が作りたかったものはちょっと方向性が違ったように思えたので今回記事にしました。
 
 前置きが冗長に感じるかもなので、さらっと読み飛ばして頂くか、本題の#どういう所がPython-likeなの?からお読みくださいm(_ _)m

(以下、目次という名の内容構成)


おさらい:Pythonでのrange関数


 一応、今回ベースにしたPython側での挙動について軽くおさらいしておきます。大丈夫な方は飛ばしオッケーです👌

rangeの基礎的な使い方


 rangeの基本的な用法と言えばforとの組み合わせでしょう。これは古事記に………………ではなく、Python公式ドキュメントにそう書かれています。

range
range 型は、数のイミュータブルなシーケンスを表し、一般に for ループにおいて特定の回数のループに使われます。

class range(stop)
class range(start, stop[, step])

https://docs.python.org/ja/3/library/stdtypes.html#ranges

 ちなみになんのこっちゃ?という人には例から入ってみるのがおすすめです。Pythonをインストール済みの方は自前のPCで、ない方はOnline Python Editor - paiza.IOでちゃちゃっと確認しましょう。

#!/usr/bin/env python3

for r in range(10):
  print(r)

上記のコードを実行したら以下のように出力されるかと思います。

0
1
2
3
4
5
6
7
8
9

forによる処理0スタートで10回行われているのが分かると思います。       
 Pythonでは「forで処理を10回まわしたいな~~」っと思った時にさらっとrange(10)と書くだけで出来上がり!と、大変便利な代物です。

 Pythonのrangeはこれだけじゃありません。できることがまだまだあります。例を見ましょう。

for r in range(4, 10, 2):
  print(r)

何やら少し数字が増えましたね。これを実行してみると、

4
6
8

このように出力されると思います。
 rangeの第1引数にはスタートする数字を。第2引数には終点を。第3引数にはその間のステップを。それぞれ指定してあげるとこのような飛び飛びの値が得られます。(ちなみに、上記のコードを日本語化すると「4から10まで2つずつ足して順繰りと(ただし終端を含まず)」となります)

 ちなみにrange関数を呼び出したものをコンソールに出力すると、

print(range(4))
# -> range(0, 4)

print(range(0, 3, 1))
# -> range(0, 3)

print(range(-10, -4, -2))
# -> range(-10, -4, -2)

と表示されます。そのまんまで分かりやすいですね。
 
 ちなみにブランケット記法(角括弧[]を用いる記法)でindexを指定してあげることで、listと同じようにアクセスできます。

print(range(1, 6)[2])
# -> 3

rangeに対するスライス操作


 趣旨とズレてくるのでここは何となくで。
 
 rangeはlistと同じく、ブランケット記法 [] とコロン : を用いることで、新たなrangeオブジェクトを呼び出すことができます。

print(range(0, 10, 2))
# -> range(0, 10, 2)
# -> 0 2 4 6 8

print(range(0, 10, 2)[1:3:1])
# -> range(2, 6, 2)
# -> 2 4

と、こんな感じで元となるrangeオブジェクトに対してスライス操作をすることができます。角括弧[]で指定するのはindexで、括弧内の左から、

始めるindex。終点のindex。その間いくつ飛ばしなのか。

を指定してあげます。例で言えば、このようになります。

range(0, 10, 2)
# -> index: 0, 1, 2, 3, 4,
# -> value: 0, 2, 4, 6, 8,


range(0, 10, 2)[1:3:1] → indexを一つ飛ばしずつ
               ↓    ↘️ 
# -> index: 0,[1],2,[3],4, -> 1, 2, (3
# -> value: 0, 2, 4, 6, 8, -> 2, 4, (6

# -> コンソールでは等価な range(2, 6, 2) と表示される

 ……ちょっと難解かも🤔

作成に至った動機とJavaScriptでの実状


 ここからが前座です(長い)。作成に至った動機とありますが、大きく分けて2つあります。
 
 まず一つに、Pythonのrange関数が楽なんじゃあああ!…と。Node.jsとPythonをちょこちょこ行き来した中で感じるお手軽さ。これはNode.jsでも扱いたい!というのが最初。
 
 あとは、純粋にどこまで実現可能なのだろうか、と気になったからです。
 
 そんなこんなで興味が湧いた(もとい湧いてしまった)ので、お次はJavaScriptサイドを見ようと思います。

JavaScriptにおけるrange関数


 JavaScriptでは組み込みとしてrange関数はありませんが、簡単なものであれば以下のコードで問題ないです。

for (const a of Array(10)) {
  /* 処理 */
}

これでforループは10回まわります。書きやすいですし、何よりも軽量で最強です。
 
 しかしながらここでconsole.logを使ってaの正体を見ると、

undefined

と出力されます。forループで数を扱いたい!という場面がある中、これはやや不便ですね。これを解決するべく「javascript range 🔍」で調べると、以下のコードたちが出てきます。

for (const a of Array.from({ length:10 }, (_, k) => k)) {
  /* 処理 */
}

for (const a of [...Array(10)].map((v, i, a) => i)) {
  /* 処理 */
}

これだと0スタートで連番が作りだせます。嬉しいですね。
 
 普通であればここまでで用たります。ですが次の問題でちょっともやもやします。

in演算子問題


 これはある時のことでした。久しぶりにNode.jsでこねこねしてたら、in演算子がどーーーーーも、想定外の挙動をしていたので両方のインタープリタを呼び出して確認してみました。すると、、、

Pythonインタープリタ
>>> range(0, 10, 2)
range(0, 10, 2)
>>> 1 in range(0, 10, 2)
False
>>> 8 in range(0, 10, 2)
True

Node.jsインタープリタ
> [ 0, 2, 4, 8 ]
[ 0, 2, 4, 8 ]
> 1 in [ 0, 2, 4, 8 ]
true
> 8 in [ 0, 2, 4, 8 ]
false

あ????と声が出ました。んなアホな、と。ですがこれがJS側のin演算子の挙動です。
 
 調べてみるとin 演算子 - JavaScript - MDN Web Docsに、このようなサンプルコードがあります。

// 配列
let trees = ['redwood', 'bay', 'cedar', 'oak', 'maple'];
0 in trees        // true を返す
3 in trees        // true を返す
6 in trees        // false を返す
'bay' in trees    // false を返す (添字の指す値ではなく、添字の数値を指定しなければならない)
'length' in trees // true を返す (length は Array のプロパティ)
Symbol.iterator in trees // true を返す (配列は反復可能。 ES2015 以上で動作する)

 簡単に言うとJSのin演算子は、連想配列(辞書)であればkeyを、配列(Array)であればindexを参照しますよと。valueやelementについては参照しないよ、と。そういうことらしいです。
 
 それならと index == element となる位置に値を挿入すると、今度はfor…ofで空のスロットからはundefinedが出てきたり………。んーと、んーとと、そんなこんなで頭を悩ませつつ、新たな問題が。

for…in問題


 連番で作成したArrayにもちょっとした問題がありました。

for (const a in [...Array(4)].map((v, i, a) => i)) {
  console.log(a);
}
/*console
0
1
2
3
*/

今までの例との違いが分かりにくいですが、注目するのはaと配列の間のinです。このコードはfor…ofで書いたつもりが、実際はfor…inでまわってしまっていました。
 
 for…ofだろうがfor…inだろうが、まわってりゃよかねぇか!?と思いますが、ここでみんな大好きJavaScript - MDN Web Docs
 
 for…inは、for...in - JavaScript | MDN
"for...in は特定の順序で並べられる保証はありません。"
……と、あるように、range関数のような順列に用いるのには不向きなのです。
 
 加えて、for…inではstringが出てくるということもあり、型としても好ましくないため、for…inで呼び出せてしまうのがややこしくて思わぬミスが出そうです。
 
 一方for…ofは順番が保証されてますし、elementがnumberであればnumberが来てくれます。こちらは実行環境に依存しないため、呼び出せるのならこっちで呼び出したいです。

どういう所がPython-likeなの?


 さて、そろそろ本題です。以上の問題を踏まえた、今回のセールスポイント的なコーナーです。(怪しさ満載!)

このあと本記事で紹介するものは……

✅Pythonのrange関数が分かればほぼ●●その流れで使用できる!
✅in演算子で存在可否を確認できる!
✅for…inでは呼び出されず、for…ofで利用可能!
✅引数を利用してスライス操作を疑似的に再現!
✅型定義ファイル付属でインテリセンスも効く!
✅コンソールにrange(args)の形式で表示される!

……と、ざっと言えばこんな感じです。(やっぱり怪しい)

How to use


 ここからは使い方の説明です。一応、note公式からのファイルアップロード機能について※ダウンロードに関する注意点をご一読頂いてから、以下のファイルをDLしてください。(2022/06/08 更新)

 上記の内、上からrange.js(本体ファイル)range.d.ts(型定義ファイル)となっています。適当にディレクトリ(フォルダ)に突っ込んだら、使えます。型定義ファイルに関してはインテリセンス用です。

 ちなみにバージョンはNode.js@16.9.1で想定しています。

requireと呼び出し


 今後の説明において、想定しているファイル構成は以下の通りです。

想定ファイル構成
.
├─ index.js
├─ range.d.ts
└─ range.js

 上記のような構成の場合、index.jsの上らへんに、

const { range } = require("./range");

と、入力してあげることでrange関数が呼び出せます。
 書式はPythonでのrange関数まんまです。例えば、

# in Python@3.10.3
range(10)
range(3, 9)
range(1, 10, 2)

というコードは jsファイルで

// in Node.js@16.9.1
const { range } = require("./range");

range(10);
range(3, 9);
range(1, 10, 2);

と書けます。

console.log


// in Node.js@16.9.1
const { range } = require("./range");


/* console.log */
console.log(range(2, 20, 2));
//=> range(2, 20, 2)
typeof range(2, 20, 2); //=> function
range(0, 10, 2).name; //=> RangeObject 

 上記のように、console.logではrange(args)の形式で表示されます。また、range関数はその返り値にRangeObject(関数)を返します。

for…of と for…in


/* for of */
let of_array = []
for (const r of range(2, 20, 2)) {
  of_array.push(r);
}
console.log("for...of:", of_array);
//=> for...of: [ 2, 4, 6, 8, 10, 12, 14, 16, 18 ]


/* for in */
let in_array = [];
for (const r in range(2, 20, 2)) {
  in_array.push(r);
}
console.log("for...in:", in_array);
/*
C:\Users\xxx.js:295
          throw new SyntaxError(text);
          ^

SyntaxError: You cannot use 'getOwnPropertyDescriptor(RangeObject...)' and 'for...of RangeObject'.
    at Object.getOwnPropertyDescriptor (C:\Users\xxx.js:295:17)
    at Object.<anonymous> (C:\Users\xxx.js:11:10)
    at Module._compile (node:internal/modules/cjs/loader:1101:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
    at node:internal/main/run_main_module:17:47

 for…ofでは期待通り動きfor…inでは処理が行われずエラーとして指摘されます。

in演算子


/* in演算子 */
const R = range(0, 7, 3);
console.log(R, "=", ...R);
//=> range(0, 7, 3) = 0 3 6

for(const r of range(10)) {
  console.log(`${r} in R:`,r in R);
}
/*
0 in R: true
1 in R: false
2 in R: false
3 in R: true
4 in R: false
5 in R: false
6 in R: true
7 in R: false
8 in R: false
9 in R: false
*/

 in演算子でRangeObjectを見てみると、該当する数でtrueを返すことが分かります。

 ちなみにJSの仕様として 1 in R"1" in R の両者は等価で、言ってることは同じです(keyが文字列に変換されて格納されるように、in演算子の前ではnumberの1が文字列に変換される)。

RangeObjectへのアクセス


 Pythonのrangeでindexを指定することで要素にアクセスできるように、今回のものはindexを引数として渡すことで実現させています。

/* RangeObject(index) */
const RangeObject = range(0, 11, 4);
//=> 0 4 8
RangeObject(-2);
//=> 4

 また、属性に start stop step を持ち、プロパティとしてアクセスできます。

RangeObject.start;//=> 0
RangeObject.stop; //=> 11
RangeObject.step; //=> 4

スライス操作


 rangeによって返されたRangeObjectの仕様は、Pythonにおけるスライス操作と似た仕様になります。

 例えば、

# in Python@3.10.3

range(0, 10, 2)  # -> 0 2 4 6 8
range(0, 10, 2)[0::2]
# -> range(0, 10, 4)
# -> 0 4 8

と書かれるコードがあったとき、

// in Node.js@16.9.1
const { range } = require("./range");

range(0, 10, 2); //=> 0 2 4 6 8
range(0, 10, 2)(0, "None", 2);
//=> range(0, 10, 4)
//=> 0 4 8

……と、書き表すことができます。

 また、上記で説明した#RangeObjectへのアクセスとの差異は、

  • 引数が1つの場合には#RangeObjectへのアクセス

  • 引数が2つ乃至3つの場合には#スライス操作

となります。

 加えて、文字列"None"省略を意味し、引数として入力することで関数の意味するところが変わります。("None"の入力は型定義ファイルによってインテリセンスが効きます)

// ...

range(0, 10, 2);
//=> range(0, 10, 2) => 0 2 4 6 8

range(0, 10, 2)(1); 
//=> 2

range(0, 10, 2)(1, "None");
//=> range(2, 10, 2) => 2 4 6 8

 ちなみにPython同様、スライス操作は非破壊で、いくらでも続けて行うことができます。

RangeObjectの等価性


 Pythonでは range(0, -20, 5) == range(13, 45, -2)True を示しますが、JavaScriptではRangeObject同士の比較は、同じ変数同士を除いて常に false を返すため比較ができません。JavaScriptでは等価性が失われています。

# in Python@3.10.3

print(range(0, -20, 5) == range(13, 45, -2))
# -> True
// in Node.js@16.9.1

console.log(range(0, -20, 5) == range(13, 45, -2));
//=> false

 これが気になる人はObject.definePropertyなどで比較用の関数を組み込むといいかもしれません。
 
 ちなみに A == B + ""  や、A + [] == B など、比較の際に一方の型を変える操作を行う場合には比較が可能です。(しかし、これはスマートな方法ではなく、加えて想定外の挙動でミスが生じやすいのであまりおすすめはしません)

const R = range(2, 10, 2);
console.log(R + "");
//=> (2, 4, 6, 8)

const EMPTY_1 = range(0, -20, 5);
const EMPTY_2 = range(13, 45, -2);

/* 比較(==) */
EMPTY_1 == EMPTY_2 // false
EMPTY_1 == EMPTY_2 + "" // true
EMPTY_1 == EMPTY_2 + [] // true
EMPTY_1 == String(EMPTY_2) // false
EMPTY_1 == new String(EMPTY_2) // false
 
/* 比較(===) */
EMPTY_1 === EMPTY_2 + "" // false
EMPTY_1 === EMPTY_2 + [] // false
EMPTY_1 + [] === EMPTY_2 + "" // true
EMPTY_1 + "" === String(EMPTY_2) // false

instanceof演算子


 実行ファイルのトップにあるrequire文に、RangeObject と追加することでinstanceof演算子による真偽が判定可能になります。

// in Node.js@16.9.1
const { range, RangeObject } = require("./range");

const R = range(2, 11, 4);
console.log(R instanceof RangeObject);
//=> true

 しかしながら、このrequireしたRangeObjectはinstanceof演算子での判別用なため、new演算子や関数としての呼び出しには対応していません。

Arrayとの比較


 Arrayとrangeにおける、可能な操作と処理速度などの比較です。あくまで参考程度にお願い致します。

$$
\begin{array}{|cc|c||c:c:c|}\hline
テスト関数&状況&TIME&in演算子での判別&数での反復&列挙操作の排除\\
\hline\hline
range(2**32-1)&(初期動作)&0.53ms&◯&◯&◯\\
 &(安定後)&0.035ms&◯&◯&◯\\
\hline
Array(2**32-1)&(初期動作)&0.077ms&△&✕&✕\\
 &(安定後)&0.007ms&△&✕&✕\\
\hdashline
A.from((_,k)=>k)&(2**26-1)&6.069s&△&◯&✕\\
\hline\hline
range(2**53-1)&(初期動作)&0.51ms&◯&◯&◯\\
 &(安定後)&0.032ms&◯&◯&◯\\
\hline
Array(2**53-1)&(エラー)&─ms&─&─&─\\
\hline
\end{array}\\
◯:問題なし \\
△:疑似的表現\\
✕:不可   \\
─:データなし\\
$$

※今回のrange関数はPython2.x系でいう所のxrange(Python3.xではrangeに統合もとい変更)に当たるもので、内部に配列を持たないものです。ただ、in演算子で判別可能にするために内部で一度イテレータを回しているため、該当する要素が多ければ多いほど動作が遅くなります。
=>解決済み。今は要素数関係なく、一定でそこそこ速いです。

Q&A


Q1. htmlやcssとともにWebブラウザ上でも使えますか?

Ans. 恐らく使えます。(あまり詳しくないですが)
 ただ少なくとも現時点で分かることとして、console.logで出力した際に range(arg1, arg2, arg3) の形式で表示されません(これはNode.js内部APIの仕様を扱っているためです)。
 加えて、Errorのために行う処理やin演算子で存在可否を調べられるようにする処理などで、不要な処理がかさむため、やや動作が遅くなる可能性があります。
 また、Node.js主体で作っている関係上、予期せぬ動作をするかもしれません。ご調整の上、ご利用くださいm(_ _)m


Q2. コード汚くないですか?恥ずかしくないの?

Ans. プログラミング初心者丸出しで申し訳ないです()
 頑張ります。


Q3. コード書き換えて公開します!

Ans. ありがとうございます。
 余白があればで構わないのですが、本記事のURLなども載せて頂けると幸いです。喜びます。


Q4. ○○で既に同じようなのあったよ。

Ans. 本記事のトップにURLを掲載いたします。


さいごに


 ここまでお読みいただき、ありがとうございます!知らないことも多く、何をするにも四苦八苦で……結構大変でしたが、今回はそこそこいいものが作れたような気がします。良ければ使ってあげてください。またの機会があればよろしくお願いします!
 それではでは。

おまけ


サムネに入り切りませんでした

参考


更新履歴


2022/04/06
#How to use の配布ファイルを改善&更新
#スライス操作 を加筆
#RangeObjectの等価性 を修正&更新
#instanceof演算子 を新たに追加
#Arrayとの比較 を加筆&修正
#Q&A Q1 Ans について加筆&修正
#参考 に参考元URLを追加で記載

2022/06/08
#How to use の配布ファイルを改善&更新
#for…of と for…in の内容変更
#Arrayとの比較 を変更に伴い修正

この記事が気に入ったらサポートをしてみませんか?