Karinoya 學習室

證照 · Cloud / AI / Python 合格實驗室

函式、模組與輸入輸出

可以用繁體中文閱讀題目與解說。講義(解說文章)僅有日文版。

查看日文版(含講義) →

第1題 | 引數的接收

只用位置引數呼叫下列帶有星號形式參數的函式。顯示的內容是哪一項?

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)print(f(1, 3, 4, 5))
  1. (1, 3, (4, 5), None)
  2. (1, 3, (4, 5), {})
  3. (1, 2, (3, 4, 5), {})
  4. (1, 3, [4, 5], {})
正確答案B. (1, 3, (4, 5), {})

位置引數會由左至右依序對應,因此a是1、b是3。b雖然有預設值2,但由於有傳入值,優先採用傳入的值。無法對應完的4與5會被彙整成元組放進1個星號的args,變成 (4, 5)。由於沒有傳入任何關鍵字引數,kwargs會是空字典 {},不會是None。若以為b會保留預設值2,就會變成 (1, 2, (3, 4, 5), {}),但位置引數的優先順序高於預設值。放進args的是元組而不是列表,因此也不會以方括號顯示。

第2題 | 以關鍵字指定

關於下列以關鍵字傳入定義中沒有的名稱所進行的呼叫,顯示的內容是哪一項?

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)print(f(1, x=9))
  1. (1, 9, (), {})
  2. (1, 2, (), {'x': 9})
  3. (1, 2, (9,), {})
  4. (1, 2, {'x': 9}, {})
正確答案B. (1, 2, (), {'x': 9})

位置引數只有1,因此a是1,b由於沒有傳入,維持預設值2。名為x的形式參數在定義中不存在,因此關鍵字引數x=9會以 {'x': 9} 的形式放進2個星號的kwargs。由於沒有多餘的位置引數,args會是空元組 ()。若以為x=9會放進b,就會變成 (1, 9, (), {}),但關鍵字引數只會放進名稱一致的形式參數。若以為9會放進args,就會變成 (1, 2, (9,), {}),但具名傳入的值不會歸到位置引數的餘數中。若把1個星號接收字典、2個星號接收元組搞混,就會誤以為args中會有 {'x': 9}、而kwargs是空的。

第3題 | 預設的空列表

呼叫下列把預設值寫成空列表的函式3次。顯示的3行是哪一項?

def add(x, lst=[]):    lst.append(x)    return lstprint(add(1))print(add(2))print(add(3))
  1. 由上而下依序顯示 None / None / None
  2. 由上而下依序顯示 [1] / [1, 2] / [1, 2, 3]
  3. 由上而下依序顯示 [1] / [2] / [3]
  4. 由上而下依序顯示 [1] / [1, 2] / [2, 3]
正確答案B. 由上而下依序顯示 [1] / [1, 2] / [1, 2, 3]

預設值 [] 只會在執行def那一行時建立1次,該同一個列表會被保存在函式物件中。並不是每次呼叫都會建立新的空列表,因此append的結果會殘留到下一次呼叫,依序延伸成 [1]、[1, 2]、[1, 2, 3]。若以為每次呼叫都會是新列表,就會變成 [1] / [2] / [3],但那是把預設值設成lst=None、並在函式主體中重新建立的行為。若把主體誤寫成 return lst.append(x),append的回傳值None就會連續出現3次。順帶一提,若不分成3行,而寫成 print(add(1), add(2), add(3)) 這1行,由於3者都指向同一個列表,全部求值完畢後的 [1, 2, 3] 就會連續顯示3次。

第4題 | 共用的預設值

呼叫3次後顯示函式物件的屬性。顯示的內容是哪一項?

def add(x, lst=[]):    lst.append(x)    return lstadd(1)add(2)add(3)print(add.__defaults__)
  1. 輸出類似 ((1, 2, 3),) 的內容
  2. 輸出類似 ([],) 的內容
  3. 輸出類似 [[1, 2, 3]] 的內容
  4. 輸出類似 ([1, 2, 3],) 的內容
正確答案D. 輸出類似 ([1, 2, 3],) 的內容

