0、錘子原理
在手裡拿着一把錘子的人眼中,世界就像一根釘子。
大多人試圖以一種思維模型來解決問題,而其思維往往隻來自某一專業學科,
但你必須知道各種重要學科的重要理論。
一一《窮查理寶典》
在過去十年的工作中,我經常看到一些不可思議的代碼,這些代碼有時候看起來相當的愚蠢。而且大部分時候,這些代碼都有非常簡單而高效的替代方案。而寫出這些代碼的人,往往是因為沒有掌握相關的基礎知識,或者是因為總是用一個思路去解決問題,形成了思維慣性。
要巧妙地解決某些問題,有時候可能需要掌握非常專業和生僻的知識;但大部分時候,你隻需要掌握一些非常基礎的知識,和一個拓展性的思維。
本文皆在抛磚引玉,用非常基礎的Python知識,用不同的思路巧妙地解決相似的問題。
1、判定元素是否存在
在清單在查找一個元素,判定元素是否存在,是一個相當常見的操作。
在貫穿本小節的所有例子中,我們都是為了查找符合某個條件的元素是否存在。如果存在,則做dealWhenFound操作;如果不存在,則做dealWhenNotFound操作。後文中用到這兩個函數,我們将直接使用,不再進行聲明。
def dealWhenFound(elem):
# 如果元素找到了,做點什麼
print("{}is found".format(elem))
def dealWhenNotFound(elem):
# 如果元素沒有找到,做點什麼
print("{}is not found".format(elem))
假如我們有一個名字清單,現在需要在其中查找某個元素是否存在。通用我們可以這麼做:
names = ["Jim", "Tom", "Mary", "Hugo", "Tim", "John", "Alby"]
is_found = False
target = "Tom"
is_found = False
for name in names:
if name == target:
is_found = True
break
if is_found:
dealWhenFound(target)
else;
dealWhenNotFound(target)
這是一份通用可行且非常樣闆式的代碼。但在Python中,我們有更加高效且解決方案。正如本小節的标題所述的,用in關鍵字就可以了。
if name in names:
dealWhenFound(target)
else:
dealWhenNotFound(target)
in操作符是用來判定一個元素是否存在一個可疊代對象中(list、tuple、dict、set等)。對于這種查找條件比較簡單的搜尋,思路就是這麼簡單,甚至不值得一提。但對于稍微複雜一點的查找條件,in就不那麼勝任了。
我們把查找條件修改為:判定是否存在以某個字母開頭的名字。這個時候,我們就沒有辦法用in操作符來直接判定了。我們發現反而是第1份for代碼,才能更好地解決我們的問題。
prefix = "J"
is_found = False
for name in names:
if name.startswith(prefix): # 判定name是否以J開頭
is_found = True
break
if is_found:
dealWhenFound(prefix)
else;
dealWhenNotFound(prefix)
Python考慮到了這種情況的普遍性,為我們提供了for/else結構。
for iter in a_list:
if some_test(iter):
break
else:
# 如果循環結果,且沒有break語句被執行,則else塊會被執行
在for/else結構中,如果for循環正常結束(即沒有break語句被執行),則else下的代碼會被執行;否則else下的代碼不會被執行。
利用這個特性,我們可以将代碼進行如下的優化:
prefix = "J"
for name in names:
if name.startswith(prefix): # 判定name是否以J開頭
dealWhenFound(prefix)
break
else:
dealWhenNotFound(prefix)
如果你知道any函數,那你應該知道這份代碼還會優化的空間。any函數接受一個可疊代對象(包括生成器)做為參數,并且隻要任意一個元素被判定為True,則傳回True。配合map,我們的代碼可以進一步地簡化:
prefix = "J"
if any(map(lambda name: name.startswith(prefix), names)):
dealWhenFound(prefix)
else:
dealWhenNotFound(prefix)
如果你覺得上面這份代碼不好了解,我們可以進行拆解。
test_func = lambda name: name.startswith(prefix)
map_obj = map(test_func, names)
if any(map_obj):
dealWhenFound(prefix)
else:
dealWhenNotFound(prefix)
除了本小節用到的一些關鍵字和函數之外,Python也為我們提供了很多其它便利。在這裡我們列舉一些比較常用的,但不再深入介紹用法。
in
any
all
for/else
map
reduce
filter
enumerate
zip
2、頻數統計
在實際開發過程,統計是另一個常見的需求。
還是以名字清單為例,将首字母相同的名字放在同一個分組(清單)裡邊。我們很容易想到使用dict資料結構:用首字母做為key,以一個list對象做為value即可。
names = ["Jim", "Tom", "Mary", "Hugo", "Tim", "John", "Alby", "Abigal", "Hamish", "Jeremy"]
groups = dict()
for name in names:
key = name[0]
if key in groups:
groups[key].append(name)
else:
g = [name]
groups[key] = g
使用dict的setdefault函數,上面這段代碼可以進行簡化:
for name in names:
key = name[0]
groups.setdefault(key, []).append(name)
d.setdefault(key, dft_val)的操作是,檢測key是否存在,如果存在則傳回value;如果不存在,則将dft_val存儲到d[key],并傳回dft_val。在上面的例子中, 我們在dict中存儲了list對象,是以我們可以通過鍊式調用,在一行代碼裡完成比較複雜的操作。
到目前為止,一切都簡單到不值得一提。但如果我們把分組的需求改為,編者首字母相同的名字的個數,那就是另一個情況了。這時候dict的value類型是int,我們不可以進行簡單的鍊式操作,是以使用setdfault也就不存在優勢了。一個比較直覺的實作,還是對第一段代碼進行簡單的改造:
names = ["Jim", "Tom", "Mary", "Hugo", "Tim", "John", "Alby", "Abigal", "Hamish", "Jeremy"]
groups = dict()
for name in names:
key = name[0]
if key in groups:
groups[key] += 1
else:
groups[key] = 1
或者使用get函數來簡化代碼:
for name in names:
key = name[0]
val = groups.get(key, 0)
groups[key] = val + 1
對于集合類型的操作,Python提供了一個更加高效便捷的庫collections。利用collections,我們可以對代碼進行進一步的簡化:
import collections
names = ["Jim", "Tom", "Mary", "Hugo", "Tim", "John", "Alby", "Abigal", "Hamish", "Jeremy"]
groups = collections.defaultdict(int)
for name in names:
groups[name[0]] += 1
collections.Counter類為我們提供了統計清單(可疊代對象)元素數量的便利,配合清單解析表達式(list comprehension),我們可以用一行代碼就完成統計的操作。
import collections
names = ["Jim", "Tom", "Mary", "Hugo", "Tim", "John", "Alby", "Abigal", "Hamish", "Jeremy"]
groups = collections.Counter(name[0] for name in names)
print(groups)
# 列印結果:
# Counter({'J': 3, 'T': 2, 'H': 2, 'A': 2, 'M': 1})
collections為我們提供了更容易使用的容器類型(如list、tuple、dict等)的子類及其它一些便利。本文隻是抛磚引玉,并不打算深入介紹collections的用法。在閱讀本文之後,各位讀者可自行深入學習。以下兩個連結都來自Python官方文檔,第一個是英文連結,第二個是中文連結。8.3. collections - High-performance container datatypes - Python 2.7.18 documentationdocs.python.orghttps://docs.python.org/zh-cn/3/library/collections.htmldocs.python.org
3、多次條件判定
我們經常會遇到一種情況,在執行特定操作之前,往往需要通過多次的條件判定。隻有在所有的條件都滿足的情況下,才會進行目标操作。
有一個改名的需要求,隻有當名字滿足一系列的條件,才可以對名字進行更改;否則提示改名失敗的原因。名字需要滿足的一系列條件是:
1、長度不得大于10
2、隻包含26個英文字母
3、有且隻有首字母大寫,其它字母都是小寫
4、最後一個字母必須是元音字母
我們先為各種判定結果定義一些常量,友善後面使用:
# Python沒有enum類型,我們可以通過class來模拟
class EAlterRet:
Succ = 0,
SizeOutOfRange = 1,
InvalidCharacter = 2,
NotCapitalized = 3,
EndWithConsonant = 4,
AlterNameErrors = (
"Succ", # 成功
"SizeOutOfRange", # 太長
"InvalidCharacter", # 非法字元
"NotCapitalized", # 非首字母大寫的
"EndWithConsonant", # 未以元音字母結尾
)
第一個實作方式,也就是最容易想到的實作方式,自然是多個if語句了。
import re
class Human:
def __init__(self, name):
self.name = name
def dealWithErrors(self, target, code):
ret = AlterNameErrors[code]
msg = "Alter name to '{}', result:{}".format(target, ret)
print(msg)
def alterName(self, target):
# 長度是否大于10
if len(target) > 10:
self.dealWithErrors(target, EAlterRet.SizeOutOfRange)
return
# 是否存在非法字元
if re.search(r'[^a-zA-z]', target):
self.dealWithErrors(target, EAlterRet.InvalidCharacter)
return
# 是否有且隻有首字母大寫
if target.lower().capitalize() != target:
self.dealWithErrors(target, EAlterRet.NotCapitalized)
return
# 是否以無意結尾
if not re.search(r'[AEIOUaeiou]$', target):
self.dealWithErrors(target, EAlterRet.EndWithConsonant)
return
# 改名成功
self.name = target
第二種方式是使用類似于do/while(false)的結構。由于Python沒有do/while(false)結構,我們可以使用一次for循環來替換。
import re
class Human:
def __init__(self, name):
self.name = name
def alterName(self, target):
error = EAlterRet.Succ
for i in range(0, 1):
if len(target) > 10:
error = EAlterRet.SizeOutOfRange
break
if re.search(r'[^a-zA-z]', target):
error = EAlterRet.InvalidCharacter
break
if target.lower().capitalize() != target:
error = EAlterRet.NotCapitalized
break
if not re.search(r'[AEIOUaeiou]$', target):
error = EAlterRet.EndWithConsonant
break
if error == EAlterRet.Succ: # 條件滿足,改名成功
self.name = target
else: # 條件不滿足,處理錯誤
ret = AlterNameErrors[code]
msg = "Alter name to '{}', result:{}".format(target, ret)
print(msg)
使用for/break的好處是,我們可以把錯誤放到後面統一處理,避免使用重複的錯誤處理代碼。
第三種方式是得用異常。雖然我們這個例子引入異常有點牽強,但舉一反三,各位讀者在以後的實際開發過程中,就可以多一個思路。
import re
class Human:
def __init__(self, name):
self.name = name
def alterName(self, target):
error = EAlterRet.Succ
try:
if len(target) > 10:
raise Exception(EAlterRet.SizeOutOfRange)
if re.search(r'[^a-zA-z]', target):
raise Exception(EAlterRet.InvalidCharacter)
if target.lower().capitalize() != target:
raise Exception(EAlterRet.NotCapitalized)
if not re.search(r'[AEIOUaeiou]$', target):
raise Exception(EAlterRet.EndWithConsonant)
expect Exception as ex:
ret = AlterNameErrors[ex.args[0]]
msg = "Alter name to '{}', result:{}".format(target, ret)
print(msg)
else:
self.name = target
finally: # 如果有需要的話,可以有finally語句
# 做點别的什麼事情
在不考慮效率的情況下,使用異常應該是三種方式中最簡潔的方式。使用異常還有一個好處,就是可以在finally中做點别的什麼事件。因為無論try中有raise還是有return,finally的語句總是會被執行。也就是說,無論發生什麼情況,我們總是可以在finally做一些清理工作,如關閉之前打開的檔案、關閉socket、或者打一些日志……
4、猜猜看
猜猜下面的這段代碼中,構造函數做了些什麼。請在評價區中進行留言和讨論 。
class FancyConstructor:
def __init__(self, a, b, c, d, e, f, g):
self.__dict__.update({k: v for k, v in locals().items() if k != 'self'})