はじめに
無惨さま、final付けてこ! nikkieです
文字列操作の小ネタです。
Streamlitの実装を見ていたところ、興味深い実装を見つけました。
目次
- はじめに
- 目次
- キャメルケースをスネークケースに変換する実装 in Streamlit
- 正規表現を使って、キャメルケースをスネークケースに変換する
- 終わりに
- P.S. その1 もっと簡潔な実装が見つかったかも
- P.S. その2 pyhumpsのdecamelize
キャメルケースをスネークケースに変換する実装 in Streamlit
キャメルケースには2種類あります。
- UpperCamelCase(単語の始まりは全部大文字で統一)
- lowerCamelCase(先頭の単語だけ小文字始まり、他の単語は大文字始まり)
どちらの場合もスネークケースに変換することを考えます。
- upper_camel_case
- lower_camel_case
こちらを行うStreamlitの実装は以下です:
https://github.com/streamlit/streamlit/blob/1.22.0/lib/streamlit/case_converters.py#L47-L57
def to_snake_case(camel_case_str): s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_case_str) return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
動作環境
- Python 3.10.9
- Streamlit, version 1.22.0
>>> from streamlit.case_converters import to_snake_case >>> to_snake_case("UpperCamelCase") 'upper_camel_case' >>> to_snake_case("lowerCamelCase") 'lower_camel_case'
2つの正規表現で単語の区切りにアンダースコアを挿入した後、小文字にしているようです。
正規表現を使った実装、何をやっているのか読み解いていきましょう。
正規表現を使って、キャメルケースをスネークケースに変換する
re.sub
の第2引数に使われている\1
や\2
は後方参照と呼ばれますね1。
https://docs.python.org/ja/3/library/re.html#re.sub
\6
のような後方参照は、パターンのグループ 6 がマッチした部分文字列で置換されます。
re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_case_str)
の場合だと、第1引数の正規表現(キャプチャの丸カッコが2つある)で見つかったグループ1とグループ2の間に_
を入れます。
ではグループ1とグループ2はどんなパターンが該当するのでしょうか。
re.search
を使って確認していきましょう。
https://docs.python.org/ja/3/library/re.html#re.search
"(.)([A-Z][a-z]+)"
に該当するのは?
>>> import re >>> re.search("(.)([A-Z][a-z]+)", "UpperCamelCase") <re.Match object; span=(4, 10), match='rCamel'> >>> m = _ >>> m.group(1) # \1 を確認 'r' >>> m.group(2) # \2 を確認 'Camel'
というわけで、re.sub
のコードでは、先頭の単語と次の単語の間にアンダースコアが入ります。
>>> re.sub("(.)([A-Z][a-z]+)", r"\1_\2", "UpperCamelCase") 'Upper_CamelCase' >>> re.sub("(.)([A-Z][a-z]+)", r"\1_\2", "lowerCamelCase") 'lower_CamelCase'
"([a-z0-9])([A-Z])"
に該当するのは?
>>> re.search("([a-z0-9])([A-Z])", "Upper_CamelCase") <re.Match object; span=(10, 12), match='lC'> >>> m = _ >>> m.group(1) # \1 'l' >>> m.group(2) # \2 'C'
re.sub
のコードでは、単語の間にアンダースコアが入ります。
>>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "Upper_CamelCase") 'Upper_Camel_Case' >>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "lower_CamelCase") 'lower_Camel_Case'
キャメルケースが長くなった場合、すべての単語区切りが見つかります。
https://docs.python.org/ja/3/library/re.html#re.findall
>>> re.findall("([a-z0-9])([A-Z])", "Upper_CamelCaseSpam") [('l', 'C'), ('e', 'S')] # (Came)lとC(ase)、(Cas)eとS(pam)
長くなった場合でもre.sub
はパターンに該当する部分=すべての単語区切りにマッチし、アンダースコアを入れるように置き換えます。
>>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "Upper_CamelCaseSpam") 'Upper_Camel_Case_Spam'
こうしてすべての単語がアンダースコアで区切られました。
あとは小文字にしたらスネークケースです!
Streamlitのconfigの値で確認
streamlit config show
でconfigを一覧できます。
さまざまな長さのキャメルケースを拾い集めました
- port (server)
- gatherUsageStats (browser)
- enforceSerializableSessionState (runner)
- showPyplotGlobalUse (deprecation)
ここまでの正規表現を元に自前で関数を実装し、それに渡してみましょう。
>>> def my_snake_case(camel_case_str): ... s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_case_str) ... return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() >>> my_snake_case("port") 'port' >>> my_snake_case("gatherUsageStats") 'gather_usage_stats' >>> my_snake_case("enforceSerializableSessionState") 'enforce_serializable_session_state' >>> my_snake_case("showPyplotGlobalUse") 'show_pyplot_global_use'
キャメルケースをスネークケースに変換するStreamlitの実装、完全に理解した!
終わりに
キャメルケースをスネークケースに変換する正規表現を使った実装を読み解きました。
キャメルケースの文字列を観察すると、単語の切れ目が正規表現で見つけられるのですね。
そこに対して、後方参照を使ってアンダースコアを挿入していく!
>>> my_snake_case("DreamWithYou") 'dream_with_you'
P.S. その1 もっと簡潔な実装が見つかったかも
確認不足感は否めないのですが、2つの正規表現のうち後者だけでもスネークケースにできているように思われます。
>>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "port").lower() 'port' >>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "gatherUsageStats").lower() 'gather_usage_stats' >>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "enforceSerializableSessionState").lower() 'enforce_serializable_session_state' >>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "showPyplotGlobalUse").lower() 'show_pyplot_global_use' >>> re.sub("([a-z0-9])([A-Z])", r"\1_\2", "UpperCamelCase").lower() 'upper_camel_case'
プルリクチャンスかな? なにか見落としているかな?
P.S. その2 pyhumpsのdecamelize
pyhumps 3.8.0
>>> import humps >>> humps.decamelize("port") 'port' >>> humps.decamelize("gatherUsageStats") 'gather_usage_stats' >>> humps.decamelize("enforceSerializableSessionState") 'enforce_serializable_session_state' >>> humps.decamelize("showPyplotGlobalUse") 'show_pyplot_global_use' >>> humps.decamelize("UpperCamelCase") 'upper_camel_case'
こちらの実装は正規表現を使っていますが、Streamlitとは同じではないと分かりました。
https://github.com/nficano/humps/blob/v3.8.0/humps/main.py#L96-L114
読み解きはこれからです〜
- 過去に取り上げたエントリがこちら: ↩