函式物件會以名為__defaults__的屬性,把預設值保存成元組。形式參數中只有lst帶有預設值,因此會是只有1個元素的元組,並以末尾帶逗號的形式顯示。其內容就是def那一行只建立1次的那個列表本身,經過3次呼叫append之後的結果會直接殘留其中,因此會是 ([1, 2, 3],)。若完全沒有呼叫過,就會是 ([],),呼叫的影響會顯現在這裡,正是可變預設引數令人擔心之處。__defaults__本身是元組,因此不會以方括號顯示,其中的列表也不會變成元組。

第5題 | 預設值的求值時機

在預設值中寫入變數後,改寫該變數。顯示的2行是哪一項?

i = 5def h(x=i):    return xi = 10print(h())print(h(i))
  1. 先5後5
  2. 先5後10
  3. 先10後10
  4. 先10後5
正確答案B. 先5後10

預設值的算式並不是每次呼叫函式時才求值,而是在執行def那一行的時間點求值1次。當時的i是5,因此5會被保存到函式物件中。之後即使把i改成10,已保存的預設值也不會改變,因此h()回傳5。另一方面,h(i)是在呼叫的時間點讀取i再傳入,因此會回傳當時的值10。若以為預設值每次呼叫都會重新求值,2行就會都是10;若以為傳入的引數也會被預設值取代,2行就會都是5。可變預設引數的陷阱,也正是源自這條相同的規則。

第6題 | 引數的排列順序

把下列程式碼存成檔案並用python3執行。會發生什麼事?

def g(a=1, b):    return a + bprint(g(2, 3))
  1. 定義時發生SyntaxError,1行都不會執行
  2. 顯示4,並正常執行到最後結束
  3. 呼叫時發生TypeError,執行到此停止
  4. 顯示5,並正常執行到最後結束
正確答案A. 定義時發生SyntaxError,1行都不會執行

不能把不帶預設值的形式參數,放在帶有預設值的形式參數後面。這項規則在語法階段就會被檢查,因此執行檔案時會先發生SyntaxError,出現non-default argument follows default argument這樣的訊息。由於語法解析是在執行前對整個檔案進行的,因此連print那一行在內,1行都不會執行。這與呼叫方式錯誤所導致的TypeError,被偵測到的階段完全不同。g(2, 3)本身在形式參數數量上是吻合的,若定義正確應該會顯示5,但根本到不了那一步。若要正確寫法,應像 def g(b, a=1) 那樣,把帶有預設值的放到後面。

第7題 | 函式的回傳值

以下列方式呼叫return有無不同的2個函式。顯示的內容是哪一項?

def one():    passdef two():    return 1, 2print(one(), type(two()).__name__)
  1. None tuple
  2. None list
  3. () list
  4. () tuple
正確答案A. None tuple

one一次都沒經過return就結束,因此回傳值是None。並不會回傳空元組或空字串。two在return之後用逗號並列了1與2,這並不是回傳2個值,而是回傳了 (1, 2) 這1個元組。因此type(two()).__name__是tuple。若以為多個回傳值會被彙整成列表,就會變成list,但用逗號並列的算式所產生的是元組。想把顯示函式回傳值賦值使用卻碰壁,原因多半就出在這個None上。

第8題 | 解包呼叫

關於下列在列表前加上星號後呼叫的程式碼,顯示的內容是哪一項?

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)vals = [1, 2, 3]print(f(*vals))
  1. ([1, 2, 3], 2, (), {})
  2. (1, 2, (3,), None)
  3. (1, 2, [3], {})
  4. (1, 2, (3,), {})
正確答案D. (1, 2, (3,), {})

在呼叫端於列表前加上1個星號,其內容就會被逐一分配為位置引數。因此a是1、b是2,多出來的3會被彙整成元組放進1個星號的args,變成 (3,)。由於只有1個元素,因此末尾帶逗號。由於沒有傳入關鍵字引數,kwargs是空字典 {},不會是None。若忘記加星號寫成f(vals),列表本身就會被放進第1個形式參數,變成 ([1, 2, 3], 2, (), {}),即使不會出錯,結果也會改變。放進args的是元組,因此也不會以方括號顯示。

