2018年12月10日

python 如何處理 json

json: JavaScript Object Notation 是 javascript 的子集合,是一種輕量級的資料交換格式,比 XML 簡單,也容易閱讀及編寫,處理過程分為序列化及反序列化兩個部分,分別是 Marshalling/Encoder 及 Unmarshalling/Decoder。

Marshalling/Encoder 就是將 python 的資料物件轉換為 json 字串,使用 json.dumps

Unmarshalling/Decoder 是將 json 字串轉換為 python 物件,使用 json.loads

使用 dumps, loads 的簡單範例

這是 python 的 dict 資料型別,因為 dict 沒有字串的表示方式,透過 repr 將 dict 轉換為可以列印的資料

json_obj = {
            'name' : 'Apple',
            'shares' : 100,
            'price' : 542.1
        }

logging.info( "json_obj type = "+str(type(json_obj))+ ", data =" + repr(json_obj) )

dict 可以透過 json.dumps 轉換為 json 字串

json_string = json.dumps(json_obj)

logging.info( "json_string type="+ str(type(json_string))+ ", data=" + json_string )

再透過 json.loads 將 json 字串轉換為 dict

json_object = json.loads(json_string)

        logging.info( "json_object type="+str(type(json_object))+", data="+repr(json_object) )

測試結果

json_obj type = <class 'dict'>, data ={'name': 'Apple', 'shares': 100, 'price': 542.1}

json_string type=<class 'str'>, data={"name": "Apple", "shares": 100, "price": 542.1}

json_object type=<class 'dict'>, data={'name': 'Apple', 'shares': 100, 'price': 542.1}

由 python 資料轉換為 json 的對照表為

python json
dict object
list, tuple array
str, unicode string
int, long, float number
True true
False false
None null

由 json 轉換為 python 資料的對照表為

json python
object dict
array list
string unicode
number(int) int,long
number(real) float
true True
false False
null None

pprint 及 dumps 的 indent, sort_keys 參數

如果 json 的字串比較長,列印到 console 時,可能會比較難查閱需要的資料,這時候有兩種方式可以處理

使用 pprint 可以直接用 pprint(json_obj)

from pprint imort pprint

json_obj = {
            'name' : 'Apple',
            'shares' : 100,
            'price' : 542.1
        }

logging.info( "json_obj type = "+str(type(json_obj))+ ", data =" + repr(json_obj) )

pprint(json_obj)

執行結果為

{'name': 'Apple', 'price': 542.1, 'shares': 100}

在 dumps 加上 indent 參數

json_string = json.dumps(json_obj, indent=4)

執行結果為

json_string type=<class 'str'>, data={
    "name": "Apple",
    "shares": 100,
    "price": 542.1
}

在 dumps 加上 sort_keys 可在轉換 json 時,同時進行 key 的排序

json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True)

執行結果

{"a": 0, "b": 0, "c": 0}

loads 的 object_pairs_hookobject_hook 參數

通常 json 會轉換為 python 的 dict 及 list,可使用 object_pairs_hook,將 json 轉換為 OrderedDict

s = '{"name": "ACME", "shares": 50, "price": 490.1}'
        from collections import OrderedDict
data = json.loads(s, object_pairs_hook=OrderedDict)

logging.info( "json_object type="+str(type(data))+", data="+repr(data) )

執行結果

json_object type=<class 'collections.OrderedDict'>, data=OrderedDict([('name', 'ACME'), ('shares', 50), ('price', 490.1)])

也可以用 object_hook 將 json 轉換為 python 物件

class JSONObject:
    def __init__(self, d):
        self.__dict__ = d

data2 = json.loads(s, object_hook=JSONObject)
logging.info( "json_object type="+str(type(data2))+", name="+data2.name+", shares="+str(data2.shares)+", price="+str(data2.price) )

執行結果

json_object type=<class '__main__.JSONObject'>, name=ACME, shares=50, price=490.1

skipkeys

在 dumps 如果遇到 key 不是字串時,會發生 TypeError,可使用 skipkeys 略過無法處理的 key

data =  { 'b' : 789 , 'c' : 456 ,( 1 , 2 ): 123 }
print( json.dumps(data,skipkeys = True) )

執行結果

{"b": 789, "c": 456}

自訂 python 物件的 轉換函數

如果是自訂的 python 類別,通常是無法直接透過 dumps 序列化,會發生 error

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
p = Point(2, 3)

print(p)

json.dumps(p)

執行結果

TypeError: Object of type 'Point' is not JSON serializable

解決方式:

首先定義兩個獨立的 util function,object2dict 是將物件轉換為 dict,dict2object 則是將 dict 轉換為 object,在 dict2object 裡面會使用 dict 的 __module____class__ 這兩個資料,用來正確找到 class 的定義

