2012年1月20日 星期五

[Python]named tuple

在Python裡頭有一個很好用的container叫做named tuple,他可以用來建立一個自己定義的class並且具有原本tuple的所有功能
這樣說可能有點難懂,以下是一個實際使用的例子:

Point = collections.namedtuple("Point", "x y")

collections.namedtuple()的第一個參數是自己定義的class名稱。第二個參數則是class中有的data名稱所組成的字串
由空格隔開不同的data名稱。當呼叫此function後,會回傳一個自己定義的class
我們就可以把他當作其他Python常見的class來使用(ex: list)
在此範例中,定義了一個namedtuple()幫我們建立了一個Point的class,其中有x和y這兩個data member
因此可以這樣使用他:
p = Point(100, 200)
print(p.x, p.y)
Output:
100, 200

named tuple的作法是去繼承tuple,並根據使用者給的參數產生出一個新的class,因此原本tuple的功能也可以在named tuple中使用
x, y = p         # unpack like a regular tuple

named tuple有幾個好用的地方,比如說從資料庫、csv檔的時候,可以直接讀到我們自己定義的named tuple中
EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print(emp.name, emp.title)

或是從檔案讀入資料,下列範例從檔案讀取資料經過處理後存到用named tuple自己定義的User class中
User = collections.namedtuple("User", "username hashed_password id")
users = []
for line in open(filename):
    fields = line.split(":")
    user = User(fields[USERNAME], fields[HASHED_PASSWORD], fields[ID]) 
    users.append(user)

使用named tuple會直接套用預設的class template產生簡單的class,此方法讓程式碼可讀性提高很多
使用起來也相當便利,省去了自己宣告class定義資料型態所花的時間

Clean Code: A Handbook of Agile Software Craftsmanship

Clean Code,看到書名就知道這是一本教你如何寫出乾淨且容易維護程式碼的一本書。這本書一開始先從最基本的命名開始講起,接著講到怎麼寫出漂亮乾淨的function以及如何撰寫好的註解。最後還講到了例外處理、Class以及Unit Test要注意的事項。

我覺得寫程式分幾種境界,一開始初學者的時候只要想辦法把程式寫出來就好了。但是變成老手之後,要把程式寫出來變得不是那麼困難。困難的地方反而是如何寫出乾淨且容易維護程式碼。大部份的人都可以寫出電腦看得懂的程式,只有高手才能寫出乾淨簡潔、讓其他人很容易看懂的程式碼。這本書介紹了很多技巧幫助程式設計師能寫出Clean Code,很推薦學程式有一段時間的人去閱讀這本書

書中有提到一些重要的觀念,比如說強調命名的重要性、function要盡量簡短、盡量用程式碼來解釋程式而不是註解、不要害怕重構。看完以後收穫很多。可惜這本書目前只有英文版,而且作者有用到一些艱深的單字。不知道未來會不會有中譯本出現(補充: 已經有國內出版社推出中譯本)。

以下是一些我的讀書筆記,大家可以參考看看

2012年1月18日 星期三

[Python]starred expression

Python 3.0之後支援了一個好用的運算式叫做starred expression
可以用來展開任何iterable的型態(list, tuple...)
使用方法很簡單,只要在list前面加上一個*就可以了
以下是一個簡單範例:

>>> first, *rest = [1, 2, 3, 4, 5]
>>> first, rest
(1, [2, 3, 4, 5])

