Pythonで符号に応じて1, -1, 0を返すsign関数を実装
数値の符号に応じて1
, -1
, 0
を返す関数を符号関数やsign, signum, sgn関数などと呼ぶが、Pythonの組み込み関数や標準ライブラリではそのような関数は提供されていない。
Pythonで符号関数sign()
を使いたい場合、NumPyのnumpy.sign()
を使うか、自分で関数を定義する必要がある。
符号関数(sign, signum, sgn)とは
符号関数は正の値に対して1
、負の値に対して-1
、0
に対して0
を返す関数。
英語ではSign functionやSignum functionなどと呼ばれ、sgnと表記される場合もある。
冒頭に書いたように、Pythonでは組み込み関数や標準ライブラリにおいて符号関数は提供されていない(バージョン3.11
時点)。
Pythonで符号関数(sign()
)が提供されていない理由は以下のStack Overflowの回答およびそのリンク先のPythonバグトラッカーのやり取りを参照。
- language design - Why doesn't Python have a sign function? - Stack Overflow
- 課題 1640: Enhancements for mathmodule - Python tracker
+0.0
, -0.0
やNaN
, -NaN
のような特殊な場合にどのような値を返すべきかユーザーが決められるように、sign()
は実装せずcopysign()
を実装することにしたらしい。
浮動小数点数の負のゼロについての詳細は以下を参照。
Pythonにおいても浮動小数点数float
では負のゼロ(= -0.0
)を表現できる。
整数int
のゼロには正負の区別はなく、-0
としても0
と同一のオブジェクトとみなされる。
- 関連記事: Pythonの==演算子とis演算子の違い
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'>
負のゼロ-0.0
も含め、ゼロはすべて正のゼロを返す。
print(np.sign(0))
# 0
print(np.sign(0.0))
# 0.0
print(np.sign(-0.0))
# 0.0
欠損値NaN
はNaN
を返す。
print(np.sign(float('nan')))
# nan
print(np.sign(float('-nan')))
# nan
numpy.sign()
はNumPy配列ndarray
を処理することもできる。詳細は以下の記事を参照。
sign()に相当する関数の実装例
比較演算子を利用
Pythonにおける比較演算は(ほとんどの場合)True
, False
のブール値を返す。また、bool
はint
のサブクラスなので、True
は1
、False
は0
として演算が可能。
print(100 > 0)
# True
print(True - False)
# 1
print(False - True)
# -1
print(False - False)
# 0
これを利用して、以下のように符号関数sign()
に相当する関数を定義できる。
def my_sign(x):
return (x > 0) - (x < 0)
結果は常に整数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'>
ゼロも欠損値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
複素数には対応しない。
# print(my_sign(3 + 4j))
# TypeError: '>' not supported between instances of 'complex' and 'int'
なお、先に「Pythonにおける比較演算は(ほとんどの場合)True
, False
のブール値を返し…」と書いたが、比較演算時に実行される__lt__
などの特殊メソッドの実装によってはTrue
, False
以外の値を返す場合がありえる。
慣例として、正常に比較が行われたときには False か True を返します。 しかし、これらのメソッドは任意の値を返すことができるので、比較演算子がブール値のコンテキスト (たとえば if 文の条件部分) で使われた場合、 Python はその値に対して bool() を呼び出して結果の真偽を判断します。 3. データモデル -
__lt__
— Python 3.11.3 ドキュメント
整数int
や浮動小数点数float
などの組み込み型を使う分には気にする必要はないが、独自に実装したクラスも扱う場合は注意。
abs()を利用
数値の絶対値を返す組み込み関数abs()
を利用して符号関数sign()
を定義することも可能。
ここでは三項演算子を利用している。
def my_sign_with_abs(x):
return 0.0 if abs(x) == 0 else x / abs(x)
この例では後述の複素数に対応できるように/
で除算しているため、浮動小数点数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'>
ゼロは0.0
、欠損値NaN
はNaN
を返す。
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
abs()
は複素数に対しても絶対値(複素数平面上の距離)を返すため、この実装は複素数にも対応する。
print(abs(3 + 4j))
# 5.0
print(my_sign_with_abs(3 + 4j))
# (0.6+0.8j)
元の複素数と同方向の単位ベクトルが求められる。
複素数に対する符号関数は、複素数平面上でベクトルに対し同方向の単位ベクトルを求める操作と同等である(ただし零ベクトル以外のとき)。 符号関数 - 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()
を実装できる。