def object2dict(obj):
    d = {
        '__class__': obj.__class__.__name__,
        '__module__': obj.__module__
    }
    # d.update( vars(obj) )
    d.update(obj.__dict__)
    return d


def dict2object(d):
    if '__class__' in d:
        module_name = d.pop( '__module__' )
        class_name = d.pop( '__class__' )
        logging.debug("module_name="+str(module_name)+", class_name="+class_name)

        # # from A import B
        import importlib
        objmodule = importlib.import_module(module_name)
        cls = getattr(objmodule, class_name)

        # objmodule = __import__(module_name)
        # cls = getattr(objmodule, class_name)

        # # use class directly
        # cls = classes[class_name]
        # # Make instance without calling __init__
        obj = cls.__new__(cls)
        for key, value in d.items():
            setattr(obj, key, value)
        return obj
    else:
        inst = d
    return inst

如果另外有一個類別定義在 test.point.py 裡面

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__( self ):
        return 'Point Object x : %d , y : %d' % ( self.x, self.y)

透過 object2dict 及 dict2object 就可以協助進行物件與 json 的轉換

from test import point
p = point.Point(2, 3)
logging.info(p)

json_str = json.dumps(p, default=object2dict)
logging.info(json_str)

o = json.loads(json_str, object_hook=dict2object)
logging.info( "json_object type="+str(type(o))+", data="+repr(o) )

執行結果

{"__class__": "Point", "__module__": "test.point", "x": 2, "y": 3}

module_name=test.point, class_name=Point
2018-09-03 16:01:00,274 INFO {95322,MainProcess} 

json_object type=<class 'test.point.Point'>, data=Point Object x : 2 , y : 3

有時會遇到 list of Point,這時需要修改 object2dict 讓他可以處理 obj 是 list 的狀況

def object2dict(obj):
    d = {}
    if isinstance(obj, list):
        return json.dumps(obj, default=object2dict)
    else:
        # d = {
        #   '__class__': obj.__class__.__name__,
        #   '__module__': obj.__module__
        # }
        d['__class__'] = obj.__class__.__name__
        d['__module__'] = obj.__module__
        # d.update( vars(obj) )
        d.update(obj.__dict__)
        return d

References

20.2. json — JSON encoder and decoder

Python處理JSON

6.2 讀寫JSON數據

Json概述以及python對json的相關操作

2018年12月3日

一樣都是 PM,Project Manager 跟 Product Manager 有什麼不一樣

英文縮寫 PM 有兩種,一個是比較常聽到的 Project Manager,一個是 Product Manager,專業的 Product Manager 比較少見,大部分都是專案管理。另外還有人也將 Product Marketing 列入討論,不過這部分變成是 Product Manager 與 Marketing 的差異,管理跟行銷本質上就不一樣,這部分比較容易理解。

先看看不同的網路文章的意見:

一個 Project Manager 最大的任務就是將 Project 如期、如規格「正確」的執行,而這過程中,我想分為兩種層面的技能:技術與溝通。

技術層面上,PM 就是把業主/老闆心中想要的功能「翻譯」成工程師可以開發的規格、確認功能可行性、與業主/老闆確認規格、評估時間、排時程、確認優先順序、掌握開發進度、測試、驗收。溝通層面上,要去對專案所有關係人,進行不同協調,主要就是業主跟工程師之間的橋樑。

Product Manager 是以「使用者」出發、產品是否解決使用者的痛點、是否真正滿足使用者需求。你要討好的對象不是老闆、不是團隊成員、不是投資者、不是合作廠商,而是「使用者」。產品,就是一個隨時都有真實 user 在使用的東西。

所以做一個 Product Manager、或者待在一個做產品的團隊都可以深刻的感覺到,壞的情況是,當產品出問題或斷線,客服電話馬上進來、客服信箱馬上開始被罵;好的情況是,你也會收到使用者溫馨的回饋與感謝。

項目是局部優化工作。我們讓一組人為客戶創建(或改進已有的)功能,然後交給一個不知道為什麼變更、也不知道這個變更如何實現的團隊。變革的成本很高,需要花很長時間才能交到客戶手中。

圍繞產品(或服務)構建的組織有優勢,因為他們的組織結構可以監督整個工作過程,就是說,從想法到客戶,並基於客戶的反饋或反應想出新點子,形成閉環。我這樣說,是因為團隊為自己負責,對所提供的方案、交付質量、客戶體驗和滿意度有主人翁精神。進行看上去也許很小的變革,但是要允許想法快速地傳遞到客戶手中。

根據美國產品管理協會PDMA的定義,所謂的「產品管理(Product Management)」是指:在新產品開發的過程當中,通過不斷監控和調整市場組合的基本要素(其中包括產品及自身特色、溝通策略、配銷通路和價格),隨時確保產品或者服務能充分滿足客戶需求(Ensuring over time that a product or service profitably meets the needs of customers by continually monitoring and modifying the elements of the marketing mix, including: the product and its features, the communications strategy, distribution channels and price.)。而產品經理就是針對上述特定產品活動肩負所有責任的人。

