作者: paul

[Effective Python] 情境:優先使用輔助類別而非使用字典或元組來管理記錄

[Effective Python] 情境:優先使用輔助類別而非使用字典或元組來管理記錄

Python的內鍵字典型別(Dictionary Type)對於維護一個物件生命期的動態內部狀態來說,非常useful。

舉例來說,,假如我們有一個銀行帳本,須要記錄客戶的存款與記錄,但我們事先並不知道他們的姓名。

那我們可能設計以下類別,透過字典來將這些名字記錄起來:

class BankBook( object ):
    def __init__(self):
        self._history = {}

    def add_customer(self, name):
        self._history[name] = []

    def save_money(self, name, amount):
        self._history[name].append(amount)

    def draw_money(self, name, amount):
        self._history[name].append(-1 * amount)

    def show_balance(self, name):
        moneys = self._history[name]
        return sum(moneys)

這個類別的使用方式很簡單:


book = BankBook()
book.add_customer('paul')
book.save_money('paul', 1200)

book.save_money('paul', -200)

print(book.show_balance('paul'))

字典因為太容易使用,以至於有過度擴充它們而寫出脆弱code的風險

例如,我們面臨了一個新的需求而要擴充這個類別,誐們希望記錄不同幣別的帳戶,我們可以修改看看,試著將_history也變成字典,用於區分不同的幣別


class CurrencyBankBook( object ):
    def __init__(self):
        self._history = {}

    def add_customer(self, name):
        self._history[name] = {} #原本串列變成字典

    def save_money(self, name, currency_name , amount):
        by_currency = self._history[name]
        currency_book_list = by_currency.setdefault(currency_name, [])
        currency_book_list.append(amount)

    def draw_money(self, name, currency_name, amount):
        self.save_money(name, currency_name, -1 * amount)

    def show_balance(self, name, currency_name):
        if not currency_name:
            raise ValueError("currency_name can't be empty")
        currency_book_list = self._history[name]
        if isinstance(currency_book_list, dict):
            if currency_name in currency_book_list.keys():
                return sum(currency_book_list[currency_name])

        raise ValueError( "currency_name not exist!!" )

這看起來變化不大,只是在於history的處理與查找上增加了一點複雜度,但仍在可以處理的範圍內,使用方式依然很簡單。

book = CurrencyBankBook()
book.add_customer('paul')
book.save_money('paul', 'ntd', 1200)
book.draw_money('paul', 'ntd', 1000)
book.save_money('paul', 'us', 300)
book.draw_money('paul', 'us', 200)

print("ntd-balance:%s" % book.show_balance('paul', 'ntd'))
print("us-balance:%s" % book.show_balance('paul', 'us'))

現在想像一下,如果我們的需求再次改變了,目前我們想要記錄每一次存款的時間點與備註,像網路銀行查詢帳戶記錄的時候,都會有每個交易時間點的餘額資訊

實作這種功能的方式之一就是變更最內層的字典,將幣別映射到的金額使用元組(tuple)來作為值。

我們改寫類別如下:

class AdvCurrencyBankBook( object ):
    def __init__(self):
        self._history = {}

    def add_customer(self, name):
        self._history[name] = {} #原本串列變成字典

    def save_money(self, name, currency_name , amount, time=None, memo=""):
        by_currency = self._history[name]
        currency_book_list = by_currency.setdefault(currency_name, [])
        currency_book_list.append((amount, datetime.now() if time == None else time, memo))

    def draw_money(self, name, currency_name, amount, time=None, memo=""):
        self.save_money(name, currency_name,-1 * amount, time, memo)

    def show_balance(self, name, currency_name, balance_time=datetime(2017, 9, 22, 10, 55, 00)):
        if not currency_name:
            raise ValueError("currency_name can't be empty")
        currency_book_list = self._history[name]
        if isinstance(currency_book_list, dict):
            if currency_name in currency_book_list.keys():
                return sum([amount for amount, datetime, memo in currency_book_list[currency_name] if datetime <= balance_time])
        raise ValueError( "currency_name not exist!!" )

