note.nkmk.me

Pythonで符号に応じて1, -1, 0を返すsign関数を実装

Posted: 2019-12-11 / Tags: Python

数値の符号に応じて1, -1, 0を返す関数を符号関数やsign, signum, sgn関数などと呼ぶが、Pythonの組み込み関数や標準ライブラリの各モジュールではそのような関数は提供されていない。

Pythonで符号関数sign()を使いたい場合、NumPyのnumpy.sign()を使うか、自分で関数を定義する必要がある。

ここでは、以下の内容について説明する。

  • 符号関数(sign, signum, sgn)
  • numpy.sign()
  • sign()に相当する関数の実装例
    • 比較演算子を利用
    • abs()を利用
  • math.copysign()の使い方
スポンサーリンク

符号関数(sign, signum, sgn)

符号関数は正の値に対して1、負の値に対して-10に対して0を返す関数。

詳しい定義はWikipediaの該当ページを参照。

英語ではSign functionやSignum functionなどと呼ばれ、sgnと表記される場合もある。

冒頭に書いたように、Pythonでは組み込み関数や標準ライブラリのモジュールにおいて符号関数は提供されていない(バージョン3.8時点)。

Pythonで符号関数(sign())が提供されていない理由は以下のStack Overflowの回答およびそのリンク先のPythonバグトラッカーのやり取りを参照。

+0.0, -0.0NaN, -NaNのような特殊な場合にどのような値を返すべきかユーザーが決められるように、sign()は実装せずcopysign()を実装することにしたらしい。

Why not implement copysign? It's a standard, familiar function with well-
documented meaning all over the web, and it can easily be used to create
whatever sign test is necessary, letting the user decide what results
(s)he wants for +/-0, +/-nan, etc.
メッセージ 59153 - Python tracker

浮動小数点数の負のゼロについての詳細は以下を参照。

Pythonにおいても浮動小数点数floatでは負のゼロ(= -0.0)を表現できる。

整数intのゼロには正負の区別はなく、-0としても0と同一のオブジェクトとみなされる。

print(0)
# 0

print(-0)
# 0

print(0 == -0)
# True

print(0 is -0)
# True

一方、浮動小数点数floatにおいては、0.0-0.0は等価だが別のオブジェクトとみなされる。ほとんどの場合気にする必要はないが、後述のmath.copysign()のように一部の処理では0.0-0.0で結果が異なる場合がある。

print(0.0)
# 0.0

print(-0.0)
# -0.0

print(0.0 == -0.0)
# True

print(0.0 is -0.0)
# False

numpy.sign()

NumPyではnumpy.sign()関数が提供されている。sign()関数が必要な場合、NumPyを使える環境であればnumpy.sign()を使うのが楽。

いくつかの例を示す。結果のデータ型は元のオブジェクトと同じになる。

import numpy as np

print(np.sign(100))
# 1

print(np.sign(-100))
# -1

print(type(np.sign(100)))
# <class 'numpy.int64'>

print(np.sign(1.23))
# 1.0

print(np.sign(-1.23))
# -1.0

print(type(np.sign(1.23)))
# <class 'numpy.float64'>
source: my_sign.py

負のゼロ-0.0も含め、ゼロはすべてゼロを返す。

print(np.sign(0))
# 0

print(np.sign(0.0))
# 0.0

print(np.sign(-0.0))
# 0.0
source: my_sign.py

欠損値NaNNaNを返す。

print(np.sign(float('nan')))
# nan

print(np.sign(float('-nan')))
# nan
source: my_sign.py

numpy.sign()はNumPy配列ndarrayを処理することもできる。詳細は以下の記事を参照。

sign()に相当する関数の実装例

比較演算子を利用

Pythonにおける比較演算は(ほとんどの場合)True, Falseのブール値を返す。また、True1False0として演算が可能。

print(100 > 0)
# True

print(True - False)
# 1

print(False - True)
# -1

print(False - False)
# 0
source: my_sign.py

これを利用して、以下のように符号関数sign()に相当する関数を定義できる。

def my_sign(x):
    return (x > 0) - (x < 0)
source: my_sign.py

結果は常に整数intとなる。

print(my_sign(100))
# 1

print(my_sign(-100))
# -1

print(type(my_sign(100)))
# <class 'int'>

print(my_sign(1.23))
# 1

print(my_sign(-1.23))
# -1

print(type(my_sign(-1.23)))
# <class 'int'>
source: my_sign.py

ゼロも欠損値NaNも正負によらず常に整数の0を返す。

print(my_sign(0))
# 0

print(my_sign(0.0))
# 0

