본문 바로가기
파이썬/python 문법 & 이론 실습

파일 입출력과 Context manager 이해

by Bentist 2022. 2. 11.

파이썬에서의 입출력 작업

  • 프로그램과 사용자 사이의 입출력: 표준 입출력 함수 input(), print() 사용
  • 프로그램과 파일 사이의 입출력: open() 함수로 파일 핸들 객체를 생성하여 파일에 저장된 내용을 읽고 씀

파일 입출력

파일의 내용을 읽으려면 먼저 파이썬에게 작업할 파일과 파일로 어떤 작업(r, w, a)을 할지 알려줘야 한다.
이 기능을 open() 함수가 수행한다.

fhand = open('파일경로/파일명', 모드) 
fhand.close()

 

  • open() 함수는 파일을 조작하는 파일 핸들 객체 반환
  • 파일 핸들파일에 접근할 수 있는 연결 통로로, 이 파일 객체로 갖고 원하는 작업을 수행한다.
  • 파일로부터 데이터를 읽어와 파일 내용을 메모리에 적재하고 있는 상태다.
  • 파일 입출력 작업이 다 끝나면 반드시 close() 함수로 파일 객체를 닫아 메모리 자원을 반환한다.
  • 파일을 open하면 파일 내용이 메모리에 저장되고 있으므로, 파일을 닫지 않으면 메모리가 계속 소모된다.
>>> f = open('python.txt', encoding='utf-8') 
>>> f.readline() 
'테스트중' 
>>> f.close() 

>>> f.readline() Traceback (most recent call last): 
File "<stdin>", line 1, in <module> 
ValueError: I/O operation on closed file.

위 결과를 보면 f.readline( )에서 에러가 발생한다. f.close()로 입출력 작업을 끝내고 메모리를 반환했기 때문이다.

아래와 같이 파일을 읽고, 종료하는 전통적인 코드를

f = open('myFile.txt', 'w', encoding='utf8')
f.write("test")
f.close()

with .. as 구문으로 더 간결하고, 안전하게 사용할 수 있다.

with open('mytextfile.txt', 'r', encoding='utf8') as my_filehand:
	my_filehand.wirte("test")

with 구문의 유용성

코드가 복잡해지다보면 파일을 닫기 전에 에러가 발생할 가능성이 높아진다.

f = open('memo.txt', 'r')
...
print(f.read())
... (예외발생)

f.close() #실행되지 않음

물론 try/except/finally 구문을 사용하면 이 문제를 해결할 수 있다.

f = open('memo.txt', 'r')

try:
    ...
    print(f.read())
    ... (예외발생)

finally:
    f.close() # 예외가 발생하더라도 실행됨

그러나 with를 이용하면 더욱 간단하게 구현할 수 있다. with문의 범위를 벗어날 때, 혹은 with문 내에서 예외가 발생하더라도 파일 종료를 보장해준다.

with open('memo.txt', 'r') as f:
    ...
    print(f.read())
    예외 발생

# with문을 빠져 나가거나, 예외가 발생하더라도 파일을 닫아준다.

Context manager로 실행 진입과 종료 관리

Context managerwith 구문에 사용되기 위해 __enter__와 __exit__ 메서드를 모두 구현한 객체이다. 파이썬 내장함수 open()과 같이 with 문에 대응한 객체를 콘텍스트 매니저라고 부르고, open() 함수는 두 메서드를 이미 갖고 있다.

콘텍스트 매니저with 구문에서 runtime context를 관리하는 객체라고 정의하기도 하는데, 여기서 runtime context는 with의 코드 블록이 실행되기 전과, 실행된 후에 수행할 행위(정보)로 볼 수 있다. 위 정의에서 context는 프로그래밍에서 자주 나오는 용어로, context는 어떤 실행이 일어나기 위해 필요한 정보들(코드 블럭)이다. 예를 들어 내가 은행 계좌에서 돈을 인출(실행)하기 위해 카드를 긁거나 비밀번호를 입력해야 하는 등의 수행할 정보가 context다.

with {with 문에 대응하는 객체} as 변수:
	코드 블럭
    
with open() as fh:
	...

1. Context manager: open() 함수

Context manager인 open() 함수의 파일 객체로 출력된 <_io.TextIOWrapper>의 부모 클래스 IOBase를 확인해보자.