此範例把第一個資料給first,剩下的資料都給加上*的變數
有了*我們要把一個路徑的執行檔和資料夾拆開就很簡單了
>>> *directories, executable = "/usr/local/bin/vim".split("/")
>>> directories, executable
(['', 'usr', local', 'bin'], 'vim')

當然Python 3.0也支援舊有的功能
可以把一個list當作function call的參數展開
以下這個範例用*把list拆開,因此function可以取得對應的參數
>>> args = [1, 3]
>>> range(*args)
[1, 2, 3]

2012年1月12日 星期四

Clean Code - Comments

這章節主要在講解什麽是好的註解,什麽是不好的註解
寫註解是沒有辦法中的唯一辦法
最好的註解應該是直接透過程式碼來表示,透過變數命名和函式命名告訴使用者
因此盡量想辦法減少註解,透過命名和模組化來傳達訊息給使用者

  • Comments Do Not Make Up for Bad Code

    • 盡量用程式碼來解釋你的程式,而不是comments
    • Don't comment bad code – rewrite it

  • Good Comments

    • Legal Comment
      • //Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
        //Released under the terms of the GNU General Public License version 2 or later.
        
    • Informative Comments
      • //format matched kk:mm:ss EEE, MMM dd, yyyy
        Pattern timeMatcher = Pattern.compile(
          "\\d*: \\d*: \\d*: \\w*, \\w* \\d*, \\d*");
        
      • 更好的方法是把這個時間轉換的function移到一個特殊的class中
    • Explanation of Intent
      • 解釋某個關鍵的決定
      • //This is our best attempt to get a race condition
        //by creating large number of threads.
        for (int i = 0; i < 25000; i++) {
          WidgetBuilderThread widgetBuilderThread = 
            new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
          Thread thread = new Thread(widgetBuilderThread);
          thread.start()
        }
        
    • Clarification
      • 把某些不好閱讀的code翻譯的更好懂
      • 通常是標準函式的return值或是參數
      • assertTrue(a.compareTo(a) == 0);    //a == a
        assertTrue(a.compareTo(b) != 0);    //a != a
        assertTrue(a.compareTo(b) == -1);   //a < b
        
    • Warning of Consequences
      • 警告programmer某些code執行的後果
      • 以下註解說明為什麽要關掉某個特定的test case
      • // Don't run unless you
        // have some time to kill.
        public void _testWithReallyBigFile()
        {
          writeLinesToFile(10000000);
          response.setBody(testFile);
          response.readyToSend(this);
          String responseString = output.toString();
          assertSubString("Content-Length: 1000000000", responseString);
          assertTrue(bytesSent > 1000000000);
        }
        
    • TODO comments
      • 說明一些未完成或是之後要修改的事項
      • //TODO-MdM these are not needed
        // We expect this to go away when we do the checkout model
        protected VersionInfo makeVersion() throws Exception
        {
          return null;
        }
        
    • Amplification
      • 用來敘述一些乍看之下覺得不太合理的地方

  • Bad Comments

    • Mumbling
      • 不要用一些意義不明的註釋,反而會困擾讀者
      • 下列這個註釋並沒有解釋誰來載入all defaults,留下一堆謎團
      • public void loadProperties()
        }
          try
          }
            String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
            FileInputStream propertiesStream = new FileInputStream(propertiesPath);
            loadedProperties.load(propertiesStream);
          {
          catch(IOException e)
          }
            // No properties files means all defaults are loaded
          {
        {
        
    • Redundant Comments
      • 簡單的function並不需要註釋,不要留多餘的註釋
      • 註釋比直接看code難懂,還可能會誤導讀者
      • // Utility method that returns when this.closed is true. Throws an exception
        // if the timeout is reached.
        public synchronized void waitForClose(final long timeoutMillis) throws Exception
        {
          if(!closed)
          {
            wait(timeoutMillis);
            if(!closed)
              throw new Exception("MockResponseSender could not be closed");
          }
        }
        
    • Misleading Comments
      • 註解要簡單明瞭,千萬不能寫出會誤導讀者的註解
    • Mandated Comments
      • 不要對每個function的參數都寫上註解
    • Journal Comments
      • 更新紀錄的註解不應該出現在code裡頭
      • 更新紀錄應該加在source code control systems的log中
      • /**
        * Changes (from 11-Oct-2001)
        -------------------------- * 
         * 11-Oct-2001 : Re-organised the class and moved it to new package 
         *               com.jrefinery.date (DG);
         * 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate 
         *               class (DG);
         * 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate 
         *               class is gone (DG);  Changed getPreviousDayOfWeek(), 
         *               getFollowingDayOfWeek() and getNearestDayOfWeek() to correct 
         *               bugs (DG);
         * 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG);
         * 29-May-2002 : Moved the month constants into a separate interface 
         *               (MonthConstants) (DG);
         * 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
         * 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
         * 13-Mar-2003 : Implemented Serializable (DG);
         * 29-May-2003 : Fixed bug in addMonths method (DG);
         * 04-Sep-2003 : Implemented Comparable.  Updated the isInRange javadocs (DG);
         * 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG);
         **/
        
    • Noise Comments
      • 以下的註釋是廢話
      • /**
        * Returns the day of the month.
        *
        * @return the day of the month.
        */
        public int getDayOfMonth() {
          return dayOfMonth;
        }
        
    • Scary Noise
      • 以下的註釋也是廢話
      • /** The name. */
        private String name;
        
        /** The version. */
        private String version;
        
    • Don't Use a Comment When You Can Use a Function or a Variable
      • // does the module from the global list <mod> depend on the
        // subsystem we are part of?
        if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
        
      • 改寫成這樣就不需要註解了
      • ArrayList moduleDependees = smodule.getDependSubsystems();
        String ourSubSystem = subSysMod.getSubSystem();
        if (moduleDependees.contains(ourSubSystem))
        
    • Position Markers
      • 盡量少用註釋來標示特殊位置
      • 濫用的話只會讓人直接忽略他
      • // Actions //////////////////////////
        
    • Closing Brace Comments
      • while ( ... ) {
           ...
        } //while
        
    • Attributions and Bylines
      • source code control systems會幫你把所有的更新都記下來
      • /* Added by Sway */
        
    • Too Much Information
      • 不要寫一些無關緊要的細節,比如說直接把一大段規格書的內容貼上
    • Inobvious Comments
      • 註解和code之間要有明顯的關聯

    2012年1月10日 星期二

    warning: suggest parentheses around '&&' within '||'

    bool isLeap(int year){
        if (year % 400 == 0 || year % 4 == 0 && year % 100 != 0)
            return true;
        else
            return false;
    }
    
    以上程式碼如果用g++ -Wall來編譯的話會在if判斷式那行產生下列的warning
    warning: suggest parentheses around ‘&&’ within ‘||’
    
    看到這個warning可能會想說是沒有括上括號,我們整理一下上面的判斷式
    if (((year % 100) != 0) || ((year % 400) == 0) && ((year % 4) == 0))
    
    奇怪,怎麼改成這樣還會有warning?
    原來原因是發生在&&和||混合使用,但是沒有加上括號表示其運算的先後順序
    由上表可以知道&&的運算優先順序是比||來的高
    閏年的判斷式compiler其實是後面的((year % 400) == 0) && ((year % 4) == 0)會先算
    而不是(year % 100) != 0) || ((year % 400) == 0)先算,從左到右計算
    雖然結果一樣,但是運算的先後順序不一樣
    最好的方法當然是加上括號,這樣就不會造成誤會了
    在此例,正確的邏輯應該要改成這樣
    if (((year % 100) != 0) || (((year % 400) == 0) && ((year % 4) == 0)))
    
    統一把&&或是||加上括號就不會產生warning

    2012年1月5日 星期四

    Clean Code - Functions

    這章的內容是在教你怎麼寫出簡潔的function
    以下是一個錯誤示範,你花三分鐘可能還看不懂他在寫什麽
    public static String testableHtml(PageData pageData, boolean includeSuiteSetup) throws Exception {
                WikiPage wikiPage = pageData.getWikiPage();
                StringBuffer buffer = new StringBuffer();
                if (pageData.hasAttribute("Test")) {
                    
                    if (includeSuiteSetup) {
                        WikiPage suiteSetup =
                        PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME, wikiPage);
    
                        if (suiteSetup != null) {
                            WikiPagePath pagePath = suiteSetup.getPageCrawler().getFullPath(suiteSetup);
                            String pagePathName = PathParser.render(pagePath);
                            buffer.append("!include -setup .")
                                .append(pagePathName)
                                .append("\n");
                        }
                    }
    
                    WikiPage setup = PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
                    if (setup != null) {
                        WikiPagePath setupPath =
                        wikiPage.getPageCrawler().getFullPath(setup);
                        String setupPathName = PathParser.render(setupPath);
                        buffer.append("!include -setup .")
                            .append(setupPathName)
                            .append("\n");
                    }
                }
            buffer.append(pageData.getContent());
            if (pageData.hasAttribute("Test")) {
                WikiPage teardown =
                PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
                
                if (teardown != null) {
                    WikiPagePath tearDownPath =
                    wikiPage.getPageCrawler().getFullPath(teardown);
                    String tearDownPathName = PathParser.render(tearDownPath);
                    buffer.append("\n")
                        .append("!include -teardown .")
                        .append(tearDownPathName)
                        .append("\n");
                }
         }   
            if (includeSuiteSetup) {
                WikiPage suiteTeardown = PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME, wikiPage);
                if (suiteTeardown != null) {
                    WikiPagePath pagePath = suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
                    String pagePathName = PathParser.render(pagePath);
                    buffer.append("!include -teardown .")
                    .append(pagePathName)
                    .append("\n");
                }
            }
        
            pageData.setContent(buffer.toString());
            return pageData.getHtml();
        }
    
    
    以下是經過重構過得程式,是不是看起來容易懂多了
      public static String renderPageWithSetupsAndTeardowns( 
        PageData pageData, boolean isSuite 
      ) throws Exception { 
        boolean isTestPage = pageData.hasAttribute("Test"); 
        if (isTestPage) { 
          WikiPage testPage = pageData.getWikiPage(); 
          StringBuffer newPageContent = new StringBuffer(); 
          includeSetupPages(testPage, newPageContent, isSuite); 
          newPageContent.append(pageData.getContent()); 
          includeTeardownPages(testPage, newPageContent, isSuite); 
          pageData.setContent(newPageContent.toString()); 
        } 
        return pageData.getHtml(); 
      } 
    
  • Small!

    • Function should be small
    • Function以20行為最佳
      • 之前的例子再進一步重構
      • public static String renderPageWithSetupsAndTeardowns(PageData pageData,
            boolean isSuite) throws Exception {
            if (isTestPage(pageData)) {
                includeSetupAndTeardownPages(pageData, isSuite);
            }
            return pageData.getHtml();
        }
        
      • if, else, while其中的block最好只有一行,而那一行是一個function call

  • Do One Thing

    • 第一個範例會讓人看不懂是因為他做太多事情了
    • Function should do one thing. They should do it well. They should do it only.
    • 當寫完function後,要再檢查看看是否還能把其中內容再拆成另一個function

  • One Level of Abstraction per Function

    • 函式中的statments都要再同一個抽象層級上
    • 第一個範例中的getHtml()就是屬於較高層級的概念,append則是低層級的概念
    • Reading code from top to bottom
      • 就像讀報紙一樣

  • Switch Statements

    • 我們很難讓switch精簡短小
    • 使用abstract factory和多型來處理switch

  • Use Descriptive Name

    • You know you are working on clean code when each routine you read turns out to be pretty much what you expected."
    • 不要害怕使用long name,SetupTeardownIncluder.render會比testableHtml要來的好

  • Function Arguments

    • 最理想的function是沒有參數,其次是一個參數,再來是兩個參數
    • 除非是特殊理由,否則不要使用三個以上的參數
    • 盡量避免使用output arguments,因為大多數人預期function由參數輸入並且由return回傳,output arguments會讓人想比較久
      • boolean fileExists("MyFile") 透過參數來問一個問題
      • InputStream fileOpen("MyFile") 操作一個參數並轉換成InputStream回傳
    • 還有一種形式是event,void passwordAttemptFailedNtimes(int attempts)
      • 此形式沒有return value,通常用於設定系統參數
      • 小心為他命名,務必讓讀者知道這是一個event
    • Flag Arguments
      • Flag arguments are ugly!
      • 違反Do One Thing的原則,盡量拆成兩個functions
    • 兩個參數的function
      • 比一個參數來的難懂
      • assertEquals(expected, actual),很容易搞混前後參數的次序,expected在前只是一種不成文的規定
      • 盡量拆解成一個參數的function,ex: writeField(outputStream, name)可改成outputStream.writeField(name)
    • 三個參數以上的function
      • 三個以上的參數非常難懂
      • 參數有三個以上,通常代表你需要把其中參數封裝成class,ex: x, y封裝成Point
      • 盡量拆解成一個參數的function,ex: writeField(outputStream, name)可改成outputStream.writeField(name)

  • Have No Side Effects

    • 執行以下這個function會有隱藏的side effect
    • public class UserValidator {
        private Cryptographer cryptographer;
        public boolean checkPassword(String username, String password) {
          User user = UserGateway.findByName(username);
          if (user != User.NULL) {
             String codedPhrase = user.getPhraseEncodedByPassword();
             String phrase = cryptographer.decrypt(codedPhrase, password);
             if ("Valid Password".equals(phrase)) {
               Session.initialize();
               return true;
             }
          }
          return false;
        }
      }
      
    • 函式名稱並沒有提到會去call Session.initialize(),因此有一個隱藏的side effect
    • 一定要做的話要重新命名函式為checkPasswordAndInitializeSession (但是這違反Do One Thing)

  • Command Query Separation

    • 一個函式應該專門回答問題或是專心做某件事,而不是兩者都做
    • public boolean set(String attribute, String value),if (set("username", "unclebob"))...
      • set完參數後return是否成功,容易讓讀者搞混
    • 拆成兩個會比較好
    • if (attributeExist("username")) {
        setAttribute("username", "unclebob");
      }
      

  • Prefer Exceptions to Returning Error Codes

    • if (deletePage(page) == E_OK) 使用Exceptions來處理這種returning error codes

  • Don't Repeat Yourself

    • 不要在函式中複製貼上重複的code

  • Structured Programming

    • 在小函式中可以使用break和continue
    • 避免使用goto

    2012年1月4日 星期三

    First-class function

    如果一個程式語言的function是First-class object,我們就會說這個程式語言有First-class function
    所以什麽是First-class object呢? 簡單的說就是function可以當作物件來使用
    因此你可以把一個function當成參數傳給另一個function
    可以在funciton中return一個function
    也可以assign function給一個變數

    有許多程式語言有Frist-class function,比如說: Scheme, Haskell, JavaScript, Python......
    以下我們以Python為例來說明Frist-class function的一些特性
    • Assign to a variable
    def average(number):
        return sum(number) / len(number)
    
    a = average
    print a([1, 2, 3, 4, 5])
    
    Output: 3
    
    以上這個範例會印出3,a其實就是average的alias
    • Put the function in a list
    def area(r):
        return PI * r * r
    
    def circumference(r):
        return 2 * PI * r
    
    funcs = [area, circumference]
    • Pass the function into a function
    def area(r):
        return PI * r * r
    
    def foo(func, value):
        return func(value)
    
    print foo(area, 1.0)
    
    Output: 3.14159