摘要:本文介紹以下幾個 Special Method Names:

  • __init__()
  • __repr__() (與 __str__())
  • __add__()__ge__()
  • __getitem__() (與 __setitem__())
  • __iter__()yield 保留字

初學 Python 一陣子的朋友,想必都被 Python 語言的簡潔、優雅、與超快的開發速度所吸引,很開心地在寫生活或工作所需的大小程式了吧!本系列文章的目標,是透過範例介紹 Python 的特殊方法 (special method names) (就是那一大堆 __xxx__()、雙底線開頭與結尾的函式啦),讓你的 Python 功力從初階銜接到中階。知道如何使用這些特殊方法,可以讓你的程式碼更簡潔、優雅、且容易維護,也可以漸漸看懂開源專案或是高手們的程式碼在寫什麼。

會自行換算的貨幣

讓我們從一個假想的貨幣問題開始吧!假設你有許多不同國家的紙鈔,幣值與匯率的換算總是有點麻煩,能不能讓不同國家的紙鈔「自己知道要怎麼換算」呢?我們可以從一個表示貨幣的類別 (class) 開始。貨幣至少要有國名及幣值:

# currency.py
class Currency:
    def __init__(self, symbol, amount):
        self.symbol = symbol
        self.amount = amount

__init__() 是撰寫 Python 物件導向程式時第一個碰到、也最常使用的特殊方法,其功用是作為建構式 (constructor),在初始化 (initialize) class 的實例 (instance) 時被呼叫。首先,創建一個 Currency 實體並印出其資訊:

>>> from currency import Currency
>>> c1 = Currency('USD', 10)
>>> print(c1)
<currency.Currency object at 0x0000015C432AE0F0>

印出創建的 10 美元紙鈔時,顯示的是 Python 中表示物件的標準輸出,並不夠直覺,我們希望 print(c1) 時,能夠看到 USD $10 之類的資訊,該怎麼做呢?可以使用 __repr__() 方法:

class Currency:
    # ... 之前定義的 __init__() 方法
    def __repr__(self):
        return '{} ${:.2f}'.format(self.symbol, self.amount)

>>> c1 = Currency('USD', 10)
>>> c1
USD $10.00
>>> print(c1)
USD $10.00

__repr__() 方法可以覆寫「物件的標準輸出」,其回傳值必須是字串,我們可以在回傳時使用易讀的格式。或許有些朋友也看過 __str__() 方法,兩者的差異是:__str__() 會在使用 str()format()print() 時特別被呼叫,若該物件本身沒有定義 __str__(),會改呼叫 __repr__() 方法。因此若沒有特意要區別不同輸出格式,只要定義 __repr__() 即可。

接著,只要在類別定義內提供匯率資訊與換算方法,就能讓貨幣自行換算成他國貨幣:

class Currency:
    rates = {
        'USD': 1,
        'NTD': 30
    }
    # ... 之前定義的 __init__() 與 __repr__() 方法
    def convert(self, symbol):
        new_amount = (self.amount * self.rates[symbol]) / self.rates[self.symbol]
        return Currency(symbol, new_amount)       
    
>>> c1 = Currency('USD', 10)
>>> c1 = c1.convert('NTD')
>>> c1
NTD $300.00

一個 Currency 物件的 convert() 函式可以把自己轉成其他國家的貨幣。有了換算方法,不同貨幣就能加總與比較大小了,只要定義 __add__()__ge__()即可:

class Currency:
    # ... 之前定義的 dict 與方法們
    def __add__(self, other):
        new_amount = self.amount + other.convert(self.symbol).amount
        return Currency(self.symbol, new_amount)
    def __gt__(self, other):  # "gt" 代表 "greater than"
       return self.amount > other.convert(self.symbol).amount

>>> c1 = Currency('USD', 10)
>>> c2 = Currency('NTD', 300)
>>> c1 + c2
USD $20.00
>>> c2 + c1
NTD $600.00
>>> c3 = Currency('NTD', 500)
>>> c3 > c1
True
>>> c1 > c3
False

利用以上特殊方法定義物件的加法與比大小的行為,就叫做運算子重載 (operator overloading),聰明如你一定想到了:既然「加法」與「大於」可以定義,那減法、乘法、相等、小於…等運算子也可以定義囉?當然,完整的列表可以參考官方文件:emulating numeric typesrich comparison methods

