【Python】None チェックと分岐でキレ散らかしたので nullutil.py 作った

**永遠に`None`チェックと条件分岐を書く作業**から逃げたいと思ったことはないだろうか? 私はある。

Python における`None`は他の言語における`null`や`nil`に相当すると思われるのだが、Python には他の言語にはよくある **Null を取り回しやすくなる演算子やメソッドがない**。

しょうがないのでそれらの中でも**定番の 3 種**を、**関数**を書いて代用していくことにした。

<details><summary>内容</summary><div>


```python:nullutil.py
# Null Coalesce
# This means:
#     lhs ?? rhs
def qq(lhs, rhs):
    return lhs if lhs is not None else rhs


# Safty Access
# This means:
#     instance?.member
#     instance?.member(*params)
def q_(instance, member, params=None):
    if instance is None:
        return None
    else:
        m = getattr(instance, member)
        if params is None:
            return m
        elif isinstance(params, dict):
            return m(**params)
        elif isinstance(params, list) or isinstance(params, tuple):
            return m(*params)
        else:
            return m(params)
# This means:
#     instance?[index]
def qL7(collection, index):
    return collection[index] if collection is not None else None


# Safety Evalate (do Syntax)
# This means:
#     params?.let{expression}
#     do
#         p0 <- params[0]
#         p1 <- params[1]
#         ...
#         return expression(p0, p1, ...)
def q_let(params, expression):
    if isinstance(params, dict):
        for param in params.values():
            if param is None:
                return None
        return expression(**params)
    elif isinstance(params, list) or isinstance(params, tuple):
        for param in params:
            if param is None:
                return None
        return expression(*params)
    else:
        return expression(params) if params is not None else None
```

どうしてもうまく書けず`Any`に頼った箇所が多いが、スタブも用意した。

<details><summary>スタブ</summary><div>


```python:nullutil.pyi
from typing import TypeVar, Hashable, Mapping, MutableMapping, Sequence, MutableSequence, Any, Union, Optional, Callable, AnyStr
from typing import overload


T = TypeVar('T')
U = TypeVar('U')
H = TypeVar('H', Hashable)

SeqT = Union[Sequence[T], MutableSequence[T]]
MapT = Union[Mapping[H, T], MutableMapping[H, T]]
C = Union[list, tuple, dict]


# Null Coalesce
# This means:
#     lhs ?? rhs
def qq(lhs: Optional[T], rhs: T) -> T: ...


# Safty Access
# This means:
#     instance?.member
#     instance?.member(*params)
def q_(instance: Optional[Any], member:AnyStr, params: Optional[Any]) -> Optional[Any]: ...
# This means:
#     instance?[index]
@overload
def qL7(collection: Optional[SeqT], index: int) -> Optional[T]: ...
@overload
def qL7(collection: Optional[MapT], index: H) -> Optional[T]: ...

# Safety Evalate (do Syntax)
# This means:
#     params?.let{expression}
#     do
#         p0 <- params[0]
#         p1 <- params[1]
#         ...
#         return expression(p0, p1, ...)
@overload
def q_let(params: Optional[T], expression: Callable[[T], U]) -> Optional[U]: ...
@overload
def q_let(params: Optional[C], expression: Callable[..., T]) -> Optional[T]: ...
```

</div></details>

</div></details>

## 一覧

### Null 合体演算子

「**ある変数の値を取り出したいが、Null だったときはデフォルト値を与えたい**」

そんな要求に答えてくれるのが **Null 合体演算子**である。

Null 合体演算子が利用できる言語では以下のように書ける。

```swift:Swift の場合
foo = bar ?? default_value
```

```kotlin:Kotlin の場合
foo = bar ?: default_value
```

これの代替として**`qq`関数**を作成した。

```python
guide = 'Mirai Hirano'
researcher = 'Kako Nanami'
curator = None

# print(guide ?? 'John Doe')
print(qq(guide, 'John Doe'))

# print(researcher ?? 'John Doe')
print(qq(researcher, 'John Doe'))

# print(curator ?? 'John Doe')
print(qq(curator, 'John Doe'))
```

```
Mirai Hirano
Kako Nanami
John Doe
```