print(my_sign(-0.0))
# 0

print(my_sign(float('nan')))
# 0

print(my_sign(float('-nan')))
# 0
source: my_sign.py

複素数には対応しない。

# print(my_sign(3 + 4j))
# TypeError: '>' not supported between instances of 'complex' and 'int'
source: my_sign.py

なお、先に「Pythonにおける比較演算は(ほとんどの場合)True, Falseのブール値を返し…」と書いたが、比較演算時に実行される__lt__などの特殊メソッドの実装によってはTrue, False以外の値を返す場合がありえる。

慣例として、正常に比較が行われたときには False か True を返します。 しかし、これらのメソッドは任意の値を返すことができるので、比較演算子がブール値のコンテキスト (たとえば if 文の条件部分) で使われた場合、 Python はその値に対して bool() を呼び出して結果の真偽を判断します。 3. データモデル lt — Python 3.8.1rc1 ドキュメント

整数intや浮動小数点数floatなどの組み込み型を使う分には気にする必要はないが、独自にクラスを実装するような場合は注意。

abs()を利用

数値の絶対値を返す組み込み関数abs()を利用して符号関数sign()を定義することも可能。

ここでは三項演算子を利用している。

def my_sign_with_abs(x):
    return 0.0 if abs(x) == 0 else x / abs(x)
source: my_sign.py

この例では後述の複素数に対応できるように/で除算しているため、浮動小数点数floatを返す。

print(my_sign_with_abs(100))
# 1.0

print(my_sign_with_abs(-100))
# -1.0

print(type(my_sign_with_abs(100)))
# <class 'float'>

print(my_sign_with_abs(1.23))
# 1.0

print(my_sign_with_abs(-1.23))
# -1.0

print(type(my_sign_with_abs(1.23)))
# <class 'float'>
source: my_sign.py

ゼロは0.0、欠損値NaNNaNを返す。

print(my_sign_with_abs(0))
# 0.0

print(my_sign_with_abs(0.0))
# 0.0

print(my_sign_with_abs(-0.0))
# 0.0

print(my_sign_with_abs(float('nan')))
# nan

print(my_sign_with_abs(float('-nan')))
# nan
source: my_sign.py

abs()は複素数に対しても絶対値(複素数平面上の距離)を返すため、この実装は複素数にも対応する。

print(abs(3 + 4j))
# 5.0

print(my_sign_with_abs(3 + 4j))
# (0.6+0.8j)
source: my_sign.py

元の複素数と同方向の単位ベクトルが求められる。

複素数に対する符号関数は、複素数平面上でベクトルに対し同方向の単位ベクトルを求める操作と同等である(ただし零ベクトル以外のとき)。 符号関数 - Wikipedia

math.copysign()の使い方

上述のように、Pythonではsign()は提供されていないが、標準ライブラリmathモジュールでcopysign()関数が提供されている。

第一引数の大きさ(絶対値)で第二引数と同じ符号の浮動小数点数floatが返される。

import math

print(math.copysign(123, -100))
# -123.0

print(math.copysign(123.0, -100.0))
# -123.0

これを利用して、符号関数sign()のような関数を定義できる。なお、後述のように0に対して0を返さないため、一般的なsign()の挙動とは異なる。

def my_sign_with_copysign(x):
    return int(math.copysign(1, x))

この例ではint()を使って常に整数intが返るようにしている。

print(my_sign_with_copysign(100))
# 1

print(my_sign_with_copysign(-100))
# -1

print(type(my_sign_with_copysign(100)))
# <class 'int'>

print(my_sign_with_copysign(1.23))
# 1

print(my_sign_with_copysign(-1.23))
# -1

print(type(my_sign_with_copysign(1.23)))
# <class 'int'>

整数のゼロは1を返し、浮動小数点数のゼロはその正負に応じて1または-1を返す。

print(my_sign_with_copysign(0))
# 1

print(my_sign_with_copysign(0.0))
# 1

print(my_sign_with_copysign(-0.0))
# -1

欠損値NaNについてもその正負に応じて1または-1を返す。

print(my_sign_with_copysign(float('nan')))
# 1

print(my_sign_with_copysign(float('-nan')))
# -1

math.copysign()は複素数に対応していないため、この関数も複素数に対してはエラーとなる。

# print(math.copysign(1, 3 + 4j))
# TypeError: can't convert complex to float

# print(my_sign_with_copysign(3 + 4j))
# TypeError: can't convert complex to float

使い所はかなり限定的だと思うが、math.copysign()を使うことで浮動小数点数のゼロや欠損値NaNの符号も考慮したsign()を実装できる。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事