book = AdvCurrencyBankBook()
book.add_customer('paul')
book.save_money('paul', 'ntd', 1200, time=datetime(2017, 9, 21, 10, 55, 00))
book.draw_money('paul', 'ntd', 1000, time=datetime(2017, 9, 22, 10, 55, 00))
book.save_money('paul', 'us', 300, time=datetime(2017, 9, 21, 10, 55, 00))
book.draw_money('paul', 'us', 200, time=datetime(2017, 9, 22, 10, 55, 00))

print("ntd-balance:%s" % book.show_balance('paul', 'ntd', balance_time=datetime(2017, 9, 21, 23, 59, 59)))
print("us-balance:%s" % book.show_balance('paul', 'us', balance_time=datetime(2017, 9, 21, 23, 59, 59)))

雖然對於save_money跟draw_money來說,只是多加一個參數,並把值變成一個元組(tuple)。但現在balance開始要用迴圈開始查找條件資料。這個類別使用方式也更加困難了。

包含位置引數所代表的意義,並不容易弄清楚,例如:book.draw_money(‘paul’, ‘us’, 200, datetime(2017, 9, 22, 10, 55, 00))

 

當我們看到這樣的複雜情況出現時,就知道該是時候放棄字典與元組了,應是改為使用型別階層架構的類別的時候了

當我們發現記錄工作變得複雜,就將它拆解成多個類別。這讓我們得以提供定義更好的介面,用較好的方式來封裝資料。

重構一下吧!

Effective Python建議我們可以從依存樹(dependency tree)的底部開始改寫為類別,也就是單一次交易,對於這種簡單的資訊來說,使用一個類別似乎太過,而一個元組(tuple)看起來好像比較適當

if isinstance(currency_book_list, dict):
    if currency_name in currency_book_list.keys():
        return sum([amount for amount, datetime, _ in currency_book_list[currency_name] if datetime <= balance_time])