Project Manager(專案經理):很重要,但通常是在食物鏈的底層 Product Marketing(產品行銷):很重要,但對於產品通常沒有實權 Product Manager(產品經理):通常在食物鏈頂層,但格局可大可小

行銷思維:盡可能讓對的受眾,精準接收到我想傳達的資訊,並做出行動。 專案管理思維:資源與風險的管理與控制,多方平衡。 產品思維:了解顧客的痛點,提出解決方法。 營運思維:流程觀與協作思維,讓各個部門順暢協作。

企業在新產品開發過程中,經常遭遇許多的瓶頸與風險,使得開發專案經常發生延遲、失敗的問題。歸咎其因,除了缺乏一套完整的新產品開發流程做好專案管理掌控之外,更缺少一位可以來整合各部門資源,掌控開發專案的品質、成本與時程,以增進產品競爭力的產品經理。

根據Wheelwright and Clark(1992)在《Creating Project Plans to Focus Product Development》的研究報告指出:企業的新產品開發專案分為以下四種類型:

1.衍生產品專案(Derivative Projects)

局部創新與改進的產品專案,包括對現有產品改良以提升功能、降低成本,或為滿足不同區隔市場客戶的需求而改變功能外型。開發過程的風險較低,專案時程較短,所需資源也比較有限。

2.突破性產品專案(Breakthrough Projects)

突破性產品專案與企業當前主流產品開發專案有很大差異,資源投入的需求量高,開發風險與不確定性很高,但短期間很難產生成果與利潤,因此開發過程中遭遇的阻力就比較大。管理的複雜度遠要高過於其他三種專案類型。

3.平台產品專案(Platform Projects)

平台產品的改變幅度遠高過於衍生產品,但卻不像突破性產品使用從未發展過的新技術與新材料。平台產品比較著重於系統上的創新,衍生產品或許只改變產品中的一項特質(如:成本、品質、產品表現等),而平台產品則針對全面的產品問題進行改善。

4.研究發展專案(R&D Projects)

研究發展專案不屬於商業化應用的範疇,但因為它是上述三種產品開發專案的先驅活動,而且也會佔用相當比例的資源。


Product 與 Project 的差異在於生命週期 以及 時程,任何 Product 產品/商品,其生命週期會比 Project 長,因為身為 Product,其概念就是企業內固定在銷售的一個商品項目,而 Product 開發本身,可以切割為多個 project。

在 美國專案管理學會 Project Management Institute PMI 的定義中,專案是指一項暫時性的任務、配置,以開創某獨特性的產品或服務。專案有起迄時間。專案團隊並不是一直存在的,而是為了某一特定目的或產品組成的。

『專案管理』是將管理知識、技術、工具、方法綜合運用到任何一個專案之上,使專案得以符合要求。 』專案經過啟動、規劃、執行、監控、結案這五大流程,而專案經理人則運用專案管理的九大知識對專案進行管理,讓專案可以如期、符合品質要求並在成本內完成。

簡單來說,Product 每一次版本的功能與規格制定,到開發最後測試完成,這樣一個週期,完成了一個版本,則本產品的本次版本的專案就已經結束。

換句話說,專案是一時的,產品則會存在很久。

因為生命週期不同,產品管理者需要承擔的責任會比較久,至於專案管理的部分,基本上只需要在專案結束時,能夠順利結案就可以了。專案管理者要承擔的責任,就是產品本身所有相關的事情,包含產品要解決的問題,解決的方法,開發的過程,版本功能的分割,除錯,到後續的問題處理,還有維護,甚至是產品的行銷,所有的面向都要承擔責任。

至於重要性,個人認為是都很重要,遇到的問題不同,也各有各的難處。更重要的是要找到正確的負責人,制度問題可以靠人去調整及解決,但是人如果錯了,就很難彌補問題。

2018年11月26日

如何使用 python logging

在 python 要列印除錯資訊,最簡單的方式是使用 print,但如果要區分 log 的等級,或是將不同的 log 資訊放到不同的檔案,這時就需要使用 logging module,但 loggin module 的設定會比較複雜一些,以下紀錄使用 logging module 的方式。

在 project 最常被呼叫及使用的 util.py 裡面,初始化 logging module,如果只需要簡單的 logging 設定,可直接在 util 裡面初始化 logging,複雜的設定可以用設定檔的方式處理。

直接在程式裡面初始化 logging 的範例

使用 TimedRotatingFileHandler,搭配 when='midnight',在每天午夜更換log 檔案,並將舊的 log 檔加上日期 postfix,backupCount 是控制保留幾個舊的 log file。formatter 是設定每一行 log 的 pattern。最後將 file handler 以及 console handler 新增到 rootLogger 裡面。

