まえがき
おはようございます。
諸事情によりPythonについての技術書を読むことになったのですが、
掲載されているいくつかのコードを見ていると「えぇ…」と思うことが多々ありましたので、ここで共有したいと思います。
なお、この記事は特定の書籍ないしは出版社、著者、その他を批判するためのものではないこと、あらかじめご了承ください。
(強いて言うなら、こういった書き方をするPythonistaを全般的に批判しているかもしれません)
None比較を==
でするな
特定のオブジェクトがNone
(多言語であればnull
)であるかどうかを検証するときの話です。
a == None
と書いているようであれば、それはa is None
に書き直したほうが良いかもしれません。
なぜ?
Pythonにおいて、None
はシングルトンです。
つまり、None
というオブジェクトはランタイム全体で共有され、同じメモリアドレスを指すということになります。
これは、id()
組み込み関数を使うと簡単に検証できます。
>>> 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は自由な言語なので、たいていの無茶には対応してくれます。
しかしそれらを黙って受けいれてくれるゆえ、タブーを踏んでしまうことも少なくありません。
例えば、以下のコードについて考えます。
str = input("名前を入力してください: ")
print(f"こんにちは、{str}さん!")
言わずもがな、実行結果は以下のようにになります:
>>> str = input("名前を入力してください: ")
名前を入力してください: 田中太郎
>>> print(f"こんにちは、{str}さん!")
こんにちは、田中太郎さん!
一見エラーもなく普通のコードに見えますが、これにより重大な被害が発生しています。
str()
組み込み関数の存在が上書きされてしまっています。
つまり、この状態でstr(1 + 1)
を実行しようものなら...
>>> 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
などの組み込み関数名を変数名に採用してしまうと、予期しないエラーのもとになります。
対処法
代わりに、もっと意味のある名前をつけましょう。
「変数を定義する」ということは、「オブジェクトにラベルを貼る」ということです。
箱に名前をつけるのではなく、期待する中身に名前をつけましょう。
- str = input("名前を入力してください: ")
+ name = input("名前を入力してください: ")
ユーザー入力を信頼するな
Pythonに限りませんが、似たような話ということで。
ユーザーからの入力を信じてはいけません。むしろ、疑うべきです。
例えば、以下のFastAPIコード:
@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
ここで、ユーザーが以下のようなリクエストを送ると:
{ "query": "' OR '1'='1'; --" }
こんなクエリが構築され:
SELECT * FROM users WHERE name LIKE '%' OR '1' = '1'; --%';
予期しない挙動を生みます。(もちろんこれは最も理想化されていることを念頭に置くべきです)
これをSQLインジェクションと呼んだりします。
対処法
今回のケースでは、ユーザーからの入力が安全なものであると信頼してしまったことが原因です。
ユーザー入力は危険なものであるという認識からはじめ、何が来ても信頼できるものに変換するという気持ちで、
ユーザー入力を信頼に値するものへ自らの手でしなければなりません。
どのようにユーザー入力を信頼するのか。大まか以下のような方法があります:
- 特殊文字や記号をエスケープする
- 不審なキーワードや記号が入っている入力を事前に弾く(エラーにしてしまう)
データベースの世界で言うならば、ORMを使うという手もあります。(というか、一般的なアプリケーションではSQLを直書きするほうが珍しいです(諸説あり))
PythonであればSQLAlchemy、.NETであればEntity Framework Core、Node.jsであればPrismaなどがあります。
セキュリティのことを考えるなら、自分で頑張ってパース・エスケープするよりも、先人たちの知恵をありがたく受取るほうが良いです。
おわりに
以上、Pythonの「これはやるな選手権」でした。
みなさんの「えっ」と思ったことはありますか?知見を広げるために、ぜひコメントで情報を広めていただくか、技術記事を投稿しましょう。エントリーお待ちしています。
また、もしこのページの内容にミスが含まれている場合、遠慮なく指摘をお願いします。それがあなたのためでも、私のためでもありますし、今後この記事を見るすべての人のためでもあります。
読んでいただきありがとうございました。