問題在於一般的元組是位置型的指派方式。當我們想要更多的資訊關聯到幣別帳戶,像是我們已經定義的memo備註,就得重新再改寫那個三元組。讓程式碼知有n個項目存在
在此我們可以使用_(底線變數名稱為,來unpack並直接skip掉(撈英文?)

這種將元組擴充得來越長的模式,就類似越來越多層的字典。只要發現元組的長度超過了二元組,就應該考慮namedtuple了
collections模組內的namedtuple可以位置引數或關鍵字引數的方式來建構,讓欄位可以以具名的屬性來存取

import collections
Transaction = collections.namedtuple('Transaction', ('amount', 'time', 'memo'))

def save_money(self, name, currency_name , amount, time=None, memo=""):
		by_currency = self._history[name]
		currency_book_list = by_currency.setdefault(currency_name, [])
		currency_book_list.append(Transaction(amount, datetime.now() if time == None else time, memo))

def show_balance(self, name, currency_name, balance_time=datetime(2017, 9, 22, 10, 55, 00)):
	if not currency_name:
		raise ValueError("currency_name can't be empty")
		currency_book_list = self._history[name]
		total = 0
		if isinstance(currency_book_list, dict):
			for data in currency_book_list[currency_name]:
				if data.time <= balance_time:
					total += data.amount
					return total
		raise ValueError( "currency_name not exist!!" )

namedtuple比tuple更具體,使用上可以寫出較易維護的程式碼,但是仍有其限制

  1. 無法為namedtuple類別指定預設的引數。如果資料特性有許多選擇性的特性。那這就會使得它們變的過於笨重。
  2. namedtuple實體的屬性仍然可透過數值的索引與迭代來存取。特別是在對外提供的api中,這可會導致非預期的使用,讓你在之後更能以改寫成一個真正的類別。
from datetime import datetime

import collections

Transaction = collections.namedtuple( 'Transaction', ('amount', 'time', 'memo') )

class Account( object ):
    def __init__(self):
        self._history = []

    def save_money(self, amount, time=None, memo=""):
        self._history.append( Transaction( amount, datetime.now() if time == None else time, memo ) )

    def draw_money(self, amount, time=None, memo=""):
        self.save_money( -1 * amount, time, memo )

    def get_balance(self, balance_time=datetime.now()):
        total = 0
        for transaction in self._history:
            if transaction.time <= balance_time:
                total += transaction.amount
        return total


class Customer( object ):
    def __init__(self):
        self.__account = {}

    def currency_account(self, currency_name):
        if currency_name not in self.__account:
            self.__account[currency_name] = Account()
        return self.__account[currency_name]

    def show_balance(self):
        for name, account in self.__account.items():
            print( "%s-balance:%s" % (name, account.get_balance(datetime.now())) )


class ObjectBankBook( object ):
    def __init__(self):
        self.__customer = {}

    def customer(self, name):
        if name not in self.__customer:
            self.__customer[name] = Customer()
        if isinstance( self.__customer[name], Customer ):
            return self.__customer[name]

這些類別的行數幾乎是之前實作的double,但程式碼比較容易閱讀與維護。

book = ObjectBankBook()
paul = book.customer( 'paul' )
usAccount = paul.currency_account('us')
usAccount.save_money(1200)
usAccount.draw_money(300)

ntdAccount = paul.currency_account('ntd')
ntdAccount.save_money(400)
ntdAccount.draw_money(200)
paul.show_balance()

us-balance:900
ntd-balance:200

註:如果必要,你可以撰寫回溯相容的方法來協助將舊API的使用方式移植到物件階層架構的新方式。

 

[Effective Python] 情境:考慮使用產生器而非回傳串列

[Effective Python] 情境:考慮使用產生器而非回傳串列

考慮一種情境,在python中,我們常常會設計一些function來查找串列中match的資料。

例如以下的程式,我們希望找出int串列中,符合特定值的所在位置,若我們按照傳統的寫法,最簡單的就是走訪所有的項目,逐一比對後,再append到result的串列中(看你想放什麼,可以是index或對應的object)

def find_matched_number_location(number, numbers):
    result = []
    if numbers:
        for index, number_in_list in enumerate(numbers):
            if number_in_list == number:
                result.append(index+1)

    return result



result = find_matched_number_location(3, [1, 2, 4, 5, 2, 3, 12, 3, 5, 7, 1])
print(result)

對於某些輸入的樣本來說,這樣能如期的運作

[6, 8]

不過這種函式有兩個問題存在(Effective Python, P41建議)

第1個問題是,這種程式碼有點過於密集,且帶有雜訊。每次符合的條件滿足後,就會呼叫一次append。這樣的呼叫體積大,其中又用了某一行建立結果串列,再一行來return它

第2個問題是,在回傳之前,它對將所有的結果儲存在串列中才行。這對於超大型輸入,可能會使得我們程式耗盡記憶體而當掉。相交之下,這種函式的改成generator版本

能夠輕易地處理任意長度的輸入。

 

撰寫這種函式比較好的方式是使用generator(產生器)。產生器是使用了yield運算式的函式。被呼叫時,產生器函式實際上還不會執行,而是立即回傳一個iterator(迭代器)

。每次呼叫next()函式時,這個iterator會將產生器到它的下一個yield運算式。因此我們來改寫這個find_matched_number_location函式吧

結果如下:

def find_matched_number_location_new(number, numbers):
    if numbers:
        for index, number_in_list in enumerate(numbers):
            if number_in_list == number:
                yield index + 1

result_new = list(find_matched_number_location_new(3, [1, 2, 4, 5, 2, 3, 12, 3, 5, 7, 1]))
print(result_new)

帶來的好處就是減少了與結果變數互動的所有地方。呼叫generator所回傳的iterator可輕易地被轉成一個串列。

 

為了突顯第2個問題,我建立了一個3MB的檔案,裡面盡是數字用逗點隔開

在此我定義了一個generator,它會從檔案接受逐行的串流輸入(不過此例只有一行),一次只產出一個比對數字的輸出。這個函式工作時的最大記憶體量,僅會是單一行輸入的最大長度

def find_matched_number_location_from_file(number, handle):
    offset = 0
    for line in handle:
        if line:
            for number_in_file in line.split(','):
                offset += 1
                if int(number_in_file) == number:
                    yield offset

執行2段的測試

start = time.time()
with open('新增資料夾/test_data.txt', 'r') as f:
    it = find_matched_number_location_from_file(3, f)
    result = list(it)
    print(len(result))
end = time.time()
print( '花費 %.3f 秒' % (end - start) )

start = time.time()
with open('新增資料夾/test_data.txt', 'r') as f:
    result = []
    for line in f:
        for index , number_in_file in enumerate(line.split( ',' )):
            if int(number_in_file) == 3:
                result.append(index + 1)
    print(len(result))
end = time.time()
print('花費 %.3f 秒' % (end - start))

執行結果如下:

307824
花費 0.551
307824
花費 0.730 秒

其實對於我在測試資料僅僅只有3MB的檔案之下,其實秒數落差不大,但是效能上還是有所區別,generator的方法比舊的比對方法快了約25%。而佔的記憶體量,舊的比對方法(用result來記錄)則會多用了了至少307824個數字所站的位元。這邊數字或許不大,但試想若是大型的資料的情況呢?就交由使用場景來決定吧。

最後,再補充一點,定義像這樣的產生器時,唯一要注意的部份就是呼叫者必須知道期回傳的iterator是有狀態的,不能被重複使用。

下篇來分享防備的做法

 

Reference:Effective Python中文版一書,做法16,改為自己的理解與實作

HDFS的其他選擇-GlusterFS in Docker的安裝記錄

HDFS的其他選擇-GlusterFS in Docker的安裝記錄

有鑑於最早時,對於hadoop的hdfs建置後,雖然對於其提供hdfs的api方便性感到相當的便利,但是苦於結合docker後,始終於無法找到”跨實體主機”的cluster建置與hadoop的備份與還原機制。(若有好心人知道也麻煩指點指點),這一點對於維運議題是不可容忍的。因此現階段我必須去找一個hdfs的替代方案…必須考慮到hdfs的特性下,我們有找到幾套方案,大如ceph,小如hdfs的落地方案都有,像是orangefs, glusterfs。而本篇,針對其資源較多的glusterfs,進行試用與評估看看。

 

GlusterFS架構如下:

Read More Read More

[Docker] docker-compose定義檔範例

[Docker] docker-compose定義檔範例

當平台漸漸成形,對於外部套件、方案相依性定調以後,每次都要手動建置測試運行環境,即使docker已經把建置動作簡化到一行指令了,但是還是令人覺得瑣碎。

這個時候,docker-compose這個解決方案,大幅的簡化了我們的部署工作:傳送門

它僅需要配置服務定義檔(yml檔名),就可以跟現有的docker images整合,立即建置出所需的架構環境!

version: '2.1'

services:
  memcache:
    image: memcached
    ports:
      - "11211:11211"
    command: memcached -m 1024m

  hadoop:
    image: "sequenceiq/hadoop-docker:2.7.0"
    ports:
     - "8030:8030"
     - "8040:8040"
     - "8042:8042"
     - "8088:8042"
     - "19888:19888"
     - "49707:49707"
     - "50010:50010"
     - "50020:50020"
     - "50070:50070"
     - "5007:5007"
    command: /etc/bootstrap.sh -d

  cassandra:
    image: "cassandra"
    ports:
     - "9042:9042"
    volumes:
     - /mnt/cassandra/data:/var/lib/cassandra
 
  mariadb:
    image: mariadb
    ports:
     - "3306:3306"
    volumes:
     - /mnt/mariadb/data:/var/lib/mysql
     - /mnt/mariadb/config:/etc/mysql/conf.d
    restart: always
    environment:
       MYSQL_ROOT_PASSWORD: 525402040966518776
       MYSQL_USER: ap_user
       MYSQL_PASSWORD: jUqJ75aFbJEU
  
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    links:
      - mariadb
    environment:
      PMA_HOST: mariadb
      PMA_PORT: 3306
    ports:
     - "8088:80"

  my-python-app:
    depends_on:
     - mariadb
     - cassandra
     - hadoop
     - memcache
    image: python35-app
    volumes:
     - /mnt/python-app/src:/usr/src/app
    ports:
     - "12345:12345"
    command: /usr/src/app/run.sh

註:

1.首先是version,據我所知,目前大部分幾個月前安裝的docker,至少可以支持到2.1,但不見得能支持到3.0的版本。相關upgrade的議題,必須持續survey

2.不需要export的port就不要開,可以透過link或是建立network來進行容器互連

3.可以善用command、environment、volumn等docker支持的掛載與注入的方式來配置容器

4.可以透過現成已build好的image或是動態透過build dockerfile來定義服務。這一段上述例子沒有,有需要時再查使用方式

 

最後,啟動服務群指令(記得加-d,否則會被lock在shell,被script轟炸):

sudo docker-compose up -d

若要下架也很簡單(其他還有run、stop等指令可用)

sudo docker-compose down

 

[CODE WAR記錄] 將Linked-List分成Front,Back2半的Linked-List(難度5)

[CODE WAR記錄] 將Linked-List分成Front,Back2半的Linked-List(難度5)

最近有點偷懶,沒有研究新的東西,blog鬧水荒,但是確實對於python的語法使用上還深深感到不足,因此還是來”高手村”練練功好了..,直接把codewar的練習結果與心得當作一篇好了XD

題目示例如下:

var source = 1 -> 3 -> 7 -> 8 -> 11 -> 12 -> 14 -> null
var front = new Node()
var back = new Node()
frontBackSplit(source, front, back)
front === 1 -> 3 -> 7 -> 8 -> null
back === 11 -> 12 -> 14 -> null

請建立frontBackSplit的程式碼

Read More Read More

[Code War記錄] 給定2參數:每個數字位數的總合與數字位數長度,找出區間內所有符合連續位數(由小到大)的數字組合

[Code War記錄] 給定2參數:每個數字位數的總合與數字位數長度,找出區間內所有符合連續位數(由小到大)的數字組合

之前為了訓練python的語感,去了codewar找題目來練練功,發現了這一題滿有趣的

1.找到所有的數字組合,其每位數的數字加總必須滿足給定的條件值
2.這些數字組合,必須是由小到大連續性的排列組合(例如, 118, 127, 136, 145, 226, 235, 244, 334)

請建立一Function,給定2個參數, x為位數加總的總合,y為預期位數長度,回傳set為3個值,a、b、c,a為滿足的個數,b為滿足的數字中,最小值,c為滿足條件的數字中,最大值

find_all(x, y)

舉例來說:

find_all(10, 3) == [8, 118, 334]
 find_all(27, 3) == [1, 999, 999]

Read More Read More

以Dockerfile建置python-app的映象檔

以Dockerfile建置python-app的映象檔

繼上一篇:初試啼聲以後,我們希望能以更少的動作來佈署我們的應用程式,當然我們也可以一步一步pull下來python環境,然後連進去做一些環境準備,最後commit回來,後續可以export成tar檔或是push到registery中,達到分享與移動。其實這個時候,我們還可以應用到Dockerfile的機制來”包裝”我們的應用程式,讓剛剛所有的事情都可以自動化做掉

Read More Read More

ELK Stack安裝的眉眉角角-Part3:FileBeat

ELK Stack安裝的眉眉角角-Part3:FileBeat

上一篇尾聲記得我還提到,當時沒有使用FileBeat的原因是因為還沒架起來

結果今天重新從 getting started with filebeat的官網文件(傳送門)重新照表操課了一次

結果意外地就架起來了(所以讀官網文件是何等重要的一件事),雖然成果相當的陽春,但是也很足夠記錄下來

首先-設定檔,僅保留最簡單且最必要的部分

我如往常的在mnt下建立了filebeat的目錄,並加入了/mnt/filebeat/filebeat.yml檔案,內容如下

filebeat.prospectors:
- type: log
  enabled: true
  paths:
    - /var/log/mesocollection/*/*.log
  
  reload.enabled: true
  reload.period: 30s

#-------------------------- Elasticsearch output ------------------------------
output.elasticsearch:
  hosts: ["elasticsearch:9200"]
  template.enabled: false
  index: "meso_sys_localfile_fb"

註:這邊path因為上一篇google 到可以用*追蹤動態目錄(例如以日期、小時分類目錄)的情況,filebeat一樣可以支持,另外關於elasticsearch的index,我們一併在這邊設定好,後續可以透過es的api來檢查寫入的狀況。關於輸入elasticsearch,我們這次與logstash的file寫入少了定義filelds parsing的流程,這一段目前查詢起來,除了general像是apache、mysql等大應用的log格式(所以可以參考他們的log格式,可以省點事),否則可能必須透過filebeat轉接logstash再透過fileter parsing後,才能寫入自定義的fields到elasticsearch的index中。(待查證)

接著key入以下指令,我們就可以把filebeat的image run起來:

docker run -d --link elasticsearch-test:elasticsearch -v /mnt/filebeat/filebeat.yml:/filebeat.yml -v /mnt/python-app/mesocollection/logs:/var/log/mesocollection prima/filebeat:5

接著我們可以觸發一下記錄log的行為,例如call api,我們輸入docker logs的指令追蹤一下看看:

2017/07/31 02:57:30.841300 log.go:116: INFO File is inactive: /var/log/mesocollection/20170731/grpc_server_10.log. Closing because close_inactive of 5m0s reached.
2017/07/31 02:57:30.841296 log.go:116: INFO File is inactive: /var/log/mesocollection/20170731/RepositorySettingsHelper_10.log. Closing because close_inactive of 5m0s reached.
2017/07/31 02:57:32.836147 log.go:116: INFO File is inactive: /var/log/mesocollection/20170731/MC_Entry_10.log. Closing because close_inactive of 5m0s reached.
2017/07/31 02:57:32.836248 log.go:116: INFO File is inactive: /var/log/mesocollection/20170731/MC_MainService_10.log. Closing because close_inactive of 5m0s reached.
2017/07/31 02:57:32.836211 log.go:116: INFO File is inactive: /var/log/mesocollection/20170731/MC_Return_10.log. Closing because close_inactive of 5m0s reached.
2017/07/31 02:57:45.646417 metrics.go:39: INFO Non-zero metrics in the last 30s: filebeat.harvester.closed=5 filebeat.harvester.open_files=-5 filebeat.harvester.running=-5 publish.events=5 registrar.states.update=5 registrar.writes=1

若出現以上的logs,代表filebeat有偵測到檔案的變動。

接著我們一樣透過$ curl -XPOST http://yourhost:9200/meso_sys_filelog_fb/_search?pretty=true來看看es寫入的狀況

{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 41,
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "meso_sys_localfile_fb",
        "_type" : "log",
        "_id" : "AV2WiYNRVf3DdKZYkE65",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2017-07-31T02:46:15.762Z",
          "beat" : {
            "hostname" : "6c8d38b686db",
            "name" : "6c8d38b686db",
            "version" : "5.3.0"
          },
          "input_type" : "log",
          "message" : "2017-07-31 10:46:11,021 - MC_MainService - INFO - entry log : behavior:update_system_user_rdb, data={\"$data$\": \"{\\\"firstName\\\": \\\"I-Ping\\\", \\\"email\\\": \\\"[email protected]\\\", \\\"lastName\\\": \\\"Huang\\\"}\", \"$query$\": \"{\\\"id\\\": \\\"0e5564e8-1ac7-45db-9d0b-6f79643ca857-1501469142.056618\\\", \\\"account\\\": \\\"paul\\\"}\"}, files_count=0",
******************************************************略

若有以上的json記錄,那代表elasticsearch也可以正常的寫入了

大部分到此,kibana也不太有什麼問題了。

但問題仍是,因為log message沒有被拆解,所以我們只能把每一行當成是log message欄位來搜尋,對於報表並沒有直接幫助。

因此關於設定log message的機制,究竟只能透過logstash轉接,還是有filebeat的plugin可以用呢?這個列為本週任務來研究看看吧~

以上先記錄了filebeat最簡單的啟用與串接方式囉

 

參考:

FileBeat getting started:https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-getting-started.html

How Filebeat works:https://www.elastic.co/guide/en/beats/filebeat/current/how-filebeat-works.html