什么是检查对象是否为数字的最Pythonic方式?

147

如何确定一个Python对象是否为数字?这里的“是”指在某些情况下表现得像数字。

例如,假设你正在编写一个向量类。如果给定另一个向量,则要计算点积。如果给定标量,则要缩放整个向量。

检查是否为intfloatlongbool很烦人,并且不包括可能像数字一样运作的用户定义对象。但是,仅仅检查__mul__等特殊方法并不够好,因为刚才描述的向量类将定义__mul__,但它不是我想要的数字类型。

16个回答

165
使用numbers模块中的Number来测试isinstance(n, Number)(自2.6版本起可用)。
>>> from numbers import Number
... from decimal import Decimal
... from fractions import Fraction
... for n in [2, 2.0, Decimal('2.0'), complex(2, 0), Fraction(2, 1), '2']:
...     print(f'{n!r:>14} {isinstance(n, Number)}')
              2 True
            2.0 True
 Decimal('2.0') True
         (2+0j) True
 Fraction(2, 1) True
            '2' False

当然,这与鸭子类型相反。如果您更关心对象的行为而不是其本质,请按照数字的方式执行操作,并使用异常来告诉您其他情况。


5
在将向量乘以X时,最好做聪明的事情,而不是鸭子式的事情。在这种情况下,您想根据X的值执行不同的操作。(它可能作为乘数,但结果可能是荒谬的。) - Evgeni Sergeev
3
这个答案的意思是True是一个数字,但这可能并不总是你想要的。如果你想排除布尔值(比如验证),我会建议使用isinstance(value, Number) and type(value) != bool - Yo Ludke
此答案中的方法将告诉您 float("-inf") 是一个数字。根据情况,这可能不合适。 - Grey Christoforo

34

您想检查某个对象在特定情况下是否像数字。

表现得像数字的一些情况

如果您使用的是 Python 2.5 或更早版本,则唯一真正的方法是检查其中一些"特定情况"并查看结果。

在 2.6 或更高版本中,您可以使用 numbers.Number(一个专门用于此目的的抽象基类(ABC))和 isinstance 来进行检查(在 2.6 中引入了很多 ABC,包括各种形式的集合/容器,在这些版本中,您还可以轻松添加自己的抽象基类,如果需要的话)。

回到 2.5 及更早版本,"可以添加到 0 并且不可迭代" 可能是某些情况下的好定义。但是,您确实需要问自己,您要考虑的 "数字" 必须绝对能够做什么以及必须绝对无法做什么,并进行检查。

这可能也需要在2.6或更高版本中,也许是为了注册自己的类型以添加您关心但尚未注册到numbers.Numbers的类型 - 如果您想要排除一些声称它们是数字但您无法处理的类型,则需要更加小心,因为ABC没有unregister方法[[例如,您可以创建自己的ABC WeirdNum并在其中注册所有这些对您来说奇怪的类型,然后首先检查其是否为isinstance以退出,然后再继续成功检查正常的numbers.Numberisinstance

顺便说一句,如果您需要检查x是否可以做某事或不能做某事,通常必须尝试以下操作:

try: 0 + x
except TypeError: canadd=False
else: canadd=True
__add__ 的存在本身并没有提供有用的信息,因为例如所有序列都具有它,目的是与其他序列连接。这个检查等同于定义“数字是这样一种东西,即这样一些东西的序列是内置函数 sum 的有效单个参数”,例如。完全奇怪的类型(例如当与0相加时引发“错误”异常的类型,例如 ZeroDivisionErrorValueError 等)将传播异常,但这没关系,让用户尽快知道这些疯狂的类型在好公司中不可接受;-);但是,可加和为标量的“向量”(Python 标准库没有,但当然作为第三方扩展很受欢迎)在这里也会给出错误的结果,因此(例如)这个检查应该在“不允许迭代”之后进行(例如,检查 iter(x) 是否引发 TypeError,或者检查特殊方法 __iter__ 的存在 - 如果您在 2.5 或更早版本中,因此需要自己进行检查)。

对这些复杂性的简要了解可能足以激励您在可能的情况下改而依赖抽象基类...;-)。


但是在numbers模块中有一个数字的ABC。文档声称:“数字模块(PEP 3141)定义了一组数字抽象基类,这些基类逐步定义更多的操作。” - Steven Rumbalski

17

这是一个很好的例子,展示了异常处理的优点。只需像处理数值类型一样操作,并捕获其他所有异常的TypeError

