关于验证:Python:验证字符串是否为没有转换的浮点数

Python: validate whether string is a float without conversion

是否有一种pythonic方法来验证字符串是否表示浮点数(任何可由float()识别的输入,例如-1.6e3),而不转换它(理想情况下,不使用抛出和捕获异常)?

前面的问题已经提交了,关于如何检查字符串是否表示整数或浮点。答案建议在用户定义的函数中使用try...except子句和int()float()内置项。

然而,这些问题并没有正确解决速度问题。虽然使用try...except的习惯用法将转换过程与验证过程联系起来(在某种程度上是正确的),但是为了验证目的而遍历大量文本的应用程序(任何模式验证器、解析器)将承受执行实际转换的开销。除了由于数字的实际转换而导致的减速之外,还有由于抛出和捕获异常而导致的减速。此Github Gist演示了与仅用户定义的验证相比,内置转换代码的成本是原来的两倍(比较True种情况),仅try..except版本的异常处理时间(False时间减去True时间)就高达7次验证。这就回答了我关于整数的问题。

有效的答案将是:以比try..except方法更有效的方式解决问题的函数,对将来允许这样做的内置功能的文档的引用,对现在允许这样做的python包的引用(并且比try..except方法更有效),或指向documentatio的解释。为什么这样的解决方案不是Python式的,否则将永远无法实现。具体来说,为了防止混乱,请避免回答"否",而不指向官方文件或邮件列表辩论,并避免重复使用try..except方法。


正如@john在评论中提到的,这似乎是另一个问题的答案,尽管在这种情况下,这不是公认的答案。正则表达式和fastnumbers模块是这个问题的两个解决方案。

然而,有人适时地指出(正如@en_Knight所做的那样),性能在很大程度上取决于输入。如果期望大部分有效的输入,那么EAFP方法更快,而且可以说更优雅。如果您不知道应该输入什么,那么lbyl可能更合适。从本质上来说,验证应该期望大部分有效的输入,因此它更适合于try..except

事实上,对于我的用例(作为问题的编写者,它具有相关性),在表格式数据文件中标识数据类型,try..except方法更为合适:列要么全部是浮点型的,或者,如果它具有非浮点型的值,则从它上面的行被认为是文本型的,因此实际测试浮点型的大多数输入在不管是哪种情况。我想其他的答案都是有道理的。

回到答案,fastnumber和正则表达式对于一般情况仍然是有吸引力的解决方案。具体来说,fastnumbers包似乎对所有值都很有效,除了特殊值,如github gist中所示的InfinityInfNaN。上述答案中的简单正则表达式也是如此(稍微修改-删除尾随的\b,因为它会导致一些输入失败):

1
^[-+]?(?:\b[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][-+]?[0-9]+\b)?$

在GIST中使用了一个更大的版本,它可以识别特殊的值,并且具有相同的性能:

1
^[-+]?(?:[Nn][Aa][Nn]|[Ii][Nn][Ff](?:[Ii][Nn][Ii][Tt][Yy])?|(?:\b[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][-+]?[0-9]+\b)?)$

对于有效输入,正则表达式的实现速度要慢2.8倍,而对于无效输入,则要快2.2倍。无效输入比使用try..except的有效输入慢5倍,或使用正则表达式快1.3倍。鉴于这些结果,这意味着当40%或更多的预期输入无效时,使用正则表达式是有利的。

对于有效输入,fastNumber的速度仅为~1.2倍,而对于无效输入,fastNumber的速度则为~6.3倍。

结果如下图所示。我用10^6次重复运行,有170个有效输入和350个无效输入(相应地加权,因此平均时间是每个输入)。因为框太窄,所以不显示颜色,但每列左侧的框描述了有效输入的时间安排,而无效输入则显示在右侧。

Timings of methods to validate whether a string holds a valid float value, according to whether inputs are valid or invalid

注意,答案被多次编辑,以反映对问题、此答案和其他答案的评论。为了清晰起见,编辑已合并。有些评论引用了以前的版本。


如果身为Python是一种正当理由,那么你应该坚持使用Python的禅。具体来说:

