Python @ Property 修飾符 初學者速理解

初學者第一次看到「@」,像我,就滿頭問號。

網路上教學很多,而且不難懂,只是沒有我的風格。(推薦: 官方文件)

@又名Property屬性,property() 是python的內置函數。在定義 類別class 的時候,也經常設定其相關屬性。

Property由四個部分組成:getter setterdeleter、 doc。
以a為例,getter、 setter、 deleter分別對應:

a # call getter
a = 1 # call setter
del a # call deleter

發生以上三種狀況,就會分別執行對應的函式。

依照官方的範例稍微改寫,例1:

class C(object):
    def __init__(self):
        self._x = None
    def getx(self):
        print("得了")
        return self._x
    def setx(self, value):
        print("設了")
        self._x = value
    def delx(self):
        print("刪了")
        del self._x
    x = property(getx, setx, delx, "docstring")

a = C()
a.x # a.getx()
a.x = 1 # a.setx()
del a.x # a.delx()

得到以下輸出。

得了
設了
刪了

使用Property後可以減少代碼,除了例1的寫法,Property可以寫得更簡化。
用上「@」,更快速使用Property,簡化代碼。例1也可以寫成例2:

class C(object):
    def __init__(self):
        self._x = None

    @property
    def x(self):
        print("得了")
        return self._x

    @x.setter
    def x(self, value):
        print("設了")
        self._x = value

    @x.deleter
    def x(self):
        print("刪了")
        del self._x

@property 就是將下方函式定義為 x 的 getter, 然後 x = property(getter),下方函式名 x 就會變成屬性。
看例2,@property 為 getter。 setter也可以寫成@屬性.setter 、 deleter 寫成@屬性.deleter。
getter 最常被使用也一定要設置,setter、 deleter  就不一定要設置。

知道使用 @property 可以將 a.getx() 省略成 a.x 。a.x 就等於 a.getx() 的回傳值。

舉一個實例,如果要定義一個長方形類別,可能會在 __init__() 中,設定其長與寬,然後透過長與寬計算出面積。

class Rectangle():
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.area = width * height

如果 width = 5、 height = 4,那麼 area = 20。

但是,要是後面只改變了 width 或是 height, area 並不會自動更新數值,仍是舊的 20。
如果用上 @property 設定 area,就會在呼叫 area 時,重新計算數值。

class Rectangle():
    def __init__(self, width, height):
        self.width = width
        self.height = height

    @property
    def area(self):
        return self.width * self.height

使用  @property 可將像 area 這類動態數值的過程簡化成屬性名,除了大幅減少代碼篇幅,使用上也變得更直覺,。

學到這裡,講完了property的基本運作,現在來看一下其他教學中的舉例。
如果有好好從前面讀懂,這個範例應該是很好理解,範例3:

def f1(f2):
    return 1

@f1
def f2(x):
    return 2
 f2(1) # Error 

先不講結果,先順著 property 的邏輯看。
@f1,getter 為 f1,f2 = property(f1),下方函式名 f2 變成屬性。
於是 f2 == 「f1的回傳值」 == 1、f2 + f2 == 2。
行7嘗試 f2(1) ,就會報錯,無法理解 f2(1) 為什麼會錯, 就換成 1(1) 理解看看吧。

行1寫成 f1(f2),是方便理解整個 f2函式 都會傳入到 f1。
由於函式內外部差異,可以將傳入的 f2 跟外部的 f2 視作不同的東西。
因此呼叫傳入的 f2 並不會觸發外部帶有@的 f2。

最後範例,如果無法理解,就回過頭看上方的說明吧。範例4:

def addition(f):
    answer = 0
    for num in f():
        answer += num
    return answer

@addition
def threePlusFive():
    return [3, 5]

@addition
def threePlusFivePlusEight():
    return [3, 5, 8]

@addition
def threePlusFivePlusEight2():
    return [threePlusFive, 8]

print(threePlusFive)
print(threePlusFivePlusEight)
print(threePlusFivePlusEight2)

Python pyqt + logging 整合範例1

當有了圖形化介面的時候,發現工作列卡一個 cmd 實在想隱藏起來。
用 pythonw + .pyw,就能不卡 cmd。
但是一旦將 cmd 隱藏起來,就看不到輸出了甚麼。
logging可以將紀錄在輸出並同時保存下來,操作起來比 open() 還便捷。

GUI的操作功能得寫在一塊,格局被侷限了。
如果想整合更多的功能,而且是單獨寫過寫好的部分,或是打開音樂播放器聽音樂與執行某串程式碼等。
大夥都進來專案,會讓專案顯得異常混雜,也可能產生多個同名同功能但不同亞種的函式,搞得你混淆誰新誰舊。
此外,這些功能不需要傳入或只有固定參數,啟動後就能自動獨立運行完成。