第9題 | 型別註解的處理

用與註解所寫型別不同的值呼叫下列函式。會發生什麼事?

def ann(a: int, b: str = "x") -> bool:    return aprint(ann("文字列", 999))print(ann.__annotations__['a'])
  1. 第1行顯示 文字串,第2行顯示 <class 'int'>
  2. 第1行發生TypeError,得知無法傳入與註解不同的型別
  3. 第1行顯示 文字串,第2行顯示 int
  4. 第1行發生ValueError,得知無法轉換成int
正確答案A. 第1行顯示 文字串,第2行顯示 <class 'int'>

註解只會被記錄在函式物件名為__annotations__的字典中,執行時並不會檢查型別。即使a寫著int,也能傳入字串;即使b寫著str,也能傳入整數。即使回傳值寫著bool,實際回傳的就是收到的那個字串本身。因此第1行會直接顯示 文字串。第2行的__annotations__['a']裡存放的是int這個型別物件,因此print出來會以 <class 'int'> 的形式顯示,並不會只顯示int這3個字母。若想實際檢查型別,需要使用其他檢查工具,或在函式主體中自行確認。

第10題 | 預設值的修正方式

關於把預設值改成None後重寫的下列程式碼,顯示的3行是哪一項?

def add2(x, lst=None):    if lst is None:        lst = []    lst.append(x)    return lstprint(add2(1))print(add2(2))print(add2(3, [9]))
  1. 由上而下依序顯示 [1] / [1, 2] / [9, 3]
  2. 由上而下依序顯示 [1] / [2] / [9, 3]
  3. 由上而下依序顯示 [1] / [2] / [3, 9]
  4. 由上而下依序顯示 [1] / [2] / [3]
正確答案B. 由上而下依序顯示 [1] / [2] / [9, 3]

把預設值設為None,在函式主體開頭確認是否為None,再建立新的空列表,這是常見的固定寫法。由於None是不可改變的值,即使只求值1次也不會產生共用的問題。省略lst的第1次與第2次呼叫,每次都會建立不同的新空列表,因此分別是 [1] 與 [2],不會殘留前一次呼叫的元素。第3次由於呼叫端傳入了 [9],lst is None為假,因此會在傳入的列表末尾加上3,變成 [9, 3]。若修改前仍維持lst=[],省略的第2次呼叫就會殘留前一次的元素,變成 [1, 2]。若忽略呼叫端傳入的列表、一律建立新列表,就會變成 [1] / [2] / [3],這樣就失去了這種固定寫法「若有傳入就使用它」的優點。若把加到末尾的append改成插入到開頭的insert(0, x),第3次就會變成 [3, 9]。

第11題 | 名稱的搜尋順序

Python搜尋變數名稱的LEGB順序,下列何者正確?

  1. 依執行中的函式、模組最上層、外層函式、內建的順序搜尋
  2. 依執行中的函式、外層函式、模組最上層、內建的順序搜尋
  3. 依模組最上層、內建、執行中的函式、外層函式的順序搜尋
  4. 依內建、模組最上層、外層函式、執行中的函式的順序搜尋
正確答案B. 依執行中的函式、外層函式、模組最上層、內建的順序搜尋

LEGB是Local、Enclosing、Global、Built-in的字首縮寫,表示由最內側往外側搜尋。首先查看執行中的函式內部,接著查看包住自己的外層函式內部,然後是模組最上層,最後才查看len或print等內建。一旦找到就結束搜尋,因此若在內側使用相同名稱,就會遮蔽外側的名稱。若把順序改成由外向內搜尋,就會變成函式內建立的區域變數輸給模組最上層同名變數的結果,與實際行為不符。把Global與Enclosing的順序對調也是常見的錯誤,但外層函式其實比模組最上層更靠內側。

第12題 | 區域賦值

關於下列在函式內對與外層相同名稱進行賦值的程式碼,顯示的2行是哪一項?

x = 'global'def f():    x = 'local'    print(x)f()print(x)
  1. 先local後local
  2. 先local後global
  3. 先global後global
  4. 先global後local
正確答案B. 先local後global

