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会汇总到单星号的 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} 的形式存入双星号的 kwargs。位置参数没有多余的,因此 args 是空元组 ()。如果认为 x=9 会进入 b,就会得到 (1, 9, (), {}),但关键字参数只会进入名字一致的形参。如果认为9会进入 args,就会得到 (1, 2, (9,), {}),但按名字传入的值不会分流到位置参数的余量中。如果把单星号接收字典、双星号接收元组这一点搞反,就会误以为 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 那一行时被创建一次,同一个列表会被保存在函数对象中。并不是每次调用都会新建一个空列表,因此 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, 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 那一行只创建过一次的那个列表本身,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 那一行被执行时求值一次。此时 i 是5,因此5会被保存到函数对象中。之后即使把 i 改写为10,已保存的默认值也不会改变,因此 h() 返回5。而 h(i) 会在调用时读取当时的 i 再传入,因此返回的是当时的值10。如果认为默认值每次调用都会重新求值,两行都会得到10;如果认为传入的参数值也会被替换成默认值,两行都会得到5。可变默认参数的陷阱,正是从这同一条规则产生的。

第6题 | 参数的排列顺序

把以下代码保存到文件并用 python3 执行。会发生什么?

def g(a=1, b):    return a + bprint(g(2, 3))
  1. 定义时发生 SyntaxError,一行也不执行
  2. 显示4,然后正常执行到最后
  3. 调用时发生 TypeError,执行到此停止
  4. 显示5,然后正常执行到最后
正确答案A. 定义时发生 SyntaxError,一行也不执行

带默认值的形参之后,不能再放不带默认值的形参。这一规则在语法阶段就会被检查,因此执行文件时首先会发生 SyntaxError,并给出 non-default argument follows default argument 这样的提示。语法解析是在执行前针对整个文件进行的,因此包括 print 那一行在内,一行也不会被执行。这与因调用方式错误而产生的 TypeError,检测阶段完全不同。g(2, 3) 本身与形参个数是吻合的,如果定义正确,本应显示5,但根本到不了那一步。要正确书写,应把带默认值的形参放到后面,如 def g(b, a=1)。

第7题 | 函数的返回值

以下这样调用有无 return 不同的两个函数。显示的内容是哪一项?

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,这不是返回两个值,而是返回了一个元组 (1, 2)。因此 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,), {})

在调用一方给列表前加上单星号,其内容会作为位置参数逐个分发。因此 a 得到1,b 得到2,多出的3会汇总到单星号的 args 中作为元组,成为 (3,)。因为只有1个元素,末尾带逗号。没有传入关键字参数,因此 kwargs 是空字典 {},不会是 None。如果忘了加星号写成 f(vals),列表本身会整体进入第一个形参,变成 ([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是不可变的值,即使只求值一次也不会产生共享的问题。省略 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。如果认为函数内的赋值会影响到外部,两行都会得到local,但那需要在函数开头声明global x。反过来,如果认为函数内的赋值被忽略、读取的是外部的值,两行都会得到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这一行。此外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三者全部被带入
  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. 第二次会创建不同的对象,状态不再共享
  3. 第二次会使用缓存,模块主体不会被重新执行
  4. 第二次会发生 ImportError,同一个名字只能使用一次
正确答案C. 第二次会使用缓存,模块主体不会被重新执行

已经加载过的模块会被缓存到名为sys.modules的字典中。第二次import只是把该名字重新绑定到那里登记的同一个模块对象,不会重新读取文件,也不会重新执行主体。如果在模块最上层写一句print,就可以确认即使import两次也只会显示一次。即使起了别名再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返回的是列表。如果认为不把换行当作分隔、只是把整个文件包成一个元素的列表,就会得到 ['one\ntwo\nthree\n'],但readlines会在每个换行处分割元素。用 for line in f 逐行取出时,传来的行同样带有换行符。

第23题 | 第二次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

文件对象会记住当前位置。第一次read会从头读到末尾,因此a中存入包含换行符在内的14个字符,位置移动到末尾。第二次read会试图从末尾读取,由于没有剩余内容,返回空字符串。因此len(a)是14,b == ''为True。如果认为第二次也会返回相同内容,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的目的,就是即使忘记写关闭处理,也一定会被执行,即便代码块中发生异常而中途退出,文件也会被关闭。用逗号分隔可以在一个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。依次来看:第一次'w'写入AAA,接下来的'a'是追加,因此变为AAABBB。但第三次以'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都会是0;如果认为seek不起作用,两次tell都会是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传为假,则会按原样的字符输出。

练习:做本页的题目

这是随机出题的练习工具(在启用 JavaScript 时运行)。即使不使用此工具,也可以阅读上方的全部题目和解说。

※ 解说是供学习用的信息。考试的出题范围和制度每年可能变化,请务必确认主办机构的官方公告。

本页面译自日文原文。如译文与原文内容不一致,以日文版为准。 查看日文原文