BOO大全/输入与输出

维基教科书,自由的教学读本

上一章:型别转换 目录 下一章:函式


输入与输出[编辑]

读取终端机文字[编辑]

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

上一章:型别转换 目录 下一章:函式