在函式內對某個名稱賦值,該名稱就會被建立為新的區域變數,模組最上層的同名變數完全不受影響。函式內的print(x)讀取的是區域的x,因此顯示local;函式結束後的print(x)讀取的則是模組最上層的x,因此顯示global。若以為函式內的賦值會影響到外部,2行就會都是local,但那需要在函式開頭宣告global x。反之若以為函式內的賦值會被忽略、讀到外部的值,2行就會都是global,但既然已經賦值,區域變數確實已被建立。這種「讀取能到達外部,但寫入無法到達」的不對稱,正是Python作用域的基本規則。

第13題 | nonlocal

在下列試圖從內層函式改寫外層變數的程式碼中,顯示的內容是哪一項?

def outer():    n = 1    def inner():        nonlocal n        n = 2    inner()    return ndef outer2():    n = 1    def inner():        n = 2    inner()    return nprint(outer(), outer2())
  1. 1 2
  2. 2 2
  3. 2 1
  4. 1 1
正確答案C. 2 1

outer的inner中寫著nonlocal n,因此對n的賦值會影響到外一層函式outer的n。呼叫inner後,outer回傳的n會是2。outer2的inner中沒有宣告,因此n = 2只會建立inner的新區域變數,outer2的n仍維持1不變。因此會顯示2 1。若以為兩者都會受影響,就會是2 2;若以為兩者都不受影響,就會是1 1,但造成差異的正是nonlocal那1行。順帶一提,nonlocal若外層函式中不存在同名變數就無法使用,也無法指向模組最上層的變數。若想改寫最上層的變數,要使用global。

第14題 | 未綁定錯誤

在較前面的行讀取一個在較後面的行有賦值的名稱。執行下列程式碼會發生什麼事?

z = 100def bad():    print(z)    z = 1bad()
  1. 發生UnboundLocalError,什麼都不顯示就停止
  2. 顯示100,之後正常執行到最後結束
  3. 顯示None,之後正常執行到最後結束
  4. 顯示1,之後正常執行到最後結束
正確答案A. 發生UnboundLocalError,什麼都不顯示就停止

Python在編譯函式主體的時間點,就會把該函式內有賦值的名稱全部判定為區域變數。這個判定是對整個函式進行的,與行的順序無關。這個bad函式由於後面的行有 z = 1 這樣的賦值,因此z會被當作這個函式的區域變數處理。於是在print(z)的時間點,值還沒有被存入,因此會發生UnboundLocalError。這個例外的訊息文字會因Python版本而異,但型別名稱UnboundLocalError不會變。即使最上層有 z = 100,也不會被參照到。若以為因為在賦值之前的行,就會讀到外側的值,結果會是100,但Python並不是這樣運作的。沒有存入值的區域變數也不會變成None。

第15題 | global宣告

在下列並列了有global宣告與沒有的函式的程式碼中,顯示的內容是哪一項?

n = 10def f():    global n    n = 20    return ndef g():    n = 30    return nprint(f(), g(), n)
  1. 20 30 30
  2. 20 30 20
  3. 20 30 10
  4. 10 30 10
正確答案B. 20 30 20

f中寫著global n,因此 n = 20 會改寫模組最上層的n。f本身回傳20,最上層的n也變成20。g中沒有宣告,因此 n = 30 只會建立g的區域變數,不影響最上層的n。g回傳的是自己的區域變數30。最後的print讀取的n是最上層的,由於已被f改寫,因此是20。若以為g的賦值也會影響到外部,就會是20 30 30;若以為global不生效,就會是20 30 10。global宣告是「把對這個名稱的賦值傳達到最上層」的指示,若只是讀取值則不需要宣告,這一點也要注意。

第16題 | __all__的效果

準備下列mymod.py,並在另一個檔案中寫下 from mymod import *。會被帶到呼叫端的名稱是哪一項?

# mymod.py__all__ = ['pub']pub = 'P'other = 'O'def hello():    return 'H'
  1. pub、other、hello這3者全部都會被帶入
  2. pub與hello會被帶入,other不會
  3. other與hello會被帶入,pub不會
  4. 只有pub會被帶入,other與hello都不會
正確答案D. 只有pub會被帶入,other與hello都不會