那些其他程式運作情形,可以靠logging紀錄,不一定要回傳給主程式。
主程式透過查閱記錄,就能在GUI上觀察到程式進度。

然而可預期的錯誤能被處理,不可預期的錯誤會讓程式跳掉而無法用logging傳出ERROR以上的紀錄。
為了避免看不到錯誤訊息的憾事,得將錯誤訊息輸出保存下來以供來日查閱。

範例1:githubyoutube

展示以下功能:
1.透過整合logging,看見額外程式運行狀況。
2.能篩選紀錄,顯示一定level以上的紀錄。
3.有做錯誤處理且ERROR以上的紀錄,會跳出通知視窗。
4.萬一沒有做錯誤處理,會把錯誤訊息保存下來。

對於這個範例不是的某些部份若不是很瞭解,請找我最近寫了幾篇範例與文章。
我想唯有這個 cmd “python pyfile.py > output.txt 2>&1 “沒有講到。
簡單來說,把pyfile.py的結果標準輸出到output.txt,cmd上有甚麼,txt就大概會有甚麼。
但是沒有錯誤訊息,因為錯誤訊息不在標準輸出裡面。而 2>&1 意思是把錯誤輸出重定向到標準輸出,錯誤訊息才會輸出到output.txt。

程式敘述:

有一個介面,上面有一個清單會顯示過去10秒的紀錄,以及對應四個程式的四個按鈕,按下去會執行對應的程式,及其他按鈕等。

四個程式分別為ProgramA、ProgramB、ProgramC、ProgramD,都有整合logging,會把紀錄傳到主程式查閱的.log檔案。

ProgramA,有錯誤處理,而且有錯誤發生
ProgramB,有錯誤處理,但沒有錯誤發生
ProgramC,沒有錯誤處理,但有錯誤發生
ProgramD,無限循環。

執行這四個程式時,會在 ./error 生成一個 txt ,如果順利執行, txt 的大小會是0,然後被刪除掉。
ProgramA 與 ProgramB 順利執行完畢,而 ProgramC 會報錯,因此 ./error 會有記錄錯誤訊息的 ProgramC 的 txt。
而ProgramD,無限循環,觀察是否使用多核。會。

語:
花了好幾天,一點一滴摸索最終達到這個架構。

如果要做一個任務管理器,執行各種任務,自然得靠多線程,不然會看到主程式執行下去動彈不得就難堪。雖然多線程無法讓主程式使用多核,但用此例方法執行的程式,不會跟主程式擠在同一核,以往寫python需要引入多進程才能用到多核,這方法經測試過後,python程式會自動分配到其他核上。

關於logging的設定想了很久,雖然上一篇教學大開使用的自由度,但是卻沒有規劃一定的架構。從好幾種設定探索,並且探索其可能遇到的問題,最後把能想到的問題都有了解法。

這範例中各程式都引入同一個自訂Logging,這避免產生同名同功能的亞種,不論哪個專案,都是引入這個。之前我就遇到複製太多造成維護不易,更新了一個,還要手動複製貼上到其他的兄弟姊妹實在太費工。

這範例對我來說,就是改善過去不良的架構,為更好維護的架構打下基礎。

Python logging 初學者速理解

給對於 logging 未曾接觸過,或剛接觸過的人。

logging 可以將記錄輸出,也可同時將記錄保存。除錯除了靠print大法,也可以靠logging保存下來的日誌中找出錯誤點。

有些程式運行會因為時間改變環境而出現問題,出現嚴重錯誤就會跳掉。沒有辦法總是在編寫或除錯模式下運行,出錯就告訴你哪裡有問題。若程式未能紀錄到當下運行狀況,就少了線索去發現問題所在。

logging有很多教學,也有繁體中文教學,寫得也不錯。

這裡就簡單寫下一點筆記,幫助初學者快速理解logging運作。


logging有程度(level)之分:最嚴重CRITIAL > ERROR > WARNING > INFO > DEBUG > 最輕微 NOTSET。

假如我只想保留 ERROR 以上的記錄在 log,顯示 INFO 以上的記錄在終端。

假設我有一個程式,命名為 HELLO.py。例1:

Import logging

loggerName = "HELLO"
fileName = "hello.log"

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(loggerName) 
FileHandler = logging.FileHandler(fileName)
FileHandler.setLevel("ERROR")
logger.addHandler(FileHandler)

logger.info("哈囉")
logger.debug("這個不會顯示出來 INFO > DEBUG")
logger.warning("注意下一個記錄")
logger.error("只有這個會被保存在log")

