高级迭代器(字母算术谜题)

来源:互联网 发布:做天猫,淘宝客服怎么样 编辑:程序博客网 时间:2024/05/01 13:26

前言

最著名的字母算术谜题是SEND + MORE = MONEY。
在这一章中,我们将深入一个最初由Raymond Hettinger编写的难以置信的Python 程序。这个程序只用14行代码来解决字母算术谜题。

import reimport itertoolsdef solve(puzzle):    words = re.findall('[A-Z]+', puzzle.upper())    unique_characters = set(''.join(words))    assert len(unique_characters) <= 10, 'Too many letters'    first_letters = {word[0] for word in words}    n = len(first_letters)    sorted_characters = ''.join(first_letters) + \        ''.join(unique_characters - first_letters)    characters = tuple(ord(c) for c in sorted_characters)    digits = tuple(ord(c) for c in '0123456789')    zero = digits[0]    for guess in itertools.permutations(digits, len(characters)):        if zero not in guess[:n]:            equation = puzzle.translate(dict(zip(characters, guess)))            if eval(equation):                return equationif __name__ == '__main__':    import sys    for puzzle in sys.argv[1:]:        print(puzzle)        solution = solve(puzzle)        if solution:            print(solution)

正则表达式

>>> import re>>> re.findall('[0-9]+', '16 2-by-4s in rows of 8')  ①['16', '2', '4', '8']>>> re.findall('[A-Z]+', 'SEND + MORE == MONEY')     ②['SEND', 'MORE', 'MONEY']

re 模块有一个漂亮的函数findall(),接受一个正则表达式和一个字符串作为参数,然后找出字符串中出现该模式的所有地方。

更复杂的正则

>>> re.findall(' s.*? s', "The sixth sick sheikh's sixth sheep's sick.")[' sixth s', " sheikh's s", " sheep's s"]

这个正则表达式寻找一个空格,一个 s, 然后是最短的任何字符构成的序列(.*?), 然后是一个空格, 然后是另一个s。

在输入字符串中,我看见了五个匹配:

The sixth sick sheikh's sixth sheep's sick.The sixth sick sheikh's sixth sheep's sick.The sixth sick sheikh's sixth sheep's sick.The sixth sick sheikh's sixth sheep's sick.The sixth sick sheikh's sixth sheep's sick. 

但是re.findall()函数值只返回了3个匹配。准确的说,它返回了第一,第三和第五个。为什么呢?因为它不会返回重叠的匹配。
第一个匹配和第二个匹配是重叠的,所以第一个被返回了,第二个被跳过了。然后第三个和第四个重叠,所以第三个被返回了,第四个被跳过了。最后,第五个被返回了。三个匹配,不是五个。

在序列中寻找不同的元素