以星號進行的匯入,通常會把所有不以底線開頭的名稱全部帶入,但若模組一方有名為__all__的字串列表,就會只限定帶入其中列出的名稱。在這個例子中,由於__all__只寫了pub,因此other與hello都不會被帶入。other雖然不是以底線開頭,但既然有__all__,就會以那份清單為優先。函式也不會因此被特別對待,因此hello同樣不會被帶入。__all__只在以星號匯入時才會生效,像 from mymod import hello 這樣指定名稱匯入、或先 import mymod 再寫 mymod.hello() 的方式,都不會受到妨礙。

第17題 | __name__的值

對於下列mymod2.py,用 python3 mymod2.py 直接執行的情況,與從另一個檔案 import mymod2 的情況,__name__的值組合是哪一項?

# mymod2.pyprint('mymod2:', __name__)if __name__ == '__main__':    print('直接実行')
  1. 直接執行是mymod2,import是__main__
  2. 不論直接執行或import,都是__main__
  3. 直接執行是__main__,import是mymod2
  4. 不論直接執行或import,都是mymod2
正確答案C. 直接執行是__main__,import是mymod2

用python3直接執行該檔案時,__name__會被存入'__main__'。從其他檔案被import時,則會存入去掉副檔名的檔名'mymod2'。因此直接執行時會顯示mymod2: __main__與直接執行這2行,被import時則只顯示mymod2: mymod2這1行。if __name__ == '__main__': 就是利用這個差異、用來切分出「只想在直接執行時才動作的處理」的常見寫法。若把組合顛倒,import時反而會執行用於確認動作的處理,與這種寫法的目的不符。若不論哪種情況都是同一個值,那就根本無法做這種切分。

第18題 | sys.argv

以 python3 args.py foo bar 這樣的指令執行下列args.py時,顯示的2行是哪一項?

# args.pyimport sysprint(len(sys.argv))print(sys.argv[1:])
  1. 先2後 ['foo', 'bar']
  2. 先3後 ['args.py', 'foo']
  3. 先3後 ['foo', 'bar']
  4. 先2後 ['args.py', 'foo']
正確答案C. 先3後 ['foo', 'bar']

sys.argv是把命令列傳入之值排列而成的列表,其開頭放的是腳本名稱本身。在這個例子中會是像 ['args.py', 'foo', 'bar'] 這樣3個元素的列表,因此len是3。取出索引1以後的sys.argv[1:],會是去掉腳本名稱的 ['foo', 'bar']。若以為傳入的引數個數就直接是長度,會得到2,但這忘記把開頭的腳本名稱也算進去。若認為sys.argv[1:]會是 ['args.py', 'foo'],是把切片的起始位置搞錯的情況。若一個引數都不傳就執行,長度會是1,sys.argv[1:]會是空列表。

第19題 | 模組重新載入

在同一支程式中對相同模組import兩次時的行為,下列何者正確?

  1. 第2次也會從頭重新讀取模組主體並再次執行
  2. 第2次會建立不同的物件,狀態不再共用
  3. 第2次會使用快取,模組主體不會重新執行
  4. 第2次會發生ImportError,相同名稱只能使用一次
正確答案C. 第2次會使用快取,模組主體不會重新執行

已載入過1次的模組會被快取在名為sys.modules的字典中。第2次import只是把該處已登記的同一個模組物件重新綁定到名稱上,不會發生檔案的重新讀取,也不會重新執行主體。若在模組最上層寫上print,即使import兩次也只會顯示1次,可以藉此確認這一點。即使用別名import,指向的仍是同一個物件。因此,若改寫某個模組的變數,從其他有import該模組的地方看到的也會是改寫後的值。同一個名稱import兩次不會出錯,若開發中想重新讀取主體,需要明確使用importlib的功能。

第20題 | 虛擬環境的判斷

要判斷目前執行中的Python是否位於venv所建立的虛擬環境內,適當的方法是哪一項?

  1. 若sys.prefix沒有定義,就能判斷位於虛擬環境內
  2. 若sys.prefix與sys.base_prefix一致,就能判斷位於虛擬環境內
  3. 若sys.base_prefix有定義,就能判斷位於虛擬環境內
  4. 若sys.prefix與sys.base_prefix不一致,就能判斷位於虛擬環境內