# _pyio.py class TextIOWrapper(TextIOBase): 
... 
class TextIOBase(IOBase): 
... 
class IOBase(metaclass=abc.ABCMeta): 
...
	### Context manager ### 
    def __enter__(self): 
        self.object

    def __exit__(self, *args): 
    	self.close()

위에서 IOBase의 __enter__는 with 코드 블럭 실행 전에, __exit__는 with 코드 블럭 실행 후에 호출되는 함수이자 약속이다. 즉, open()은 Context manager인 TextIOWrapper 객체를 만들어 반환한다. with는 코드 블럭 실행 전에 manager의 __enter__을 호출하여 반환 값(자신)을 fhand에 할당한다. 그리고 실행 후에 __exit__을 호출하여 파일을 닫는다.

2. Context manager: requests.Session() 함수

파이썬 HTTP 라이브러리인 requests의 Session() 함수도 Context manager다. 

https://docs.python-requests.org/en/latest/user/advanced/#session-objects 문서를 보면,

 

Sessions can also be used as context managers:

import requests

with requests.Session() as s:
    s.get('https://httpbin.org/cookies/set/sessioncookie/123456789')

with 구문 안에서 Session을 생성하면, with의 코드 블록이 끝나고 자동으로 세션을 종료한다. 또한 처리되지 않은 예외가 발생하더라도 with block이 종료되는 즉시 세션도 닫히게 된다.

 

참고로 여러 페이지를 연속으로 크롤링할 때 Session 객체를 사용해서 요청을 보내면, http 헤더 또는 인증 등의 설정을 한 번만 수행하고 그 상태 값을 계속 유지하면서 서버와 통신하므로 더 빠르게 데이터를 가져올 수 있다.

3. Context manager: concurrent.futures.ThreadPoolExecutor()

ThreadPool을 만드는 ThreadPoolExecutor() 객체도 Context Manager이기 때문에 with 문법을 사용하여 간편하게 Threads Pool을 만들 수도 해제할 수도 있다.

from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=5) as executor:
	코드 블럭

 

open()함수처럼 with as에 사용 가능한 클래스 만들기

with 문을 사용하려면 반드시 with 문의 {대응 객체}에 __enter__메서드와 __exit__메서드가 구현되어 있어야 한다.

즉, with 문은 이 두 메서드를 차례대로 실행하는 것이다. 파일을 여는 컨텍스트 매니저를 만들어보자.

 

__enter__(self): with 문에 진입하는 시점에 자동으로 호출
__exit__(self, type, value, trace_back): with 문이 끝나기 직전에 자동으로 호출

__exit__ 메소드에서 받는 세개의 인자는 해당 객체와 연관된 컨텍스트 내에서 예외가 발생되었을 때, 예외 처리에 관한 정보로 필수 인자이다.


파일을 여는 컨텍스트 매니저를 만들어보자.

class FileTest:
    def __init__(self, file_name, method):
        self.file_obj = open(file_name, method)
        
    def __enter__(self):
        return self.file_obj
        
    def __exit__(self, type, value, trace_back):
        self.file_obj.close()

방금 정의한 __enter__ 문과 __exit__ 문을 with문으로 사용할 수 있다.

with FileTest('sample.txt', 'w') as fhand:
    fhand.write('hello!')

with 구문에서 FileTest 클래스의 __enter__함수가 실행되면, 파일을 열고 반환된 객체가 as에 지정한 변수에 들어간다.

 

여러 개의 파일 관리하기

두 개 이상의 파일을 동시에 사용할 때 with as 문을 사용하는 방법이다. 단순히 두 개의 with as 문을 겹쳐도 되고, 하나의 with문에 두 개 이상의 파일을 열어도 된다.

with open('a.txt','w') as a:
	with open('b.txt','w') as b:
		a.write('hello world')
		b.write('hello b')
with open('a.txt','w') as a, open('b.txt','w') as b:
	a.write('hello world')
	b.write('hello b')

한 번에 다루고자 하는 파일이 많은데 with문을 사용하면 with문이 옆으로 길어진다. 그래서 파이썬 버전 3.10.0 b1 버전부터는 괄호와 함께 여러 개의 파일들을 여러 줄에 걸쳐서 작성할 수 있다.

# from python version 3.10.0b1
with (
	open('a.txt','w') as a,
	open('b.txt','w') as b,
	open('c.txt','w') as c
):
	a.write('aaa')
	b.write('bbb')
	c.write('ccc')

댓글