はじめに
新世代は この未来だ♪1 nikkieです(ユーフォ アンコン編観た)
タイトルの件が「わたし、気になります!」と、ソースコードを読んで知ったことをまとめます。
目次
- はじめに
- 目次
- pycodestyleのソースコードを読みたくなった経緯
- 正規表現COMPARE_TYPE_REGEXを読み解く
- 正規表現を使ってE721をチェックする実装
- 終わりに
- P.S. 最近pycodestyleのE721がアップデートされました
pycodestyleのソースコードを読みたくなった経緯
flake8がお怒りなので2、typeの返り値を型と比較するコードの代わりに、isinstanceを使いましょうという記事を書きました
今週はLintについてインプットしていて、その中でこの検出の実装を興味深いと感じ始めました。
トークン列でもASTでもなく、ソースコード(つまり単なる文字列)に対して正規表現を使って検出しているのです!
正規表現COMPARE_TYPE_REGEX
を読み解く
https://github.com/PyCQA/pycodestyle/blob/2.11.0/pycodestyle.py#L128-L131
COMPARE_TYPE_REGEX = re.compile( r'[=!]=\s+type(?:\s*\(\s*([^)]*[^ )])\s*\))' r'|\btype(?:\s*\(\s*([^)]*[^ )])\s*\))\s+[=!]=' )
reモジュールのドキュメントにあたりながら読み解きましょう
r'[=!]=\s+type(?:\s*\(\s*([^)]*[^ )])\s*\))'
r'\btype(?:\s*\(\s*([^)]*[^ )])\s*\))\s+[=!]='
いくつか共通部分がありますよね。
これから読み解いていきますが、大まかな全体像は以下です。
- 1は、「== type(...)」や「!= type(...)」にマッチする正規表現です
- 2は、「type(...) ==」や「type(...) !=」にマッチします
(?:\s*\(\s*([^)]*[^ )])\s*\))
ってなんだ?
(?:...)
は「普通の丸括弧の、キャプチャしない版」です。
つまり内側の「\s*\(\s*([^)]*[^ )])\s*\)
」にマッチする文字列をキャプチャしません。
内側を読み解いていくと
([^)]*[^ )])
ですが、キャプチャの()
が付いています。
外側の(?:...)
はキャプチャしませんが、内側の丸カッコでは再度(?:...)
としない限りキャプチャします。
キャプチャする文字列[^)]*[^ )]
は、文字の集合で表されています
[^)]
:閉じカッコでない文字- これが0個以上
[^ )]
:半角スペースでも閉じカッコでもない文字(1文字)
つまり、typeに引数が与えられたような場合(type(hoge)
やtype(1)
)は、この正規表現にマッチし、引数部分がキャプチャされます。
確認例(この記事ではPython 3.11.4で確認しています)
>>> re.match(r'[^)]*[^ )]', "hoge") <re.Match object; span=(0, 4), match='hoge'> >>> re.match(r'[^)]*[^ )]', ")hoge") # )で始まるのでマッチしない >>> re.match(r'[^)]*[^ )]', "hoge ) ") # 末尾の半角スペースや)を除いてマッチ <re.Match object; span=(0, 4), match='hoge'>
落ち穂拾い
[=!]=
は==
または!=
にマッチしますね- 組み込みのtypeの返り値の同値性を比較する実装で使われます
- この前後に入る
\s+
は1つ以上の空白文字 - 2にある
\b
空文字列にマッチしますが、単語の先頭か末尾でのみです。
\btype
なので、先頭に何も来ないtype
でマッチしますcompute_type
のような文字列にはマッチしません
>>> re.match(r'\btype', "type(") <re.Match object; span=(0, 4), match='type'> >>> re.match(r'\btype', "compute_type(") # typeが単語の途中のため、マッチしない
以上、組み込みのtypeの返り値を==
や!=
で比較するコードを表す正規表現を見てきました。
正規表現を使ってE721をチェックする実装
comparison_type
関数にあります。
(そうそう、pycodestyleの実装は1ファイルなんですよ!)
https://github.com/PyCQA/pycodestyle/blob/2.11.0/pycodestyle.py#L1451-L1470
# 一部抜粋 def comparison_type(logical_line, noqa): match = COMPARE_TYPE_REGEX.search(logical_line) if match and not noqa: yield ( match.start(), "E721 do not compare types, ...", )
- 引数
logical_line
は論理行です- docstringにある「
if type(obj) == type(1):
」のような文字列が渡ってくると理解しています4 - 1行以上の物理行からなります ref: https://docs.python.org/ja/3/reference/lexical_analysis.html#logical-lines
- 物理行に細かく改行を入れた場合、これは1行ずつ処理すると正規表現にマッチしませんが、論理行で扱えば正規表現にマッチします!
- docstringにある「
type( value ) == int type(value) \ == int
検出できるか確認です。
>>> from pycodestyle import COMPARE_TYPE_REGEX >>> COMPARE_TYPE_REGEX.search("if isinstance(obj, int):") # Okay >>> COMPARE_TYPE_REGEX.search("if type(obj) is int:") # Okay >>> COMPARE_TYPE_REGEX.search("if type(obj) == type(1):") # E721の例 <re.Match object; span=(3, 15), match='type(obj) =='> >>> COMPARE_TYPE_REGEX.search("if type(obj) == int:") <re.Match object; span=(3, 15), match='type(obj) =='>
論理行(文字列)の中に、正規表現に該当する部分文字列があるかsearchして、ある場合はE721を送出。
正規表現で実装できていますね〜
終わりに
pycodestyleがE721を送出するのに正規表現を使った実装でどうやっているのか見てきました。
typeの返り値を==
や!=
で比較する実装って正規表現で表せるんですね!
ソースコード中に正規表現に該当する部分文字列があるか探して検出しています。
今回、pycodestyleは2.11.0を使って動作確認しました。
P.S. 最近pycodestyleのE721がアップデートされました
私がE721に気づいた裏には、どうやらpycodestyle v2.11.0のリリースがあるようです。
https://github.com/PyCQA/pycodestyle/blob/2.11.0/CHANGES.txt#L4
E721: adjust handling of type comparison. Allowed forms are now
isinstance(x, t)
ortype(x) is t
. PR #1086, #1167.
NG🙅♂️
type(x) == t
OK
type(x) is t
isinstance(x, t)
typeの返り値にis
を使って同一性を比較する場合、サブクラスは考慮されません。
class User: ... class RoyalUser(User): ... user = User() if type(user) is User: # True print("userはUserです") royal_user = RoyalUser() if type(royal_user) is RoyalUser: # True print("royal_userはRoyalUserです") if type(royal_user) is User: # False print("royal_userはUserです")
% pycodestyle okorare.py # 上記のコードは一切怒られません
% python okorare.py
userはUserです
royal_userはRoyalUserです
なので私のオススメはisinstanceです。
-
↩去年の夏はウタさまが新時代の幕開けを告げてたけど
— nikkie にっきー (@ftnext) 2023年8月4日
今年の夏は久美子部長が新世代の始まりを告げてる!? -
flake8に怒られないように
# noqa
するのではなく、コードに手を入れて怒りを鎮めましょう。この変更の妥当性については、PEP8に「isinstanceを使おう」と記載があることを確認しました(「経緯」に埋め込んだうちの最初の記事にて)↩ - 外部に依存を持たず、可能な限り速く動かすためにこの方針のようです。ref: https://pycodestyle.pycqa.org/en/latest/developer.html#direction↩
- tokenizeモジュールを使った実装を見つけたのですが、今回はアウトプットの範囲外とします。またの機会に↩