关于python:检查对象是否为数字的最pythonic方法是什么?

What is the most pythonic way to check if an object is a number?

对于任意的python对象,确定它是否是数字的最佳方法是什么?这里,is定义为acts like a number in certain circumstances

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

检查是否有东西是intfloatlongbool是令人讨厌的,并且不包括用户定义的可能像数字一样的对象。但是,例如,检查__mul__还不够好,因为我刚才描述的向量类将定义__mul__,但它不是我想要的那种数字。


使用来自numbers模块的Number测试isinstance(n, Number)(从2.6开始提供)。

1
2
3
4
5
6
7
8
9
10
11
>>> 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 '%15s %s' % (n.__repr__(), isinstance(n, Number))
              2 True
            2.0 True
 Decimal('2.0') True
         (2+0j) True
 Fraction(2, 1) True
            '2' False

当然,这与鸭子打字相反。如果您更关心一个对象是如何工作的,而不是它是什么,请像有数字一样执行操作,并使用异常来告诉您其他情况。


你想检查一下

acts like a number in certain
circumstances

如果您使用的是Python2.5或更高版本,唯一真正的方法是检查其中的一些"特定情况",然后查看。

在2.6或更高版本中,您可以使用带数字.number的isinstance—一个抽象基类(abc),它正是为此目的而存在的(对于各种形式的集合/容器,collections模块中存在更多abc,同样从2.6开始;而且,也只有在这些版本中,如果需要,您可以轻松地添加自己的抽象基类到)

巴赫到2.5及更早,在某些情况下,"可以添加到0中,并且不可重复"可能是一个很好的定义。但是,你真的需要问问你自己,你在问你想要考虑的"一个数"一定能做什么,以及它绝对不能做什么——然后检查。

在2.6或更高版本中也可能需要这样做,可能是为了让您自己注册以添加您所关心的尚未注册到numbers.Numbers上的类型——如果您想排除某些声称它们是数字的类型,但您只是无法处理,那就需要更加小心,因为ABC没有unregister方法[例如e.你可以自己制作一个abc WeirdNum,在那里登记所有你喜欢的类型的奇怪的东西,然后先检查一下isinstance是否可以保释,然后再继续检查正常numbers.Numberisinstance是否能成功地继续。

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

1
2
3
try: 0 + x
except TypeError: canadd=False
else: canadd=True

__add__本身的存在告诉你没有什么有用的,因为例如所有序列都有它用于与其他序列连接。例如,这种检查等价于"一个数字就是这样一个东西的序列,它是内置函数sum的有效单个参数"的定义。完全奇怪的类型(例如,当求和为0时引发"错误"异常的类型,例如,ZeroDivisionErrorValueError&c)将传播异常,但这是可以的,请尽快让用户知道这样的疯狂类型在好公司是不可接受的;-);但是,可以求和为标量的"向量"(python的标准库不支持ave one,但是它们当然作为第三方扩展很受欢迎,在这里也会给出错误的结果,所以(例如)这个检查应该在"不允许是不可更改的"检查之后(例如,检查iter(x)是否提高TypeError,或者是否存在特殊方法__iter__--如果您在2.5或更早的版本中,因此需要您自己的检查)

对这种复杂情况的简要了解可能足以激励您在可行的情况下依赖抽象基类……;-)。


这是一个很好的例子,在这个例子中,异常情况确实很突出。只需对数字类型做您想做的,并从其他所有类型捕获TypeError

但很明显,这只检查操作是否有效,而不是是否有意义!唯一真正的解决方案是永远不要混合类型,并且总是确切地知道值属于什么类型类。


把物体乘以零。任何乘以零的数字都是零。任何其他结果表示对象不是数字(包括异常)

1
2
3
4
5
def isNumber(x):
    try:
        return 0 == x*0
    except:
        return False

使用isNumber会得到以下输出:

1
2
3
4
5
6
7
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())

输出:

1
2
3
4
5
6
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__定义为当乘以零时返回零,但这是一个极端的例外。这个解决方案应该涵盖所有正常和健全的代码,您生成/编码。


为了重新表述您的问题,您试图确定某个东西是集合还是单个值。试图比较某个东西是向量还是数字,这是在比较苹果和橙子——我可以有一个字符串或数字的向量,我可以有一个字符串或单个数字。你感兴趣的是你有多少(1个或更多),而不是你实际拥有的类型。

对于这个问题,我的解决方案是通过检查__len__的存在来检查输入是单个值还是集合。例如:

1
2
3
4
5
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]

或者,对于duck类型方法,您可以尝试首先在foo上迭代:

1
2
3
4
5
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]

归根结底,测试某个东西是否像向量,比测试某个东西是否像标量要容易得多。如果您有不同类型的值(如字符串、数字等),那么您的程序逻辑可能需要一些工作-您是如何在第一时间尝试将字符串乘以数字向量的?


总结/评估现有方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

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

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/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))


也许最好换一种方式来做:检查它是否是向量。如果是,则执行点积,在所有其他情况下,则尝试标量乘法。

检查向量很容易,因为它应该是向量类类型(或者继承自它)。你也可以试着先做一个点积,如果失败了(=它不是一个真正的向量),然后回到标量乘法。


只是为了补充。也许我们可以使用isInstance和isDigit的组合,如下所示来确定一个值是否是一个数字(int、float等)

如果isInstance(num1,int)或isInstance(num1,float)或num1.isDigit():


如果要根据参数类型调用不同的方法,请查看multipledispatch

For example, say you are writing a vector class. If given another vector, you want to find the dot product. If given a scalar, you want to scale the whole vector.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from multipledispatch import dispatch

class Vector(list):

    @dispatch(object)
    def __mul__(self, scalar):
        return Vector( x*scalar for x in self)

    @dispatch(list)
    def __mul__(self, other):
        return sum(x*y for x,y in zip(self, other))


>>> Vector([1,2,3]) * Vector([2,4,5])   # Vector time Vector is dot product
25
>>> Vector([1,2,3]) * 2                 # Vector times scalar is scaling
[2, 4, 6]

不幸的是,(据我所知)我们不能编写@dispatch(Vector),因为我们仍在定义Vector类型,所以还没有定义类型名。相反,我使用的是基本类型list,它甚至允许您找到Vectorlist的点积。


在实现一类向量类时,我遇到了类似的问题。检查数字的一种方法是将其转换为一,即使用

1
float(x)

这应该会拒绝x不能转换为数字的情况;但也可能会拒绝其他类型的数字,例如可能有效的结构,例如复数。


对于假设向量类:

假设v是一个向量,我们将它乘以x。如果用EDOCX1的每个分量(1)乘以EDOCX1的每个分量(2)是有意义的,我们可能是指这个,所以首先尝试一下。如果没有,也许我们可以点?否则是类型错误。

编辑——下面的代码不起作用,因为2*[0]==[0,0]而不是提升TypeError。我离开它是因为它被评论过。

1
2
3
4
5
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 )


您可以使用isdigit()函数。

1
2
3
4
5
6
>>> x ="01234"
>>> a.isdigit()
True
>>> y ="1234abcd"
>>> y.isdigit()
False