rootLogger = logging.getLogger()
# 用 rootLogger 的 handler 數量判斷是否已經有初始化 logging print( "len(logger.handlers)="+str(len(logger.handlers)) )

if len(rootLogger.handlers) == 0:
    from logging.handlers import TimedRotatingFileHandler
    logger.setLevel(logging.DEBUG)
    
    log_file_path = os.path.join('.', logs_directory+'/'+'project.log').replace('\\', '/')
    fh = TimedRotatingFileHandler(log_file_path,when='midnight',interval=1,backupCount=30)
    fh.suffix = "%Y-%m-%d"

    datefmt = '%Y-%m-%d %H:%M:%S'
    # datefmt = '%Y-%m-%d'
    # format_str = '%(asctime)s %(levelname)s %(message)s '
    format_str = '%(asctime)s %(levelname)s %(module)s.%(funcName)s %(lineno)d: %(message)s'
    # formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
    formatter = logging.Formatter(format_str, datefmt)
    fh.setFormatter(formatter)
    logger.addHandler(fh)

    # 定義 handler 輸出 sys.stderr
    console = logging.StreamHandler()
    console.setLevel(logging.DEBUG)
    console.setFormatter(formatter)

    logger.addHandler(console)

使用時只需要 import logging 預設就會取得 root logger

import logging

logging.info("test")

logging 設定檔

如果需要複雜一些的 logging,例如某個 module 的 log 放到某些特別的檔案中,區分 module 讓不同 module 的 log 不會全部混在一個 log file 裡面。可利用設定檔的方式處理。

logging.cfg 設定檔,定義了四個 logger,root logger 有兩個 handler,module logger 有兩個 handler,其中 console handler 是一樣的,不同的是對應到不同 log 檔案的 file handler,至於 log 的 pattern 樣式跟上面的範例設定一樣。

[loggers]
keys=root,consoleLog,projectFileHandler,moduleLog

[formatters]
keys=consoleFormatter,fileFormatter

[handlers]
keys=consoleHandler,projectFileHandler,moduleFileHandler

[logger_root]
level=DEBUG
handlers=projectFileHandler,consoleHandler

[logger_moduleLog]
level=DEBUG
qualname=moduleLog
handlers=moduleFileHandler,consoleHandler
propagate=0

[logger_consoleLog]
level=NOTSET
handlers=consoleHandler
qualname=consoleLog
propagate=0

[formatter_consoleFormatter]
format=%(asctime)s %(levelname)s {%(process)d,%(processName)s} {%(filename)s} [%(funcName)s] %(lineno)d - %(message)s
datefmt=

[formatter_fileFormatter]
format=%(asctime)s %(levelname)s {%(process)d,%(processName)s} {%(filename)s} {%(module)s} [%(funcName)s] %(lineno)d - %(message)s
datefmt=


[handler_consoleHandler]
class=StreamHandler
level=NOTSET
formatter=consoleFormatter
args=(sys.stdout,)


[handler_projectFileHandler]
class=handlers.TimedRotatingFileHandler
level=NOTSET
formatter=fileFormatter
args=('%(projectlogpath)s','midnight',1,30)

[handler_moduleFileHandler]
class=handlers.TimedRotatingFileHandler
level=NOTSET
formatter=fileFormatter
args=('%(modulelogpath)s','midnight',1,30)

在 util.py 初始化 logging

# logging
from logging import config
import logging

rootLogger = logging.getLogger()
# print( "len(logger.handlers)="+str(len(logger.handlers)) )

if len(rootLogger.handlers) == 0:
    logging_cfg_file_path = os.path.join('.', 'conf/logging.cfg')
    # print("logging_cfg_file_path="+logging_cfg_file_path)

    # create logs directory
    logs_directory = "logs"
    if not os.path.exists(logs_directory):
        try:
            os.makedirs(logs_directory)
        except OSError:
            if not os.path.isdir(logs_directory):
                raise

    project_log_file = 'project.log'
    module_log_file = 'module.log'

    project_log_path = os.path.join('.', logs_directory+'/'+ project_log_file).replace('\\', '/')
    module_log_path = os.path.join('.', logs_directory+'/'+ module_log_file).replace('\\', '/')

    logging.config.fileConfig(logging_cfg_file_path,
                            defaults={'projectlogpath': project_log_path,
                                    'modulelogpath': module_log_path},
                            disable_existing_loggers=False)

在使用時,如果直接用 logging,會對應使用到 root logger

import logging

logging.info("test")

但如果是透過 logging.getLogger(name="moduleLog") 可取得 moduleLog 的 logger,這時候的 log 會寫入到 module 的 log file

import logging

logger = logging.getLogger(name="moduleLog")
logger.info("test")

References

[Python] logging 教學

[Python] logging 教學