正確答案D. 若sys.prefix與sys.base_prefix不一致,就能判斷位於虛擬環境內

sys.prefix指的是目前執行的Python所安裝的位置,sys.base_prefix指的則是其原本所依據的Python位置。在虛擬環境之外,兩者會是相同的值,但在用python3 -m venv建立的環境中,sys.prefix會指向所建立環境的目錄,而sys.base_prefix則仍指向原本的Python,因此兩者會不一致。這種不一致與否,就是判斷的線索。「一致就代表在虛擬環境內」的說法方向相反。sys.base_prefix在虛擬環境外也一直有定義,因此無法用是否有定義來判斷。sys.prefix也一直都有定義。具體的路徑字串或pip的版本會因環境而異,只要記住這種關係就夠了。

第21題 | write的回傳值

關於下列接收寫入方法回傳值的程式碼,顯示的內容是哪一項?

with open('/tmp/sample.txt', 'w') as f:    n = f.write("one\ntwo\nthree\n")print(n)
  1. 顯示None
  2. 顯示3
  3. 顯示14
  4. 顯示11
正確答案C. 顯示14

write會把寫入的字元數作為回傳值回傳。"one\ntwo\nthree\n" 中one是3個字元、two是3個字元、three是5個字元,合計11個字元,再加上3個換行符,總共是14個字元。若忘記數換行符,會得到11。若以為回傳的是寫入的行數,會得到3,但write計算的終究是字元數。以為什麼都不會回傳而選擇None也是常見的錯誤,容易與append或sort這類回傳None的方法搞混,但write回傳的是數字。順帶一提,write不會自動加上換行,若想分行,需要自行在字串中寫入。

第22題 | readlines

用readlines讀取一份寫有3行的檔案。顯示的內容是哪一項?

with open('/tmp/sample.txt', 'w') as f:    f.write("one\ntwo\nthree\n")with open('/tmp/sample.txt') as f:    print(f.readlines())
  1. 顯示 ['one\ntwo\nthree\n']
  2. 顯示 'one\ntwo\nthree\n'
  3. 顯示 ['one\n', 'two\n', 'three\n']
  4. 顯示 ['one', 'two', 'three']
正確答案C. 顯示 ['one\n', 'two\n', 'three\n']

readlines會把所有行彙整成列表回傳,但各元素行末的換行字元會原樣保留。因此會是 ['one\n', 'two\n', 'three\n']。若以為換行會被去除,會得到 ['one', 'two', 'three'],但若想要那樣的結果,需要對各元素套用rstrip。不做分行、以單一字串回傳的是read,readlines回傳的是列表。若以為不把換行當作分隔、只把整個檔案包成1個元素的列表,就會變成 ['one\ntwo\nthree\n'],但readlines會在每個換行處分割元素。用 for line in f 逐行取出時,傳來的行同樣也帶有換行符。

第23題 | 第2次的read

對同一個檔案物件連續讀取2次。顯示的內容是哪一項?

with open('/tmp/sample.txt') as f:    a = f.read()    b = f.read()print(len(a), b == '')
  1. 顯示 14 False
  2. 顯示 14 True
  3. 顯示 0 False
  4. 顯示 0 True
正確答案B. 顯示 14 True

檔案物件會記住目前的位置。第1次read會從頭讀到尾,因此a會存入含換行符在內的14個字元,位置移到末尾。第2次read會試圖從末尾開始讀取,由於已無剩餘內容,會回傳空字串。因此len(a)是14,b == ''是True。若以為第2次也會回傳相同內容,b == ''會變成False,但若想重新讀取相同內容,需要用f.seek(0)把位置移回開頭。len(a)會是0的情況,是檔案原本就是空的、或位置原本就已在末尾的情況。看到什麼都沒顯示,容易誤以為讀取失敗,但原因其實是位置在末尾。

第24題 | with與close

在with區塊內外分別確認closed。顯示的2行是哪一項?