聰明的錢包

有了貨幣,再來創建一個聰明的錢包吧!錢包裡面當然要有貨幣,也要有能夠把錢放入錢包的方法:

class Wallet:
    def __init__(self):
        self.currencies = []
    def put(self, money):
        self.currencies.append(money)

wallet = Wallet()
wallet.put(Currency('USD', 10))
wallet.put(Currency('USD', 100))
wallet.put(Currency('NTD', 300))

如果想知道「錢包裡有多少美元」,當然可以這樣寫:

>>> for c in wallet.currencies:
...     if c.symbol == 'USD':
...         amount += c.amount
...
>>> print(amount)
110

但是,有沒有一個簡潔的作法,可以用讓我們用 wallet['USD'] 這種方式取得錢包內所有美元 (或其他國家的貨幣)呢?有的!__getitem__() 就是用來定義 [] 運算子的行為:

class Wallet:
    # ... 之前定義的方法們
    def __getitem__(self, symbol):
        amount = 0
        for c in self.currencies:
            if c.symbol == symbol:
                amount += c.amount
        return amount

>>> wallet['USD']
110
>>> wallet['NTD']
300

__getitem__() 相對的地,__setitem__() 方法是定義「透過 [] 指定 value 給 key」的行為,如 wallet['NTD'] = 300 時會發生什麼事,不過此處 __setitem__() 並沒有適合這個錢包物件的語義,所以省略。

最後,讓我們把錢包裡的貨幣一一列出吧!最直覺的寫法是這樣:

>>> for c in wallet.currencies:
...     print(c)
...
USD $10.00
NTD $300.00
USD $100.00

但是我們也可以用 __iter__() 方法定義巡覽 (iterate) 物件時的行為:

class Wallet:
    # ... 之前定義的方法們
    def __iter__(self):
        for c in self.currencies:
            yield c

>>> for c in wallet:
...     print(c)
...
USD $10.00
NTD $300.00
USD $100.00

__iter__() 方法中用到了 yield 關鍵字,這大概是這篇文章最困難的部份。簡單來說,yieldreturn 關鍵字很類似,函式遇到 returnyield 時都會回傳一個值,但不同之處在於:函式遇到 return 時會結束,遇到 yield 時不會結束,而只會暫時把執行權還給呼叫者,下次被呼叫時,會接續之前的執行狀態,直到又遇到 yield 為止 (若沒遇到 yield 則結束函式)[註]。

本文透過範例介紹了 Python 的一些特殊方法。下次寫程式時可以試著使用它們,讓你的程式碼更優雅且容易維護。

補充資料

[註] 事實上,含有 yield 關鍵字的函式是一個 generator,它實作了 Python 的 iterator protocol,讓物件自動支援巡覽所需的 __next__() 方法。例如:

>>> w = wallet.__iter__()
>>> w.__next__()
USD $10.00
>>> w.__next__()
NTD $300.00
>>> w.__next__()
USD $100.00
>>> w.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

不了解以上程式碼的意思沒關係,不影響本文範例的使用。關於 yield 的用法與原理我們將另文說明。


SIR (Software-artifact Infrastructure Repository) is a collection of software programs, test cases, and faults for controlled experiments of software testing or program analysis research. A typical usage of SIR is to run the subject programs with accompanying test cases and (artificial) faults, and then collect the coverage and fault-revealing information of the test cases for further study. This note briefly introduces this workflow, with an example of make, a GNU tool controlling compile and build process.

Download data

While we follow the workflow of SIR, the subject programs coming with SIR are reported too old to compile and run correctly [1]. As a result, we are using the artifacts organized by Henard et al. [1]. The subject programs are basically the same as the SIR ones, but with configuration scripts that make the compile and build easier in recent OS and machines. They also come with more mutants (faults) than the original SIR programs.

To run the following example, download the subject programs, regression test suites, and mutants from here:

https://henard.net/research/regression/ICSE_2016/

Unzip the files and put them together under a folder (I use SIR-note here). We are using make_v5 in our example, so make sure you have something in these places:

subject_programs/make/v5
test_suites/make
mutants/make_v5_mutants/

To run the test cases of make, we also need the test scripts of make in the original SIR folder. Download make_1.4.tar.gz from here:

http://sir.unl.edu/php/showfiles.php (free registration required)

