小議程序的優雅

主要還是因爲我太蔡了。

事情的緣起是這樣的,在學習《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!

— From https://www.journaldev.com/34519/python-typing-module

使用 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

那麽,如何寫出優雅的代碼?把握代碼的性能和運行時間,占用 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

作爲一個初學者,我定這篇水文題目《小議程序的優雅》肯定是大了。我目前沒有能力來議論什麽是優雅的代碼,但我慶幸能遇到一些熱心的大佬,也很想知道未來的我看到現在的我寫下的代碼的心情除了“幼稚”和一絲惡心之外還有什麽。可能未來我也有能力寫一手優雅的代碼吧。

正如 Python 之禪裏說的:

Beautiful is better than ugly.
優美優於醜陋,

Explicit is better than implicit.
明瞭優於隱晦;

Simple is better than complex.
簡單優於複雜,

Complex is better than complicated.
複雜優於凌亂,

Flat is better than nested.
扁平優於嵌套,

Sparse is better than dense.
稀疏優於稠密,

Readability counts.
可讀性很重要!

……


好吧,我才知道,Jam 是 Python 開發工程師、安全研究員,現就職於西安某信息安全公司。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.