Miao's blog

小议程序的优雅

主要还是因为我太菜了。

事情的缘起是这样的,在学习《Python 编程:从入门到到放弃》这本书中我遇到了一道题:

练习 9-15 创建一个列表或元组,其中包含 10 个数和 5 个字母。从这个列表或元组中随机地选择 4 个数或字母,并打印一条消息,指出只要彩票上是这 4 个数或字母,就中大奖了。可使用循环来搞明白中彩票大奖有多难。为此,创建一个名为 my_ticket 的列表或元组,再编写一个循环,不断地随机选择数或字母,直到中大奖为止。请打印一条消息,指出执行循环多少次才中了大奖。

自己写的代码始终跑不出结果,百思不得其解遂请教 Jam 和 Poka 大佬。

Poka 给出的代码如下:

from random import choice

def random_game():
    possibilities = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "a", "b", "c", "d", "e"]
    tickets = []
    while len(tickets) < 4:
        v = choice(possibilities)
        if v not in tickets:
            tickets.append(v)
    return tickets

winner_tickets = random_game()
print(f"The Winner tickets is {winner_tickets}!")

user_tickets = []
num = 0

while user_tickets != winner_tickets:
    user_tickets = random_game()
    # 如果需要考虑列表中元素的顺序
    # user_tickets.sort()
    # winner_tickets.sort()
    num = num + 1
    print(user_tickets)

print(f"Congratulation! The user tickets is {user_tickets},")
print(f"His odds of winning are 1/{num}!")

Jam 给出的代码如下:

from random import sample
from typing import List, Set

def random_game(possibilities: List, nums: int = 4) -> Set:
    return set(sample(possibilities, nums))

def main():
    possibilities = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "a", "b", "c", "d", "e"]
    nums = 4

    winner_tickets = random_game(possibilities, nums)
    print(f"The winner tickets is {winner_tickets}.")
    num = 0
    while True:
        user_tickets = random_game(possibilities, 4)
        # print(f"The user tickets is {user_tickets}.")
        num = num + 1
        if user_tickets == winner_tickets:
            print("Congratulations")
            break
    print(num)

if __name__=='__main__':
    main()

两位大佬给出的代码都能很快的计算出结果。再看看我的渣代码:

from random import choice

def random_game():
    tickets = []
    possibilities = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, "a", "b", "c", "d", "e"]
    while len(tickets) < 4:
        value = choice(possibilities)
        if value not in tickets:
            tickets.append(value)
    return tickets

winner_tickets = random_game()
print(f"The winner tickets is {winner_tickets}.")

active = True
num = 0
while True:
    user_tickets = random_game()
    #print(f"The user tickets is {user_tickets}.")
    num = num + 1
    for value in user_tickets:
        if value in winner_tickets:
            break
print(num)

当然我的代码是跑不出结果的。在随后与两位大佬们的交流中,以及对这道题的回顾里我有了一些想法。这道题不能算难题。为什么我没解出来呢,是因为我真的太蔡了,基础不够扎实,还不能很灵活的应用学到的知识。

这道题在于对随机的列表的生成和对比。在 Poka 的解法里,首先使用 choice() 函数在 15 个元素中先挑选 1 个元素通过 append 补充到 winner_tickets 列表中,虽然再重复从这 15 个元素中再抽取一个对象并于已存在与 winner_tickets 中的元素相比对,如果该元素不存在于 winner_tickets 中则 appendwinner_tickets 中。如此处理直到 winner_tickets 中凑满 4 个元素。依旧如此处理 user_tickets 列表。获得两个列表,即为得奖的彩票和用户抽到的彩票,由于题目不要求两个列表需要顺序相同,因此使用 score() 对 user_ticketswinner_tickets中的元素进行排序后相比对。如果排序后两个列表相同,则视为中奖。用 while 设置 user_tickets 生成并于 winner_tickets 比对的循环,并计数。当比对成功后,输入计数和相关结果。

在 Jam 的解法里,引入了模块 typing

Introduced since Python 3.5, Python's typing module attempts to provide a way of hinting types to help static type checkers and linters accurately predict errors.

Due to Python having to determine the type of objects during run-time, it sometimes gets very hard for developers to find out what exactly is going on in the code.

Even external type checkers like PyCharm IDE do not produce the best results; on average only predicting errors correctly about 50% of the time, according to this answer on StackOverflow.

Python attempts to mitigate this problem by introducing what is known as type hinting (type annotation) to help external type checkers identify any errors. This is a good way for the programmer to hint the type of the object(s) being used, during compilation time itself and ensure that the type checkers work correctly.

This makes Python code much more readable and robust as well, to other readers!

使用 typing 模块,可使代码有更好的 readibility,也便于查错。在随机挑选元素的过程中,Jam 选择了 sample() 函数:

The sample() method returns a list with a randomly selection of a specified number of items from a sequnce.

sample() 相比于之前使用的 choice() 更加简便,写起来也有着更少的步骤。 sample() 提供了随机采样,得到的结果可能包含重复的元素,但由于在 def 中使用了 -> Set,则 random_game() 得到的是一个集合,不会包含重复元素。

搞定了随机生成接下来就是 user_ticketswinner_tickets 的比较了。由于 winner_ticketswinner_tickets 均是受 set() 处理的集合,因此可以直接比较:

if user_tickets == winner_tickets:

轻松解决。由于在这道题目里面不在意顺序,只在意里面有没有,使用 list 肯定不是最好的选择。

比较 list 通常的两种方法,一种是 for 轮询,另一种就是 Poka 使用的 sort() 先排序后比对。如果使用轮询就需要遍历一次和比较四次才能获取两者是否相同,这并不是个高效的方案。如果 num 更大,则显然需要更多的时间来进行比对。如果使用 sort() 排序,则在时间复杂度上是非常低效的,尤其在 num 巨大、元素复杂时。那么在那么就可以直接采用 set 的方式,用集合来存储数据,可以高效的进行比对。 Jam在这里提到了一个时间复杂度的概念,我目前还没看懂。基本逻辑是,set 在 Python 内部实现是 dict,而 dict 的效率要高于 list

https://i.loli.net/2021/01/22/MaYHjJunbP7XBq1.png

那么,如何写出优雅的代码?把握代码的性能和运行时间,占用 CPU,以及使用内存,还有可读性、延展性,最后阅读者会感叹一句,“真优雅!”。当然这一切的前提是写了大量的不优雅的代码,看过无数的 sample,和持续的思考。 Jam 使用的方法和提到的概念,是我不曾接触过的。为什么学 Python 呢?我期待将编程和医学结合起来,尝试能不能用编程来对付那些重复劳动,和给我带来更多的东西。对于编程,我什至现在觉得一切技能都是基础,除了思考。长路漫漫。

“The most important thing you'll learn through out the three years is how to use google."

- From Poka's lecturer

作为一个初学者,我定这篇水文题目《小议程序的优雅》肯定是大了。我目前没有能力来议论什么是优雅的代码,但我庆幸能遇到一些热心的大佬,也很想知道未来的我看到现在的我写下的代码的心情除了“幼稚”和一丝恶心之外还有什么。可能未来我也有能力写一手优雅的代码吧。


Jam 是 Python 开发工程师、安全研究员,现就职于西安某信息安全公司。