with open('/tmp/sample.txt') as f:    print(f.closed)print(f.closed)
  1. 先False後False
  2. 先False後True
  3. 先True後False
  4. 先True後True
正確答案B. 先False後True

在with區塊內,檔案仍處於開啟狀態,因此closed是False。離開區塊時會自動關閉,因此之後的closed會是True。所以會顯示先False後True。使用with的目的,就是即使忘記寫關閉的處理,也能確保一定會執行,即使區塊內發生例外而中途跳出,也會被關閉。用逗號分隔,也能用1個with陳述式一次開啟多個檔案,這種情況下也都會同樣被關閉。試圖從已關閉的檔案物件讀取會產生ValueError,因此無法在with之外重新讀取。

第25題 | 寫入模式

把同一個檔案開啟3次並寫入後再讀出。顯示的內容是哪一項?

p = '/tmp/sample.txt'with open(p, 'w') as f:    f.write('AAA')with open(p, 'a') as f:    f.write('BBB')with open(p, 'w') as f:    f.write('C')with open(p) as f:    print(f.read())
  1. 檔案內容為 AAABBB
  2. 檔案內容為 AAABBBC
  3. 檔案內容為 C
  4. 檔案內容為 CAABBB
正確答案C. 檔案內容為 C

以'w'開啟時,不論當下既有內容存在與否,檔案都會被截斷為長度0。依序來看,第1次以'w'開啟寫入AAA,接著的'a'是追加,因此變成AAABBB。然而第3次以'w'開啟的瞬間,AAABBB就會被清除,接著寫入C,因此最終只剩下C。若以為3次都是以'a'開啟,就會變成AAABBBC;若以為最後的寫入被忽略,就會變成AAABBB。不截斷、從開頭覆寫的是'r+'的行為,那種情況下會像CAABBB那樣只替換開頭1個字元。截斷發生在開啟的瞬間,而不是寫入的瞬間,因此只是以'w'開啟卻什麼都不寫,檔案就會變空。若打算追加就用'a',打算重做就用'w',用模式來表明意圖。

第26題 | 關閉後的操作

試圖讀取close之後的檔案物件,會發生什麼事?

f = open('/tmp/sample.txt')f.close()print(f.read())
  1. 發生ValueError,得知是對已關閉檔案的操作
  2. 顯示空字串,正常執行結束
  3. 自動重新開啟,顯示檔案的全部內容
  4. 發生OSError,得知無法重新開啟檔案
正確答案A. 發生ValueError,得知是對已關閉檔案的操作

對close之後的檔案物件呼叫read會發生ValueError,並出現I/O operation on closed file.這樣的訊息。物件本身會記得自己已被關閉,因此不會因為之後有需要就自動重新開啟。這也不是「沒有東西可讀」而回傳空字串的意思,那是讀到末尾之後read的行為。找不到檔案時的FileNotFoundError等屬於OSError的同類,但操作已關閉檔案時的例外並不屬於這一類,而是ValueError。若使用with陳述式,離開區塊後想讀取這件事本身在結構上就會被避免。

第27題 | 唯讀

試圖對省略模式而開啟的檔案寫入,會發生什麼事?

with open('/tmp/sample.txt', 'w') as f:    f.write('data')with open('/tmp/sample.txt') as f:    f.write('more')
  1. 發生io.UnsupportedOperation,得知無法寫入
  2. 發生ValueError,得知模式指定不正確
  3. 在內容末尾加上more,正常執行結束
  4. 內容被替換成more,正常執行結束
正確答案A. 發生io.UnsupportedOperation,得知無法寫入

省略open的第2引數時,會變成唯讀的'r'。對以唯讀開啟的檔案物件呼叫write,會發生io.UnsupportedOperation,並附上not writable的訊息。由於這種寫法在語法上是可以通過的,因此要到執行後才會發現模式弄錯了。若想覆寫要用'w'重新開啟,想加到末尾要用'a',想同時讀寫則要用'r+'重新開啟。反過來,若對以'w'或'a'開啟的檔案呼叫read,也會發生同樣的io.UnsupportedOperation,並被告知not readable。可以說模式就是宣告這個檔案物件被允許做什麼的地方。

第28題 | seek與tell