### 安全呼び出し演算子

Null かもしれない (Nullable な) メンバを直接呼び出そうとすると、もし本当に Null だった場合例外が飛ぶ。

```python
import numpy as np
import pandas as pd

np.random.seed(365)

score = np.clip(np.rint(np.random.normal(80., 15., 500)).astype(int), 0, 100)
mean = np.mean(score)
std = np.std(score)
mean_difference = score - mean
standard_score = mean_difference * (10. / std) + 50.

column_dict = {'成績': score, '平均との差': mean_difference, '偏差値': standard_score,}
column_list = ['成績', '平均との差', '偏差値',]
score_df = pd.DataFrame(column_dict)[column_list]
none_df = None

display(score_df.sort_values('成績'))
display(none_df.sort_values('成績'))
```

|      | 成績 | 平均との差 |    偏差値 |
| ---: | ---: | ---------: | --------: |
|  249 |   34 |    -45.632 | 16.784097 |
|   82 |   36 |    -43.632 | 18.239913 |
|   89 |   36 |    -43.632 | 18.239913 |
|  372 |   41 |    -38.632 | 21.879453 |
|  112 |   42 |    -37.632 | 22.607361 |
|  ... |  ... |        ... |       ... |
|  197 |  100 |     20.368 | 64.826033 |
|   43 |  100 |     20.368 | 64.826033 |
|  337 |  100 |     20.368 | 64.826033 |
|  334 |  100 |     20.368 | 64.826033 |
|  280 |  100 |     20.368 | 64.826033 |

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-50-badfe23fbcf4> in <module>
      1 display(score_df.sort_values('成績'))
----> 2 display(none_df.sort_values('成績'))

AttributeError: 'NoneType' object has no attribute 'sort_values'
```

「**Nullable なインスタンスのメンバを呼び出したい。 Null なら返り値も Null でいい**」

そんな要求に答えてくれるのが**安全呼び出し演算子**である。

安全呼び出し演算子が利用できる言語では以下のように書ける。

```swift:Swift の場合
foo?.bar()
```

```kotlin:Kotlin の場合
foo?.bar()
```

これの代替として**`q_`関数**を作成した。

```python
# display(score_df?.sortvalues('成績'))
display(q_(score_df,'sort_values','成績'))

# display(none_df?.sortvalues('成績'))
display(q_(none_df,'sort_values','成績'))
```

|      | 成績 | 平均との差 |    偏差値 |
| ---: | ---: | ---------: | --------: |
|  249 |   34 |    -45.632 | 16.784097 |
|   82 |   36 |    -43.632 | 18.239913 |
|   89 |   36 |    -43.632 | 18.239913 |
|  372 |   41 |    -38.632 | 21.879453 |
|  112 |   42 |    -37.632 | 22.607361 |
|  ... |  ... |        ... |       ... |
|  197 |  100 |     20.368 | 64.826033 |
|   43 |  100 |     20.368 | 64.826033 |
|  337 |  100 |     20.368 | 64.826033 |
|  334 |  100 |     20.368 | 64.826033 |
|  280 |  100 |     20.368 | 64.826033 |

```
None
```

**複数の引数**を指定する場合は**リストやタプルや辞書**で与える。

```python
# score_df?.sort_values(by='偏差値', ascending=False)
q_(score_df, 'sort_values', {'by': '偏差値', 'ascending': False})
```

メソッドではなく**フィールド**を呼び出すときは第三引数を**省略**する。

```python
# score_df?.index
q_(score_df, 'index')
```

メソッドに対して第三引数を省略した場合、単に呼び出し可能な関数オブジェクトが返るので、**引数のないメソッド**を呼ぶときは**空のリストやタプルや辞書**を与える。 

```python
# standard_score?.min()
q_(standard_score, 'min', ())

# Noneは呼び出し可能ではないので、以下の書き方は例外が飛ぶ可能性がある。
# q_(standard_score, 'min')()
```

言語によっては`?[]`記法も存在している。 リストや辞書、Numpy テンソルに対して添え字によって要素にアクセスするために`qL7`関数も作成した。 ~~一般的な表記に似せた関数名にしてきたがそろそろ限界であることを感じさせる。~~

```python
# standard_score?[5]
qL7(standard_score, 5)
```

### 安全な式評価

Nullable な値を引数にとる式は、もし本当に Null だった場合例外が飛ぶかもしれない。

```python
import numpy as np

