위치 인수는 왼쪽부터 순서대로 대응되므로 a에 1, b에 3이 들어간다. b에는 기본값 2가 있지만 값이 전달되었으므로 그쪽이 우선된다. 대응되지 못한 4와 5는 별표 하나짜리 args에 튜플로 모여 (4, 5) 가 된다. 키워드 인수는 하나도 넘기지 않았으므로 kwargs는 빈 딕셔너리 {} 가 되며 None이 되지 않는다. b에 기본값 2가 남는다고 생각하면 (1, 2, (3, 4, 5), {}) 가 되지만, 위치 인수는 기본값보다 우선된다. args에 들어가는 것은 리스트가 아니라 튜플이므로 대괄호로 표시되지도 않는다.
위치 인수는 1뿐이므로 a에 1이 들어가고, b는 넘기지 않았으므로 기본값 2 그대로이다. x라는 이름의 가인수는 정의에 없으므로 키워드 인수 x=9 는 별표 두 개짜리 kwargs에 {'x': 9} 로 들어간다. 위치 인수의 나머지는 없으므로 args는 빈 튜플 () 이다. x=9 가 b에 들어간다고 생각하면 (1, 9, (), {}) 가 되지만 키워드 인수는 이름이 일치하는 가인수에만 들어간다. 9가 args에 들어간다고 생각하면 (1, 2, (9,), {}) 가 되지만 이름을 붙여 넘긴 값이 위치 인수의 나머지로 돌아가는 일은 없다. 별표 하나가 딕셔너리를, 별표 두 개가 튜플을 받는다고 혼동하면 args에 {'x': 9} 가 들어가고 kwargs가 비는 형태로 보여 버린다.
기본값 [] 는 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번 나열되어 표시된다.
함수 객체는 기본값을 __defaults__ 라는 속성에 튜플로 가지고 있다. 가인수 중 기본값을 가진 것은 lst뿐이므로 요소가 하나인 튜플이 되어 끝에 쉼표가 붙은 형태로 표시된다. 그 내용은 def 줄에서 한 번만 만들어진 리스트 그 자체로, 3번의 호출로 append된 결과가 그대로 남아 있으므로 ([1, 2, 3],) 이 된다. 한 번도 호출하지 않은 상태라면 ([],) 이지만, 호출의 영향이 여기에 나타나는 것이 가변 기본 인수의 무서움이다. __defaults__ 자체는 튜플이므로 대괄호로 표시되지 않으며 안의 리스트가 튜플로 바뀌지도 않는다.
문제 5 | 기본값의 평가
기본값에 변수를 쓴 뒤 그 변수를 고쳐 썼다. 표시되는 2줄은 어느 것인가?
i = 5def h(x=i): return xi = 10print(h())print(h(i))
5 다음에 5
5 다음에 10
10 다음에 10
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))
정의 시점에 SyntaxError 가 되어 한 줄도 실행되지 않는다
4 가 표시되고 그대로 끝까지 정상적으로 실행을 마친다
호출 시점에 TypeError 가 되어 거기서 실행이 멈춘다
5 가 표시되고 그대로 끝까지 정상적으로 실행을 마친다
정답A. 정의 시점에 SyntaxError 가 되어 한 줄도 실행되지 않는다
기본값을 가진 가인수 뒤에 기본값이 없는 가인수를 둘 수는 없다. 이 규칙은 구문 단계에서 검사되므로 파일을 실행하면 먼저 SyntaxError가 되고 non-default argument follows default argument 라는 메시지가 나온다. 구문 분석은 파일 전체에 대해 실행 전에 이루어지므로 print 줄을 포함해 한 줄도 실행되지 않는다. 호출 방식의 오류로 일어나는 TypeError와는 검출되는 단계가 전혀 다르다. g(2, 3) 자체는 가인수 개수와 맞으므로 정의가 올바르다면 5가 표시되어야 하지만 거기까지 도달하지 못한다. 올바르게 쓰려면 def g(b, a=1) 처럼 기본값을 가진 것을 뒤로 돌린다.
문제 7 | 함수의 반환값
return의 유무가 다른 두 함수를 다음과 같이 호출했다. 표시되는 내용은 어느 것인가?
one은 return을 한 번도 거치지 않고 끝나므로 반환값은 None이 된다. 빈 튜플이나 빈 문자열이 반환되는 것은 아니다. two는 return 뒤에 쉼표로 구분해 1과 2를 나열했는데, 이것은 두 개의 값을 반환하는 것이 아니라 (1, 2) 라는 튜플 하나를 반환하는 것이다. 따라서 type(two()).__name__ 은 tuple이 된다. 여러 반환값이 리스트로 모인다고 생각하면 list가 되지만, 쉼표로 나열한 식이 만드는 것은 튜플이다. 표시된 함수의 반환값을 대입해 쓰려다 막히는 것은 대개 이 None 때문이다.
호출하는 쪽에서 리스트 앞에 별표 하나를 붙이면 그 내용물이 위치 인수로 하나씩 나뉘어 전달된다. 따라서 a에 1, b에 2가 들어가고, 남은 3이 별표 하나짜리 args에 튜플로 모여 (3,) 이 된다. 요소가 하나인 튜플이므로 끝에 쉼표가 붙는다. 키워드 인수는 넘기지 않았으므로 kwargs는 빈 딕셔너리 {} 이며 None이 되지 않는다. 별표를 빠뜨리고 f(vals) 라고 쓰면 리스트 자체가 첫 번째 가인수에 들어가 ([1, 2, 3], 2, (), {}) 가 되어 오류 없이 결과만 바뀌어 버린다. args에 들어가는 것은 튜플이므로 대괄호로 표시되지도 않는다.
어노테이션은 함수 객체의 __annotations__ 라는 딕셔너리에 기록될 뿐이며 실행 시에 타입이 검사되지는 않는다. a에 int라고 써 있어도 문자열을 넘길 수 있고, b에 str이라고 써 있어도 정수를 넘길 수 있다. 반환값에 bool이라고 써 있어도 실제로 반환되는 것은 받은 문자열 그 자체이다. 따라서 첫 줄에는 문자열 이 그대로 표시된다. 둘째 줄의 __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, 2] / [9, 3] 이 표시된다
위에서부터 [1] / [2] / [9, 3] 이 표시된다
위에서부터 [1] / [2] / [3, 9] 가 표시된다
위에서부터 [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 순서로 옳은 것은 어느 것인가?
실행 중인 함수, 모듈 최상위, 바깥쪽 함수, 내장 순으로 찾는다
실행 중인 함수, 바깥쪽 함수, 모듈 최상위, 내장 순으로 찾는다
모듈 최상위, 내장, 실행 중인 함수, 바깥쪽 함수 순으로 찾는다
내장, 모듈 최상위, 바깥쪽 함수, 실행 중인 함수 순으로 찾는다
정답B. 실행 중인 함수, 바깥쪽 함수, 모듈 최상위, 내장 순으로 찾는다
LEGB는 Local, Enclosing, Global, Built-in의 머리글자로 가장 안쪽부터 바깥쪽을 향해 찾는다는 것을 나타낸다. 먼저 실행 중인 함수 안을 보고, 다음으로 자신을 감싸고 있는 바깥쪽 함수 안, 다음으로 모듈 최상위, 마지막으로 len이나 print 같은 내장을 본다. 찾은 시점에 탐색은 끝나므로 안쪽에서 같은 이름을 쓰면 바깥쪽 이름은 가려진다. 바깥쪽에서 안쪽으로 찾는다는 순서로 하면 함수 안에서 만든 로컬 변수가 모듈 최상위의 같은 이름 변수에 밀리게 되어 실제 동작과 맞지 않는다. Global과 Enclosing의 순서를 바꾸는 실수도 많은데, 바깥쪽 함수 쪽이 모듈 최상위보다 안쪽에 있다.
문제 12 | 로컬 대입
함수 안에서 바깥쪽과 같은 이름에 대입한 다음 코드에 대해 표시되는 2줄은 어느 것인가?
x = 'global'def f(): x = 'local' print(x)f()print(x)
local 다음에 local
local 다음에 global
global 다음에 global
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 2
2 2
2 1
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()
UnboundLocalError 가 되어 아무것도 표시되지 않고 멈춘다
100 이 표시되고 그 뒤 끝까지 정상적으로 실행을 마친다
None 이 표시되고 그 뒤 끝까지 정상적으로 실행을 마친다
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)
20 30 30
20 30 20
20 30 10
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 * 라고 썼다. 호출하는 쪽으로 들어오는 이름은 어느 것인가?
별표에 의한 임포트는 보통 밑줄로 시작하지 않는 이름을 모두 들여오지만, 모듈 쪽에 __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__ 값 조합은 어느 것인가?
그 파일을 python3으로 직접 실행했을 때는 __name__ 에 '__main__' 이 들어간다. 다른 파일에서 import되었을 때는 확장자를 뺀 파일명인 'mymod2' 가 들어간다. 따라서 직접 실행에서는 mymod2: __main__ 과 직접 실행 두 줄이 나오고, import되었을 때는 mymod2: mymod2 한 줄만 나온다. if __name__ == '__main__': 은 이 차이를 이용해 '직접 실행했을 때만 동작시키고 싶은 처리'를 구분하는 정석적인 작성법이다. 조합을 반대로 하면 import했을 때 동작 확인용 처리가 실행되어 버려 이 작성법의 목적과 맞지 않는다. 어느 경우든 같은 값이 된다면 애초에 구분을 할 수 없다.
문제 18 | sys.argv
다음 args.py를 python3 args.py foo bar 라는 명령으로 실행했을 때 표시되는 2줄은 어느 것인가?
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했을 때의 동작으로 옳은 것은 어느 것인가?
두 번째도 모듈 본체가 처음부터 다시 읽혀 다시 실행된다
두 번째는 별개의 객체가 만들어져 상태가 공유되지 않게 된다
두 번째는 캐시가 사용되어 모듈 본체가 다시 실행되지 않는다
두 번째는 ImportError 가 되어 같은 이름은 한 번밖에 쓸 수 없다
정답C. 두 번째는 캐시가 사용되어 모듈 본체가 다시 실행되지 않는다
한 번 읽어들인 모듈은 sys.modules 라는 딕셔너리에 캐시된다. 두 번째 import는 거기에 등록되어 있는 같은 모듈 객체를 이름에 다시 연결할 뿐이므로 파일을 다시 읽는 일도 본체를 다시 실행하는 일도 일어나지 않는다. 모듈 최상위에 print를 써 두면 두 번 import해도 한 번만 표시되는 것으로 확인할 수 있다. 다른 이름을 붙여 import해도 가리키는 것은 같은 객체이다. 따라서 어떤 모듈의 변수를 고쳐 쓰면 그 모듈을 import하고 있는 다른 곳에서도 고쳐 쓴 뒤의 값이 보인다. 같은 이름을 두 번 import해도 오류가 되지 않으며, 개발 중에 본체를 다시 읽고 싶을 때는 importlib의 기능을 명시적으로 써야 한다.
문제 20 | 가상 환경 판정
지금 동작 중인 Python이 venv로 만든 가상 환경 안인지 판정하는 방법으로 적절한 것은 어느 것인가?
sys.prefix 가 정의되어 있지 않으면 가상 환경 안이라고 판정할 수 있다
sys.prefix 와 sys.base_prefix 가 일치하면 가상 환경 안이라고 판정할 수 있다
sys.base_prefix 가 정의되어 있으면 가상 환경 안이라고 판정할 수 있다
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)
None 이 표시된다
3 이 표시된다
14 가 표시된다
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
3줄이 쓰인 파일을 readlines로 읽었다. 표시되는 내용은 어느 것인가?
with open('/tmp/sample.txt', 'w') as f: f.write("one\ntwo\nthree\n")with open('/tmp/sample.txt') as f: print(f.readlines())
['one\ntwo\nthree\n'] 가 표시된다
'one\ntwo\nthree\n' 가 표시된다
['one\n', 'two\n', 'three\n'] 가 표시된다
['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 == '')
14 False 가 표시된다
14 True 가 표시된다
0 False 가 표시된다
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)
False 다음에 False
False 다음에 True
True 다음에 False
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())
파일의 내용은 AAABBB 로 되어 있다
파일의 내용은 AAABBBC 로 되어 있다
파일의 내용은 C 로 되어 있다
파일의 내용은 CAABBB 로 되어 있다
정답C. 파일의 내용은 C 로 되어 있다
'w'로 열면 그 시점에 기존 내용이 있든 없든 파일은 길이 0으로 잘린다. 순서대로 따라가면 처음의 '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())
ValueError 가 발생해 닫힌 파일에 대한 조작이라고 알려 준다
빈 문자열이 표시되고 그대로 정상적으로 실행을 마친다
자동으로 다시 열려 파일 내용이 모두 표시된다
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')
io.UnsupportedOperation 이 발생해 쓸 수 없다고 알려 준다
ValueError 가 발생해 모드 지정이 올바르지 않다고 알려 준다
내용 끝에 more가 더해지고 그대로 정상적으로 실행을 마친다
내용이 more로 바뀌고 그대로 정상적으로 실행을 마친다
정답A. io.UnsupportedOperation 이 발생해 쓸 수 없다고 알려 준다
open의 두 번째 인수를 생략하면 읽기 전용인 '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())
'one\ntwo\nthree\n' 14 0
'one\nt' 5 0
'one\nt' 5 5
'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줄은 어느 것인가?
JSON에는 배열만 있고 튜플이라는 타입은 존재하지 않는다. 그 때문에 dumps 시점에 튜플은 배열로 변환되어 출력은 {"t": [1, 2]} 가 된다. 다시 읽어들일 때도 배열은 리스트로 복원되므로 back['t'] 의 타입은 list이며 튜플로 돌아가지 않는다. 원래 튜플이었다는 정보는 어디에도 남지 않으므로 왕복시키면 타입이 바뀐다. 튜플인 채로 다루고 싶다면 다시 읽어들인 뒤 스스로 tuple() 을 적용해야 한다. JSON 출력에 소괄호가 나타나는 일은 없으며 튜플이 문자열로 변환되는 것도 아니다. 같은 이유로 딕셔너리의 키에 정수를 쓰면 dumps에서 문자열로 변환되고 다시 읽어도 정수로 돌아가지 않는다.
정답C. 첫 줄에 {"1": "a"} 가 나오고 둘째 줄에서 TypeError 가 발생한다
JSON 객체의 키는 문자열이어야 하므로 정수를 키로 한 딕셔너리를 dumps하면 키는 문자열로 변환되어 {"1": "a"} 로 출력된다. 이는 정상적으로 성공하는 처리이며 여기서 예외가 되지 않는다. 다만 다시 읽어들였을 때의 키는 문자열 그대로이며 정수로 돌아가지 않는다는 점에 주의해야 한다. 둘째 줄의 집합은 JSON에 대응하는 타입이 없으므로 dumps하려 하면 TypeError가 되고 Object of type set is not JSON serializable 이라고 알려 준다. 집합이 배열로 쓰인다고 생각하면 [1, 2] 가 되지만 그렇게 변환되기를 원한다면 스스로 list() 를 적용한 뒤 넘겨야 한다. 일본어를 포함한 값에 대해서는 기본값 그대로면 이스케이프되어 출력되고 ensure_ascii에 거짓을 넘기면 그대로의 문자로 출력된다.
연습: 이 페이지의 문제 풀기
무작위로 출제하는 연습 도구입니다(JavaScript가 활성화된 경우에 작동합니다). 위의 문제와 해설은 그대로 모두 읽을 수 있습니다.
※ 해설은 학습용 정보 제공입니다. 시험 출제 범위와 제도는 연도에 따라 바뀌므로, 반드시 시행 기관의 공식 발표를 확인하십시오.
이 페이지는 일본어 원문을 번역한 것입니다. 번역과 원문의 내용이 다를 경우 일본어판이 우선합니다. 일본어 원문 보기