在下列從開頭只讀取5個字元並確認位置的程式碼中,顯示的內容是哪一項?

with open('/tmp/sample.txt') as f:    head = f.read(5)    pos = f.tell()    f.seek(0)    print(repr(head), pos, f.tell())
  1. 'one\ntwo\nthree\n' 14 0
  2. 'one\nt' 5 0
  3. 'one\nt' 5 5
  4. 'one\nt' 0 0
正確答案B. 'one\nt' 5 0

對read傳入數字時,只會讀取該字元數。從開頭起的5個字元是one的3個字元加上1個換行符,再加上two的t,因此是 'one\nt'。由於位置會依讀取量前進,緊接著的tell會回傳5。接下來的seek(0)會把位置移回開頭,因此最後的tell是0。若以為read不會移動位置,tell就會連續2次都是0;若以為seek不生效,tell就會連續2次都是5。若把read的引數誤認為是行數而非字元數,就會誤以為要讀到第5行,而誤選出檔案全部內容 'one\ntwo\nthree\n' 與14。使用seek(0),就能在同一個檔案物件不變的情況下,從開頭重新讀取。

第29題 | JSON與元組

在下列把含有元組的字典轉成JSON再讀回的程式碼中,顯示的2行是哪一項?

import jsons = json.dumps({'t': (1, 2)})back = json.loads(s)print(s)print(back, type(back['t']).__name__)
  1. 先輸出 {"t": "(1, 2)"},後輸出 {'t': '(1, 2)'} str
  2. 先輸出 {"t": [1, 2]},後輸出 {'t': (1, 2)} tuple
  3. 先輸出 {"t": (1, 2)},後輸出 {'t': (1, 2)} tuple
  4. 先輸出 {"t": [1, 2]},後輸出 {'t': [1, 2]} list
正確答案D. 先輸出 {"t": [1, 2]},後輸出 {'t': [1, 2]} list

JSON只有陣列,不存在元組這種型別。因此在dumps的時間點,元組就會被轉換成陣列,輸出會是 {"t": [1, 2]}。讀回時,陣列也會被還原成列表,因此back['t']的型別是list,不會變回元組。由於原本是元組這項資訊不會留存在任何地方,來回一次之後型別就會改變。若想維持元組來處理,需要在讀回之後自行套用tuple()。JSON的輸出中不會出現圓括號,元組也不會被轉換成字串。基於同樣的道理,若把整數用作字典的鍵,在dumps時也會被轉換成字串,讀回後也不會變回整數。

第30題 | JSON的鍵

試圖把帶有整數鍵的字典與集合轉成JSON,會發生什麼事?

import jsonprint(json.dumps({1: 'a'}))print(json.dumps({1, 2}))
  1. 第1行輸出 {1: "a"},第2行輸出 [1, 2]
  2. 第1行發生TypeError,第2行不會執行
  3. 第1行輸出 {"1": "a"},第2行發生TypeError
  4. 第1行輸出 {"1": "a"},第2行輸出 [1, 2]
正確答案C. 第1行輸出 {"1": "a"},第2行發生TypeError

JSON物件的鍵必須是字串,因此對以整數作為鍵的字典執行dumps時,鍵會被轉換成字串,輸出為 {"1": "a"}。這是能正常成功的處理,此處不會產生例外。不過要注意的是,讀回時的鍵仍維持字串,不會變回整數。第2行的集合由於沒有對應的JSON型別,試圖dumps時會發生TypeError,並被告知Object of type set is not JSON serializable。若以為集合會被寫成陣列,會得到 [1, 2],但若希望這樣轉換,需要自行套用list()後再傳入。含有中文或日文等的值,若維持預設狀態會被跳脫後輸出,若把ensure_ascii傳入False,則會以原本的文字輸出。

練習:作答本頁的題目

這是隨機出題的練習工具(在啟用 JavaScript 時運作)。即使不使用此工具,也能閱讀上方的所有題目與解說。

※ 解說是供學習用的資訊。考試的出題範圍與制度每年可能變動,請務必確認主辦機構的官方公告。

本頁面譯自日文原文。若譯文與原文內容不一致,以日文版為準。 查看日文原文