跳转到内容

BOO大全/輸入與輸出

维基教科书,自由的教学读本
(重定向自BOO/InputAndOutput

上一章:型別轉換 目錄 下一章:函式


輸入與輸出

[编辑]

讀取終端機文字

[编辑]

Boo 已經內建了用來處理輸入的函數,名字是prompt。它會提示使用者並等待輸入,只適用於終端機應用程式:

>>> prompt('enter some text:')
enter some text:here we go again
'here we go again'

也可以改使用 .NET 內建的 Console:

>>> import System
>>> Console.In.ReadLine()
here we go again
'here we go again'

我們也可以讀取輸入,作處理之後,再進行輸出。下面就是一個用來將輸入的字串轉換為大寫的代碼:

# uppercase.boo
text = System.Console.In.ReadToEnd()
print text.ToUpper()

在編譯之後,就可以像其他指令一樣使用。使用方法有兩種,第一種方法,你可以將輸入檔案指定為程式的標準輸入,然後將程式的標準輸出導向到一個輸出檔案。第二種方法,可以使用 pipe ( 就是 | ),將 sort 的輸出導向到 uppercase,再輸出到 myfile-upper-sorted.txt。

uppercase < myfile.txt > myfile-upper.txt
sort myfile.txt | uppercase > myfile-upper-sorted.txt

當然也可以一次讀取一行,下面程式就是在讀取每行之後,再加上行號並輸出:

import System
k = 0
while (line = Console.In.ReadLine()) != null:
	print ++k,line

我覺得這程式有點笨拙,感覺上就是直接以 Boo 改寫 C# 程式。所以下面改用 for 迴圈取代複雜的 while 迴圈,來迭代文字流裡的每一行,這是一個很棒的特性,不是嗎?(注意,Boo 的變數指派是一個運算式,而非述句,會有值傳出。譯註,C/C++/C# 也很常看到同樣的用法。)

import System
k = 0
for line in Console.In:
	print ++k,line

這是只有兩行的版本:

for k,line in enumerate(System.Console.In):
	print k,line

內建的 enumerate 函式就是為了這種情況產生的,它被設計來迭代集合並保有索引值。

最後再介紹一個把文字檔裡的每行放到陣列裡的方法,只有一行:

lines = array(string,line for line in System.Console.In)

尋找檔案

[编辑]

.NET 在處理檔案與目錄上,提供了豐富的函式。這些函式都在 System.IO 命名空間裡。舉例來說,Directory.GetFiles 回傳一個包含某目錄下所有檔案的陣列。這裡, "." 通常用來表示當前的目錄,這兒並使用 GetFullPath 取得完整路徑。

>>> import System.IO
>>> files = Directory.GetFiles(".","*.boo")
>>> files
('.\\ast.boo', '.\\AttemptMacro.boo', '.\\PlotMacro.boo', 
'.\\SampleMacro.boo', '.\\TestAttemptMacro.boo', '.\\TestPlotMacro.boo',
'.\\TestSampleMacro.boo')
>>> Path.GetFullPath(files[0])
'C:\\net\\sciboo\\examples\\macros\\ast.boo'

讀取檔案

[编辑]

常常我們會想要存取任意的檔案﹔讀取的方法都一樣。與前面用過的 System.Console.In 的不同點,在於輸入文字流必須要先打開,使用完則必須關閉。

>>> import System.IO
>>> fin = File.OpenText("tmp.txt")
System.IO.StreamReader
>>> fin.ReadLine()
'Here are '
>>> fin.ReadLine()
'a few lines'
>>> fin.Close()

這邊把前面加上行號的例子再改寫,讓它可以依據程式參數來讀取指定檔案,並且加上一些基本的必要檢查:

import System.IO
if len(argv) == 0:
	print "usage: <text file>"
	return	
if not File.Exists(argv[0]):
	print "file does not exist: ",argv[0]
	return
fin = File.OpenText(argv[0])
k = 0
for line in fin:
	print ++k,line
fin.Close()

(我知道在一個程式裡使用 return 來離開顯得很奇怪,但別忘了,其實這裡面有個隱性的 main 函式。)

我建議你改用 using 述句。不只是因為它看起來比較簡潔 (把讀取的動作放到程式區塊裡,看來很清楚),而是因為不管發生什麼事,它保證你的檔案一定會關閉。所以最後五行可以再改寫為:

using fin = File.OpenText(argv[0]):
	k = 0
	for line in fin:
		print ++k,line

讀取數值

[编辑]

當我開始學 C# 時,我很驚訝,C# 居然沒有一個簡單的方法用以讀取數值。(也許對一個新語言來說,這太過時了,顯然,Java 也有此問題。) 你必須要分開輸入,並將字串轉為數值。這裡有個程式,它從檔案裡讀取所有數值,並計算其平均值:

import System
import System.IO

sum = 0.0
k = 0
for line in Console.In:
	words = line.Split(char(' '),char('\t'))
	for w in words:
		if len(w) > 0:
			try:
				x = double.Parse(w)
				sum += x
				++k
			except:
				print w,"was not a number"
				return
print "numbers " + k + " average was " + sum/k

這不是個漂亮的程式,字串的處理與累加被綁在一起了。

這裡使用 Generator 方法(使用 yield),將字串處理與累加的邏輯分開,

import System
import System.IO

def Numbers():
	for line in Console.In:
		words = line.Split(char(' '),char('\t'))
		for w in words:
			if len(w) > 0:
				yield double.Parse(w)


sum = 0.0
k = 0
try:
	for x in Numbers():
		sum += x
		++k
except e as Exception:
	print e.Message
	return
print "numbers " + k + " average was " + sum/k

如果我們在 double.Parse 失敗時,假設結果為 0 的話,可以讓程式更簡單些。前面我們在 Parse 發生錯誤時提出例外,雖然這看來很愚蠢,不過這卻是某些 script 語言,如 AWK,所作的事情。這邊我們再加上 SafeParse 來專注於解析數值的任務。(當然我們可以假設檔案的格式都是正確的,不做處理,但難保不會遇到不正常的狀況,你說是嗎?)

def SafeParse(w as string):
	try:
		return double.Parse(w)
	except:
		return 0.0

def Numbers():
	for line in Console.In:
		words = line.Split(char(' '),char('\t'))
		for w in words:
			if len(w) > 0:
				yield SafeParse(w)

主要的迴圈現在看來更簡單了。

sum = 0.0
k = 0
for x in Numbers():
	sum += x
	++k
print "numbers " + k + " average was " + sum/k

下一節會提到將字串與數值分開的方法,請參考讀取字串

(如果想再把主要迴圈作成函式的話,可以參考其他的作法)。

讀取字串

[编辑]

TextReader 類別並不具有讀取有分隔符號文字的能力,它只能讀取行。你可以逐字元讀取,然後自行收集。Read 回傳一個整數,可以轉為字元。在遇到檔案結尾時,則傳回 -1(-1 不是個合法的字元)。所以我們可以一次讀取一個字元,並將其附加到 StringBuilder 物件裡,直到發現非空白字元或是遇到 -1 時,再把結果傳回。使用StringBuilder來組字串的原因是因為它比較有效率。

import System.Text
import System.IO

def ReadString(tr as TextReader):
        ch = tr.Read()
        while ch != - 1 and char.IsWhiteSpace(cast(char,ch)):
                ch = tr.Read()
        # 結束了,傳回 null
        if ch == -1:
                return null
        # 遇到非空白字元!
        sb = StringBuilder()
        while ch != -1 and not char.IsWhiteSpace(cast(char,ch)):
                sb.Append(cast(char,ch))
                ch = tr.Read()
        return sb.ToString()

s = """
1 2.3 hello
2 5.6 dolly
"""

sr = StringReader(s)
s = ReadString(sr)
while s:
        print s
        s = ReadString(sr)
print "that's all, folks!"

如果要處理整數或浮點數的話,可以這樣用:

def ReadInt(tr as TextReader):
	return int.Parse(ReadString(tr))
	
def ReadDouble(tr as TextReader):
	return double.Parse(ReadString(tr))

其他的作法

[编辑]

這兒有個方法可以用來計算序列的加總,並回傳元素總數與加總值。這兒宣告了 duck 型別的序列,然後迭代這個序列並加總,所以這個函式可以處理任何序列,序列裡的所有元素都將被視作 double 型別再加總。

def sum(seq as duck):
	y = 0.0
	k = 0
	for x as duck in seq:
		y += x
		++k
	return k,y
import Tokens
	
print "array sum is ",sum((2.3,4.2,5.5))[1]
print "integers ",sum((10,20,30,40,50))[1]
y = sum(Tokenizer("numbers.txt").Numbers())
print "num ",y[0],"sum is ",y[1]

我們可以做的更好。這個版本可以支援所有型別,只要型別支援 + 運算子:

def sum(seq as duck):
	y as duck
	k = 0
	for x as duck in seq:
		if k == 0:
			y = x
		else:
			y += x
		++k
	return k,y

上一章:型別轉換 目錄 下一章:函式