行6,先跳過。
行7,得到一個名字為 “HELLO” 的 Logger 物件。
行8,得到一個 FileHandler 物件。Handler 功能是把 Logger 的記錄傳出去,看官網文件,Handler的種類有很多種,這次用的 FileHandler ,簡單來說就是把 Logger 的記錄傳到指定檔案。
行6,logging基本設定,level是指 root 的 level,會自動生成一個 StreamHandler 添加到 root 上去。上行解釋 Handler 功能,這自動生成的 StreamHandler 會將記錄傳出然後顯示在終端。至於要多高層級的訊息才會顯示,就看這裡設定 root 的 level,之後新增 Logger 默認level也會跟這裡設定的相同 。官網文件
行9,設定 FileHandler 的 level 為 ERROR。
行10,讓 logger 添加 FileHandler。

行12~15,用 logger 傳入不同 level 紀錄,看看實際效果。

INFO:test:哈囉
WARNING:test:注意下一個訊息
ERROR:test:只有這個會被保存在log

複習一下:CRITIAL > ERROR > WARNING > INFO > DEBUG >  NOTSET
或者:CRITIAL(50) > ERROR(40)  > WARNING(30)  > INFO(20)  > DEBUG(10)

因自動生成 StreamHandler 的 level 為 INFO ,故只將行12、行14、行15紀錄輸出到終端。而行13的 level 為 DEBUG,小於 StreamHandler 的 level  INFO,所以被刷掉。
FileHandler 的 level 為 ERROR ,所以只有行15被保存在 hello.log 裡。

注意:這裡顯示在終端的紀錄,是經由 root 的 StreamHandler ,不是 logger 的,這可以利用 Logger.handlers 查看 logger有甚麼 handlers。root 是最上層的 Logger,可觀察到將資料傳入 logger 時,也會往上層傳到 root。

瞭解了 例1 之後,其實我們能把每個 Logger 顯示在終端的 level 有不同設定。如果有兩個 Logger ,一個要 INFO 以上,一個 ERROR 以上,只要分別針對兩者的 StreamHandler 有不同設定。

例2:

import logging

