マネーフォワード社内PRに見られるRubyの書き方について (3) | Money Forward Engineers' Blogの記事はRubyだけどPythonにも適用できそうだから、考えてみた。
また、Rubyは一切やったことがないため、解釈が間違っているかもしれません。もし間違っていたら、教えていただけると嬉しいです。
自分が気になったところだけ書いたので、書いていない部分は記事を見てください。
動作環境
Python 3.7.1
文字列の生成や検証
必要もないのに正規表現を使ってやろうとしていたり、特定のメソッドに固執してそれを乱用しているということに集約される
...
正規表現の乱用の例とメソッド別の不的確な用例を挙げていきます。
正規表現の乱用 (単一の文字列を表している)
「単一の文字列」とのマッチはre.split()
(正規表現での分割)ではなく、str.split()
(文字列での分割)を使う
>>> s = '1, 2, 3, 4'
# No:
>>> re.split(r',', s)
['1', ' 2', ' 3', ' 4']
# Yes:
>>> s.split(',')
['1', ' 2', ' 3', ' 4']
また、Rubyの場合String#split
に正規表現を渡すことができるが、Pythonのstr.split()
に正規表現は渡せないため、re.split()
を使う
str.find()
やre.search()
を使わなくてよいところは、極力使わない
正規表現でのマッチの場合、re.match()
だと先頭にしかマッチしないため、re.search()
を使うこと
文字列内に文字が含まれているかはin
演算子を使う
- 文字列内に、ある文字(リテラル)が含まれているかのチェックは、
in
演算子を使う - 文字列内に、あるパターンが含まれているかのチェックは、
re.search()
を使う
文字列内に、ある文字が含まれているかの真偽値が欲しい場合、str.find()
(マッチした文字の位置を返すメソッド)ではなく、obj in str
(含まれているかどうかを返すin
演算子)を使う。
ドキュメントにも書いてあった
>>> s = 'Hello world!'
# No:
>>> s.find('H') != -1
True
# Yes:
>>> 'H' in s
True
パターンの場合、re.search()
しかないため何も考えずにre.search()
を使う
# Yes:
>>> import re
>>> re.search(r'[hH]ello', s)
<re.Match object; span=(0, 5), match='hello'>
完全一致しているかどうかは==
を使う
==
を使えば一発で判断できる
わざわざ、^str$
のように正規表現を使わなくても良い
# No:
>>> re.search(r'^hoge$', 'hoge')
<re.Match object; span=(0, 4), match='hoge'>
# Yes:
>>> 'hoge' == 'hoge'
True
先頭や末尾での一致はstrの関数を使う
先頭や末尾での一致は正規表現ではなく、str.startswith()
やstr.endswith()
を使う
# No:
>>> re.match(r'^sample', s) is not None
True
>>> re.search(r'.py$', s) is not None
True
# Yes:
>>> s.startswith('sample')
True
>>> s.endswith('.py')
True
str.split()
とre.split()
の乱用
区切り文字が、文字列(リテラル)の場合、str.split()
を使い、パターンの場合、re.split()
を使う
文字や行への分解
str.split()
が多く使われれる処理
- (文字への分割)
- 行への分割
Pythonでは、list()
に文字列を渡すことで簡単に文字への分割ができる
>>> s = 'abc\ndef\n'
# Yes:
>>> list(s)
['a', 'b', 'c', '\n', 'd', 'e', 'f', '\n']
行への分割をするには、str.splitlines()
を使う
>>> s = 'abc\ndef\n'
# No:
>>> s.split('\n')
['abc', 'def', '']
# Yes:
>>> s.splitlines()
['abc', 'def']
# また、改行を残したい場合、`keepends`引数に`True`を渡す
>>> s.splitlines(keepends=True)
['abc\n', 'def\n']
無駄な分割をしないようにする
もし、分割した要素の「最初」と「最後」のみ取得したい場合、「最初」と「最後」の要素さえ分割できればいいため、すべてを分割する必要はない
# No:
>>> s = 'foo##bar##baz##qux'
>>> s.split('##')
['foo', 'bar', 'baz', 'qux']
# 最初の要素
>>> s.split('##')[0]
'foo'
# 最後の要素
>>> s.split('##')[-1]
'qux'
'bar'
(2つ目の要素)と'baz'
(3つ目の要素)は一切使わないため、要素としてはいらない
無駄がないように処理をするには、2つの方法がある。
-
str.split()
(とstr.rsplit()
)の第2引数で分割を行う回数を指定する -
str.partition()
(とstr.rpartition()
)を使い、最低限の分割しか行わないようにする
1つ目の方法の「str.split()
(とstr.rsplit()
)の第2引数で分割を行う回数を指定する」でやってみる
# Yes:
# 最初の要素
# 第2引数で、分割回数を1回に指定している
>>> s.split('##', 1)
['foo', 'bar##baz##qux']
>>> s.split('##', 1)[0]
'foo'
# 最後の要素
>>> s.rsplit('##', 1)
['foo##bar##baz', 'qux']
>>> s.rsplit('##', 1)[-1]
'qux'
次に2つ目の方法の「str.partition()
(とstr.rpartition()
)を使い、最低限の分割しか行わないようにする」でやってみる
# Yes:
# first
>>> s.partition('##')
('foo', '##', 'bar##baz##qux')
>>> s.partition('##')[0]
'foo'
# last
>>> s.rpartition('##')
('foo##bar##baz', '##', 'qux')
>>> s.rpartition('##')[-1]
'qux'
partition(sep)
は、先頭に近い位置のsep
で分割し、3つの要素のタプルを返す。1つ目はsep
より前の文字列、2つ目はsep
そのもの、3つ目はsep
より後の文字列となっている。
また、rpartition(sep)
は、末尾に近い位置のsep
で分割し、3つの要素のタプルを返す。1つ目はsep
より後ろの文字列、2つ目はsep
そのもの、3つ目はsep
より前の文字列となっている。
単純なパターンで書ける方を採用する
re
モジュールにはsplit()
とfindall()
というメソッドがある
-
re.split()
は、指定したパターンにマッチした文字列を区切り文字とし、分割した結果のリストを返す。 -
re.findall()
は、指定したパターンにマッチする文字列のリストを返す。
もし、どちらかが複雑なパターンになってしまった場合には、簡単に理解できる方で書くようにする
>>> import re
>>> l = ['2019年2月1日', '2019-2-1', '2019/2/1']
# 複雑...
>>> [re.split(r'[年月日/-]', x) for x in l]
[['2019', '2', '1', ''], ['2019', '2', '1'], ['2019', '2', '1']]
# 簡単!
>>> [re.findall(r'\d+', s) for s in l]
[['2019', '2', '1'], ['2019', '2', '1'], ['2019', '2', '1']]
このように、分割する文字列([年月日/-]
)よりも、取得したい文字列(\d+
)のほうが簡単に理解できるため、この場合、re.findall()
を使うほうが良い。
re.replace()
の乱用
1文字のパターンをバラバラに繰り返し使っている
1文字のパターンの置換を繰り返しているコードがあったとする
>>> s = "foo:bar,baz-gux"
>>> s.replace(';', ':').replace(',', '.').replace('-', '|')
'foo:bar.baz|gux'
大変...
Rubyでは、マッチ文字と置き換え文字の対応をした文字列を渡せるString#tr
というメソッドがあるらしい(Ruby全く知らないからグーグル先生にきいた...)
p "foo".tr("f", "X") # => "Xoo"
p "foo".tr('a-z', 'A-Z') # => "FOO"
p "FOO".tr('A-Z', 'a-z') # => "foo"
Pythonには、RubyのString#tr
のようなメソッドなさそう...
それ通りに実装してみた
import re
def str_tr(s: str, old: str, new: str):
for o, n in zip(old, new):
s = re.sub(o, n, s)
return s
str_tr("foo;bar,baz-gux", ";,-", ":.|")
# 'foo:bar.baz|gux'
一応できたけど、もっと良い書き方無いのかな...
まとめ
正規表現を使うのではなく、strのメソッドを使うことで簡単に書けることもある