但是,显然,这只检查操作是否“可行”,而不是它们是否“有意义”!真正的解决方法是永远不要混合类型,并始终知道您的值属于哪个类型类。


1
+1 for Duck Typing:数据的类型并不重要,重要的是我是否能够按照自己的意愿使用它。 - systempuntoout
12
这是传统的方法,但ABCs已被引入,部分原因是为了远离纯鸭子类型,并将一些距离转向一个世界,在这个世界中isinstance在许多情况下实际上可以是有用的(例如“检查它是否有意义”以及操作的正式适用性)。对于长期只使用Python的人来说,这是一个困难的转变,但这是Python哲学中非常重要而微妙的趋势,忽视它将是一个严重的错误。 - Alex Martelli
@Alex:没错,我也喜欢类型类(尤其是collections.Sequence和相关类别)。不过据我所知,针对数字、向量或其他数学对象没有这样的类别。 - Jochen Ritzel
2
没有反对鸭子类型。这是我会做的事情。但是有一个抽象基类用于数字:numbers.Number。 - Steven Rumbalski
我认为在这种情况下,鸭子类型并不是一个好的解决方案。想象一下,如果我有一个嵌套结构,其中可能包含数字或序列。我想要将数字相加。可惜的是,序列也可以被“添加”,但(1,) + (3,)显然不是与1 + 3相同的“加法”。 - Stef

4

将对象乘以零。任何数乘以零都是零。任何其他结果都意味着该对象不是一个数字(包括异常情况)。

def isNumber(x):
    try:
        return bool(0 == x*0)
    except:
        return False

如下使用isNumber将会得到以下输出结果:
class A: pass 

def foo(): return 1

for x in [1,1.4, A(), range(10), foo, foo()]:
    answer = isNumber(x)
    print('{answer} == isNumber({x})'.format(**locals()))

输出:

True == isNumber(1)
True == isNumber(1.4)
False == isNumber(<__main__.A instance at 0x7ff52c15d878>)
False == isNumber([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
False == isNumber(<function foo at 0x7ff52c121488>)
True == isNumber(1)

世界上可能有一些非数字对象,当它们与零相乘时,定义了 __mul__ 方法并返回零,但这是一个极端情况。这个解决方案应该覆盖您生成/遇到的所有正常理智的代码。

numpy.array 示例:

import numpy as np

def isNumber(x):
    try:
        return bool(x*0 == 0)
    except:
        return False

x = np.array([0,1])

answer = isNumber(x)
print('{answer} == isNumber({x})'.format(**locals()))

输出:

False == isNumber([0 1])

8
True * 0 == 0 的翻译是: 真乘以零等于零。 - endolith
4
您的函数会错误地将布尔值识别为数字。 - endolith
1
@endolith,布尔值在行为上与数字完全相同。True始终等于1,False始终等于0。这正是提问者所要求的,“这里'is'被定义为'在某些情况下的数字行为'。” - shrewmouse
1
@endolith,实际上,布尔值是数字。 布尔值源于int,因此我的函数将正确地表示布尔值是数字。 - shrewmouse
1
不适用于numpy数组。isNumber(np.array([1, 1]))返回array([True, True]),它不是“可布尔化”的。 - Nicolas Abril
1
@NicolasAbril,请在isNumber函数内将0*x==0转换为bool类型。 - shrewmouse

3
重新表述您的问题,您想确定某些内容是集合还是单个值。试图比较某些内容是向量还是数字就像是比较苹果和橙子-我可以有一组字符串或数字,也可以只有一个字符串或数字。你关心的是你拥有多少(1个或更多),而不是你实际拥有什么类型。
我的解决方案是通过检查__len__的存在来检查输入是否为单个值或集合。例如:
def do_mult(foo, a_vector):
    if hasattr(foo, '__len__'):
        return sum([a*b for a,b in zip(foo, a_vector)])
    else:
        return [foo*b for b in a_vector]

或者,采用鸭子类型的方法,您可以首先尝试迭代foo
def do_mult(foo, a_vector):
    try:
        return sum([a*b for a,b in zip(foo, a_vector)])
    except TypeError:
        return [foo*b for b in a_vector]

最终,测试某物是否类似于向量比测试某物是否类似于标量更容易。如果您有不同类型的值(例如字符串、数字等)传入,则程序逻辑可能需要进行一些调整 - 您如何首先尝试将字符串乘以数值向量?


3

总结/评估现有的方法:

Candidate    | type                      | delnan | mat | shrewmouse | ant6n
-------------------------------------------------------------------------
0            | <type 'int'>              |      1 |   1 |          1 |     1
0.0          | <type 'float'>            |      1 |   1 |          1 |     1
0j           | <type 'complex'>          |      1 |   1 |          1 |     0
Decimal('0') | <class 'decimal.Decimal'> |      1 |   0 |          1 |     1
True         | <type 'bool'>             |      1 |   1 |          1 |     1
False        | <type 'bool'>             |      1 |   1 |          1 |     1
''           | <type 'str'>              |      0 |   0 |          0 |     0
None         | <type 'NoneType'>         |      0 |   0 |          0 |     0
'0'          | <type 'str'>              |      0 |   0 |          0 |     1
'1'          | <type 'str'>              |      0 |   0 |          0 |     1
[]           | <type 'list'>             |      0 |   0 |          0 |     0
[1]          | <type 'list'>             |      0 |   0 |          0 |     0
[1, 2]       | <type 'list'>             |      0 |   0 |          0 |     0
(1,)         | <type 'tuple'>            |      0 |   0 |          0 |     0
(1, 2)       | <type 'tuple'>            |      0 |   0 |          0 |     0

(我是通过这个问题来到这里的)

代码

#!/usr/bin/env python

"""Check if a variable is a number."""

import decimal


def delnan_is_number(candidate):
    import numbers
    return isinstance(candidate, numbers.Number)


def mat_is_number(candidate):
    return isinstance(candidate, (int, long, float, complex))


def shrewmouse_is_number(candidate):
    try:
        return 0 == candidate * 0
    except:
        return False


def ant6n_is_number(candidate):
    try:
        float(candidate)
        return True
    except:
        return False

# Test
candidates = (0, 0.0, 0j, decimal.Decimal(0),
              True, False, '', None, '0', '1', [], [1], [1, 2], (1, ), (1, 2))

methods = [delnan_is_number, mat_is_number, shrewmouse_is_number, ant6n_is_number]

print("Candidate    | type                      | delnan | mat | shrewmouse | ant6n")
print("-------------------------------------------------------------------------")
for candidate in candidates:
    results = [m(candidate) for m in methods]
    print("{:<12} | {:<25} | {:>6} | {:>3} | {:>10} | {:>5}"
          .format(repr(candidate), type(candidate), *results))

自己的TODO:float('nan'),'nan','123.45','42','42a','0x8','0xa',添加math.isnan - Martin Thoma

2
也许最好反过来做:先检查是否为向量。如果是,则进行点积,在其他情况下尝试标量乘法。
检查向量很容易,因为它应该是您的向量类类型(或从中继承)。您也可以先尝试执行点积,如果失败(即它不是真正的向量),则回退到标量乘法。

1
可以在简单的try except块中实现。
def check_if_number(str1):
    try:
        int(float(str1))
        return 'number'
    except:
        return 'not a number'

a = check_if_number('32322')
print (a)
# number

1

补充一下。 也许我们可以使用isinstance和isdigit的组合来判断一个值是否为数字(int,float等)

if isinstance(num1, int) or isinstance(num1 , float) or num1.isdigit():


0

对于假设的向量类:

假设v是一个向量,我们正在将其乘以x。如果每个v的分量乘以x都有意义,那么我们可能是这个意思,因此先尝试这样做。如果不行,也许我们可以点积?否则就是类型错误。

编辑——下面的代码不起作用,因为2*[0]==[0,0]而不是引发TypeError。我把它留下来,因为有评论。

def __mul__( self, x ):
    try:
        return [ comp * x for comp in self ]
    except TypeError:
        return [ x * y for x, y in itertools.zip_longest( self, x, fillvalue = 0 )

如果 x 是一个向量,那么 [comp * x for comp in self] 将产生 xv 的外积。这是一个二阶张量,而不是标量。 - aaronasterling
不是一个标量,而是一个向量。至少在原始的向量空间中不是。 - aaronasterling
呵呵,我们两个都错了。你假设comp*x将通过comp缩放x,而我则假设它会引发TypeError。不幸的是,它实际上会将x与自身连接comp次。糟糕。 - Katriel
如果 x 是一个向量,那么它应该有一个 __rmul__ 方法 (__rmul__ = __mul__),这样 comp * x 就可以按照 x * comp 明显意图的方式缩放 x - aaronasterling

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接