Untar the file, and make sure there is something under make/testplans.alt/testscripts/

Finally, to follow the SIR workflow, we have to create some folders. Let’s create a new folder called exp first, and the following subfolders:

castman@castman-vm-mint ~/SIR-note/exp $ tree -d
.
├── inputs
├── inputs.alt
├── outputs
├── outputs.alt
├── scripts
├── source
└── testplans.alt
    └── testscripts

That’s it. We are ready to go!

The workflow

In general, the workflow to collect fault-revealing information of test cases for an SIR subject program is as follows:

  1. Copy original version of the program to source.
  2. Configure and build original code in source, usually by ./configure; make, or make build. (Some environment variables and compiler options may need to be set to build the SIR C programs. Refer to here and here)
  3. Generate a test script (usually by the MTS tool) to execute test suite against original version, and save the results to outputs for future reference.
  4. Move the oracle outputs from outputs to outputs.alt. Clear source.
  5. Copy original version of the program to source again, and then replace the files in source with a mutant (one or more faulty files).
  6. Repeat step 2 to build the faulty version of the program in source.
  7. Repeat step 3 to execute test suite against the faulty version and save the results to outputs
  8. Outputs for the same test cases against the original version and the mutant are in outputs and outputs.alt, respectively. Compare them to know if the mutant got killed by a test case. If you are using the MTS tool, the comparison is automatically done by the test script.

In the original SIR workflow, original versions of programs are in versions.alt/versions.orig/vK, and mutants are in versions.alt/versions.seeded/vK. Moreover, different mutants may be triggered by modifying source/FaultSeeds.h. Here we are using subject programs and mutants from [1], so the locations of original versions and mutants are different in the following example.

A running example with make_v5

We starts from the exp directory

castman@castman-vm-mint ~/Desktop/SIR-note/exp $ pwd
/home/castman/Desktop/SIR-note/exp

Commands to run the above workflow are as follows:

# Clear data
rm -rf source/*
rm -rf outputs/*
rm -rf outputs.alt/*
rm -rf scripts/*
rm -rf inputs

# Copy data
# the original version of program
cp -r ../subject_programs/make/v5/* source/

# Test suites for make
cp -af ../test_suites/make/inputs .

# Additional test scripts for make
cp -af ../make/testplans.alt/testscripts/* testplans.alt/testscripts/

# An executable, rm-makestuff, is needed to execute the generated test script.
# Rebuild to make it working on our OS/machine
(cd testplans.alt/testscripts; gcc rm-makestuff.c -o rm-makestuff)

# Build the original version
(cd source; ./configure CFLAGS="-g"; make)

# Generate a test script to execute test suite
# Refer to http://sir.unl.edu/content/mts-usage.php
# to install and configure the MTS tool 
(cd scripts; mts .. ../source/make ../../test_suites/make/make.tests R test.sh NULL NULL)

# test.sh was generated. Adjust some paths in the file
# to accommodate our current folder structure
(cd scripts; sed -i 's/..\/source/..\/..\/source/g' test.sh; sed -i 's/> ..\/outputs/> ..\/..\/outputs/g' test.sh)
(cd scripts; sed -i 's/hello ..\/outputs\//hello ..\/..\/outputs\//g' test.sh)

# Run the test cases
(cd scripts; ./test.sh)

>>>>>>>>running test 1
>>>>>>>>running test 2
>>>>>>>>running test 3
...
>>>>>>>>running test 109
>>>>>>>>running test 110
>>>>>>>>running test 111

# Move oracle outputs to outputs.alt
mv outputs/* outputs.alt

# Clear data and build the faulty version
rm -rf source/*
rm -rf outputs/*
rm -rf scripts/*
rm -rf inputs

# Original version and test suite
cp -r ../subject_programs/make/v5/* source/
cp -af ../test_suites/make/inputs .

# Mutant #18985
cp ../mutants/make_v5_mutants/18985/* source/

# Build the faulty version
(cd source; ./configure CFLAGS="-g"; make)

# Generate a test script to execute test suite
# Note that here the parameters for the MTS tools are different
(cd scripts; mts .. ../source/make ../../test_suites/make/make.tests D test.sh NULL ../outputs.alt)

# test.sh was generated. Adjust some paths in the file
# to accommodate our current folder structure
(cd scripts; sed -i 's/..\/source/..\/..\/source/g' test.sh; sed -i 's/> ..\/outputs\//> ..\/..\/outputs\//g' test.sh)
(cd scripts; sed -i 's/hello ..\/outputs\//hello ..\/..\/outputs\//g' test.sh)

# Run the test cases
(cd scripts; ./test.sh)

>>>>>>>>running test 1
>>>>>>>>running test 2
>>>>>>>>running test 3
...
>>>>>>>>running test 31
different results
>>>>>>>>running test 32
different results
>>>>>>>>running test 33
different results
different results
...

In the second run, test.sh runs each test case and compares the results under outputs and outputs.alt. If the results are different, different results is printed, indicating that the mutant was killed by the test case. Let’s check the results of test 33. From test.sh we can see one of the output files by test 33 is t92.out:

castman@castman-vm-mint ~/Desktop/SIR-note/exp $ diff outputs/t92.out outputs.alt/t92.out 
11a12,15
> gcc -c  -DSTDC_HEADERS=1 -DHAVE_STRING_H=1 -DHAVE_FCNTL_H=1 -DHAVE_SYS_FILE_H=1 -DHAVE_ALLOCA_H=1 -g version.c
> gcc -c  -DSTDC_HEADERS=1 -DHAVE_STRING_H=1 -DHAVE_FCNTL_H=1 -DHAVE_SYS_FILE_H=1 -DHAVE_ALLOCA_H=1 -g getopt.c
> gcc -c  -DSTDC_HEADERS=1 -DHAVE_STRING_H=1 -DHAVE_FCNTL_H=1 -DHAVE_SYS_FILE_H=1 -DHAVE_ALLOCA_H=1 -g getopt1.c
> gcc -g -o hello hello.o version.o getopt.o getopt1.o  

The results of the same test case (t33) are different, so t33 killed the mutant. However, if we check the results (t88.out) of test 31:

@castman-vm-mint ~/Desktop/SIR-note/exp $ diff outputs/t88.out outputs.alt/t88.out 
13c13
< # Make data base, printed on Thu Apr 26 23:46:08 2018
---
> # Make data base, printed on Thu Apr 26 23:45:53 2018
1377c1377
< # Finished Make data base on Thu Apr 26 23:46:08 2018
---
> # Finished Make data base on Thu Apr 26 23:45:53 2018

The only difference is the timestamps. Should we say test 31 kills the mutant? I don’t think so. From this example, you can see that we may have to further check the results reported by the MTS tool.

Collect coverage information

We can also collect coverage information of test cases by following the first three steps of the workflow. Here we are using GNU Gcov to collect coverage information.

# Clear data
rm -rf source/*
rm -rf outputs/*
rm -rf outputs.alt/*
rm -rf scripts/*
rm -rf inputs

# Copy data
cp -r ../subject_programs/make/v5/* source/
cp -af ../test_suites/make/inputs .
cp -af ../make/testplans.alt/testscripts/* testplans.alt/testscripts/
(cd testplans.alt/testscripts; gcc rm-makestuff.c -o rm-makestuff)

# Build. Note that here we add some parameters to use Gcov
(cd source; ./configure CFLAGS="-g -fprofile-arcs -ftest-coverage"; make)

# Generate a test script to execute test suite
(cd scripts; mts .. ../source/make ../../test_suites/make/make.tests R test.sh NULL NULL)
(cd scripts; sed -i 's/..\/source/..\/..\/source/g' test.sh; sed -i 's/> ..\/outputs/> ..\/..\/outputs/g' test.sh)
(cd scripts; sed -i 's/hello ..\/outputs\//hello ..\/..\/outputs\//g' test.sh)

# Move test.sh to source/ for Gcov, and run
(mv scripts/test.sh source/)
(cd source; ./test.sh)

# Generate coverage files
(cd source; gcov -b *.c)

You can see the coverage information is generated as *.gcov. This is the coverage information for the whole test suite. To get the information for each test case, a possible way is to modify the generated test.sh.

[1] Christopher Henard, Mike Papadakis, Mark Harman, Yue Jia, and Yves Le Traon. Comparing white-box and black-box test prioritization. ICSE’16.


相信許多人初學 Python 時,常會在範例程式內看到類似的段落:

if __name__ == '__main__':
    main()  # 或是任何你想執行的函式

對於 __name__ == '__main__' 到底是什麼意思,一直存有疑問;在我的爬蟲課程的問題討論區中,也不時看到同樣的問題。如果你上網搜尋之後還是似懂非懂,這篇文章會嘗試用初學者的角度再說明一次。

首先,如果你永遠都只執行一個 Python 檔,而不引用別的 Python 檔案的話,那你完全不需要了解這是什麼。例如你寫了一個很棒的函式 cool.py

# cool.py

def cool_func():
    print('cool_func(): Super Cool!')

print('Call it locally')
cool_func()

然後你永遠都是直接執行它,像這樣:

>> python cool.py

Call it locally
cool_func(): Super Cool!

這完全沒有問題。問題會出在當你想要在別的檔案中使用你在 cool.py 中定義的函式時,例如你在相同目錄下有一個 other.py

# other.py

from cool import cool_func

print('Call it remotely')
cool_func()

當你執行 other.py 時,你應該預期只會看到 Call it remotelycool_func(): Super Cool! 兩段輸出,但實際上你看到的是:

>> python other.py

Call it locally
cool_func(): Super Cool!
Call it remotely
cool_func(): Super Cool!

看到問題了嗎?cool.py 中的主程式在被引用的時候也被執行了,原因在於:

  1. 當 Python 檔案(模組、module)被引用的時候,檔案內的每一行都會被 Python 直譯器讀取並執行(所以 cool.py內的程式碼會被執行)
  2. Python 直譯器執行程式碼時,有一些內建、隱含的變數,__name__就是其中之一,其意義是「模組名稱」。如果該檔案是被引用,其值會是模組名稱;但若該檔案是(透過命令列)直接執行,其值會是 __main__;。

所以如果我們在 cool.py 內加上一行顯示以上訊息:

# cool.py

def cool_func():
    print('cool_func(): Super Cool!')

print('__name__:', __name__)
print('Call it locally')
cool_func()

再分別執行 cool.pyother.py,結果會是:

>> python cool.py

__name__: __main__
Call it locally
cool_func(): Super Cool!

>> python other.py

__name__: cool
Call it locally
cool_func(): Super Cool!
Call it remotely
cool_func(): Super Cool!

你就可以看到 __name__ 的值在檔案被直接執行時與被引用時是不同的。所以回到上面的問題:要怎麼讓檔案在被引用時,不該執行的程式碼不被執行?當然就是靠 __name__ == '__main__'做判斷!

# cool.py

def cool_func():
    print('cool_func(): Super Cool!')

if __name__ == '__main__':
    print('Call it locally')
    cool_func()

執行結果是:

>> python cool.py

Call it locally
cool_func(): Super Cool!

>> python other.py

Call it remotely
cool_func(): Super Cool!

問題就完美解決了!最後一點要說明的是,之所以常看見這樣的寫法,是因為該程式可能有「單獨執行」(例如執行一些本身的單元測試)與「被引用」兩種情況,因此用上述判斷就可以將這兩種情況區分出來。希望這篇文章有解答到你的疑問。

參考資料: https://docs.python.org/3/library/__main__.html https://stackoverflow.com/questions/419163/what-does-if-name-main-do


[補充:本文的後續以及我畢業後的情況請看: 為什麼要到 UC Irvine 念軟體工程 – 後記]

有聽過 HTTP 跟 RESTful API 嗎?你知道 HTTP 協定的主要設計者及 REST 架構的發明人是哪裡畢業的博士嗎?UC Irvine

有聽過 DNS 嗎?你知道 DNS 的共同發明人是哪裡畢業的博士嗎?UC Irvine

你知道 Kobe Bryant 曾經被目擊到在那間學校的體育館自主練習?你知道全美唯一校內附設網咖的大學是哪一家? UC Irvine

你知道 Blizzard, Broadcom, WD 這些公司的總部,以及 BenQ 跟 Toshiba 的北美總部在哪裡嗎?Irvine (好啦 BenQ 其實在隔壁的 Costa Mesa)

你知道哪個城市連續 11 年蟬聯全美最安全城市?哪個城市總是在全美最宜居城市最適合養小孩城市最佳戶外活動城市榜上有名?當然還是 Irvine

所以來 UCI 還需要什麼理由嗎 :)

好啦,正題開始,這篇文章是給打算出國唸書的人,為什麼要到 UCI 念 CS/CE/HCI,尤其是作軟體工程研究的理由。當然如果你已經錄取了大家都叫得出名字的傳統名校,我不認為這篇文章能夠說服你 (或是當初的我自己) 來 UCI,但如果你的落點在這附近,或你申請時沒有考慮過 UCI,這篇文章可以提供一些資訊。由於我是 Informatics Dept. 下的 Software Engineering program 博士生, 我會對自己的 program 著墨最多,也會提及其他 CS 相關領域的情況。

硬指標:研究發表

博士生的主要工作就是做研究及發 paper, 在 CS 領域, 發 top conference paper 又比 journal paper 重要 (或至少一樣重要)。當然許多大師說過 Paper is cheap, 我們應該專注在改變世界、影響人類生活的 idea. 我完全贊同,但是我們不是大師,身為學術圈浮游生物的我們,還是只能功利點多發幾篇 paper, 才能畢業跟找到工作,因此選擇一個 productive 的環境就很重要。如果你已經確定有興趣的研究領域,我推薦要選校的人直接看 csrankings.org 的排名跟教授名單。大學排名百百種,csrankings.org 的特色就是它是依照完全的硬指標:在頂級會議的發表篇數,比如說 AI 各領域的頂級會議: ICML, CVPR, AAAI 等,發表多的學校就排名前面,完全跟其他因素如學校名聲,師生比,校園規模無關。在這邊你會看到 UCI 的 overall ranking in the US 是 23,應該比其他機構排出來的名次還前面一些;更令人驚訝的是,在軟體工程領域,UCI 是全美第 2!代表這邊的教授非常積極在軟工的頂級會議 (ICSE/FSE) 發 paper,相對地對你畢業也會較有幫助,這是一般的綜合排名看不到的資訊;另外,UCI Informatics Dept. 除了一小群軟體工程的教授之外,我感覺整個系都在做人機互動/人機介面的研究,所以你會看到 UCI 在 HCI 領域的發表也高居全美第 4。除了 SE 與 HCI,我們 ICS school 在其他 CS 相關領域例如 Machine learning 或 Bioinformatics 的研究也都是有名氣的。不管你對哪個 CS 相關領域有興趣,選校時都可以參考這個網站。

Software Engineering program/faculty at UCI

UCI 的軟體工程 program,不管是在台灣或美國都相對少見,能夠把軟體工程獨立成一個 program, 代表這裡不管是相關課程的廣度,或 faculty 的數量及研究深度都超過其他學校。舉例來說,軟體工程在台灣大多就是一堂大學部的選修課,當然有少數學校像是中央軟工所可能會有比較多課程,但 UCI SE program 的 curriculum 可是洋洋灑灑將近 30 堂課(當然有許多是與 CS 合開),很多課都是我在台灣的資工系沒看過的。再者,這邊軟工研究群的五個教授, 就有兩個是 ICSE 2018 Technical paper 的 program committee,加上前一段提過的頂級會議發表量,都證明了這邊 faculty 的研究影響力,如果你需要發 paper,來這邊抱大腿是個好選擇,常言道:站在浪頭上,豬也能飛上天。

找 intern 及就業情況

出國唸書的人以碩士生為大宗,因為我是博士生,並沒有第一個暑假就要急著找 intern 的壓力,加上我又是系邊 (i.e., 系上邊緣人),對於碩士生找 intern 及就業情況了解得不多,但還是能夠提供一些我周遭朋友的個案:

SE program 的博士生方面,我們 lab 今年要畢業的博士生去過兩次 Google intern, 已拿到 return offer; 明年要畢業的博士生去過 Intel intern;J 教授今年畢業的學生去過 MS intern, 畢業後要回 MS, 另一個博士生這個暑假去 Google intern; R 教授的博士生今年去了 Uber intern; L 教授的學生今年去了一間 startup 做 intern,他太太也是 UCI CS 的碩士畢業生,在學時有做過 intern,現在在 startup 工作;其他 CS 博士生方面,一個做 embedded system 的朋友連續兩年暑假在 startup intern,也拿到了 return offer,另一個做 deep learning 的朋友也在 MS, 百度做過 intern. 碩士生方面,一個韓國朋友今年在 Amazon intern,一個台灣朋友在 Sandisk intern,以上的個案至少有一半以上是拿學生簽證,沒有身份的。當然,我已經說過我是系邊,所以找到 intern/工作的人數絕對比我知道的多得多,但是我也認為找工作是個人造化,就算我提供再多個案,跟你/我能不能找到工作也不會有太多關聯,因為找 CS 相關工作的 SOP 都已經很制式化了(刷題/修履歷/找內推/海投),也跟你是否有身份,業界景氣,還有運氣有很大的關係,但至少 UCI 的學位是有能見度的,我們的 career fair 規模頗大,Google, Facebook 也都有來辦 info session.

結語

軟體工程不是個熱門的研究領域,如果你也對軟體工程研究有興趣,希望這篇文章能提供你多一個選擇;如果你是要出國念 CS/CE/HCI,UCI 不管是在生活機能、研究環境或就業機會,都不會讓你失望。


170827

之前的文章已經提過,我對於這邊的資格考筆試非常苦惱,因為它不是考你數學或演算法等專業科目,反倒像作文比賽,考你建立答題框架與論述的能力。簡單來說,我們有一份 reading list,裡面有約 50 篇軟體工程各領域的經典文獻,你必須先讀完(可以透過修兩個 quarter 的討論課來讀及與同學教授討論),然後在考試時回答一些大哉問的問題,表現出你有架構問題及引用文獻的能力。考試分成早上下午各 3 小時,早上回答兩題必答題,下午回答兩題 8 選 2 的選答。考試結果有 PhD Pass, Master Pass 及 Fail 三種 (沒錯,UCI 的 Software Engineering program 碩士畢業也要考這個筆試,或是選擇寫論文)。今年我回答的題目如下:

  1. 敏捷式開發流程已經征服了全世界,為什麼?既然我們崇尚「快速產出雛型再根據反饋修改」的流程,那幾十年來在軟體開發流程,分析,架構,測試等子領域上的軟體工程研究,究竟有什麼意義?有哪些研究為實務打下了基礎?又有哪些研究是「沒用」的?
  2. 不同的軟體工程研究方法 (質化與量化的各種方法,如問卷、數據分析、理論推導、訪談、個案研究等),各有什麼優缺點?舉出幾個例子說明哪些研究子領域特別適合哪些研究方法 (例如 Usability 的研究特別適合用訪談或問卷,Formal verification 特別適合用理論推導等);舉出一個在文獻上很少搭配在一起的研究領域及研究方法,但你覺得應該要搭配的理由。
  3. 有許多的軟體架構框架 (e.g., Spring, Django, Android) 被提出用來增加工程師的生產力,但同時也引入了新的問題,例如框架的 bug, 安全漏洞,及誤用等,你覺得這些問題的原因何在?有哪些研究領域能夠幫助 framework 的設計者與使用者緩解這些問題?
  4. 研究者對於什麼是好的研究方法 (或說什麼才叫做知識) 有不同的立場。有些人認為:根據可觀察及量測的事實現象推演而來的,才叫做知識,推演過程及結果不因人而異;有些人認為:知識形成的過程不能抽離應用該知識的人與環境,什麼叫做「有用」是見仁見智的。討論這兩種看法的哲學立場,並舉出一個可以同時應用這兩種立場的研究領域。

每個問題有一個半小時,如果是你,會怎麼回答這些問題?光是第一題就要你質疑自己作研究的意義,其他題目則是研究方法與哲學立場的探討。坦白說,對於 Native speaker 且有許多實務經驗的人來說,可能不太需要熟讀文獻就能行雲流水的寫出想法,但我只能戰戰兢兢的準備。

讀這些東西有用嗎?這剛好呼應了考試的題目:什麼叫做「有用」?怎麼定義有沒有用? 一方面來說,我之前有講過這個考試對自己的研究領域不一定有幫助,而且早期的文獻 (8, 90 年代) 多半還在爭論「軟體工程」作為一門 engineering discipline 的正當性,當時的爭論在今天答案已經很明顯。但另一方面,這樣的考試對於非英文母語的我來說,是練習讀寫及思考的大好機會,這也是研究的基礎能力;再者有些文獻是我的研究領域一輩子也不會讀到的,讀這些 paper 目前看來沒用,但總是增加了知識廣度,哪一天可能會派上用場。因此我還是很高興有機會準備這個考試。