LoginSignup
15
11

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-11-01

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

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

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

内容
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に頼った箇所が多いが、スタブも用意した。

スタブ
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]: ...

一覧

Null 合体演算子

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

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

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

Swift の場合
foo = bar ?? default_value
Kotlin の場合
foo = bar ?: default_value

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

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 だった場合例外が飛ぶ。

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 の場合
foo?.bar()
Kotlin の場合
foo?.bar()

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

# 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

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

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

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

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

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

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

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

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

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

安全な式評価

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

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 の場合
foo.map { $0 * 2 }

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

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

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

# 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

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

np.random.seed(365)

n01 = np.random.randn(10)

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

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

# 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 な変数が複数あるとmapletがネストして大変である。 Haskell では do 記法というものを使うことでこれを書きやすくできるらしい。 q_let関数は所詮関数なので、最初から引数としてコレクションをとれるようにしておいた。

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 におけるチェーンの例。

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

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

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

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

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

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

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

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

15
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11