天天看點

python粘性拓展_Python基礎之:拓展解決問題的思路

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 documentation​docs.python.orghttps://docs.python.org/zh-cn/3/library/collections.html​docs.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'})