【Python】「マネーフォワード社内PRに見られるRubyの書き方について (3) 」をPythonに置き換えて考えてみた

[マネーフォワード社内PRに見られるRubyの書き方について (3) | Money Forward Engineers' Blog](https://moneyforward.com/engineers_blog/2019/02/05/ruby-code-3/)の記事はRubyだけどPythonにも適用できそうだから、考えてみた。
また、Rubyは一切やったことがないため、解釈が間違っているかもしれません。もし間違っていたら、教えていただけると嬉しいです。

自分が気になったところだけ書いたので、書いていない部分は記事を見てください。

## 動作環境

```
Python 3.7.1
```

## 文字列の生成や検証

> 必要もないのに正規表現を使ってやろうとしていたり、特定のメソッドに固執してそれを乱用しているということに集約される
> ...
> 正規表現の乱用の例とメソッド別の不的確な用例を挙げていきます。

### 正規表現の乱用 (単一の文字列を表している)

「単一の文字列」とのマッチは`re.split()`(正規表現での分割)ではなく、`str.split()`(文字列での分割)を使う

```python
>>> 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`演算子)を使う。

[ドキュメント](https://docs.python.jp/3/library/stdtypes.html#str.find)にも書いてあった

```python
>>> s = 'Hello world!'
# No:
>>> s.find('H') != -1
True

# Yes:
>>> 'H' in s
True
```

パターンの場合、`re.search()`しかないため何も考えずに`re.search()`を使う

```python
# Yes:
>>> import re
>>> re.search(r'[hH]ello', s)
<re.Match object; span=(0, 5), match='hello'>
```

#### 完全一致しているかどうかは`==`を使う

`==`を使えば一発で判断できる
わざわざ、`^str$`のように正規表現を使わなくても良い

```python
# No:
>>> re.search(r'^hoge$', 'hoge')
<re.Match object; span=(0, 4), match='hoge'>

# Yes:
>>> 'hoge' == 'hoge'
True
```

#### 先頭や末尾での一致はstrの関数を使う

先頭や末尾での一致は正規表現ではなく、`str.startswith()`や`str.endswith()`を使う

```python
# 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()`に文字列を渡すことで簡単に文字への分割ができる

```python
>>> s = 'abc\ndef\n'

# Yes:
>>> list(s)
['a', 'b', 'c', '\n', 'd', 'e', 'f', '\n']
```

行への分割をするには、`str.splitlines()`を使う

```python
>>> 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']
```

#### 無駄な分割をしないようにする

もし、分割した要素の「最初」と「最後」のみ取得したい場合、「最初」と「最後」の要素さえ分割できればいいため、すべてを分割する必要はない

```python
# 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つの方法がある。

1. `str.split()`(と`str.rsplit()`)の第2引数で分割を行う回数を指定する
2. `str.partition()`(と`str.rpartition()`)を使い、最低限の分割しか行わないようにする

1つ目の方法の「`str.split()`(と`str.rsplit()`)の第2引数で分割を行う回数を指定する」でやってみる

```python
# 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()`)を使い、最低限の分割しか行わないようにする」でやってみる

```python
# 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()`は、指定したパターンにマッチする文字列のリストを返す。

もし、どちらかが複雑なパターンになってしまった場合には、簡単に理解できる方で書くようにする

```python
>>> 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文字のパターンの置換を繰り返しているコードがあったとする

```python
>>> s = "foo:bar,baz-gux"
>>> s.replace(';', ':').replace(',', '.').replace('-', '|')
'foo:bar.baz|gux'
```

大変...

Rubyでは、マッチ文字と置き換え文字の対応をした文字列を渡せる`String#tr`というメソッドがあるらしい(Ruby全く知らないからグーグル先生にきいた...)

```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`のようなメソッドなさそう...
それ通りに実装してみた

```python
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のメソッドを使うことで簡単に書けることもある

## 参考文献

- [【Python】上手に文字列が含まれているかを判定する](https://www.tech-tech.xyz/string-in-python.html)
- [Python Tips：文字列を検索したい - Life with Python](https://www.lifewithpython.com/2015/04/python-search-string-pattern.html)
- [Pythonで文字列を分割（区切り文字、改行、正規表現、文字数） | note.nkmk.me](https://note.nkmk.me/python-split-rsplit-splitlines-re/)
- [マネーフォワード社内PRに見られるRubyの書き方について (3) | Money Forward Engineers' Blog](https://moneyforward.com/engineers_blog/2019/02/05/ruby-code-3/)