loggerName = "test"
loggerName2 = "test2"
fmt = logging.Formatter("%(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(loggerName)
logger2 = logging.getLogger(loggerName2)
logger.setLevel("DEBUG")
logger2.setLevel("DEBUG")
StreamHandler = logging.StreamHandler()
StreamHandler.setLevel("INFO")
StreamHandler.setFormatter(fmt)
StreamHandler2 = logging.StreamHandler()
StreamHandler2.setLevel("ERROR")
StreamHandler2.setFormatter(fmt)
logger.addHandler(StreamHandler)
logger2.addHandler(StreamHandler2)

for lg in [logger, logger2]:
    print(lg.getEffectiveLevel())
    lg.info("哈囉")
    lg.debug("這個如何")
    lg.warning("那這個呢")
    lg.error("最後一個")

執行結果為:

10
test - INFO - 哈囉
test - WARNING - 那這個呢
test - ERROR - 最後一個
10
test2 - ERROR - 最後一個

這裡沒有用到 logging.basicConfig() ,所以 Logger 的 level 默認為 WARNING。
若不在行8、行9設定 Logger 的 level,結果會變成如下:

30
test - WARNING - 那這個呢
test - ERROR - 最後一個
30
test2 - ERROR - 最後一個

Logger 默認的 level 為 WARNING 換算等級數值為 30,如果有logging.basicConfig()就會先以這裡設定的 level 為主,於是行21 INFO、行22 DEBUG的紀錄在 logger 這關就會被默認的 level WARNING 濾掉,接下來就沒有辦法進到 StreamHandler。

每個 level 都有對應的數值,在 Logger.setLevel() 時,也可以輸入正整數, Logger.setLevel(30) == Logger.setLevel(“WARNING”)。

透過 例2 ,瞭解如何分別設定顯示在終端的條件,自然也可以分別設定輸出檔案跟級別等。

雖然可以分別設定 Logger 但會讓程式碼看起來相當的長,不過也有一個方法,只要兩句,然後就能簡化成 logging.getLogger() 解決掉。

from logging import config
config.fileConfig(configName)

這邊在資料夾要多新增一個檔案名為 logging.conf,裡面寫 Logging 的設定,可以用記事本讀寫。

logging.conf:

[loggers]
keys=root, test, test2

[handlers]
keys=consoleHandler, StreamHandler, StreamHandler2

[formatters]
keys= default

[logger_root]
level=WARNING
handlers=consoleHandler

[logger_test]
level=DEBUG
handlers=StreamHandler
qualname=test
propagate=1

[logger_test2]
level=DEBUG
handlers=StreamHandler2
qualname=test2
propagate=1

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=default
args=(sys.stdout,)

[handler_StreamHandler]
class=StreamHandler
level=INFO
formatter=default
args=(sys.stdout,)

[handler_StreamHandler2]
class=StreamHandler
level=ERROR
formatter=default
args=(sys.stdout,)

[formatter_default]
format=%(name)s - %(levelname)s - %(message)s
datefmt=

內容很明顯,就是將 例2 的那些設定寫進來,於是 例2 可以簡化成這個樣子。

例2改:

import logging
from logging import config

configName = "logging.conf"
loggerName = "test"
loggerName2 = "test2"

config.fileConfig(configName)
logger = logging.getLogger(loggerName)
logger2 = logging.getLogger(loggerName2)

for lg in [logger, logger2]:
    print(lg.getEffectiveLevel())
    lg.info("哈囉")
    lg.debug("這個如何")
    lg.warning("那這個呢")
    lg.error("最後一個")

這樣是不是就簡潔很多了。

這邊簡單整理一下 logging 的流程,舉個例:

假設我傳入一個 warning 的紀錄到 logger, logger 的級別若是超過 30,就會被濾掉。
沒有,則該紀錄傳往其所屬 handlers 跟上層的 root  handler 傳入。
若 logger handler 的級別小於等於 30,就會把該紀錄傳出去。
若 root handler 的級別小於等於 30,就會把該紀錄傳出去。
不重要:若 root 的級別超過 30與否都不影響,並沒有在判斷流程。

官網上也有流程圖,那裡寫得會更完整。

為了讓各位更清楚看見流程細節,最後一個範例4:

import logging
from logging import config
    
configName = "logging.conf"
loggerName = "test"
loggerName2 = "test2"

config.fileConfig(configName)
logger = logging.getLogger(loggerName) 
logger2 = logging.getLogger(loggerName2) 

fmt = logging.Formatter("%(name)s - %(levelname)s - %(message)s - root SH 20")
StreamHandler = logging.StreamHandler()
StreamHandler.setFormatter(fmt)
StreamHandler.setLevel(20)
fmt2 = logging.Formatter("%(name)s - %(levelname)s - %(message)s - root SH2 40")
StreamHandler2 = logging.StreamHandler()
StreamHandler2.setFormatter(fmt2)
StreamHandler2.setLevel(40)
logging.root.addHandler(StreamHandler)
logging.root.addHandler(StreamHandler2)

logger.setLevel(10)
logger2.setLevel(30)
logging.root.setLevel(20)

for lg in [logger, logger2]:
    lg.info("20")
    lg.debug("10")
    lg.warning("30")
    lg.error("40")

這裡將 root level = 20,並加了兩個 StreamHandler ,其 level 分別為 20、40。

test - INFO - 20
test - INFO - 20 - root SH 20
test - WARNING - 30
test - WARNING - 30 - root SH 20
test - ERROR - 40
test - ERROR - 40 - root SH 20
test - ERROR - 40 - root SH2 40
test2 - WARNING - 30 - root SH 20
test2 - ERROR - 40
test2 - ERROR - 40 - root SH 20
test2 - ERROR - 40 - root SH2 40

第一眼眼花撩亂,不要忘記:
test level = 10, test SH level = 20
test2 level = 30, test2 SH level = 40
root level = 20, root SH level = 20, root SH2 level = 40

順著 logger -> logger handlers & root handlers 這樣的流程就可以慢慢湊出每行輸出是怎麼來的。

如果不想要讓 Logger 往上傳遞,可以將其propagate設為 False。

總結一下:
如何設定 Logger 指定其輸出檔案跟終端的方式。
如何分別設定 Logger 。
如何使用 config.fileConfig(),簡化 Logger 設定。
描述 logging flow,瞭解傳播的方向與規則。

例1~例4的程式碼放在 github 。

祝各位學習順利,若有建議或想法可以留言告知。

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

用 工作排程器 在早上自動播放是我的習慣,不過有一個問題,它不一定每次都能播放出來。

這邊講一下基本準備:

將想要播放的音樂清單,用 potplayer 輸出成播放清單(.dpl)。
操作:把音樂擺入播放清單,按F2儲存。

在win10系統,螢幕左下有放大鏡,搜尋「工作排程器」就可以叫出來。
不然「開始」→「windows 系統管理工具」→「工作排程器」。

透過建立工作,觸發條件自行設定,我個人是設定每日早上七點。

動作新增一個:
啟動程式
程式或指令碼輸入程式名也就是 PotPlayerMini64.exe
引數為播放清單的位置如 xxx/xxx/xxx.dpl
開始位置為執行程式所在資料料夾,x:\Program Files\DAUM\PotPlayer


用cmd的概念講白話。
「cd x:\Program Files\DAUM\PotPlayer」進入到 x:\Program Files\DAUM\PotPlayer
「PotPlayerMini64.exe xxx/xxx/xxx.dpl」打開 PotPlayerMini64.exe 傳入 xxx/xxx/xxx.dpl

這樣的設定,在運行上是沒問題的,對著建立好的工作按右鍵執行,可以看到 potplayer 順利載入播放清單。

BUT!!!

如果剛開機或是重新開機,卻沒有辦法順利播放。可以看到清單載了,卻沒有播放。但這僅限於第一次,如果再重新執行工作一次,這次就能播放出來了。

基於觀察到的現象,個人做了許多不同變種的嘗試,都沒有辦法確保一定能播放出來。只有一種土法煉鋼的方法是一定可行的。

寫成批次檔(.bat),然後工作的動作就執行這個批次檔,這邊直接放上內容。

J:

cd J:\Program Files\DAUM\PotPlayer\

start ""PotPlayerMini64.exe 音樂.dpl"

timeout /t 4

taskkill /f /im PotPlayerMini64.exe

start ""PotPlayerMini64.exe 音樂.dpl"

exit

這裡逐行解釋:
第1行,進入J槽。因為我的播放器在J槽。
第3行,進入 J:\Program Files\DAUM\PotPlayer\ 。播放器所在的位置。
第5行,執行 PotPlayerMini64.exe 傳入 播放清單。這邊我把播放清單放在跟播放器同一個資料夾。用 start “”file”,能夠執行後繼續下一行,如果直接用 file,cmd就會卡在這一行指令。
第7行,等帶4秒。給第5行執行的potplayer跑好的時間。
第9行,關掉potplayer。因為開機後第一次執行不會播放,所以關掉重開。
第11行,同第5行。
第13行,離開cmd。

既然第一次不會自動播放,那就載入後關掉,再開第二次就能解決了。

雖然個人覺得這方法很醜,不過個人程度也就到這裡了。如果有更好的作法,歡迎留言告訴我。

Python pyqt5 與 matplotlib 結合示範之範例2

範例2是關於一元二次方程的線圖。

展示以下功能
1.可以添加一元二次方程至清單或從清單刪除之
2.依清單內的項目勾選狀況顯示或隱藏一元二次方程
3.在狀態列會顯示滑鼠指到線的相關資料

這次的重心,是要做出一個可調整的清單,也可分別勾選項目顯示與否。

如果線的數量未知,那就得以可擴充可變動的前提去寫。

對於不同情況,可能想看的線是不同的。有時候會覺得太多線,而想減少顯示在繪圖區的線。

這次各線都呈現在一個軸上,就沒有做區分。要控多軸,就再加點條件判斷就可以了。

因為範例1的cmd不會被錄到,所以這次把資訊顯示在狀態列上。

以下是範例2:程式碼(github) 展示(youtube)

因為有範例1,所以就沒有特別標上範例1有寫到的說明。

程式敘述:

有一個介面,有繪圖區跟控制區,控制區有清單,添加與刪除的按鈕等。

預設會添加三條方程,分別為: 「2x^2 + 1x^1 – 2」「0x^2 + 1x^1 – 2」「0x^2 + 0x^1 – 2」。

從控制區的清單,可以決定要顯示哪幾條線。

可以添加新的線,也可以刪除舊的線。

圖例可以顯示也可以隱藏,一旦線圖有變動,圖例也會變動。

在狀態列會顯示滑鼠指到的該線相關資料。

Adobe Acrobat Reader DC 造成的記憶體不足

最近買了一條記憶體,跟我電腦上的是同樣大小8GB。原因是電腦出現記憶體不足崩潰的現象。

記憶體比兩年前便宜很多,順便一看SSD也是,這題外了。

查了一下解法,既然記憶體便宜許多,那就多裝一條不就解了?我看我的記憶體使用率普遍達70%以上,一個要認真操起來,絕對爆掉。

遺憾是沒有因為裝上一條新的記憶體而解決問題。

尋找根源,我學習到一些找出真兇的方法。可是那時我並沒有找到出來,狀況也隨裝上記憶體好轉。

今天再發生一次,很幸運的是,這次只有提示「記憶體不足」,沒有像之前整台電腦崩潰,看來裝新記憶體還是有幫助的。

在工作管理員的「效能」頁面
記憶體「使用中」的部分沒有全滿。
記憶體「已認可」的部分達到上限!

這樣子,推論出只要「已認可」用罄就會造成記憶體不足的現象發生。

尋找「誰」吃掉記憶體的方法:
打開工作管理員,開啟資源監視器,留意誰是吃爆記憶體的元兇。

有一個程式,吃掉1GB的量,叫做「AcroRd32.exe」。
程式全名叫 Adobe Acrobat Reader DC,是開 .pdf 的程式,可以用來看看文件或是電子書等。

只要用它打開特定電子書,就會發現記憶體「已認可」不斷上升!只要把這程式關掉,狂吃記憶體的現象就會馬上解決。

回想有甚麼怪異之處?

當我打開電子書時,它通知,大概是這樣的意思:

「文件內有些部分需要下載字體才能正常顯示。」

那時我沒有下載,因為關掉通知後,一切看起來很正常,沒有亂碼或缺少會影響閱讀。

試了新版本與用edge打開文件,edge打開是沒有狂吃記憶體。而新版本也一樣會跳通知,吃記憶體的現象會發生。綜合判斷,需要下載字體了。

下載完字體安裝之後,再打開那本電子書,沒有跳通知,也沒有吃記憶體的問題發生。

問題已解決。

話說從需要下載字體的通知下載的安裝檔,會跑出當前的Adobe Acrobat Reader DC版本不能安裝,雖然我是從官網上下載也從程式內檢查更新過都是最新了,版本號還是不符合字體檔要求的。得找過去的字體安裝檔,就能解決。

為什麼我新購記憶體前沒有發現?因為那時一旦出現記憶體不足,整個電腦就會崩潰無法使用,只剩重新開機一途,當然也就沒有辦法開工作管理員及時觀察。也因為有裝新記憶體,才給我能及時找到兇手的機會。

Python pyqt5 與 matplotlib 結合示範之範例1

對於寫 python 的人來說,為了讓程式更方便使用,會想要寫點圖形化介面。

對於 python 來說,最基本的圖形化介面是 tkinter ,網路上也有許多相關教學。不過用程式碼去定義視窗與物件排版,是需要比較學習與辛苦的。

之前我寫過 c# ,用 visual studio 去寫。按鈕、標籤可以直接拖曳到版面上,效率是比較高的,也易懂執行出來的成果會長怎樣。

對於 python 來說,有 qt 可以直接設計界面的,用 qt designer 也生成一個版面,將按鈕、標籤等拖曳到版面中排版,基本樣貌可以直接反應出來。

想要學習 pyqt ,建議直接找書,按部就班,基本上就能學習到讓 python + pyqt 搭配 qt designer 設計。

寫這範例,並不是手把手從零教學,而是展示一些搭配的效果。適合給初學者想再進階的人。

像是將 matplotlib 的繪圖鑲入,並且可以用qt物件去操控繪圖。

如果想要寫股票走勢,用選單選取股票,就能讓繪圖區跑出折線圖或K線圖,甚至可以做即時走勢,如果有辦法弄到即時資料就絕對可以。

以下是範例1:程式碼(github) 、 展示(youtube)

敘述:

有一個介面,繪圖區有兩條sin線,第一條為 sin(x)*2,第二條為 sin(x)*-1,兩條線在不同的軸上。看起來兩條線對稱於 y=0,但實際上兩條線的y值並沒有,兩條線分處的軸上的y軸範圍是不同的。

介面上有按鈕,可以控制 qtimer,qtimer每跑過一個intetval,兩條線就會新增一個值。所以按下 start按鈕,就可以看到兩條線動起來。而按下 stop按鈕,就可以停住qtimer。

介面有一條scrollbar,可以移動圖表X軸範圍,可以看舊的資料,也可以看新的資料。
也有 +按鈕 與  -按鈕 ,可以改變範圍寬度,最短 1 最長 30。

有一個追蹤最新的qcheckbox,勾選後,會以最新資料的x為範圍上限。如果開啟qtimer時有勾選,就可以保持在圖表能顯示最新資料。

滑鼠在圖表區,可以查詢線名、所在軸名、x值與y值。

滑鼠在圖表區,會有一條水平虛線輔助線追蹤滑鼠位置,可以按下 顯示輔助線 切換。
(這代表能夠讓使用者選擇想要顯示甚麼線在圖上)

Python win32gui LoadImage Error 一個很容易解決的情況

ERROR:root:Some trouble with the icon (檔案位置): (0, 'LoadImage', 'No error message is available')

吃到這個消息,如果又想用自製 ico 檔的,將原圖片透過這個網站將圖片轉成 .ico 就可以解決了。

JPG轉ICO – 在線轉換圖標文件

本來我也認為是 ico 類的檔案,不過卻是用小畫家隨便畫了個 16*16 的圖片,然後將 副檔名 改成 .ico ,結果卻過不去,依然報錯。改回 .jpg 報錯。

上網搜尋,遇到問題不多,畢竟這是很小問題,哈哈。網路上的解法大多認為所用 .ico 是正確格式,有人用了網路大神的方法依舊無法解決問題,跟我一樣。

印象中 .ico 也是圖片檔,於是直覺用小畫家弄一個,但 .ico 需要有有固定格式,所以得透過轉換程序才能正常使用,不能用修改副檔名的方式。

Python, Matplotlib, 中文字體顯示的問題

雖然網路上有許多篇文章探討,不過最近再碰到這個問題的時候,仍舊看得有點似懂非懂的。

這裡綜合整理一下個人的看法,並且添上一些說明。

關於中文字體的顯示這是新手關卡,幾乎會碰到也幾乎需要解決。

要解決這個問題,首先可對兩個地方進行改善。

一、 系統磁碟:\\使用者\使用者名稱\.matplotlib
二、python環境\Lib\site-packages\matplotlib\mpl-data (如果使用anaconda,環境就是anaconda的資料夾。如果使用visual studio,可以從python環境打開檔案管理員進入資料夾。)

【如果你想增加字體,一定要做的步驟】

首先第一個地方,進入後會發現一個 fontList.json 的檔案,簡單來說,就是 matplotlib 讀取字體的紀錄。如果想要加入新的字體在 matplotlib 的繪圖中顯示,就得刪除  fontList.json ,讓 matplotlib 重新讀取並生成新的 json。萬一沒有刪,這個檔案就不會變更,新增的字體就不會被讀取到。(如果刪了matplotlib的字體,該json檔也會被變更。)

想顯示中文字,也不一定要新增中文字體,你可以用記事本打開來看,其中有一行「”C:\\WINDOWS\\Fonts\\kaiu.ttf”,」這表示它讀取到系統字形的 kaiu.ttf,也就是知名的「標楷體」,於是在程式中只要指定字體為標楷體,就可以正常顯示中文。

plt.rcParams['font.sans-serif'] = ['DFKai-SB'] #可以直接用上此行顯示中文字,如果不嫌棄標楷體的話

“”” 一定要使用「字體名稱」,不是字檔名kaiu。可以從新生的 json 檔搜尋字檔名(如: kaiu.ttf),會看到有一項 “name”: “DFKai-SB”,其餘字體也可以找到對應的name。”””

【新增字體】

從第二地方 mpl-data 進入 fonts 再進入 ttf 資料夾,這裡可以丟上你喜歡的字體,從網路上下載的也行。

假設我有一個喜歡的字型,範例字型,把它的 ttf 檔放進來後,去第一地方刪除 fontList.json 然後重載(import matplotlib),然後打開 json 搜尋字檔名 I.Ngaan.ttf ,就會找到該字體的 “name”: “I.Ngaan”。

plt.rcParams['font.sans-serif'] = ['I.Ngaan']

這樣就能讓 matplotlib 以這個字體顯示。

【一勞永逸,懶得在程式裡加上那一行】

第二地方有個名為 matplotlibrc 的檔案,可用記事本打開它。搜尋「font.sans-serif」,把字體名,注意!是字體名!要怎麼得知字體名?可由【新增字體】這一部分去了解。在該行的冒號後面加入字體名(範例: I.Ngaan, 或是 DFKai-SB,),然後刪除前方註解(#)。

font.sans-serif     : I.Ngaan,  DFKai-SB,  DejaVu Sans, Bitstream Vera Sans, Computer Modern Sans Serif, Lucida Grande, Verdana, Geneva, Lucid, Arial, Helvetica, Avant Garde, sans-serif

至於其他很像的「font.cursive」或是「font.fantasy」,從這裡可以了解他們的意思。

!!接著這裡也可以順便解決另外一個常見的matplotlib的問題「負號無法正常顯示」。

找到解決方法通常是加上一句:

plt.rcParams['axes.unicode_minus']=False

在這個 matplotlibrc 的檔案中,搜尋「axes.unicode_minus」可以找到一句

#axes.unicode_minus  : True

將其改成

axes.unicode_minus  : False

就可以一次解決負號顯示的問題。

會特別紀錄這篇,是因為字體名的關係,網路上有教學如何添加字體,給了範例,卻沒有說為什麼字體名是這個。於是我嘗試用[‘標楷體’]卻失敗,用字檔名[‘kaiu’]也失敗,可是,一定得有一個可以找到對照的地方,搜尋方法,最後從json檔找到答案,。
這些方法與說明是目前我在網路沒有找到的,也許日後還會碰到這問題,到時來到在這裡就能找到解答。
個人是不推薦第三種一勞永逸的解法,因為換台電腦就要再去改寫,實在麻煩。所以推薦使用 [‘DFKai-SB’] (標楷體),因為對繁體中文的系統都會自帶這個字體,這樣就不需要額外再改動任何檔案。

群益API,PYTHON,錯誤代碼2017

按下登入,得到錯誤代碼2017,查表之後得到解釋:

「SK_WARNING_REGISTER_REPLYLIB_ONREPLYMESSAGE_FIRST」

這邊是給無法順利從其他正規解決方法的人,一個解法。即便順利回報OnReplyMessage,用範例都會得到錯誤代碼2017的替代方案

使用舊版的SKCOM.dll(64位元)。下載連結

印象中這是去年我找到的方法,不過今年卻找不到出處,於是在此紀錄。因為今天剛好又再一次碰到。

以下是替換成舊版的SKCOM.dll的步驟:

一、按照群益API說明,從解壓縮後資料夾中元件裡的Uninstall.bat,解除註冊COM元件。
(依照自己的電腦位元版本選擇x86或是x64)

二、將下載來的舊版SKCOM.dll複製過去,替換同資料夾的SKCOM.dll。

三、按照正常的註冊流程,重新註冊元件:
1.用系統管理員身分執行 cmd ,進入到舊版SKCOM.dll所在的資料夾(cd 資料夾路徑),然後輸入「regsvr32.exe SKCOM.dll」。
2.用系統管理員身分執行 install.bat 。

群益API可以下載新版的,只要裡面的SKCOM.dll替換成舊版的就可以,不一定要找到舊的API才能執行。

這招大概只能撐到舊版不被支援的那一天,姑且就先將就吧。

Pyqt5 calendarWidget 標註特定日期

加入一個calendar時,除了周末之外,平日為黑色字。今天一月一日這世界普遍放假的日子,卻是黑色字標示,而不是紅色字。

除了固定日期的日子,其他像是五月第二個星期天這樣有特定序數的日子,如果能用calendar標記出來那該多好。

標記第三個週三與一月一日

首先,要先決定什麼是「特別」的日子,弄成一份表並將其讀入。
「2020/01/01, 元旦, 假日, 國定假日」
「2020/01/24, 除夕, 假日, 國定假日」如此類的格式

個人是用字典型式,以日期為鍵,其他資料包裝成List為該鍵的內容。

接著回到Pyqt上面,calendar為「QCalendarWidget」,繪製每cell時,呼叫「paintCell」。

為了分離功能與介面,所以定義了新類別「MyCalendarWidget」並繼承QCalendarWidget,之後就在新類別下編寫功能。

class MyCalendarWidget(QtWidgets.QCalendarWidget):
    def __init__(self, parent=None):
        super(MyCalendarWidget, self).__init__(parent)

在定義MyCalendarWidget類別,來修改def paintCell,當碰到特定日期時,用特別的繪製。

    def paintCell(self, painter, rect, date):
        QtWidgets.QCalendarWidget.paintCell(self, painter, rect, date)
        if date.toString(“yyyy/MM/dd”) in 特定日期字典.keys():
            painter.setPen(QtGui.QColor(255,0,255))
            painter.drawText(QtCore.QRectF(rect), QtCore.Qt.TextSingleLine|QtCore.Qt.AlignCenter, str(date.day()))

特別注意,這裡傳入的date是QDate類型。用toString(“yyyy/MM/dd”),可以得到類似”20200101”的日期字串。用此字串去搜尋特定日期字典,如果有,就看自己想要怎麼標註吧。

這邊我是上了紫色(255,0,255)。

如果想上紅底之類的,可以去尋找Qpainter,然後加入相關程式碼。

【MyCalendarWidget.py】
from PyQt5 import QtWidgets, QtGui, QtCore
class MyCalendarWidget(QtWidgets.QCalendarWidget):
    def __init__(self, parent=None):
        import calendarLoad #讀取特定日期表(dict),需要自定義
        self.calendar = calendarLoad.calendarDict
        super(MyCalendarWidget, self).__init__(parent)

    def paintCell(self, painter, rect, date):
        QtWidgets.QCalendarWidget.paintCell(self, painter, rect, date)
        if date.toString(“yyyy/MM/dd”) in self.calendar.keys():
            painter.setPen(QtGui.QColor(255,0,255))
            painter.drawText(QtCore.QRectF(rect), QtCore.Qt.TextSingleLine|QtCore.Qt.AlignCenter, str(date.day()))

【calendarLoad.py】
calendarDict = {“2020/01/01”: [“元旦”, “假日”, “國定假日”]}

如果想要第三個周三之類的,個人認為將計算後直接存成資料較好,往後直接取出來用,不用每次都經過計算才能得到資料。

PYTHON MATPLOTLIB 折線圖線條隨公式變色之心得

關於matplotlib的顏色設定實在是不上手,最初的發想是要由「公式」去決定線條各段的顏色,不過官方文件看了兩三遍也不太懂,於是無法精簡地有效率地達成所想。
搜尋一下作法,九成不太合個人胃口,終於找到一篇對味的,是以土法煉鋼的方式,即把一條長線以只有兩點相連的短線的方式依序拆開,這樣就能在畫短線時分別設定顏色。
缺點就是速度慢,但對於初學者來說,這個方法是最好理解的。不論想依照值的大小、值的相對大小、是該點的斜率或者是其他自定義的公式,只要多產出一串跟xy同大小的c(顏色),就能清楚掌握顏色使用,也能針對某段的顏色作調整。