先日GitHubの利用実績の年次レポートが公開され、TypeScriptがGitHubで使われている最も多い言語であることが発表されました。
https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/
次点で引き続きPythonがランクインしており、需要の高さが伺えます。
エンジニアの中には実務上で両方の言語を書いているという人も多いのではないでしょうか?
今回は需要の高いJavaScript/TypeScriptとPythonの二言語を同時に扱う際、言語仕様によって不具合を引き起こしかねない、気をつけたいポイントをまとめてみました。
Falsyな値の扱い
Falsyな値とは、条件分岐式などで用いられた際、偽値つまりFalseと扱われる値のことを指します。
https://developer.mozilla.org/ja/docs/Glossary/Falsy
Falsyとなる値はJavaScriptやPythonに限らず、プログラミング言語ごとに異なります。そのため、実装をそのまま別言語にリライトすると異なった挙動をとることがあります。
e.g. 配列の場合
def process_users(users):
if users:
print(f"処理開始: {len(users)}人のユーザーがいます。")
else:
print("データなし: ユーザーがいません。")
user_list = []
process_users(user_list)
# 【実行結果】データなし: ユーザーがいません。
function processUsers(users) {
if (users) {
console.log(`処理開始: ${users.length}人のユーザーがいます。`);
} else {
console.log("データなし: ユーザーがいません。");
}
}
const userList = [];
processUsers(userList);
// 【実行結果】処理開始: 0人のユーザーがいます。
e.g. 辞書型、オブジェクト型の場合
def load_settings(settings):
if settings:
print(f"カスタム設定を適用します: {settings}")
else:
print("設定が見つかりません。デフォルト設定を使用します。")
user_config = {}
load_settings(user_config)
# 【実行結果】設定が見つかりません。デフォルト設定を使用します。
function loadSettings(settings) {
if (settings) {
console.log("カスタム設定を適用します:", settings);
} else {
console.log("設定が見つかりません。デフォルト設定を使用します。");
}
}
// テスト実行:空のオブジェクトを渡す
const userConfig = {};
loadSettings(userConfig);
// 【実行結果】カスタム設定を適用します: {}
三項演算子のようなショートハンドで記述した場合も同じルールが適用されるので、JavaScriptやPythonに限らず、言語のFalsyとなる値はしっかりと押さえた上で条件式を書くようにしたいですね。
sort()メソッドのデフォルトの挙動
Pythonの場合、配列内のデータ形式を把握した上でソーティングされるので、そのまま使用しても正しい結果となります。
しかし、JavaScriptのsort()関数の場合、比較関数を引数に渡していない場合は文字列として比較されるため、想定とは異なる結果が返されることがあります。
compareFn が与えられなかった場合、undefined 以外のすべての配列要素は文字列に変換され、文字列が UTF-16 コード単位順でソートされます。例えば、"banana" は "cherry" の前に来ます。数値のソートでは、9 が 80 の前に来ますが、数値は文字列に変換されるため、Unicode 順で "80" が "9" の前に来ます。undefined の要素はすべて、配列の末尾に並べられます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
prices = [5, 100, 1, 25]
prices.sort()
print(f"並び替え結果: {prices}")
# 【実行結果】
# 並び替え結果: [1, 5, 25, 100]
const prices = [5, 100, 1, 25];
prices.sort();
console.log(`並び替え結果: ${JSON.stringify(prices)}`);
// 【実行結果】
// 並び替え結果: [1, 100, 25, 5]
JavaScriptのsort()メソッドで数値型の変数をソーティングさせる場合は、きちんとsort()メソッドに比較用の引数を渡してやることを意識しましょう。
const prices = [5, 100, 1, 25];
prices.sort((a, b) => a - b);
console.log(`並び替え結果: ${JSON.stringify(prices)}`);
// 【実行結果】
// 並び替え結果: [1, 5, 25, 100]
デフォルト引数の挙動
Pythonでは、関数のデフォルト引数は定義した時一度だけ作られる仕様になっています。
そのため、ミュータブルな値をデフォルト値に与えてしまうと、リセットされないまま使い回されてしまうというケースが存在します。
JavaScriptにこの仕様は存在せず、常に初期化されるので気にする必要はありません。
def create_shipment(item, shipment_list=[]):
shipment_list.append(item)
return shipment_list
# 1回目の配送
package1 = create_shipment("Apple")
print(f"配送1: {package1}")
# -> 配送1: ['Apple']
# 2回目の配送
package2 = create_shipment("Banana")
print(f"配送2: {package2}")
# 【実行結果】
# 配送2: ['Apple', 'Banana']
function createShipment(item, shipmentList = []) {
shipmentList.push(item);
return shipmentList;
}
// 1回目の配送
const package1 = createShipment("Apple");
console.log(`配送1: ${JSON.stringify(package1)}`);
// -> 配送1: ["Apple"]
// 2回目の配送
const package2 = createShipment("Banana");
console.log(`配送2: ${JSON.stringify(package2)}`);
// 【実行結果】
// 配送2: ["Banana"]
この仕様のため、Pythonでデフォルト引数を与えるときはイミュータブルな値に限定するのが鉄則。
迷ったら引数にデフォルト値を与えない方が良いかと思います。
ループ変数(など)の寿命
昨今のプログラミングでは変数の寿命は極力短くすることが望ましいとされていますが、Pythonの場合、ループ処理内で定義した一時変数がそのまま外部参照できてしまいます。
JavaScriptの場合では、letのようなブロックスコープ内でのみ有効な変数を使った場合は問題ありませんが、レガシーなvarで変数定義した場合はPython同様に外部から参照できてしまいます。
def loop_test():
print("--- Python Loop Start ---")
for i in range(3):
print(f"ループ中: {i}")
print(f"ループ終了後の i の値: {i}")
loop_test()
# 【実行結果】
# --- Python Loop Start ---
# ループ中: 0
# ループ中: 1
# ループ中: 2
# ループ終了後の i の値: 2
function loopTest() {
console.log("--- JS Loop Start ---");
for (let i = 0; i < 3; i++) {
console.log(`ループ中: ${i}`);
}
try {
console.log(`ループ終了後の i の値: ${i}`);
} catch (e) {
console.log("エラー発生: 変数 i は存在しません (ReferenceError)");
}
}
loopTest();
// 【実行結果】
// --- JS Loop Start ---
// ループ中: 0
// ループ中: 1
// ループ中: 2
// エラー発生: 変数 i は存在しません (ReferenceError)
ループ変数に限らない話にはなりますが、Pythonにはブロックスコープの概念が存在しません。
そのため、上述のループ処理をグローバルスコープの中に書いてしまうと変数iを汚染してしまうことになってしまいます。
そのため、Pythonの場合はループ変数のような特定の処理内でしか使われない変数は、きちんと関数内のローカル変数として定義してやることが重要になります。
辞書型・オブジェクト型の存在しないキーへのアクセス
PythonとJavaScriptではエラーが発生した際の挙動が異なり、即座にエラーとなりプログラムが停止するPythonに対し、JavaScriptは例外が明示的にthrowされなければそのまま進行する、という動き方をします。
JavaScriptではオブジェクトに存在しないkeyにアクセスがあった際は例外を送出しません、そのため、そのまま処理が進んでしまい結果にundefinedが出力されてしまいます。
def get_user_age(user_data):
print("--- 処理開始 ---")
age = user_data["age"]
print(f"年齢は {age} 歳です") # ここには到達しない
user = {"name": "Alice"}
get_user_age(user)
# 【実行結果】
# --- 処理開始 ---
# KeyError: 'age'
# (プログラムはここでクラッシュして終了)
function getUserAge(userData) {
console.log("--- 処理開始 ---");
const age = userData["age"];
console.log(`年齢は ${age} 歳です`);
console.log(`来年は ${age + 1} 歳ですね`);
}
const user = {name: "Alice"};
getUserAge(user);
// 【実行結果】
// --- 処理開始 ---
// 年齢は undefined 歳です
// 来年は NaN 歳ですね
JavaScriptで対策するには以下のように存在チェックをするべきですが、
function getUserAge(userData) {
console.log("--- 処理開始 ---");
const age = userData["age"];
if(age === undefined) {
throw new Error("年齢情報がありません");
}
console.log(`年齢は ${age} 歳です`);
console.log(`来年は ${age + 1} 歳ですね`);
}
const user = {name: "Alice"};
getUserAge(user);
TypeScriptであればIDEで警告が表示され、すぐにエラーに気づくことができるので、単純ミスを防ぐ意味でもTypeScriptの導入は効果的です。
function getUserAge(userData: { name: string; age: number }) {
console.log("--- 処理開始 ---");
// ここでは確実に age が存在することが保証される
const age = userData.age;
console.log(`年齢は ${age} 歳です`);
console.log(`来年は ${age + 1} 歳ですね`);
}
const user = { name: "Alice" };
getUserAge(user); // type error
等価演算子==の仕様の違い
Pythonをメインでコードライティングしていると馴染みが浅いかもしれませんが、JavaScriptの等価演算子は2つあり、Pythonのそれと同じ挙動をするのは厳密等価演算子 === といいます。
これは == と別の振る舞いを取り、JavaScriptにおいて普通の等価演算子は型変換を勝手に行なって比較をしてしまいます。
# 文字列と数値は別物
if 1 == "1":
print("同じ")
else:
print("違う")
# 結果: "違う"
// 文字列と数値でも、なんとなく合わせてしまう
if (1 == "1") {
console.log("同じ");
} else {
console.log("違う");
}
// 結果: "同じ"
Python -> JavaScriptに書き直すときは特に注意しましょう。
ESLintで等価演算子の入力を制限するのも良いかと思います。
https://eslint.org/docs/latest/rules/eqeqeq
巨大な桁数を用いる整数
リレーショナルデータベース(特に金融系)を扱ったことがある人には馴染みがあるかもしれませんが、BigInt型という巨大な桁数を有する数値型があります。
Pythonの場合はメモリの許す限り、通常のint型で巨大な桁数を扱うことができますが、JavaScriptの通常の数値型には限界が存在します。
そのため、計算結果が想定とは異なる結果が返されることがあります。
big_num = 10**100
print(big_num + 1)
# 正しく 100...001 と計算される
const bigNum = Math.pow(10, 16); // 10000000000000000
console.log(bigNum + 1);
// -> 10000000000000000 (1が足されていない!誤差で消滅)
ES2020以降のバージョンでBigInt型を扱うことができるようになっているので、こちらを使うようにしましょう。
const bigIntNum = BigInt("10000000000000000");
console.log(bigIntNum + BigInt(1));
// -> 10000000000000001n (正しく1が足されている)
いかがでしたでしょうか?
複数の言語を取り扱うようになるほど、細かな言語の仕様の差から致命的な不具合を作ってしまった、という事もあるかと思います。
上述の内容は静的コード解析のようなツールである程度潰せるようになってきていますし、AIコードレビューという便利な手法も生まれました。
実装ミスを防ぐのは普段から意識する事も大切ですが、マンパワーに頼らない方法も確立できるといいのかなと思いました。
以上です。