練習 17:字典
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
你應(yīng)該熟悉 Python 的dict
類。無論什么時候,你編寫這樣的代碼:
cars = {'Toyota': 4, 'BMW': 20, 'Audi': 10}
你在使用字典,將車的品牌(“豐田”,“寶馬”,“奧迪”)和你有的數(shù)量(4,20,10)關(guān)聯(lián)起來。現(xiàn)在使用這種數(shù)據(jù)結(jié)構(gòu)應(yīng)該是你的第二本能,你可能甚至不考慮它是如何工作的。在本練習中,你將通過從已經(jīng)創(chuàng)建的數(shù)據(jù)結(jié)構(gòu),實現(xiàn)自己的Dictionary
來了解dict
的工作原理。你在本練習中的目標是,根據(jù)我在這里寫的代碼實現(xiàn)自己的Dictionary
版本。
挑戰(zhàn)性練習
在本練習中,你將完全記錄并理解我編寫的一段代碼,然后盡可能地,根據(jù)記憶編寫自己的版本。本練習的目的是,學習剖析和理解復雜的代碼。能夠內(nèi)在化或記憶,如何創(chuàng)建一個簡單的數(shù)據(jù)結(jié)構(gòu)(如字典)是很重要的性。我發(fā)現(xiàn),學習剖析和理解一段代碼的最好方法是,根據(jù)自己的學習和記憶來重新實現(xiàn)它。
將其看做一個“原件”類。原件來自繪畫,其中你繪制一幅由他人創(chuàng)作的畫,優(yōu)于創(chuàng)作它的副本。這樣做會教你如何繪畫并且提高你的技能。代碼和繪畫是相似的,因為所有的信息都為復制準備好了,所以你可以通過復制他們的工作,輕松地向別人學習。
制作一份“代碼大師的副本”
要創(chuàng)建一份“代碼大師副本”,你將遵循這個的流程,我稱之為 CASMIR 流程:
- 復制代碼,使其正常工作。你的副本應(yīng)該完全一樣。這有助于你了解它,并強制你仔細研究它。
- 使用注釋來標注代碼,并為所有代碼寫一個分析,確保你了解每一行以及它的作用。這可能涉及到你編寫的其他代碼,來將整個概念結(jié)合在一起。
- 使用簡潔的說明,為這個代碼的工作原理總結(jié)一般結(jié)構(gòu)。這是函數(shù)列表和每個函數(shù)的作用。
- 記住這個算法和關(guān)鍵代碼段的簡潔描述。
- 根據(jù)記憶實現(xiàn)可以實現(xiàn)的東西,當你用盡細節(jié)時,回顧你的筆記和原始代碼來記住更多內(nèi)容。
- 當你需要從你的記憶中復制的時候,重復此過程多次。你的記憶中的副本并不必須是完全一樣的,但應(yīng)接近,并通過你創(chuàng)建的相同測試。
這樣做將使你深入了解數(shù)據(jù)結(jié)構(gòu)的工作原理,但更為重要的是,幫助你內(nèi)在化和回憶此數(shù)據(jù)結(jié)構(gòu)。你終將能夠理解該概念,并在需要創(chuàng)建數(shù)據(jù)結(jié)構(gòu)時實現(xiàn)數(shù)據(jù)結(jié)構(gòu)。這也是訓練你的大腦,在未來記住其他的數(shù)據(jù)結(jié)構(gòu)和算法。
警告
我要做的唯一的警告是,這是一個很簡單,愚蠢,緩慢的
Dictionary
實現(xiàn)。你真的復制了一個簡單愚蠢的Dictionary
,它具有所有的基本元素和作用,但需要大量改進來用于生產(chǎn)。當我們到達練習 19 并研究性能調(diào)整時,會進行這些改進。現(xiàn)在,只需實現(xiàn)這個簡單的版本,就可以了解數(shù)據(jù)結(jié)構(gòu)的基礎(chǔ)知識。
復制代碼
首先我們查看Dictionary
的代碼,你需要復制它:
from dllist import DoubleLinkedList
class Dictionary(object):
def __init__(self, num_buckets=256):
"""Initializes a Map with the given number of buckets."""
self.map = DoubleLinkedList()
for i in range(0, num_buckets):
self.map.push(DoubleLinkedList())
def hash_key(self, key):
"""Given a key this will create a number and then convert it to
an index for the aMap's buckets."""
return hash(key) % self.map.count()
def get_bucket(self, key):
"""Given a key, find the bucket where it would go."""
bucket_id = self.hash_key(key)
return self.map.get(bucket_id)
def get_slot(self, key, default=None):
"""
Returns either the bucket and node for a slot, or None, None
"""
bucket = self.get_bucket(key)
if bucket:
node = bucket.begin
i = 0
while node:
if key == node.value[0]:
return bucket, node
else:
node = node.next
i += 1
# fall through for both if and while above
return bucket, None
def get(self, key, default=None):
"""Gets the value in a bucket for the given key, or the default."""
bucket, node = self.get_slot(key, default=default)
return node and node.value[1] or node
def set(self, key, value):
"""Sets the key to the value, replacing any existing value."""
bucket, slot = self.get_slot(key)
if slot:
# the key exists, replace it
slot.value = (key, value)
else:
# the key does not, append to create it
bucket.push((key, value))
def delete(self, key):
"""Deletes the given key from the Map."""
bucket = self.get_bucket(key)
node = bucket.begin
while node:
k, v = node.value
if key == k:
bucket.detach_node(node)
break
def list(self):
"""Prints out what's in the Map."""
bucket_node = self.map.begin
while bucket_node:
slot_node = bucket_node.value.begin
while slot_node:
print(slot_node.value)
slot_node = slot_node.next
bucket_node = bucket_node.next
該代碼使用你現(xiàn)有的DoubleLinkedList
代碼來實現(xiàn)dict
數(shù)據(jù)結(jié)構(gòu)。如果你不完全了解DoubleLinkedList
,那么你應(yīng)該嘗試使用代碼復制過程,讓我們更好地理解它。一旦你確定你了解DoubleLinkedList
,你可以鍵入此代碼并使其正常工作。記住,在開始標注之前,它必須是完美的副本。你可以做的最糟糕的事情,是標注我的代碼的破損或不正確的副本。
為了幫助你獲得正確的代碼,我寫了一個快速和簡陋的小型測試腳本:
from dictionary import Dictionary
# create a mapping of state to abbreviation
states = Dictionary()
states.set('Oregon', 'OR')
states.set('Florida', 'FL')
states.set('California', 'CA')
states.set('New York', 'NY')
states.set('Michigan', 'MI')
# create a basic set of states and some cities in them
cities = Dictionary()
cities.set('CA', 'San Francisco')
cities.set('MI', 'Detroit')
cities.set('FL', 'Jacksonville')
# add some more cities
cities.set('NY', 'New York')
cities.set('OR', 'Portland')
# print(out some cities
print('-' * 10)
print("NY State has: %s" % cities.get('NY'))
print("OR State has: %s" % cities.get('OR'))
# print(some states
print('-' * 10)
print("Michigan's abbreviation is: %s" % states.get('Michigan'))
print("Florida's abbreviation is: %s" % states.get('Florida'))
# do it by using the state then cities dict
print('-' * 10)
print("Michigan has: %s" % cities.get(states.get('Michigan')))
print("Florida has: %s" % cities.get(states.get('Florida')))
# print(every state abbreviation
print('-' * 10)
states.list()
# print(every city in state
print('-' * 10)
cities.list()
print('-' * 10)
state = states.get('Texas')
if not state:
print("Sorry, no Texas.")
# default values using ||= with the nil result
# can you do this on one line?
city = cities.get('TX', 'Does Not Exist')
print("The city for the state 'TX' is: %s" % city)
我希望你也可以正確地鍵入這個代碼,但是當你進入大師副本的下一個階段時,你會把它變成一個正式的自動測試,你可以運行pytest
。現(xiàn)在,只要讓這個腳本工作,就可以讓Dictionary
類工作,之后你可以在下一個階段清理它。
標注代碼
確保我的代碼的副本完全一樣,并通過測試腳本。然后,你可以開始標注代碼,并研究每一行來了解其作用。一個非常好的方式是,編寫一個“正式”的自動化測試,并在你工作時標注代碼。獲取dictionary_test.py
腳本,并將每個部分轉(zhuǎn)換成一個小型測試函數(shù),然后標注Dictionary
類。
例如,test_dictionary.py
中的第一部分測試創(chuàng)建一個字典,并執(zhí)行一系列Dictionary.set
調(diào)用。我會把它轉(zhuǎn)換成一個test_set
函數(shù),然后在dictionary.py
文件中標注Dictionary.set
函數(shù)。當你標注Dictionary.set
函數(shù)時,你必須潛入到Dictionary.get_slot
函數(shù)中,然后是Dictionary.get_bucket
函數(shù),最后是Dictionary.hash_key
。這迫使你通過一個測試和有組織的方式,來標注和了解Dictionary
類的大段代碼。
總結(jié)數(shù)據(jù)結(jié)構(gòu)
你現(xiàn)在可以總結(jié)你在dictionary.py
中,通過標注代碼所學到的內(nèi)容,并將dictionary_test.py
文件重寫為真正的pytest
自動測試。你的摘要應(yīng)該是數(shù)據(jù)結(jié)構(gòu)的清晰和細微描述。如果你可以把它寫在一張紙上,那么你做得很好。并不是所有的數(shù)據(jù)結(jié)構(gòu)都可以簡明扼要地總結(jié)出來,但是保持摘要簡潔將有助于你記住它。你可以使用圖表,圖紙,單詞,或你能夠記住的任何內(nèi)容。
此摘要的目的是為你提供一組快速注解,你可以“掛載”更多的細節(jié),當你的記憶進行到下一步的時候。摘要不一定包括所有內(nèi)容,但應(yīng)該包括一些細節(jié),可以觸發(fā)你對“標注”階段的代碼的記憶,從而觸發(fā)你對“復制”階段的記憶。這被稱為“分塊”,你可以將更詳細的記憶和信息附加到信息的細微碎片。在撰寫摘要時記住這一點。少即是多,但太少沒有用。
記憶摘要
你可以用任何方式記住摘要和帶標注的代碼,但我將給出一個基本的記憶過程,你可以使用它。老實說,記住復雜的東西是每個人的不斷嘗試和犯錯的過程,但有些技巧有幫助:
- 確保你有一個紙質(zhì)的筆記本,以及摘要和代碼的打印。
- 花3分鐘,只需閱讀摘要并嘗試記住它。靜靜地看著它,大聲讀出來,然后閉上眼睛,重復你所讀的內(nèi)容,甚至嘗試記住紙上的單詞的“形狀”。聽起來很愚蠢,但相信我,它完全奏效。記住你的大腦比你想象的更好。
- 把摘要翻過來,并嘗試從你記住的內(nèi)容中再次寫出來,當你卡住時,將其快速翻過來并查看。在你快速瞥見之后,把摘要翻過來,并嘗試完成更多。
- 一旦從(大部分)記憶中寫出了摘要的副本,請使用摘要,花另一個 3 分鐘,試圖記住帶標注的代碼。僅僅閱讀摘要的一部分,然后再看看代碼的相關(guān)部分,并嘗試記住它。甚至每個函數(shù)只能花 3 分鐘。
- 一旦你花時間試圖記住帶標注的代碼,把它翻過去,使用摘要,嘗試回憶你筆記本中的代碼。同樣,當你陷入困境時,快速把標注翻過來并查看。
- 繼續(xù)這樣做,直到你可以在紙上寫出代碼的完整副本。你紙上的代碼不一定是完美的 Python 代碼,但應(yīng)該非常接近原始代碼。
看起來這可能是無法實現(xiàn),但是當你這么做時,你會感到驚訝。完成此操作后,你也會驚訝于你了解了字典的概念。這不是簡單的記憶,而是建立一個概念圖,當你嘗試自己實現(xiàn)字典時,你可以實際使用它。
警告
如果你是那種擔心記不住任何東西的人,那么這個練習會為你將來帶來巨大的幫助。能夠遵循流程來記住某些東西,有助于克服任何記憶的挫折。你并不是沉浸在“失敗”中,而是可以在堅持中看到緩慢的改進。當你這樣做,你會看到改善你的回憶的方式和黑魔法,并且你會做得更好。你只需要相信我,這似乎是一種緩慢的學習方式,但它比其他技術(shù)要快得多。
從記憶中實現(xiàn)
現(xiàn)在是時候走到你的電腦旁邊 - 把你的紙質(zhì)筆記放在另一個房間或地板上 - 并根據(jù)記憶嘗試你的第一個實現(xiàn)。你的第一次嘗試可能完全是一場災難,也可能完正確。你最可能不習慣從記憶中實現(xiàn)任何東西。只要放下任何你記得的東西,當你到達你的記憶的彼端,回到另一個房間,記憶更多東西。經(jīng)過幾次到你的記憶空間的旅行,你會進入它,記憶會更好地流出來。你完全可以再次訪問你的記憶筆記。這一切都關(guān)于,試圖保持代碼的記憶并提高自己的技能。
我建議你首先寫下你的想法,無論是測試,代碼還是兩者。然后使用你可以回憶的內(nèi)容,來實現(xiàn)或回憶代碼的其他部分。如果你首先坐下來并記住test_set
函數(shù)名和幾行代碼,然后把它們寫下來。當他們在你的頭腦中,立即利用它們。一旦你完成了,盡你最大的努力,使用這個測試來記住或?qū)崿F(xiàn)Dictionary.set
函數(shù)。你的目標是使用任何信息來構(gòu)建或者其它信息。
你也應(yīng)該嘗試,用你對Dictionary
的理解來實現(xiàn)代碼。不要簡單以攝影方式來回憶每一行。這實際上是不可能的,因為沒有人有攝影記憶(去查一下,沒有人)。大多數(shù)人的記憶都不錯,能夠觸發(fā)他們可以使用的概念性理解。你應(yīng)該做同樣的事情,并使用你的Dictionary
的知識來創(chuàng)建自己的副本。在上面的示例中,你知道Dictionary.set
以某種方式運行,你需要一種方法來獲取插槽(鏈表節(jié)點)和桶(鏈表本身)...所以這意味著,你需要get_slot
和get_bucket
。你不是以攝影方式記住每個字符;而是記住所有關(guān)鍵概念并使用它們。
重復
這個練習最重要的部分是,重復幾次這個流程,使其沒有錯誤,才能使其更好。你會對這本書中的其他數(shù)據(jù)結(jié)構(gòu)這樣做,所以你會得到大量的練習。如果你必須回去記憶 100 次才行,也是可以的。最終你只需要做 50 遍,然后下一次只有 10 遍,然后最終你將能夠輕易從記憶中實現(xiàn)一個Dictionary
。盡管繼續(xù)嘗試,并嘗試像冥想那樣接近它,所以你這樣做的時候可以放松。
深入學習
- 我的測試非常有限。寫一個更廣泛的測試。
- 練習 16 的排序算法如何有助于這個數(shù)據(jù)結(jié)構(gòu)?
- 當你將鍵和值隨機化,用于這個數(shù)據(jù)結(jié)構(gòu)時,會發(fā)生什么?排序算法有幫助嗎?
-
num_buckets
對數(shù)據(jù)結(jié)構(gòu)有什么影響?
破壞它
你的大腦可能宕機了,要休息一下,然后嘗試破壞這個代碼。這個實現(xiàn)很容易被數(shù)據(jù)淹沒和壓倒。奇怪的邊界情況如何呢?你可以將任何東西添加為一個鍵,或者只是字符串?會造成什么問題?最后,你是否可以對代碼暗中耍一些花招,使其看起來像是正常工作,但實際上是以一些機智的方式來破壞它?