Explicit is better than implicit.

Simple is better than complex.

Readability counts.

There should be one-- and preferably only one --obvious way to do it.

If the implementation is hard to explain, it's a bad idea.

所有这些都支持尝试,除了方法。转换是明确的、简单的、可读的、明显的和易于解释的。

另外,知道某个东西是否是浮点数的唯一方法是测试它是否是浮点数。这听起来可能是多余的,但不是

现在,如果主要问题是在尝试测试太多假定的浮点数时速度问题,那么可以使用一些带有cython的C扩展来同时测试所有浮点数。但我并不认为它会在速度方面给你带来太多的改进,除非要尝试的字符串数量真的很大

编辑:

python开发人员倾向于使用EAFP方法(请求原谅比请求允许更容易),使得尝试except方法更像Python(我找不到PEP)

这里(Python中异常处理程序的成本)比较了try-except方法和if-then方法。事实证明,在Python中,异常处理并不像在其他语言中那样昂贵,而且只有在必须处理异常的情况下才更昂贵。在一般的用例中,您不会试图验证一个具有很高概率的字符串,而实际上它不是一个浮点数(除非在您的特定场景中有这种情况)。

正如我在评论中所说。如果没有特定的用例、要测试的数据和时间度量,整个问题就没有那么多意义。只谈最一般的用例,试一下"例外"是一种方法,如果你有一些实际的需求不能足够快地满足它,那么你应该把它添加到问题中。


如果你确定你想要的话,快速解决

看看这个参考实现——在python中转换为float是在C代码中进行的,而且执行效率非常高。如果您真的担心开销,可以将该代码逐字复制到自定义C扩展中,但不必提升错误标志,而是返回一个指示成功的布尔值。

特别是,看一下强制十六进制变为浮点的复杂逻辑。这是在C级别完成的,有很多错误案例;这里似乎不太可能有捷径(请注意,40行注释支持一个特定的保护案例),或者在保存这些案例的同时,任何手动执行都将更快。

但是…有必要吗?

作为一个假设,这个问题很有趣,但是在一般情况下,我们应该尝试分析它们的代码,以确保try-catch方法增加了开销。Try/Catch通常是惯用的,而且根据您的使用情况可以更快。例如,对于Python中的循环,请使用try/catch by design。

还有我为什么不喜欢它们

为了澄清,问题是

any input that would be recognizable by float()

备选方案1——Regex怎么样?

我很难相信你会得到一个正则表达式来解决这个问题。虽然regex可以很好地捕获浮点文本,但有很多角情况。看看这个答案上的所有情况-你的regex处理NaN吗?指数?bools(但不是bool字符串)?

备选2:手动打开python检查:

总结需要捕获的困难情况(Python本身就是这样做的)

  • NAN的不区分大小写捕获
  • 十六进制匹配
  • 语言规范中列举的所有案例
  • 符号,包括指数中的符号
  • 布尔值

我还将向您指出下面的情况:语言规范中的浮点;虚数。浮动方法通过识别它们是什么来优雅地处理它们,但在转换时会引发类型错误。你的自定义方法会模仿这种行为吗?


为了证明这一点,一个字符串不需要遵守多少条件,就可以使它成为float型。但是,在Python中检查所有这些条件将非常缓慢。

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
ALLOWED ="0123456789+-eE."
def is_float(string):
    minuses = string.count("-")
    if minuses == 1 and string[0] !="-":
        return False
    if minuses > 1:
        return False

    pluses = string.count("+")
    if pluses == 1 and string[0] !="+":
        return False
    if pluses > 1:
        return False

    points = string.count(".")
    if points > 1:
        return False

    small_es = string.count("e")
    large_es = string.count("E")
    es = small_es + large_es
    if es > 1:
        return False
    if (es == 1) and (points == 1):
        if small_es == 1:
            if string.index(".") > string.index("e"):
                return False
        else:
            if string.index(".") > string.index("E"):
                return False

    return all(char in ALLOWED for char in string)

我没有测试过这个,但我敢打赌这比try: float(string); return True; except Exception: return False慢得多。