>>> a_list = ['The', 'sixth', 'sick', "sheik's", 'sixth', "sheep's", 'sick']>>> set(a_list)                      ①{'sixth', 'The', "sheep's", 'sick', "sheik's"}>>> a_string = 'EAST IS EAST'>>> set(a_string)                    ②{'A', ' ', 'E', 'I', 'S', 'T'}>>> words = ['SEND', 'MORE', 'MONEY']>>> ''.join(words)                   ③'SENDMOREMONEY'>>> set(''.join(words))              ④{'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}
  • Set()使得在序列中查找不同的元素变得很简单。 给出一个有若干字符串组成的列表,set()函数返回列表中不同的字符串组成的集合。同样的技术也适用于字符串,因为一个字符串就是一个字符序列。
  • 给出一个字符串列表, ”.join(a_list)将所有的字符串拼接成一个。

作出断言

和很多编程语言一样,Python 有一个assert语句。这是它的用法。

>>> assert 1 + 1 == 2                                     ①>>> assert 1 + 1 == 3                                     ②Traceback (most recent call last):  File "<stdin>", line 1, in <module>AssertionError>>> assert 2 + 2 == 5, "Only for very large values of 2"  ③Traceback (most recent call last):  File "<stdin>", line 1, in <module>AssertionError: Only for very large values of 2

assert 语句后面跟任何合法的Python 表达式。
结果为 True, assert 语句没有做任何事情。结果为 False, assert 语句会抛出一个 AssertionError. 你可以提供一个人类可读的消息,AssertionError异常被抛出的时候它可以被用于打印输出。

assert len(unique_characters) <= 10, 'Too many letters'…等价于:if len(unique_characters) > 10:    raise AssertionError('Too many letters')

生成器表达式

生成表达式类似生成器函数,只不过它不是函数。
使用生成器表达式取代列表解析可以同时节省CPU 和 内存(RAM)。如果你构造一个列表的目的仅仅是传递给别的函数,(比如 传递给tuple() 或者 set()), 用生成器表达式替代

>>> unique_characters = {'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}>>> gen = (ord(c) for c in unique_characters)  ①>>> gen                                        ②<generator object <genexpr> at 0x00BADC10>>>> next(gen)                                  ③69>>> next(gen)68>>> tuple(ord(c) for c in unique_characters)   ④(69, 68, 77, 79, 78, 83, 82, 89)

chr()和ord()函数

chr()函数用一个范围在range(256)内的(就是0255)整数作参数,返回一个对应的字符。ord()函数是chr()函数(对于8位的ASCII字符串)或unichr()函数(对于Unicode对象)的配对函数.>>> chr(65)'A'>>> ord('a')97>>> unichr(12345)u'\u3039'

排列

循环器模块 (itertools)

  1. permutations() 函数接受一个序列(这里是3个数字组成的列表) 和一个表示你要的排列的元素的数目的数字。函数返回迭代器,你可以在for 循环或其他老地方使用它。这里我遍历迭代器来显示所有的值。
>>> import itertools                              ①>>> perms = itertools.permutations([1, 2, 3], 2)  ②>>> next(perms)                                   ③(1, 2)>>> next(perms)(1, 3)>>> next(perms)(2, 1)                                            ④#记住排列是有序的: (2, 1) 和 (1, 2)是不同的。 >>> next(perms)(2, 3)>>> next(perms)(3, 1)>>> next(perms)(3, 2)>>> next(perms)                                   ⑤Traceback (most recent call last):  File "<stdin>", line 1, in <module>StopIteration#像(1, 1) 或者 (2, 2)这样的元素对没有出现,因为它们包含重复导致它们不是合法的排列。当没有更多排列的时候,迭代器抛出一个StopIteration异常。 

2.字符串就是一个字符序列。对于查找排列来说,字符串’ABC’和列表 [‘A’, ‘B’, ‘C’]是等价的。
permutations()函数并不一定要接受列表。它接受任何序列 — 甚至是字符串。

>>> import itertools>>> perms = itertools.permutations('ABC', 3)  ①>>> next(perms)('A', 'B', 'C')                               ②>>> next(perms)('A', 'C', 'B')>>> next(perms)('B', 'A', 'C')>>> next(perms)('B', 'C', 'A')>>> next(perms)('C', 'A', 'B')>>> next(perms)('C', 'B', 'A')>>> next(perms)Traceback (most recent call last):  File "<stdin>", line 1, in <module>StopIteration>>> list(itertools.permutations('ABC', 3))    ③[('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]

3.itertools.product()函数返回包含两个序列的笛卡尔乘积的迭代器。
itertools.combinations()函数返回包含给定序列的给定长度的所有组合的迭代器。这和itertools.permutations()函数很类似,除了不包含因为只有顺序不同而重复的情况。itertools.combinations(‘ABC’, 2) 不会返回(‘B’, ‘A’) ,因为它和(‘A’, ‘B’)是重复的,只是顺序不同而已。

 >>> import itertools>>> list(itertools.product('ABC', '123'))   ①[('A', '1'), ('A', '2'), ('A', '3'),  ('B', '1'), ('B', '2'), ('B', '3'),  ('C', '1'), ('C', '2'), ('C', '3')]>>> list(itertools.combinations('ABC', 2))  ②[('A', 'B'), ('A', 'C'), ('B', 'C')]

4.rstrip() 字符串方法移除每一行尾部的空白。
(字符串也有一个lstrip()方法移除头部的空白,以及strip()方法头尾都移除。)
sorted() 函数接受一个列表并将它排序后返回。默认情况下,它按字母序排序。
然而,sorted()函数也接受一个函数作为key 参数, 并且使用key来排序。在这个例子里,排序函数是len()。

>>> names = list(open('examples/favorite-people.txt', encoding='utf-8'))  ①>>> names['Dora\n', 'Ethan\n', 'Wesley\n', 'John\n', 'Anne\n','Mike\n', 'Chris\n', 'Sarah\n', 'Alex\n', 'Lizzie\n']>>> names = [name.rstrip() for name in names]                             ②>>> names['Dora', 'Ethan', 'Wesley', 'John', 'Anne','Mike', 'Chris', 'Sarah', 'Alex', 'Lizzie']>>> names = sorted(names)                                                 ③>>> names['Alex', 'Anne', 'Chris', 'Dora', 'Ethan','John', 'Lizzie', 'Mike', 'Sarah', 'Wesley']>>> names = sorted(names, key=len)                                        ④>>> names['Alex', 'Anne', 'Dora', 'John', 'Mike','Chris', 'Ethan', 'Sarah', 'Lizzie', 'Wesley']

5.迭代器没有“重置”按钮。你一旦耗尽了它,你没法重新开始。如果你想要再循环一次(例如, 在接下去的for循环里面), 你得调用itertools.groupby()来创建一个新的迭代器。

itertools.groupby()函数接受一个序列和一个key 函数, 并且返回一个生成二元组的迭代器。
在这个例子里,给出一个已经按长度排序的名字列表, itertools.groupby(names, len)将会将所有的4个字母的名字放在一个迭代器里面,所有的5个字母的名字放在另一个迭代器里,以此类推。groupby()函数是完全通用的; 它可以将字符串按首字母,将数字按因子数目, 或者任何你能想到的key函数进行分组。

itertools.groupby()只有当输入序列已经按分组函数排过序才能正常工作。在上面的例子里面,你用len() 函数分组了名字列表。这能工作是因为输入列表已经按长度排过序了。

>>> import itertools>>> groups = itertools.groupby(names, len)  ①

6.itertools.chain()函数接受两个迭代器,返回一个迭代器,它包含第一个迭代器的所有内容,以及跟在后面的来自第二个迭代器的所有内容。(实际上,它接受任何数目的迭代器,并把它们按传入顺序串在一起。)

zip()函数的作用不是很常见,结果它却非常有用: 它接受任何数目的序列然后返回一个迭代器,其第一个元素是每个序列的第一个元素组成的元组,然后是每个序列的第二个元素(组成的元组),以此类推。
zip() 在到达最短的序列结尾的时候停止。range(10, 14) 有四个元素(10, 11, 12, 和 13), 但是 range(0, 3)只有3个, 所以 zip()函数返回包含3个元素的迭代器。

相反,itertools.zip_longest()函数在到达最长的序列的结尾的时候才停止, 对短序列结尾之后的元素填入None值.

>>> list(range(0, 3))[0, 1, 2]>>> list(range(10, 13))[10, 11, 12]>>> list(itertools.chain(range(0, 3), range(10, 13)))[0, 1, 2, 10, 11, 12]>>> list(zip(range(0, 3), range(10, 13)))[(0, 10), (1, 11), (2, 12)]>>> list(zip(range(0, 3), range(10, 14)))[(0, 10), (1, 11), (2, 12)]>>> list(itertools.zip_longest(range(0, 3), range(10, 14)))[(0, 10), (1, 11), (2, 12), (None, 13)]

7.

>>> characters = ('S', 'M', 'E', 'D', 'O', 'N', 'R', 'Y')>>> guess = ('1', '2', '0', '3', '4', '5', '6', '7')>>> tuple(zip(characters, guess))  ①(('S', '1'), ('M', '2'), ('E', '0'), ('D', '3'), ('O', '4'), ('N', '5'), ('R', '6'), ('Y', '7'))>>> dict(zip(characters, guess))   ②{'E': '0', 'D': '3', 'M': '2', 'O': '4', 'N': '5', 'S': '1', 'R': '6', 'Y': '7'}

8.解决字母算术谜题
操作字符串的技术: translate() 方法。
一个字符串的translate()方法接收一个转换表,并用它来转换该字符串。换句话说,它将出现在转换表的键中的字节替换为该键对应的值。在这个例子里, 将MARK “翻译为” MORK.

>>> translation_table = {ord('A'): ord('O')}  ①>>> translation_table                         ②{65: 79}>>> 'MARK'.translate(translation_table)       ③'MORK' 

eval–强大,危险

将任何字符串作为Python表达式求值
经过华丽的字符串操作,我们得到了类似’9567 + 1085 == 10652’这样的一个字符串。但那是一个字符串,字符串有什么好的?
输入eval(), Python 通用求值工具。eval() 并不限于布尔表达式。它能处理任何 Python 表达式并且返回任何数据类型。

>>> eval('1 + 1 == 2')True>>> eval('1 + 1 == 3')False>>> eval('9567 + 1085 == 10652')True>>> eval('"A" + "B"')'AB'>>> eval('"MARK".translate({65: 79})')'MORK'>>> eval('"AAAAA".count("A")')5>>> eval('["*"] * 5')['*', '*', '*', '*', '*']>>> x = 5>>> eval("x * 5")         25>>> eval("pow(x, 2)")     25>>> import math>>> eval("math.sqrt(x)")  2.2360679774997898

邪恶部分是对来自非信任源的表达式进行求值。你应该只在信任的输入上使用eval()。
当然,关键的部分是确定什么是“可信任的”。但有一点我敢肯定: 你不应该将这个字母算术表达式放到网上最为一个小的web服务。不要错误的认为,“Gosh, 这个函数在求值以前做了那么多的字符串操作。我想不出 谁能利用这个漏洞。” 会有人找出穿过这些字符串操作把危险的可执行代码放进来的方法的。(更奇怪的事情都发生过。), 然后你就得和你的服务器说再见了。

GEEK

最后, Python 表达式的求值是可能达到某种意义的“安全”的, 但结果是在现实生活中没什么用。如果你只是玩玩没有问题,如果你只给它传递安全的输入也没有问题。但是其它的情况完全是自找麻烦。

把所有东西放在一起

总的来说: 这个程序通过暴力解决字母算术谜题, 也就是通过穷举所有可能的解法。为了达到目的,它

  • 通过re.findall()函数找到谜题中的所有字母
  • 使用集合和set()函数找到谜题出现的所有不同的字母
  • 通过assert语句检查是否有超过10个的不同的字母 (意味着谜题无解)
  • 通过一个生成器对象将字符转换成对应的ASCII码值
  • 使用itertools.permutations()函数计算所有可能的解法
  • 使用translate()字符串方法将所有可能的解转换成Python表达式
  • 使用eval()函数通过求值Python 表达式来检验解法
  • 返回第一个求值结果为True的解法

…仅仅14行代码.

参考

http://old.sebug.net/paper/books/dive-into-python3/advanced-iterators.html#generator-expressions

0 0
原创粉丝点击