Leetcode題解 Python: Sudoku Solver

解數獨,非常地生活化。

這跟上一個 N-Queens 不同,你要直接修改數獨表,最後要是已經完成的狀態,這次要把答案紀錄好就不再刪除了。

而且一排排往下的概念並不好用,因為有些已經填滿了。

先把主架構寫出來,寫出一個回溯法函數(Backtracking),把排換個概念換成層,而每一層就是單一個還沒下過的位置,靠著遍歷數獨表就可以知道有哪些位置。

接者就可以 Backtrack 一路往下搜尋。

當順利解出來時,就不會再有下一層,這時會出現報錯,出現別的情況,只要當這個情況出現時,回傳一個值,就可以用回傳值判斷要不要刪掉最後下過的位置,這樣就能阻止刪掉解法的最後一步。

class Solution:
def solveSudoku(self, board) -> None:
"""
Do not return anything, modify board in-place instead.
"""
def getCandidate(y, x):
Candidates = {}.fromkeys([str(i) for i in range(1,10)])
for x2 in range(9):
if board[y][x2] in Candidates: del Candidates[board[y][x2]]
for y2 in range(9):
if board[y2][x] in Candidates: del Candidates[board[y2][x]]
for y2 in range(y//3*3, y//3*3+3):
for x2 in range(x//3*3, x//3*3+3):
if board[y2][x2] in Candidates: del Candidates[board[y2][x2]]
return Candidates.keys()

def nextStep(level):
try:
row, col, _ = stepPos[level]
for num in getCandidate(row, col):
board[row][col] = num
if nextStep(level+1):
return True
else:
board[row][col] = "."
except:
return True

stepPos = []
for y in range(9):
for x in range(9):
if board[y][x] == ".":
stepPos.append((y, x, len(getCandidate(y, x))))

stepPos.sort(key=lambda x: x[2])
nextStep(0)

當中多了一作法,在找出所有可下位置時算出可能的數字數,接著再用可能數字數從小到大排序,就會從最少可能組合開始下起,這就像我們玩數獨一般,會從最少可能性的地方找起。

一看可以優化,用memo紀錄各col各row各area的所有數字,這樣用 in 找值的時候,能夠最快效率。

沒有排序問題,也沒有設值需求,然後需要求出兩數組差(123456789 與 各col各row各area),所以使用集合 set ,這樣就能方便計算。

剛好可以安插在找所有可下位置的時候,將各col各row各area的所有數字都添加進去。

class Solution:
def solveSudoku(self, board) -> None:
"""
Do not return anything, modify board in-place instead.
"""
Candidates = {*'123456789'}
memoCol = {}
memoRow = {}
memoArea = {}
for i in range(9):
memoCol[i] = set()
memoRow[i] = set()
memoArea[i] = set()

def getCandidate(y, x):
return Candidates - memoArea[x//3+y//3*3] - memoCol[x] - memoRow[y]

def nextStep(level):
try:
row, col = stepPos[level]
for num in getCandidate(row, col):
memoCol[col].add(num)
memoRow[row].add(num)
memoArea[col//3+row//3*3].add(num)
board[row][col] = num
if nextStep(level+1):
return True
else:
board[row][col] = "."
memoCol[col].discard(num)
memoRow[row].discard(num)
memoArea[col//3+row//3*3].discard(num)
except:
return True


stepPos = []
for y in range(9):
for x in range(9):
v = board[y][x]
if v == ".":
stepPos.append((y,x))
else:
memoCol[x].add(v)
memoRow[y].add(v)
memoArea[x//3+y//3*3].add(v)

nextStep(0)

Leetcode題解 Python: 四月挑戰DAY1 Single Number

「Given a non-empty array of integers, every element appears twice except for one. Find that single one.」

給一串非空整數字串列,每個元素會重複兩次,只有一個只出現一次,找出來。

這題非常簡單,一開始就有各種解法。不過我當作可能重複不只兩次,用這樣的前提寫出:

class Solution:    
    def singleNumber(self, nums: List[int]) -> int:
        memo = {}
        memo2 = {}
        for num in nums:
            if num in memo2:
                continue                    
            else:
                if num in memo:
                    del memo[num]
                    memo2[num] = 0
                    continue
                memo[num] = num   
        
        for key in memo: return memo[key]

我在想,有沒有甚麼只循環過一遍就能得到解法?不要有跑完N兩次或以上。看了前段班解法,回頭看題目才知道頂多重複兩次。(又再一次沒有看清楚題目

我的解法也只會跑過一次N。


偷看大神的解答,覺得這個相當精巧。

class Solution:       
    def singleNumber(self, nums: List[int]) -> int:        
        count = 0
        for num in nums:
            count ^= num            
        return count

運用「 ^= 」,重複的數字會重複兩次,於是會抵消掉,最後的值會等於只出現一次的數字。這方法不需要排序,也不用比較,也只需要遍歷串列過一次。

Leetcode題解 Python: N-Queens II

求出在 N * N的棋盤放上 N 個彼此無法互吃的解法數量。

最直觀的就是暴力解法,若 N = 4,就會有 16 格,於是第一步的可能下法就會有16種。

取出前面的結果,找出第二步下法的地方,得到第二步符合規則的所有下法,依序往下傳。

傳到第 N 步下完後,剩下多少種不相同的可行下法就是解答。(這樣會找到重複解。

這種暴力解法多計算了不需要的「流程」,每一步先後順序都沒有差。

換一種想法,有沒有更快速的下法?

每排上面一定有一個皇后,於是可以逐排安排皇后。

在逐排之下,要是該欄可以放皇后,就往下排從頭搜尋,到底則次數+1,不行則取走上排的皇后,退回上排再往下一欄搜尋。


回去偷看一下範例,才想到這樣的解法。(誰叫我當初選擇跳過不看呢,呵呵。

class Solution:
def totalNQueens(self, n: int) -> int:
def is_not_under_attack(Queens, y, x):
for queen in Queens:
qy, qx = queen
if qy == y: return False
if qx == x: return False
if abs(qx-x) == abs(qy-y): return False
return True

Queens = []
row, col = 0, 0
count = 0
while col <= n:
#print(row,col)
if col == n:
# remove queen
if Queens:
row, col = Queens.pop()
col = col+1
else: break
elif row == n:
count += 1
row, col = Queens.pop()
col = col+1
elif is_not_under_attack(Queens, row, col):
# place queen
Queens.append((row,col))
row, col = row + 1, 0
else:
col += 1

return count

當走到最後一排,順利放完就會觸發超出排數,這代表順利找到一個解法,成功將 N 個皇后放在棋盤上。取走上一個的皇后,然後繼續搜尋。

當走到最後一欄,超出欄數,就代表該排無法放置皇后,取走上一個皇后。如果在第 0 排,沒有皇后可取,就跳出迴圈,回傳計數。

有了這種思路,解答速度就會有所保證,接下來就是優化,讓速度更快速。

class Solution:
def totalNQueens(self, n: int) -> int:
result = []
queens, xy_sum, xy_dif = {}, {}, {}
def searchNext(row):
if row == n:
result.append("")
else:
for col in range(n):
xyS = row + col
xyD = row - col
if not col in queens and not xyS in xy_sum and not xyD in xy_dif:
queens[col] = xy_sum[xyS] = xy_dif[xyD] = None
searchNext(row+1)
del queens[col], xy_sum[xyS], xy_dif[xyD]
searchNext(0)
return len(result)

用了一個串列 result 接收結果,要解法可以添加 queens 當下所有鍵,只要解法數目最後用 len() 就能得到所有解的數目,要添加甚麼到result就隨便喜好。

這不得不說是參考前段班大神的解法,用xy相加與相減就能快速找到是否在斜線上,由於是一排排的下,所以也不會有同排(同y)的狀況要排除。

考慮到「查值是否存在」的速度差異,使用字典比起串列還快不少,所以都換成字典,畢竟只會使用 in 去查鍵是否存在,該鍵的值是甚麼就不重要。

在串列使用 append() 添加比 + 還快,用 + 會回傳一個新串列,如果沒有需要新生成或是有別的辦法替代,先用 append() 再傳會比較快。

這優化解法一開始就會直接到深處開始,不像前面的解法是一排排往下找,相快速度跟代碼長度就會快很多跟少很多。找完就刪掉最後一項(最後下的Queen)然後往右下一欄,走完所有欄,就會回上層繼續。

Leetcode題解 Python: Search a 2D Matrix II

這題是搜尋矩陣(Matrix)尋找是否存在該值。

被列在「遞迴」的範圍內,所以用遞迴的方式解決。

該矩陣有一個特性,相同 y 或 相同 x 的欄列會照順序排。

得利用此特性來強化搜尋機制,不然硬方法全部遍歷(Traversal)一次就一定找得到。

按照前面的範例,選一個中間的點,該值小於目標值則捨棄右下方,該值大於目標值則捨棄左上方,然後遍歷同y或同x軸的元素。

如果只是單純的目標值小就往左上找,反之找右下,這樣是行不通的。

設想一下情境,找了一個元素,但不是目標值,接著就可以將矩陣切成四分,直接捨棄左上或右下的,剩下左下、右上及右下或左上的部分矩陣也需要尋找,於是最後會產生三個矩陣再繼續遞迴下去搜尋。
於是可以簡單地寫成:

class Solution:        
def searchMatrix(self, matrix, target):
"""
:type matrix: List[List[int]]
:type target: int
:rtype: bool
"""
def serach(yl, yu, xl, xu, target):
if yl > yu or xl > xu:
return False
xm = (xu-xl)//2 + xl
ym = (yu-yl)//2 + yl
if matrix[ym][xm] == target:
return True
elif matrix[ym][xm] > target:
# 往左下查詢
if serach(ym, yu, xl, xm-1, target): return True
# 往左上查詢
if serach(yl, ym-1, xl, xm-1, target): return True
# 往右上查詢
if serach(yl, ym-1, xm, xu, target): return True
else:
# 往右上查詢
if serach(yl, ym, xm+1, xu, target): return True
# 往右下查詢
if serach(ym+1, yu, xm+1, xu, target): return True
# 往左下查詢
if serach(ym+1, yu, xl, xm, target): return True
return False

if not matrix or not matrix[0]: return False
return serach(0, len(matrix)-1, 0, len(matrix[0])-1, target)

要注意不在左上與不在右下的差異,跟元素同x或同y座標的元素仍未被搜尋過,所以會被分給左下或是右上,因此兩者的右上、左下搜尋會有差異。

如果在搜尋的時候先把同x或同y座標的元素給找過,就可以再減少要搜尋的範圍。

class Solution:
def searchMatrix(self, matrix, target):
"""
:type matrix: List[List[int]]
:type target: int
:rtype: bool
"""
def serach(yl, yu, xl, xu, target):
if yl > yu or xl > xu:
return False
xm = (xu-xl)//2 + xl
ym = (yu-yl)//2 + yl
#print(yl, yu, xl, xu)
if matrix[ym][xm] == target:
return True
elif matrix[ym][xm] > target:
# 往上,決定右上查詢範圍
yu2 = ym - 1
for y in range(yl, yu2+1):
if matrix[y][xm] == target:
return True
elif matrix[y][xm] > target:
yu2 = y - 1
break
if serach(yl, yu2, xm+1, xu, target):
return True
# 往左,決定左下查詢範圍
xu2 = xm - 1
for x in range(xl, xu2+1):
if matrix[ym][x] == target:
return True
elif matrix[ym][x] > target:
xu2 = x -1
break
if serach(ym+1, yu, xl, xu2, target):
return True
# 往左上方查詢
if serach(yl, ym-1, xl, xm-1, target):
return True
else:
# 往右,決定右上查詢範圍
xl2 = xm+1
for x in range(xl2, xu+1):
if matrix[ym][x] == target:
return True
elif matrix[ym][x] > target:
xl2 = x
break
if serach(yl, ym-1, xl2, xu, target):
return True
# 往下,決定左下查詢範圍
yl2 = ym+1
for y in range(yl2, yu+1):
if matrix[y][xm] == target:
return True
elif matrix[y][xm] > target:
yl2 = y
break
if serach(yl2, yu, xl, xm-1, target):
return True
# 往右下查詢
if serach(ym+1, yu, xm+1, xu, target):
return True
return False

if not matrix or not matrix[0]: return False
return serach(0, len(matrix)-1, 0, len(matrix[0])-1, target)

雖然這樣的寫法未必找得快,但是如果碰上要找重複幾個時候,只要改一改就能通用了。

雖然一開始的直覺是直接一個個搜尋,但是這樣就不合使用遞迴的期待。

更快的寫法是從左下開始搜尋,不斷往右上方逼進,最大次數就是長寬相加。這妥善利用該矩陣的特性,只是沒有遞迴。

Leetcode題解 Python: Unique Binary Search Trees II

今天碰到這一題,卡關很久,來記錄一下。

二元樹算是目前遇到稍微棘手級別,棘手級別目前只有莫隊算法,還沒有花時間去弄通它。

這一題屬於「遞迴」的範圍之內,給一個數字,要你回傳有相同節點數的所有可能。

一開始直覺,就是把數列排序,取一個數字,由於是二元樹,左邊皆小於該節值,右邊則大於,所以左邊樹結就是該數字左邊的所有項目組合,右邊等同處理。

接下來,要設一個節點,該節點左邊的等於左邊的遞迴結果,右邊等同處理。


就在這裡卡了一整個下午,之前相關的遞迴幾乎是把 node 往下傳,然後回傳也是 node。如果node 傳下來,左右邊的有數種組合,但是根節點只有一個,左邊要哪一個組合?右邊要哪一個組合?還是一開始就傳許多根節?直接以根結點指定左右是不可行的。

由上而下的想法是行不通的,這題需要的是由下而上。但一開始的想法碰壁之後就一直在思考由上而下的想法,直到最後才又回歸一開始的寫法改進。

既然有各種組合,就不能回傳節點,要能涵蓋各種組合,首選是用串列。

將回傳改成串列,左右邊會各有一個串列包含數種組合,最後再左右合併,往頭傳遞。這點順利改好後,一切就順多了。

用兩個迴圈跑完所有的左右組合,這時才把該子二元樹根節實例化,就不會有共同祖先的問題,也能產出對應數量且沒有重疊的子二元樹根節。

如果左或右沒有組合,就需要換成一個 [None] 來代替,沒有組合的那邊就只會是 None 也算一種組合。

寫完整理後就如下:

class Solution:
def generateTrees(self, n: int):
def nextNode(ns):
NTS = []
for i in range(len(ns)):
lntree = nextNode(ns[:i])
rntree = nextNode(ns[i+1:])
if not lntree: lntree = [None]
if not rntree: rntree = [None]
for l in lntree:
for r in rntree:
root = TreeNode(ns[i])
root.left, root.right = l, r
NTS.append(root)
return NTS
return nextNode(list(range(1,n+1)))

相當地直觀,但是效率並不到前中段班的。

前段班在代碼中加上 memo,將組合儲存起來,如果有就優先返回該值,沒有才遞迴並建立。

在一個區間的組合是固定的,就像1234組合起來,只會有同一種各組合可能。根如果為5、6、7…,1234就會被重複計算到,有memo就不用再算一次。

索引就以左界右界,建立二維串列儲存。

class Solution:
def generateTrees(self, n: int):
memo = [[None]*n for _ in range(n)]

def nextNode(l, r):
if l > r: return [None]
elif memo[l][r]: return memo[l][r]

NTS = []
for i in range(l,r+1, 1):
print(l, i, i+1, r,range(l,r+1, 1))
lntree = nextNode(l, i-1)
rntree = nextNode(i+1, r)
for lt in lntree:
for rt in rntree:
root = TreeNode(i+1)
root.left, root.right = lt, rt
NTS.append(root)
memo[l][r] = NTS
return NTS

if n == 0: return []
return nextNode(0, n-1)

python 群益API SKCOM工具

SKCOM-tool Github

python 群益API SKCOM工具 版本(API):2.13.20

檢查三格位置的SKCOM.dll版本差異與位置: 1.當前的COM元件 2.系統已註冊的COM元件 3.comtypes client所用的COM元件

並且給予相對應的建議動作。 方便查詢出目前電腦上所使用的版本。

「三個位置的SKCOM都是最新版本就沒有問題。」

若不需要GUI,執行「F.py」,查看輸出即可。


昨晚突然看到了自己有存下一個別人用python寫的SKCOM工具,仔細看了一下代碼,覺得那對我來說並不到能接受的標準,方向不是最正確的。

但是我也忘記是從哪裡找到的,不過我寫的SKCOM工具主體也不是參考它為主。

對我來說,我覺得「不清不楚」是最大的問題根源,COM版本差異可能會造成執行上的問題,所以這個SKCOM工具揭露的資訊就是為了不要不清不楚。

對於SKCOM.dll的版本檢測,最理想的狀態就是三者合一,都是最新版本的COM。

然而SKCOM.dll位置未必會相同,也未必會使用使用群益API的套件,也許換一個地方執行,換一個程式執行,也許會對某些部分就會產生不同的結果。

譬如說你有個程式A使用舊元件,程式B使用新元件,因為在官方範例寫法會把COM元件放在與py檔同一資料夾,也許你就無意之間造成了各種COM版本差異。

用這個工具看看,看到各位置的COM資訊,就能採取對應行動確保彼此間是一致的。

python 初學者速理解 yield與generator

這我很久以前碰過,但是也不清不楚,return 與 yield 都能回傳值,那為什麼有甚麼區分?

yeild用於function中回傳值,回傳的類型是產生器generator,這個產生器會產生回傳值,但本身並不是想要的回傳值。

所以很直覺地使用len()或是[:]切片在產生器上,會回報錯誤。

yeild產生器像是在函式中的斷點,可以得到當下變數的數值。意味著你可以在函式外影響函式內的值,也會直接牽連到yield。

要從產生器內取出回傳值,用 next() 或用 for迴圈 都可以,for迴圈會跑到產生器停止,無法再用next()取得值就跳出。

而generator進度是會保存下來的,因此一旦全部跑完一次,想要重頭,那就得重新產生generator一次。ex1a(Github)

yield能用在哪?從generator取值感覺並不直覺好用。

當資料量小的時候,使用時間並不會很明顯,一旦資料量變大,就能看出yield在某些時刻能起到龐大的優勢。

這裡假設一個情境:批次batch。 ex1(Github)

有一個長度10000000的串列,要切成每份大小10的串列,然後取出。(這算是一維變成二維,用numpy也可以輕易做到,萬一無法整除呢?

不過呢,當你生成一個新的(1000000,10)大小的二維串列,也代表於記憶體內有兩份一樣並不同形式的資料。

批次是為了解決一次性傳輸碰上量大問題,切小再送避免卡頓。在量大的前提,要是再生出一份對應整理好的資料,也勢必會影響執行時間。

用yield去做切割,原資料不變,也不會產生整理過的新資料,相對就會快上許多。

"""https://stackoverflow.com/questions/8290397/how-to-split-an-iterable-in-constant-size-chunks"""
def batch_yield(iterable, n=1):
    l = len(iterable)
    for ndx in range(0, l, n):
        yield iterable[ndx:min(ndx + n, l)]

如果 range(10) 要切成每份大小為 3, batch_yield(range(10), n=3)。

用 sys.getsizeof() 查看大小,使用generator是比起新產生一個整理過的資料還省下不少空間,執行速度也相對更快速。

不過「批次」這樣情形不一定得要用到yield,用[:]迴圈也可以不佔用太多記憶體空間,只是yield可以展開過程,會比較容易理解。

因為yield可以先取部分,再判斷情況去影響函式內部,讓generator提前結束或是再延長,也就是說長度可以變動,所以len()跟[:]就派不上用場。

講到一個經典情況:抽球。 程式碼:Github

今天有一個球池,裡面一開始只有紅球、黃球。每次抽球前會先攪拌打散順序,要是抽出黃球就放入黃球跟紅球;抽到紅球就放入藍球;抽到藍球就放入綠球;抽到綠球就放入白球;最後抽到白球就結束了。

進行一次 next() 抽球動作generator,會抽出 yield (產生)一個球。請問這個抽球動作長度len()多少?第5次抽球[4]是甚麼顏色?沒有產生結果之前,誰也說不準。

(球池補充,使用類別:MyBallPool(GitHub) MyBallPool2(GitHub)

如果有更多的狀況、更多的條件判斷與回饋,用上function yeild就能變成一行呼叫函式的代碼,這能使得版面看起來整潔許多。

接下來講的常見的迴圈加刪除的例子,個人認為對新手是相當難理解的。

a = list(range(10)) # a = [0,1,...,9]

for item in a:
    print(item, end = " ")
    item = 9999
    del a[0]

print("\n" + " ".join([str(item) for item in a]))   

->0 2 4 6 8
5 6 7 8 9

新手可能覺得這個for迴圈會執行10次,但是結果只執行了5次。

這是不是跟 yeild 外部影響內部非常相似?想一想,「for item in a」的 a,到底是那串資料還是另有甚麼?

其實是從 a 中取出 a.__iter__() 的回傳值:迭代器iterator。這個迭代器用 next() 依序傳值,運作跟for index in range(len(a)) a[index]是類似的。ex2(Github)

由於刪除串列中其中一個項目,但在迭代器的標籤位置不變,因此下次next()往後一格,也只是標籤位置+1,導致看起來像被跳過一格。 ex2a(Github)

若想用 for item in sequence 這種簡便的寫法循環全部項目,那麼加上[:],改用 for item in sequence[:] 也能辦到,即便中間刪了sequence的某個值,也能老老實實有十個就跑十次。

sequence 跟 sequence[:] 是不一樣的,用id()查詢也能知道,就像 sequence[:5] 明顯就不是 sequence。

因此在迴圈中刪掉 sequence 裡的項目,不影響 sequence[:].__itre__() 的迭代器所使用的資料序列,就能乖乖地跑完十次。 (補充:ex2b(Github) 在function yield中刪資料)

總結:

yield本身並不是想要的回傳值,而是一個產生器generator,能產生函式中的yield斷點的函式內變數。想要從產生器取值,需要使用next()。

因為yield本身並不是回傳值,所以能夠節省空間,像是批次處理。

yield產生的值是回傳當下的函式內變數,因此外部影響函式內部的話,yield的值也會受影響。(反之也能在function yield影響外部

也因為外部能夠影響內部,len長度跟slice切片位置可能會在過程中改變,所以這兩項並不重要。(generator內部也沒有這屬性)

for in 迴圈的運作模式,跟使用yield得到的產生器運作是相似的,因此在for in中增減序列的項目,也會影響For迴圈的執行。

而 for each in sequence,實際上是將 sequence.__iter__()回傳的 iterator 不斷 next() 直到無法取值為止,each 為 next(iterator) 的回傳值。

整篇文章的範例程式碼都收錄在 yield(Github)

個人語:

yield 可以把「for in if」濃縮起來,寫 for in if 除了那一行很占版面,也不容易一眼看出,而且[for in if]會產生新的串列,但  yield 不會。若後續要從中一一取出做運用,當然使用 yield 會是更便捷快速於[for in if]。

如果有一筆A資料,提升效率的關鍵就是不要再產生一份相似的資料。要是沒有很明白到底變數都導向去哪裡,除了可能產生冗贅資料,也可能對資料產生改動而牽動整體,導致實際運行出乎自己的預期。

我也不是很會說物件導向觀念到底哪裡容易讓人誤會,不過一開始學物件導向語言,說 a = b,把 a 指向 b,但是原先 b = 1,接下來 b = 2,但是 a 的值仍是 1 並不是 2 表面上看來也沒有 a = b 去。

雖然寫完這篇,我覺得對一個沒有基礎的初學者,yield還是有一點困難。

因為對於物件導向不是很熟悉的話,就會在各種變數的指向間混淆。yield厲害之處在於不用一次全部跑完,要妥善使用這個特性,在各個指定之間都要清楚掌握資料存在於哪,改動資料會影響所有對該資料的指向,改動指向並不會影響資料。

如果以實體跟指向來看,b並不是實體,b 指向的實體原先是 1, a 要是單純地指向 b 是沒有任何實體的,所以 a 的指向會跟 b 一樣是實體1。如果 b 是實體,像是類別物件等, 那麼 a = b之後,修改 b 也會反映到 a 上。

這裡用「實體」是想給人這是一種真實存在的東西說法,正確該說是「物件」。

int(3)都會指向同一個 id,不論 a=3、b=4-1、c=9/3,abc三者的id會是跟int(3)一樣的。(abc三者都是int的話

虛實問題,也反應到各種傳遞資料問題,到底傳的是實體?還只是一個指向?少用 = ,多用內部方法增修清除。串列是一個可修改自己的實體,裡面是一連串的指向。字串是不可修改自己的實體(設計上),增修則是變成新的實體回傳。

如果把類別的觀念擴大至整個架構,就會懂得 a = int(1) + int(3) ,其中 + 是呼叫該類別 int 的 __add__ ,得到回傳值 int(4)。a 也就指向新的實體 int(4)。

為什麼 a = int(1)、b = a、a = int(1) + int(3)、b = int(1)。一開始,a 指向 int(1)、b 也透過 a 指向 int(1),對 int 加減乘除會得到(回傳)一個 int 的答案,a 指向新的值,並沒有對 int(1) 做修改,故 b 仍然為 int(1)。

也許有天寫 leetcode 的時候,不明白為什麼運行時間這麼慢的時候,就能想想這個物件導向的虛實問題,看看自己的程式碼是不是產生過多不必要的實體。

PYTHON 群益API 2.13.20 錯誤代碼2017 錯誤原因與解法

從2.13.16版本之後,登入前就需要 SKReplyLib_OnReplyMessage 回傳指定值 0xFFFF 才可以登入。

一開始我也是矇了,不管怎麼傳就好像沒有作用?明明照著說明做了啊!
錯誤通常分兩種,第一種就是真的忘記寫到 OnReplyMessage 然後回傳 0xFFFF或寫錯,這種就不在這裡討論。第二種就是都按照說明寫了也確認無誤,但還是不知所以然的錯誤。

到底  2017 SK_WARNING_REGISTER_REPLYLIB_ONREPLYMESSAGE_FIRST 是從何而來?網路上沒有一個明確的解釋與通用的解法,這可以是基於討論的人並不多。
的確,確實回傳值設甚麼都沒有作用,那不是錯覺,那是真的。COM接口的Py檔中 OnReplyMessage 是沒有回傳值。
於是這形成了最怪異的事情,不管怎麼樣登入,由於接口的 OnReplyMessage 沒有回傳值 sConfirmCode,所以 SKCenterLib_Login 永遠都接收不到註冊公告,永遠的錯誤代碼 2017。
問題點是錯在哪裡?本來有個見解,在 2020-03-20 被提醒之後,終於確認是暫存檔的問題。(不是dll

執行以下幾行就能夠刪除掉舊版COM的py檔模型。

import comtypes.client, os
try:
    os.remove(comtypes.client.gen_dir + r"\_75AAD71C_8F4F_4F1F_9AEE_3D41A8C9BA5E_0_1_0.py")
    os.remove(comtypes.client.gen_dir + r"\SKCOMLib.py")
except:
    pass
繼續追根究柢下去。
來講解一下運作機制。
在python中,最初的三行通常是:
import comtypes.client
comtypes.client.GetModule(os.path.split(os.path.realpath(__file__))[0] + r'\SKCOM.dll') #加此行需將API放與py同目錄
import comtypes.gen.SKCOMLib as sk
第二行是從 SKCOM.dll 取得模型,顧名思義。而第三行就是取出第二行建立的模型。

然而 GetModule() 會先從 comtypes.client.gen_dir 路徑上尋找模型,如果路徑上已經有對應的模型,就會優先讀入,沒有才會從COM中生成py檔模型。

如果以前曾經使用過  GetModule(舊版COM),那麼就會優先讀取已存在的舊版COM的py模型,而不是從新版COM去創造一個新的覆蓋掉舊的,這也就是版本差異的所導致的錯誤。

透過 print(comtypes.client.gen_dir) 可以看到模型py檔放在哪裡。

有人會在快取夾 cache,有人會在 anaconda 中 /Lib/site-packages/comtypes/gen裡。複製貼上路徑,可以找到兩個檔案 SKCOMLib.py 及 _75AAD71C-8F4F-4F1F-9AEE-3D41A8C9BA5E.py 這就是從 SKCOM.dll 生出的 .py檔,也是這次的目標。

兩者位置差異在於該專案的解譯器使用甚麼環境,從專案→屬性就能查看到。Visual Studio可供選安裝的環境通常會是在 C:\Users\使用者名稱\AppData\Roaming\Python\ 裡面,如果是官網下載的Anaconda,則會在 anaconda環境/Lib/site-packages/comtypes/gen。

刪了檔案之後,重新執行 GetModule(新版COM),就能產生新版COM的Py檔,這樣便解決到舊版COM的Py檔在OnReplyMessage沒有回傳值( [‘out’], c_short, ‘sConfirmCode’ ),所導致的錯誤代碼2017。

就能順利運行了。

雖然一開始有找到問題所在,但是我卻誤認了真相。被提醒COM版本差異之後,就回頭更注重比對不同COM版本的差異,確實如提醒所指的方向去找就把不確定性解決了。

昨日基於一個忽略,就沒有測驗不同COM版本產生的Py檔模型差異。忘記刪除掉重新產生,只在乎修改,卻沒有發覺使用GetModule並沒有影響到模型py檔,沒有被修改,沒有產生新模型去覆蓋掉舊的,更應該去注意修改時間。

網路上是有人說刪掉gen資料夾,就能順利運作,但是不一定會有這個資料夾啊!當時我用VS的Anaconda,在comtypes裡並沒有gen這個資料夾,當時沒有想知道真相的衝勁,所以也就沒有花時間多思考為什麼可行。

如今可以解釋了,不確定也在提醒之後被解決了,當初是覺得.dll -> .py 錯誤,最後只有.py錯誤。正也因為確認錯誤在哪裡,終於也有了一套滿意的解決方法,能適用於各種的python環境。

思考自己的在尋找答案的過程,一有甚麼想法就去試,如果可行很快就會結束。也就沒有好好釐清各種可能的時間,排序去做確認。

如果先入為主,先設想認為dll問題,所以找解法的過程才不會刪除補好的檔案。
但如果先把其他人的方法整理,自然就會認為是暫存檔的問題,那就會先試刪掉後再用新版COM產生。

兩者同樣都能解決錯誤,但是解決完成度是不一樣的。

官方Documentation並沒有提到會保存檔案且優先讀入已有檔案,或是可供選擇覆寫掉,如果有,那會更快找到答案,直找documentation是優先得順位。所以看來還是不得不進入看看機制為何。

從package裡順一遍邏輯再得出設想,是最硬的,不過也是最根本的,如果當初有更深入去撈,也會發現它會優先讀入已有檔案吧,並不是產生新的覆蓋過去。

Python 群益API Error in sys.excepthook

從一開始使用群益API時,就經常收到這個錯誤輸出。

Error in sys.excepthook:

Original exception was:

不以為意,因為運行不會受到任何影響。

關於這個錯誤,用GOOGLE搜尋會找到一篇文章,有做探討。

確實是直指出錯誤問題來源,但是並沒有解釋到底為什麼會出錯。

前幾天著手多進程,因此更瞭解當中的運作模式,這篇會更詳細講看法與解法。


首先要來到基本運作解釋:

 comtypes.client.GetEvents(),是可以把COM event傳到對應接收函式。

根據官方文件 COM events,comtypes.client.GetEvents() 會收到一個連接物件 ,只要保持這個物件,就能保持狀態。

群益API範例沒有寫很多,後來看確實都有意義在的。

當登入報價之後,若是只有使用 time.sleep(),是會收不到回傳資料。

於是使用 pythoncom.PumpWaitingMessages(),好讓那些訊息有進到到主線程的時間。

訊息跟主線程並不在同一條線程上,因此可想有個子線程專門接收資料,到時候在送到對應的接收函式。

comtypes.client.GetEvents()的回傳值,就相當於一個這樣功能的子線程。

重新回到 Error in sys.excepthook 錯誤上,從網路上可以很容易查到解釋,就是主線程關掉了,但子線程卻還在運行。

這個物件的有無,對應著子線程運行有無,這可以解釋探討錯誤該文章的十八格狀態。

為什麼函式內用 comtypes.client.GetEvents() 就不會報錯,而在全域用就會報錯?函式結束時,函式內部變數也就被刪除,所以變數指定透過 comtypes.client.GetEvents() 得到的連接物件也就被刪除,連接子線程跟著中止。

在全域使用,在主線程結束時,自然是不會刪去這個連接物件。

要刪去這個連接物件,只需要使用 del 就能刪除了,是相當簡單的。

導致錯誤的關鍵點,就只是有沒有在主線程結束前刪掉這個連接物件。

其實不論在哪個域或類別中使用,也只需要在主線程結束之前,刪掉這個物件,或從類別中刪除這個屬性,就不會報錯了。

Python 資料讀寫方法比較與資料壓縮比較 (二) 多進程

上一回邊寫時就在思考多進程是否能夠幫助提升資料讀寫的速度。

答案是否定的,為什麼?
首先是資料大小,資料規模不到一定程度,光是多進程的準備時間就輸了一大截。
接著是沒有考慮到資料轉傳,測試皆採取原地寫入原地讀出,未經壓縮的資料寫入比較快,同時讀出也比較快,即便它的容量可能是上百倍甚至更多,省下處理的時間就有優勢。
多進程的優勢在於啟動更多的 cpu 計算能力,對於 壓縮 與 解壓縮 需要用到 cpu 計算的,如果需要越多計算,多進程的優勢就會能體現。如 lzma 。

此外,將越多任務交給多進程處例,有助於提升速度。例如把資料json化、bytes化、壓縮化及解壓縮等盡可能在各進程上執行,主線程只是發包資料到各進程去。

雖然讀取也可以多進程(搭配多進程寫入成不同檔案),但是往往輸給主線程直接一一讀取,因為多進程的讀取結果最後還是得回到一個主線程上顯示,這使得多進程讀取最後還是會回到單核上,失去了多核的優勢。

多進程在寫入的時候,可以不等待寫完,就讓主線程繼續到讀取階段。如果資料量一大,會碰到讀取時多進程卻沒有寫完而報錯,解決方法是在多進程讀取中做判斷。由於這是寫入時偷跑,看似寫入時間少很多是假象,少掉的時間會跑到讀取時間。

mp開頭代表使用多進程,bj 和 zl 和 xz 代表主要方法,後面  j  與 z 代表多進程執行 json 與 zip 。「n」代表不等待寫完的  no wait。

像是 mpzlzjn,使用 mp 多進程 ,主要方法為 zl(Json Zlib) ,每個進程 zip 壓縮 json 化且 no wait 寫完就繼續到讀取,就有優於 json zlib單核與其他多核但多核任務量不同的速度。

當使用多進程的時候,在運行中修改目標函式的代碼,也會直接反應到目前運行的當中,這表示多進程的是從實體取出目標函式。這是我在運行中目標函示中添加sleep時,直接觀察到數據有立即的反應。

多進程是使用上也有一定的局限,使用上來說不是那麼直接。如果沒有容量限制,也沒有轉傳的需要,用 bstr json 是最好的方法。

bstr json 是將資料 bytes(json.dumps(data), encoding = “acsii”) 用 ‘wb’ 寫入 f,再用 ‘rb’ 開啟 f 以 json.loads(f) 讀出。跟 str json 的差別在於寫入 f 時是用 ‘w’ 而 bstr json 是 ‘wb’。

如果需要轉傳,使用 mpzljzn 會是非常棒的方法,光是每秒 1 MB,用 bstr json 就不知道要傳多久。而 mpxzz系列只能在可憐的(< 1MB/s)傳輸速度上有優於 mpzljzn,考量到方便性,用 zl 而不使用多核也不錯。

""" 100萬筆資料寫讀
data = [(i,"阿明", "身體狀況", {"體重":78.9, "肝功能": "正常"}, 90, 85, 97, 81, 85, True) for i in range(100000)]
[Statistics]
Method: bstr json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.46935, avgRtime:3.14803, avgUtime:5.61739
Data size: 8697464(8.49MB), Wspeed:3.43961 MB/s, Rspeed:2.69807 MB/s, Uspeed:1.51202 MB/s
File size: 141888890(138.56MB), Wspeed:56.11319 MB/s, Rspeed:44.01585 MB/s, Uspeed:24.66687 MB/s
Method: json zlib, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.35145, avgRtime:3.29203, avgUtime:5.64348
Data size: 8697464(8.49MB), Wspeed:3.61208 MB/s, Rspeed:2.58005 MB/s, Uspeed:1.50503 MB/s
File size: 3117715(3.04MB), Wspeed:1.29480 MB/s, Rspeed:0.92485 MB/s, Uspeed:0.53950 MB/s
Method: json xz, Compress Ratio: 71.11:1
Average Use Time: avgWtime:14.73717, avgRtime:3.62832, avgUtime:18.36548
Data size: 8697464(8.49MB), Wspeed:0.57634 MB/s, Rspeed:2.34093 MB/s, Uspeed:0.46248 MB/s
File size: 122316(0.12MB), Wspeed:0.00811 MB/s, Rspeed:0.03292 MB/s, Uspeed:0.00650 MB/s
Method: mpbj, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.60734, avgRtime:3.34684, avgUtime:5.95418
Data size: 8697464(8.49MB), Wspeed:3.25758 MB/s, Rspeed:2.53781 MB/s, Uspeed:1.42650 MB/s
File size: 141888890(138.56MB), Wspeed:53.14356 MB/s, Rspeed:41.40131 MB/s, Uspeed:23.27163 MB/s
Method: mpbjn, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.19224, avgRtime:3.98733, avgUtime:6.17957
Data size: 8697464(8.49MB), Wspeed:3.87440 MB/s, Rspeed:2.13015 MB/s, Uspeed:1.37447 MB/s
File size: 141888890(138.56MB), Wspeed:63.20630 MB/s, Rspeed:34.75091 MB/s, Uspeed:22.42282 MB/s
Method: mpbjj, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.05410, avgRtime:3.13490, avgUtime:5.18900
Data size: 8697464(8.49MB), Wspeed:4.13496 MB/s, Rspeed:2.70937 MB/s, Uspeed:1.63685 MB/s
File size: 141888890(138.56MB), Wspeed:67.45707 MB/s, Rspeed:44.20021 MB/s, Uspeed:26.70329 MB/s
Method: mpbjjn, Compress Ratio: 0.06:1
Average Use Time: avgWtime:0.06299, avgRtime:4.78647, avgUtime:4.84947
Data size: 8697464(8.49MB), Wspeed:134.83090 MB/s, Rspeed:1.77450 MB/s, Uspeed:1.75145 MB/s
File size: 141888890(138.56MB), Wspeed:2199.60744 MB/s, Rspeed:28.94895 MB/s, Uspeed:28.57290 MB/s
Method: mpzl, Compress Ratio: 2.95:1
Average Use Time: avgWtime:2.93715, avgRtime:3.43182, avgUtime:6.36897
Data size: 8697464(8.49MB), Wspeed:2.89179 MB/s, Rspeed:2.47496 MB/s, Uspeed:1.33359 MB/s
File size: 2950294(2.88MB), Wspeed:0.98093 MB/s, Rspeed:0.83954 MB/s, Uspeed:0.45237 MB/s
Method: mpzlz, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.57715, avgRtime:3.46488, avgUtime:6.04203
Data size: 8697464(8.49MB), Wspeed:3.29574 MB/s, Rspeed:2.45134 MB/s, Uspeed:1.40575 MB/s
File size: 3118214(3.05MB), Wspeed:1.18159 MB/s, Rspeed:0.87886 MB/s, Uspeed:0.50399 MB/s
Method: mpzlzn, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.19316, avgRtime:3.75349, avgUtime:5.94665
Data size: 8697464(8.49MB), Wspeed:3.87278 MB/s, Rspeed:2.26286 MB/s, Uspeed:1.42830 MB/s
File size: 3118214(3.05MB), Wspeed:1.38847 MB/s, Rspeed:0.81128 MB/s, Uspeed:0.51207 MB/s
Method: mpzljz, Compress Ratio: 2.79:1
Average Use Time: avgWtime:2.07069, avgRtime:3.32371, avgUtime:5.39440
Data size: 8697464(8.49MB), Wspeed:4.10182 MB/s, Rspeed:2.55547 MB/s, Uspeed:1.57452 MB/s
File size: 3117371(3.04MB), Wspeed:1.47019 MB/s, Rspeed:0.91594 MB/s, Uspeed:0.56435 MB/s
Method: mpzljzn, Compress Ratio: 2.79:1
Average Use Time: avgWtime:0.04629, avgRtime:4.76512, avgUtime:4.81140
Data size: 8697464(8.49MB), Wspeed:183.49081 MB/s, Rspeed:1.78246 MB/s, Uspeed:1.76531 MB/s
File size: 3117371(3.04MB), Wspeed:65.76732 MB/s, Rspeed:0.63887 MB/s, Uspeed:0.63273 MB/s
Method: mpxz, Compress Ratio: 71.11:1
Average Use Time: avgWtime:14.85270, avgRtime:4.02111, avgUtime:18.87381
Data size: 8697464(8.49MB), Wspeed:0.57186 MB/s, Rspeed:2.11226 MB/s, Uspeed:0.45002 MB/s
File size: 122316(0.12MB), Wspeed:0.00804 MB/s, Rspeed:0.02971 MB/s, Uspeed:0.00633 MB/s
Method: mpxzz, Compress Ratio: 69.15:1
Average Use Time: avgWtime:7.99850, avgRtime:2.42350, avgUtime:10.42200
Data size: 8697464(8.49MB), Wspeed:1.06190 MB/s, Rspeed:3.50469 MB/s, Uspeed:0.81497 MB/s
File size: 125776(0.12MB), Wspeed:0.01536 MB/s, Rspeed:0.05068 MB/s, Uspeed:0.01179 MB/s
Method: mpxzzj, Compress Ratio: 71.62:1
Average Use Time: avgWtime:6.89620, avgRtime:3.59600, avgUtime:10.49220
Data size: 8697464(8.49MB), Wspeed:1.23164 MB/s, Rspeed:2.36197 MB/s, Uspeed:0.80952 MB/s
File size: 121444(0.12MB), Wspeed:0.01720 MB/s, Rspeed:0.03298 MB/s, Uspeed:0.01130 MB/s
Method: mpxzzjn, Compress Ratio: 71.62:1
Average Use Time: avgWtime:0.04867, avgRtime:8.94961, avgUtime:8.99828
Data size: 8697464(8.49MB), Wspeed:174.53017 MB/s, Rspeed:0.94905 MB/s, Uspeed:0.94392 MB/s
File size: 121444(0.12MB), Wspeed:2.43699 MB/s, Rspeed:0.01325 MB/s, Uspeed:0.01318 MB/s
"""

透過 win10工作排程器 定時以 potplayer 播放音樂(二)

既上一篇之後,又出現了沒有辦法自動播放的現象。

然而這次就有找到要如何觸發這種沒有辦法自動播放的現象,就是打開別的播放清單,然後方形的停止鍵,等到播放器完整停止後關掉,下次運行以開啟播放清單就不會順利自動播放。

這次又再一次面對這個問題,花時間搜尋,卻發現想找的cmd指令在播放器所在的資料夾有 CmdLine64.txt ,終於可以了解有甚麼cmd指令。

最終嘗試各種結果,我找到了更好的指令。

在工作排程器,工作,內容,動作,改成:

程式碼:PotPlayerMini64.exe
引數:"C:\BGM.dpl"
位置:J:\Program Files\DAUM\PotPlayer\

就不用再掉原本執行的批次檔 (.bat)。 上一篇

已經知道甚麼清況下無法自動播放,就很好測試各種情況下的影響。

雖然cmd指令裡面有 /autoplay 但是並不能順利解決掉自動播放的問題。上一次的引數是使用相對路徑,然而 CmdLine64.txt 裡面的例子是使用絕對路徑。

於是最新版就改成了 絕對路徑 ,同時避免中字出現造成亂碼錯誤而改成全英文路徑。

最後加上壓力測試,連續執行十五次,每一次都能夠順利播放,也不會因為此文章找到無法自動播放的操作方式給影響到。

一個使用絕對路徑,難道就真的是解決自動播放的方式嗎?

再多看看幾天,希望這一次就能夠解決。

Python 資料讀寫方法比較與資料壓縮比較

python內置的讀寫方式是透過 open() 。

如果有一筆資料,例如有五萬筆的串列,要寫入檔案並且讀出,甚麼樣的方式會是最快的?

這裡先寫結論,個人依照讀寫完成速度的私心排行:

「bstr json > json zlib > json gz > str json」
""" 完成速度排行榜
Method: bstr json, Compress Ratio: 0.06:1
Data size: 400064(0.39MB), Wspeed:4.16025 MB/s, Rspeed:2.97668 MB/s, Uspeed:1.73516 MB/s
Method: json zlib, Compress Ratio: 15.32:1, level = 6
Data size: 400064(0.39MB), Wspeed:2.99731 MB/s, Rspeed:2.76445 MB/s, Uspeed:1.43809 MB/s
Method: str json, Compress Ratio: 0.06:1
Data size: 400064(0.39MB), Wspeed:3.16940 MB/s, Rspeed:2.35598 MB/s, Uspeed:1.35141 MB/s
Method: json gz, Compress Ratio: 15.31:1, CompressLevel = 9
Data size: 400064(0.39MB), Wspeed:2.93210 MB/s, Rspeed:2.36939 MB/s, Uspeed:1.31044 MB/s
Method: json xz, Compress Ratio: 330.09:1, preset = 6
Data size: 400064(0.39MB), Wspeed:0.81887 MB/s, Rspeed:1.98104 MB/s, Uspeed:0.57938 MB/s
Method: json json, Compress Ratio: 0.06:1
Data size: 400064(0.39MB), Wspeed:0.57614 MB/s, Rspeed:2.28234 MB/s, Uspeed:0.46002 MB/s
Method: bstr list, Compress Ratio: 0.08:1
Data size: 400064(0.39MB), Wspeed:3.77832 MB/s, Rspeed:0.34935 MB/s, Uspeed:0.31978 MB/s
Method: str str(list), Compress Ratio: 0.10:1
Data size: 400064(0.39MB), Wspeed:3.58643 MB/s, Rspeed:0.33802 MB/s, Uspeed:0.30890 MB/s
Method: str str(tuple), Compress Ratio: 0.10:1
Data size: 400064(0.39MB), Wspeed:3.59002 MB/s, Rspeed:0.33762 MB/s, Uspeed:0.30860 MB/s
Method: json bz, Compress Ratio: 137.76:1, CompressLevel = 9
Data size: 400064(0.39MB), Wspeed:0.22510 MB/s, Rspeed:1.76317 MB/s, Uspeed:0.19962 MB/s
"""

先教大家如何使用 json zlib 壓縮並寫讀資料 (※其他壓縮方法大同小異

import json, zlib

data = [("阿明", "身體狀況", {"體重":78.9, "肝功能": "正常"}, 90, 85, 97, 81,  85, True)]*50000

# 寫入
with open(file, 'wb') as f:
    f.write(zlib.compress(bytes(json.dumps(data), encoding="ascii")))
f.close()

# 讀取
with open(file, "rb") as f:
    data = json.loads(zlib.decompress(f.read()))
f.close()

初學者講解:
json.dumps(data) -> 將資料轉換成json格式的string
bytes(data) -> 將資料 (string or int or list[int]) 轉換成 bytes。int 為正整數限制在 0 – 255間。
zlib.compress(data, level=-1) -> 將資料(bytes)轉換成經zlib壓縮後的帶有壓縮後資料的物件
如此以來就能用 open(file, ‘wb’) 來寫入壓縮後的資料。

zlib.decompress(data) -> 將壓縮後的資料解壓縮還原
json.loads(data) -> 將json格式資料string讀取還原。

範例程式碼:github

由於資料在 cpu 內的處理速度比硬碟讀寫速度快上許多,因此減少輸出檔案大小並讓 cpu 承擔解壓工作,將可以加速資料傳輸速度。

對於 pyhon 內建 讀寫io,最簡單的方式是將資料經 str(data) 後寫入,讀出後用 list(eval(data))還原。

用 open() 讀寫時, bytes讀寫速度都優於 string,使用mode: ‘wb’, ‘rb’ 快於 mode: ‘w’, ‘r’ 。因此將 string 解碼後用 ‘wb’, ‘rb’,雖然寫入的資料容量大小會比用 ‘w’, ‘r’ 大一些,但是會稍微快一點。

除了 str() -> list(eval)之外,另外一種能將一個有複雜結構保存的方法是使用 json。

在 python 中有 json5 跟 json,個人會比較推薦使用 json,因為 json 比較快。兩者在使用上的方法都大同小異,不過此例測試,使用 json5 不僅會報錯,壓縮還慢上數倍。

json的優勢在於它的讀取還原速度,用 json.load(fp) 比一般的 list(eval(read()) 約快上 6-7倍,但是在使用其模組的 json.dump(fp) 寫入io時,比起 open(f, ‘w’) -> f.write(str(data)) 還慢上 6-7倍,而讀取會花比較久的時間,所以 json 整體寫讀是比較快的。

既然 json 的優勢在於讀, 而 內置io 的優勢在於寫,兩者結合 string json 就能讓整體讀寫速度提升到 3-4 倍。 用 內置io  f.write(json.dumps(data)) 寫,用 json 的 json.load(fp) 讀。

""" 輸出檔案大小,依完成速度排列
Method: json zlib, Compress Ratio: 15.32:1, level = 6
Average Use Time: avgWtime:0.13035, avgRtime:0.14133, avgUtime:0.27167
File size: 26112(0.03MB), Wspeed:0.19563 MB/s, Rspeed:0.18043 MB/s, Uspeed:0.09386 MB/s
Method: str json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:0.12327, avgRtime:0.16583, avgUtime:0.28910
File size: 6700000(6.54MB), Wspeed:53.07888 MB/s, Rspeed:39.45637 MB/s, Uspeed:22.63245 MB/s
Method: json gz, Compress Ratio: 15.31:1, CompressLevel = 9
Average Use Time: avgWtime:0.13324, avgRtime:0.16489, avgUtime:0.29813
File size: 26124(0.03MB), Wspeed:0.19146 MB/s, Rspeed:0.15472 MB/s, Uspeed:0.08557 MB/s
Method: json xz, Compress Ratio: 330.09:1, preset = 6
Average Use Time: avgWtime:0.47711, avgRtime:0.19721, avgUtime:0.67432
File size: 1212(0.00MB), Wspeed:0.00248 MB/s, Rspeed:0.00600 MB/s, Uspeed:0.00176 MB/s
Method: bstr list, Compress Ratio: 0.08:1
Average Use Time: avgWtime:0.10340, avgRtime:1.11832, avgUtime:1.22172
File size: 4750000(4.64MB), Wspeed:44.86032 MB/s, Rspeed:4.14789 MB/s, Uspeed:3.79683 MB/s
Method: str str(list), Compress Ratio: 0.10:1
Average Use Time: avgWtime:0.10894, avgRtime:1.15582, avgUtime:1.26476
File size: 4100000(4.00MB), Wspeed:36.75500 MB/s, Rspeed:3.46412 MB/s, Uspeed:3.16575 MB/s
Method: json bz, Compress Ratio: 137.76:1, CompressLevel = 9
Average Use Time: avgWtime:1.73560, avgRtime:0.22158, avgUtime:1.95718
File size: 2904(0.00MB), Wspeed:0.00163 MB/s, Rspeed:0.01280 MB/s, Uspeed:0.00145 MB/s
"""

不過本來資料大小為 400064 bytes(0.39MB),不論用 str() 或是 json.dump(),輸出的檔案都大上十倍多來到 4100000 bytes(4.00MB)、6700000(6.54MB)。因為檔案大,這會使得硬碟讀寫速度影響比較大,這不符合減少硬碟負擔並讓 cup 承擔的想法。

將資料壓縮後寫入,將壓縮後資料讀出後還原。這符合加速資料讀寫的設想。

python的資料壓縮有四個常見套件:zlib, gzip, lzma, bz2。
四者使用上大同小異,不過表現卻各有不同。共同點是,經過壓縮之後,檔案大小都遠小於沒有壓縮過且直接輸出的資料。這裡四者壓縮時皆使用預設的壓縮等級。

資料壓縮後優勢的檔案大小,彌補了在讀取速度上的劣勢,反而更優於沒有壓縮的資料寫讀。

到底影響速度上限,如果是資料壓縮,那麼就是cpu,如果直接輸出,那麼就是硬碟寫讀速度。個人是用SSD硬碟,以下加大資料筆數進行HDD跟SSD的測試讀寫。

""" 資料筆數1000000
Method:[SSD][壓縮] json zlib, Compress Ratio: 15.39:1
Average Use Time: avgWtime:2.45219, avgRtime:3.07391, avgUtime:5.52610
Data size: 8000064(7.81MB), Wspeed:3.18596 MB/s, Rspeed:2.54157 MB/s, Uspeed:1.41376 MB/s
File size: 519928(0.51MB), Wspeed:0.20706 MB/s, Rspeed:0.16518 MB/s, Uspeed:0.09188 MB/s
Method:[HDD] [壓縮]json zlib, Compress Ratio: 15.39:1
Average Use Time: avgWtime:2.47452, avgRtime:3.15085, avgUtime:5.62538
Data size: 8000064(7.81MB), Wspeed:3.15720 MB/s, Rspeed:2.47951 MB/s, Uspeed:1.38881 MB/s
File size: 519928(0.51MB), Wspeed:0.20519 MB/s, Rspeed:0.16114 MB/s, Uspeed:0.09026 MB/s
Method:[SSD][直輸] str json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:2.52982, avgRtime:3.39660, avgUtime:5.92642
Data size: 8000064(7.81MB), Wspeed:3.08819 MB/s, Rspeed:2.30011 MB/s, Uspeed:1.31826 MB/s
File size: 134000000(130.86MB), Wspeed:51.72675 MB/s, Rspeed:38.52662 MB/s, Uspeed:22.08069 MB/s
Method:[HDD] [直輸] str json, Compress Ratio: 0.06:1
Average Use Time: avgWtime:3.08978, avgRtime:3.42463, avgUtime:6.51441
Data size: 8000064(7.81MB), Wspeed:2.52852 MB/s, Rspeed:2.28129 MB/s, Uspeed:1.19927 MB/s
File size: 134000000(130.86MB), Wspeed:42.35231 MB/s, Rspeed:38.21123 MB/s, Uspeed:20.08767 MB/s
"""

在直輸方面,可以看到HDD的速度就比在SSD上慢了。
在壓縮方面,其實在HDD跟SSD上差不多,兩者在誤差範圍內。

資料壓縮的速度上限取決於 cpu,而資料壓縮程度 compresslevel 的高低會有不同的工作量。由於python執行若沒有特殊設定之下,是使用單核,資料壓縮與解壓縮時通常這個單核會吃爆,因此降低壓縮程度,減少單核的運算工作,可以提升資料寫讀速度。

""" 資料筆數1000000,壓縮程度 6 vs 3
Method: json zlib, Compress Ratio: 15.39:1, level = 6
Average Use Time: avgWtime:2.45219, avgRtime:3.07391, avgUtime:5.52610
Data size: 8000064(7.81MB), Wspeed:3.18596 MB/s, Rspeed:2.54157 MB/s, Uspeed:1.41376 MB/s
File size: 519928(0.51MB), Wspeed:0.20706 MB/s, Rspeed:0.16518 MB/s, Uspeed:0.09188 MB/s
Method: json zlib, Compress Ratio: 8.21:1, level = 3
Average Use Time: avgWtime:2.10440, avgRtime:3.07207, avgUtime:5.17648
Data size: 8000064(7.81MB), Wspeed:3.71248 MB/s, Rspeed:2.54309 MB/s, Uspeed:1.50924 MB/s
File size: 974407(0.95MB), Wspeed:0.45218 MB/s, Rspeed:0.30975 MB/s, Uspeed:0.18383 MB/s
"""

對於這四個套件: zlib, gzip, lzma, bz2,在此例上的表現。

壓縮率最好的是 lzma,產生的 .xz ,大小又小於zlib及gzip許多,速度也優於直輸。

然而 bz2,在壓縮上面的速度太劣勢,而且壓縮比在此例也不如 lzma,速度也輸給直輸。

而 zlib 和 gzip 不論在壓縮比跟速度都很接近,因為 gzip 是引入 zlib 用,所以 zlib 速度總是略贏 gzip。