永遠にNone
チェックと条件分岐を書く作業から逃げたいと思ったことはないだろうか? 私はある。
Python におけるNone
は他の言語におけるnull
やnil
に相当すると思われるのだが、Python には他の言語にはよくある Null を取り回しやすくなる演算子やメソッドがない。
しょうがないのでそれらの中でも定番の 3 種を、関数を書いて代用していくことにした。
内容
# 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
に頼った箇所が多いが、スタブも用意した。
スタブ
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 合体演算子が利用できる言語では以下のように書ける。
foo = bar ?? default_value
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 でいい」
そんな要求に答えてくれるのが安全呼び出し演算子である。
安全呼び出し演算子が利用できる言語では以下のように書ける。
foo?.bar()
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
メソッド**を持っているため、これにクロージャを与えることで安全に評価できる。
foo.map { $0 * 2 }
Kotlin の場合は Non-Null なインスタンスが持っている**let
メソッド**を安全呼び出しすることで実現する。
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 な変数が複数あるとmap
やlet
がネストして大変である。 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
)
こう書けば多少は見やすくならねえわこれ。