Python:高效检查整数是否在*多个*范围内

46 投票
11 回答
35602 浏览
提问于 2025-04-16 17:56

我正在开发一个邮政应用程序,需要检查一个整数邮政编码是否在多个邮政编码范围内,并根据匹配的范围返回不同的代码。

每个代码对应多个邮政编码范围。例如,如果邮政编码在1000-2429、2545-2575、2640-2686之间,或者等于2890,就应该返回M代码。

我可以这样写:

if 1000 <= postcode <= 2429 or 2545 <= postcode <= 2575 or 2640 <= postcode <= 2686 or postcode == 2890:
    return 'M'

但考虑到有27个可返回的代码和77个需要检查的范围,这样的代码行数似乎有点多。有没有更高效(最好是更简洁)的方式来用Python匹配这些范围呢?


补充:有很多优秀的解决方案出现,所以我实现了所有我能找到的方案,并对它们的性能进行了基准测试。

这个程序的环境是一个网络服务(实际上是基于Django的),需要逐个检查邮政编码区域代码。我的理想实现是能够快速处理每个请求,不需要在内存中保持任何进程,也不需要批量处理多个邮政编码。

我使用timeit.Timer进行了以下解决方案的测试,默认重复1000000次,每次使用随机生成的邮政编码。

IF解决方案(我最初的方案)

if 1000 <= postcode <= 2249 or 2555 <= postcode <= 2574 or ...:
    return 'M'
if 2250 <= postcode <= 2265 or ...:
    return 'N'
...

1百万次的时间:5.11秒。

元组中的范围(Jeff Mercado)

在我看来,这种方法更优雅,输入和阅读范围也更容易。特别是如果范围会随时间变化,这种方法会更好。但在我的实现中,它的速度慢了四倍。

if any(lower <= postcode <= upper for (lower, upper) in [(1000, 2249), (2555, 2574), ...]):
    return 'M'
if any(lower <= postcode <= upper for (lower, upper) in [(2250, 2265), ...]):
    return 'N'
...

1百万次的时间:19.61秒。

集合成员资格(gnibbler)

正如作者所说,“只有在你一次性构建集合以检查多个邮政编码时,这种方法才更好”。但我还是想测试一下。

if postcode in set(chain(*(xrange(start, end+1) for start, end in ((1000, 2249), (2555, 2574), ...)))):
    return 'M'
if postcode in set(chain(*(xrange(start, end+1) for start, end in ((2250, 2265), ...)))):
    return 'N'
...

1百万次的时间:339.35秒。

二分查找(robert king)

这个方法可能有点超出我的理解水平。我在阅读bisect模块时学到了很多,但就是没能搞清楚给find_ge()传什么参数才能运行测试。我预计如果有很多邮政编码的循环,它会非常快,但如果每次都要设置,那就不一定了。所以,对于只处理一个邮政区域代码(即M代码,包含四个范围)时,填充numbersedgepairsedgeanswers等的1百万次,但实际上并没有运行fast_solver

1百万次的时间:105.61秒。

字典(sentinel)

使用一个预生成的字典,每个邮政区域代码一个,存储在源文件中(106 KB),每次运行时加载。我本来期待这种方法能有更好的性能,但在我的系统上,IO真的拖了后腿。我的服务器是一个不是特别快的高端Mac Mini。

1百万次的时间:5895.18秒(从10000次运行推算得出)。

总结

我本以为会有人给出一个简单的“显而易见”的答案,但事实证明这个问题要复杂得多(甚至有点争议)。

如果每个纳秒的效率都很重要,我可能会保持一个单独的进程运行,使用二分查找或字典解决方案,并将结果保存在内存中以便快速查找。然而,由于IF树运行一百万次只需五秒,这对我的小企业来说已经足够快,所以我最终会在我的应用中使用这个方法。

感谢大家的贡献!

11 个回答

8

可能最快的方法是检查一个集合中的成员资格

>>> from itertools import chain
>>> ranges = ((1000, 2429), (2545, 2575), (2640, 2686), (2890, 2890))
>>> postcodes = set(chain(*(xrange(start, end+1) for start, end in ranges)))
>>> 1000 in postcodes
True
>>> 2500 in postcodes
False

不过这样做会占用更多的内存,而且建立这个集合也需要时间,所以只有在你只建立一次集合,然后在一个循环中检查很多邮政编码时,这种方法才更好

编辑:似乎不同的范围需要映射到不同的字母

>>> from itertools import chain
>>> ranges = {'M':((1000,2429), (2545,2575), (2640,2686), (2890, 2890)),
              # more ranges
              }
>>> postcodemap = dict((k,v) for v in ranges for k in chain(*imap(xrange, *zip(*ranges[v]))))    
>>> print postcodemap.get(1000)
M
>>> print postcodemap.get(2500)
None
9

这里有一个快速简洁的解决方案,使用了numpy库:

import numpy as np
lows = np.array([1, 10, 100]) # the lower bounds
ups = np.array([3, 15, 130]) # the upper bounds

def in_range(x):
    return np.any((lows <= x) & (x <= ups))

比如说,现在可以这样做:

in_range(2) # True
in_range(23) # False
19

你可以把你的范围放进元组里,然后把这些元组放到一个列表中。接着使用 any() 函数来帮助你检查某个值是否在这些范围内。

ranges = [(1000,2429), (2545,2575), (2640,2686), (2890, 2890)]
if any(lower <= postcode <= upper for (lower, upper) in ranges):
    print('M')

撰写回答