sequence = np.arange(0, 10)
none_array = None

print(sequence * 2)
print(none_array * 2)
```

```
[ 0  2  4  6  8 10 12 14 16 18]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-82-44094c5f4f90> in <module>
      1 print(sequence * 2)
----> 2 print(none_array * 2)

TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'
```

「**Nullable な値を引数に取る式を評価したい。 この式は Non-Null を期待しているので Null なら返り値も Null でいい**」

そんな要求に答えてくれるのが**安全に式を評価するシステム**である。

Swift の場合は Nullable なインスタンスは**`map`メソッド**を持っているため、これにクロージャを与えることで安全に評価できる。

```swift:Swift の場合
foo.map { $0 * 2 }
```

Kotlin の場合は Non-Null なインスタンスが持っている**`let`メソッド**を安全呼び出しすることで実現する。

```kotlin:Kotlin の場合
foo?.let { it * 2 }
```

これらの代替として**`q_let`関数**を作成した。

```python
# print(sequence?.let { it * 2 } )
print(q_let(sequence, lambda it: it * 2))

# print(none_array?.let { it * 2 } )
print(q_let(none_array, lambda it: it * 2))
```

```
[ 0  2  4  6  8 10 12 14 16 18]
None
```

ラムダ式の部分は当然**定義済みの関数**で代替できる。

```python
np.random.seed(365)

n01 = np.random.randn(10)

# n01?.let { np.mean(it) }
q_let(n01, np.mean)
```

お気づきかもしれないが、`q_let`関数があれば`q_`関数ができることは代替できる。

```python
# score_df?.sort_values('偏差値', ascending=False)
# <=> score_df?.let { it.sort_values('偏差値', ascending=False) }
q_let(score_df, lambda it: it.sort_values('偏差値', ascending=False))
```

位置引数と名前引数が渾然一体となってひとつのリストや辞書で与えることができない / 難しい場合は、`q_let`関数で代用すればよい。 ただし、この場合チェーンが非常に書きづらいため、その場合は`q_`関数を使用したほうが簡易である。

Nullable な変数が複数あると`map`や`let`がネストして大変である。 Haskell では do 記法というものを使うことでこれを書きやすくできるらしい。 `q_let`関数は所詮関数なので、最初から**引数としてコレクションをとれる**ようにしておいた。

```python
import math

r = 5
pi = math.pi

# r?.let { x -> pi?.let { y -> x**2 * y } }
q_let([r, pi,], lambda x, y: x**2 * y)
```

## 弱点

まず、無理に関数で実装しているので**どうしても文字が増える**。 `??`演算子は 2 文字なのに`qq(,)`で 5 文字だ。 `?.`などは本来必要ないクォーテーションでさらに悲惨なことになっている。

もう一つ、**演算子と違って中置できない**ので**チェーンの見た目がすごい悲惨**なことになっている。

以下は Swift におけるチェーンの例。

```swift
foo?.bar()?.baz?.qux() ?? default_value
```

非常にスッキリしているが、これと同じことを今回作成した関数で書こうとするとこうなる。

```python
qq(q_(q_(q_(foo,'bar',()),'baz'),'qux',()), default_value)
```

もはや**チェーンでなく入れ子と化し**、なにがどこまでを囲っているのかさっぱりわからない。 あまりにもひどい。 ここまで来ると、

```python
if foo is None:
    ret = default_value
else:
    temp = foo.bar()
    if temp is None:
        ret = default_value
    else:
        temp = temp.baz
        if temp is None:
            ret = default_value
        else:
            temp = temp.qux()
            ret = temp if temp is not None else default_value
```

の方がまだマシに見える。

```python
qq(
    q_(
        q_(
            q_(
                foo, 'bar', ()
            ), 'baz'
        ), 'qux', ()
    ), default_value
)
```

こう書けば多少は見やすく**ならねえわこれ**。