【Python】これはするな選手権

# まえがき

おはようございます。  
諸事情によりPythonについての技術書を読むことになったのですが、
掲載されているいくつかのコードを見ていると「えぇ…」と思うことが多々ありましたので、ここで共有したいと思います。

なお、この記事は特定の書籍ないしは出版社、著者、その他を批判するためのものではないこと、あらかじめご了承ください。
（強いて言うなら、こういった書き方をするPythonistaを全般的に批判しているかもしれません）

# None比較を`==`でするな

特定のオブジェクトが`None`（多言語であれば`null`）であるかどうかを検証するときの話です。
`a == None`と書いているようであれば、それは`a is None`に書き直したほうが良いかもしれません。

## なぜ？

Pythonにおいて、`None`は**シングルトン**です。
つまり、`None`というオブジェクトはランタイム全体で共有され、同じメモリアドレスを指すということになります。

これは、`id()`組み込み関数を使うと簡単に検証できます。

```py
>>> a = None
>>> b = None
>>> id(a)
125434902446576
>>> id(b)
125434902446576
```

このようなシングルトンに対しては、等価演算子 `==` ではなく `is` または `is not`を用いるべきです。

PythonのスタイルガイドであるPEP 8にも、似たようなことが書いてあります:

> Comparisons to singletons like None should always be done with is or is not, never the equality operators.
> Also, beware of writing if x when you really mean if x is not None – e.g. when testing whether a variable or argument that defaults to None was set to some other value. The other value might have a type (such as a container) that could be false in a boolean context!
> 
> 引用: https://peps.python.org/pep-0008/#programming-recommendations

# 組み込み関数名を変数名に使うな

変数名をつけるときに、`str`や`int`といった組み込み関数の名前を使用しないでください。
（`str` `int`だけに限定されません。）

## なぜ？

Pythonは自由な言語なので、たいていの無茶には対応してくれます。
しかしそれらを黙って受けいれてくれるゆえ、タブーを踏んでしまうことも少なくありません。

例えば、以下のコードについて考えます。

```py
str = input("名前を入力してください: ")
print(f"こんにちは、{str}さん！")
```

言わずもがな、実行結果は以下のようにになります:

```py
>>> str = input("名前を入力してください: ")
名前を入力してください: 田中太郎
>>> print(f"こんにちは、{str}さん！")
こんにちは、田中太郎さん！
```

一見エラーもなく普通のコードに見えますが、これにより重大な被害が発生しています。
**`str()`** 組み込み関数の存在が上書きされてしまっています。
つまり、この状態で`str(1 + 1)`を実行しようものなら...

```py
>>> str(1 + 1)   # <-- 2を期待している
Traceback (most recent call last):
  File "<python-input-9>", line 1, in <module>
    str(1 + 1)
    ~~~^^^^^^^
TypeError: 'str' object is not callable
```

「文字列（str）オブジェクトが呼び出し可能ではない」というエラーが発生します。

このように、安易に`str`や`int`などの組み込み関数名を変数名に採用してしまうと、予期しないエラーのもとになります。

## 対処法 

代わりに、もっと意味のある名前をつけましょう。
「変数を定義する」ということは、「オブジェクトにラベルを貼る」ということです。
箱に名前をつけるのではなく、期待する中身に名前をつけましょう。

```diff_python
- str = input("名前を入力してください: ")
+ name = input("名前を入力してください: ")
```

# ユーザー入力を信頼するな

Pythonに限りませんが、似たような話ということで。

ユーザーからの入力を信じてはいけません。むしろ、疑うべきです。
例えば、以下のFastAPIコード:

```py
@app.post("/search")
async def search_users(schema: SearchRequestSchema):
    found_users = await database.execute(f"SELECT * FROM users WHERE name LIKE '%{schema.query}%';")
    return found_users
```

ここで、ユーザーが以下のようなリクエストを送ると:

```json
{ "query": "' OR '1'='1'; --" }
```

こんなクエリが構築され:

```sql
SELECT * FROM users WHERE name LIKE '%' OR '1' = '1'; --%';
```

予期しない挙動を生みます。（もちろんこれは最も理想化されていることを念頭に置くべきです）

これを**SQLインジェクション**と呼んだりします。

## 対処法

今回のケースでは、ユーザーからの入力が安全なものであると信頼してしまったことが原因です。
ユーザー入力は危険なものであるという認識からはじめ、**何が来ても信頼できるものに変換する**という気持ちで、
ユーザー入力を信頼に値するものへ**自らの手で**しなければなりません。

どのようにユーザー入力を信頼するのか。大まか以下のような方法があります:

* 特殊文字や記号をエスケープする
* 不審なキーワードや記号が入っている入力を事前に弾く（エラーにしてしまう）

データベースの世界で言うならば、**ORM**を使うという手もあります。（というか、一般的なアプリケーションではSQLを直書きするほうが珍しいです（諸説あり））
Pythonであれば[SQLAlchemy](https://www.sqlalchemy.org/)、.NETであれば[Entity Framework Core](https://learn.microsoft.com/ja-jp/ef/core/)、Node.jsであれば[Prisma](https://www.prisma.io/)などがあります。
セキュリティのことを考えるなら、自分で頑張ってパース・エスケープするよりも、先人たちの知恵をありがたく受取るほうが良いです。

---

# おわりに

以上、Pythonの「これはやるな選手権」でした。
みなさんの「えっ」と思ったことはありますか？知見を広げるために、ぜひコメントで情報を広めていただくか、技術記事を投稿しましょう。エントリーお待ちしています。
また、もしこのページの内容にミスが含まれている場合、遠慮なく指摘をお願いします。それがあなたのためでも、私のためでもありますし、今後この記事を見るすべての人のためでもあります。

読んでいただきありがとうございました。