프로그램이 다루는 데이터는 프로그램이 종료되는 순간 메모리에서 소실되고 만다. 데이터를 나중에도 계속 사용하기 위해서는 하드 디스크 같은 저장 매체에 기록해 두어야 한다. 이 절에서는 데이터를 파일로 기록하는 방법, 기록된 파일을 읽는 방법, 운영 체제가 제공하는 파일 시스템을 다루는 방법을 알아본다.

11.4.1 텍스트 파일 읽고 쓰기

파일 사용의 기본은 파일을 열고 닫는 법을 아는 것이다. 파일은 운영 체제가 통제하는 파일 시스템에 의해 제공된다. 응용 프로그램이 파일 입출력을 하려면 운영 체제에 요청하여 허락을 구해야 하며(파일 열기), 파일을 다 작성한 후에도 운영 체제에 끝났음을 알려주어야 한다(파일 닫기). 파이썬에서는 다음과 같은 세 단계를 거친다.

  1. open() 함수로 파일을 열어, 파일 객체를 얻는다.
  2. 파일 객체에서 입력 또는 출력을 수행한다.
  3. 파일 객체의 close() 메서드로 파일을 닫는다.

파일을 열 때 사용하는 open() 함수는 사용하려는 파일의 경로를 첫번째 매개변수로 전달받는다. open() 함수는 지정된 경로의 파일을 열어 파일 객체를 반환한다. 파일 객체는 파일에서 정보를 읽거나 기록하는 메서드를 제공한다. 파일 객체를 다 사용한 후에는 close() 메서드로 닫아준다.

다음은 이 세 단계에 따라 years.txt라는 텍스트 파일을 열고, 그 내용을 읽어 화면에 출력하고, 파일을 닫는 프로그램이다.

코드 11-56 파일을 열고, 사용하고, 닫는 프로그램

file = open('years.txt')  # 텍스트 파일 열기
for line in file:         # 파일 내용을 한 행씩 읽어 화면에 출력
    print(line)
file.close()              # 파일 닫기

위 코드에서는 파일을 닫을 때 파일 객체의 close() 메서드를 직접 호출했다. 하지만 9.5 뒷정리에서 설명했듯이, with 문을 사용해 파일 객체가 자동으로 닫히도록 하는 편이 더 좋다.

코드 11-57 with 문으로 파일 읽기

with open('years.txt') as file:  # 텍스트 파일 열기
    for line in file:            # 파일 내용을 한 행씩 읽어 화면에 출력
        print(line)

코드 11-57은 'years.txt' 파일을 열고, 파일 객체를 file 변수에 대입하여 사용한다. with 문의 본문에서 file 객체를 자유롭게 사용할 수 있다. 본문이 다 실행된 후에는 파일이 저절로 닫히니 파일 객체의 close() 메서드를 직접 호출하지 않아도 된다.

텍스트 파일에 정보 기록하기

텍스트 파일에 텍스트 데이터를 출력하는 것은 화면에 텍스트 데이터를 출력하는 것과 비슷하다. 출력 대상을 화면이 아니라 파일로 지정해야 한다는 점이 다를 뿐이다. 다음은 1장에서 만들어 본 첫 파이썬 프로그램의 실행 결과를 화면이 아니라 파일에 출력하도록 한 예다.

코드 11-58 쓰기 모드로 텍스트 파일을 열어 텍스트 출력하기

print('당신의 이름은 무엇인가요?')
name = input()

with open('hello.txt', 'w') as file:
    print(name, '님 반가워요.', file=file)

파일을 열 때는 ‘읽기’ 또는 ‘쓰기’ 모드로 지정하여 열어야 한다. 파일은 기본적으로 ‘읽기’ 모드로 열리기 때문에 파일에 무언가를 기록하고자 할 때는 ‘쓰기’ 모드로 열도록 지정해야 한다. 위 코드에서 open('hello.txt', 'w') 함수 호출을 자세히 보면, open() 함수의 두 번째 매개변수에 'w'라는 문자열을 전달했다. open() 함수의 두 번째 매개변수가 바로 파일의 열기 모드를 지정하기 위한 변수다. ‘w’는 쓰기 모드(write mode)의 머릿말으로, 파일을 쓰기 모드로 열겠다는 뜻이다.

print() 함수는 file 매개변수로 텍스트를 출력할 대상을 지정받을 수 있다. 위 코드에서는 print(..., file=file)로 파일 객체(file)를 file 매개변수에 전달하여 파일에 기록할 것을 지시했다.

위 코드를 실행한 예는 다음과 같다.

당신의 이름은 무엇인가요?
박연오

평소와 달리 print() 함수를 호출한 결과가 화면에 출력되지 않았다. 이는 출력 대상을 파일로 지정했기 때문이다. 파일 관리 프로그램으로 hello.txt 파일을 찾아 열어보면 다음과 같이 파일에 텍스트가 출력된 것을 확인할 수 있다.

그림 11-1 출력 결과가 기록된 파일의 내용

그림 11-1 출력 결과가 기록된 파일의 내용

print() 함수를 사용하는 대신 파일 객체의 write() 메서드를 사용해도 된다.

코드 11-59 file.write() 메서드로 기록하기

from datetime import date
today = date.today()
text = f'안녕하세요! 오늘은 {today.month}월 {today.day}일 입니다.\n'

with open('hello.txt', 'w') as file:
    file.write(text)

print() 함수는 전달된 인자가 문자열이 아니더라도 텍스트로 자동 변환해 주지만, 파일 객체의 write() 메서드에는 데이터를 문자열로 변경해 전달해야 한다. 또한, 개행 문자(\n)도 직접 출력해야 한다.

코드 11-59를 실행한 뒤 hello.txt 파일의 내용을 다시 확인해보면, 앞서 기록되어 있던 ‘박연오 님 안녕하세요!’ 는 사라졌고 ‘안녕하세요! 오늘은 11월 27일 입니다.’라는 새로운 내용만 기록된 것을 확인할 수 있을 것이다. 쓰기 모드('w')로 파일을 열면 파일의 기존 내용은 삭제된다.

파일이 이미 존재할 때 기존 내용 뒤에 새로운 덧붙여 작성하려면, 파일을 덧붙이기 모드(append mode)로 열어야 한다. open() 함수에서 열기 모드를 'a'로 지정하면 된다.

코드 11-60 덧붙이기 모드로 텍스트 파일을 열어 텍스트 출력하기

from datetime import datetime
now = datetime.now()
text = f'현재 시각: {now.hour:02}:{now.minute:02}:{now.second:02}\n'

with open('time.txt', 'a') as file:
    file.write(text)

코드 11-60을 여러 번 실행해보면, 실행할 때마다 파일이 지워지는 것이 아니라, 기존 내용에 이어서 계속 출력되는 것을 확인할 수 있다. 내 경우에는 프로그램을 다섯 번 실행하여 ‘time.tzt’ 파일의 내용이 다음과 같이 기록되었다.

현재 시각: 16:38:49
현재 시각: 16:38:50
현재 시각: 16:39:01

만약 파일이 이미 존재하지 않을 때만 새로 작성하고 싶다면 파일을 배타적 쓰기 모드(exclusive write mode)로 열면 된다. open() 함수의 'x' 모드다. 이 모드로 파일을 열 때, 파일이 이미 존재하는 경우에는 FileExistsError 예외가 발생한다. 따라서 다음 예와 같이 예외 처리를 해 주면 된다.

코드 11-61 배타적 쓰기 모드로 텍스트 파일 열기

try:
    with open('한번만_기록.txt', 'x') as file:
        file.write('이 파일은 한 번만 작성됩니다.')
except FileExistsError:
    print('파일이 이미 존재합니다.')

파일에 정보를 기록하는 방법을 살펴보면서, 파일을 여는 모드에 여러 가지가 있다는 것을 함께 알게 되었다. open() 함수에서 지정할 수 있는 다양한 열기 모드는 아래의 표에 정리해두었다.

열기 모드 의미
r 읽기(read) 모드 (기본값이며 생략 가능)
w 쓰기(write) 모드
a 덧붙이기(append) 모드
x 배타적(exclusive) 쓰기 모드
t 텍스트(text) 모드 (기본값이며 생략 가능)
b 이진(binary) 모드

표 11-14 open() 함수에 지정할 수 있는 열기 모드

이진 파일과 텍스트 파일

파일은 크게 이진(binary) 파일과 텍스트(text) 파일로 구별할 수 있다. 이진 파일은 파일의 내용이 이진수로 이루어진 파일이고, 텍스트 파일은 파일의 내용이 아스키, 유니코드 등의 텍스트 부호로 이루어진 파일이다. 텍스트 부호도 결국은 이진수로 표현되므로 모든 텍스트 파일은 이진 파일이기도 하다. 텍스트 파일을 이진 파일로도 텍스트 파일로도 처리할 수 있으므로 open() 함수로 파일을 열 때, 이진·텍스트 모드를 지정할 수 있다.

이진 파일의 종류로는 압축 파일, 이미지 파일 등 여러 가지가 있다. 이런 파일 형식을 제어해주는 라이브러리가 많기 때문에 이진 파일을 직접 다뤄야 하는 상황은 과거에 비해 적다. 입문자에게 맞지 않다고 생각해, 바이트 데이터와 이진 파일 다루는 법은 설명하지 않겠다.

텍스트 파일에서 정보 읽기

프로그램에서 파일로 정보를 기록(출력)하는 것과 반대로, 파일에서 프로그램으로 정보를 읽어들이는(입력) 것도 가능하다. open() 함수에서 파일을 읽기 모드(read mode)로 열면 된다. 읽기 모드는 'r'로 지정할 수 있는데, open() 함수의 기본 동작이 읽기 모드로 수행되므로 생략해도 무방하다.

읽기 모드로 파일을 연 뒤에는 파일 객체의 read() 메서드를 호출해 파일 내용을 읽어들일 수 있다. 다음은 코드는 앞서 작성한 ‘time.txt’ 파일을 읽기 모드로 열고, read() 메서드로 파일 내용을 읽어들인다.

코드 11-62 읽기 모드로 텍스트 파일을 열어 텍스트 입력받기

with open('time.txt', 'r') as file:
    data = file.read()

print(data)   # 읽어들인 내용을 확인해보자

읽어들인 내용을 확인하기 위해 print() 함수로 화면에 출력해 보았더니 다음과 같이 출력되었다. 텍스트 파일의 전체 내용이 다 읽어진 것을 확인할 수 있다.

현재 시각: 16:38:49
현재 시각: 16:38:50
현재 시각: 16:39:01

파일 객체의 read() 메서드는 파일 내용의 전체를 다 읽어들인다. 파일의 용량이 큰 경우에는 메모리가 부족하지 않도록 주의해야 한다. 만약 파일을 한 행씩 읽어들이고 싶다면, 파일 내용을 한 행씩 읽어 반환하는 readline() 메서드를 사용한다. 이번에는 대화식 셸에서 읽어들여 보자.

코드 11-62 읽기 모드로 텍스트 파일을 열어 텍스트 입력받기

>>> file = open('time.txt')

>>> file.readline()   # 파일 내용을 한 행씩 읽어 반환한다
'현재 시각: 16:38:49\n'

>>> file.readline()
'현재 시각: 16:38:50\n'

>>> file.readline()
'현재 시각: 16:39:01\n'

>>> file.readline()   # 더이상 읽을 것이 없으면 빈 문자열이 반환된다
''

>>> file.close()      # 파일을 닫는 것을 잊지 말자

파일 객체는 파일의 각 행을 읽어들여 출력하는 반복자(7.4 절)를 제공한다. 따라서 파일 객체를 for 문으로 순회하는 것도 가능하다. 파일의 모든 행을 읽어들일 때까지 각 행을 읽어들인다.

코드 11-63 파일 객체를 순회하며 파일 내용 읽어들이기

>>> with open('time.txt') as file:
...     for line in file:
...         print(line, end='')
... 
현재 시각: 16:38:49
현재 시각: 16:38:50
현재 시각: 16:39:01

연습문제

연습문제 11-7 텍스트 파일 복사

파일 경로를 나타내는 문자열 두 개를 인자로 전달받아, 텍스트 파일을 복사하는 함수cp(src, dst)를 작성하라. 예를 들어, 이 함수를 다음과 같이 호출하면 original.txt 파일을 clone.txt 파일로 복사한다.

cp('original.txt', 'clone.txt')

11.4.2 데이터 직렬화하기

텍스트 파일로는 단순한 텍스트 정보만을 출력할 수 있다. 리스트·사전 등 컬렉션을 중첩한 입체적인 데이터를 파일로 저장하고 읽어들이려면 어떻게 해야 할까? 입체적인 데이터를 텍스트로 나타내기 위한 일정한 약속이 필요하다고 생각했다면 정답이다. 이런 약속에 따라 입체적인 데이터를 텍스트나 바이트 배열로 표현하는 것을 직렬화(serialization)라고 한다.

직렬화를 위한 형식에는 여러 가지가 있다. 그 가운데 요즘 가장 많이 사용되는 것은 CSV와 JSON이다. CSV와 JSON은 파이썬 뿐 아니라 다양한 프로그래밍 환경에서 지원된다. 파이썬에서 작성한 데이터를 다른 프로그래밍 언어로 작성한 프로그램으로 전송할 때에도 유용하게 사용할 수 있다.

CSV: 표 형식의 정보 나타내기

CSV(comma-separated values, 쉼표로 구분된 값)는 표(table) 형식로 저장된 데이터를 표현하기 위한 형식이다. CSV에 관해 설명하기 전, 먼저 표 형식의 데이터에 관해 잠시 알아보자.

표는 가로·세로의 2차원으로 칸(cell)을 배열하고 칸 안에 정보를 기록하는 도구다. 표를 가로로 잘라내면 잘라내면 행(row)이, 세로로 잘라내면 열(column)이 된다.

title genre year
Interstella SF 2014
Braveheart Drama 1995
Mary Poppins Fantasy 1964
Gloomy Sunday Drama 2000

CSV 형식은 표를 텍스트 데이터로 직렬화하기 위한 형식이다. CSV는 표의 각 열(column)을 쉼표(,)로 구별하며, 값과 쉼표를 모은 텍스트 한 행으로 표의 각 행(row)을 나타낸다. 다음은 앞의 표를 CSV 형식으로 나타낸 것이다. 매우 단순한 방식이므로 이해하기 어렵지 않을 것이다. 메모장 등의 텍스트 편집기 프로그램을 이용해 다음과 동일한 파일을 작성하고, ‘movies.csv’라는 이름으로 저장해 두자.

title,genre,year
Interstella,SF,2014
Braveheart,Drama,1995
Mary Poppins,Fantasy,1964
Gloomy Sunday,Drama,2000

CSV 형식의 텍스트 파일로 작성된 표 데이터를 파이썬으로 읽어들이려면 어떻게 해야 할까? 파일을 행 단위로 읽어들인 뒤 각 행을 다시 쉼표(,) 분리하면 해석할 수 있을 것이다.

하지만 CSV 형식을 해석해주는 파이썬 내장 모듈 csv를 사용하는 편이 더 좋다. csv 모듈은 표 형식의 데이터를 CSV 형식으로 표현하거나, CSV 형식의 데이터를 리스트를 담은 리스트 또는 사전을 담은 리스트로 해석해 준다. 다음은 csv 모듈을 이용해 CSV 파일을 읽어들이는 예다.

코드 11-64 CSV 파일 읽어들이기

import csv  # csv 모듈 임포트
import pprint

# movies.csv 파일 열기
with open('movies.csv') as file:
    reader = csv.reader(file)  # CSV 파일을 읽어들이는 읽기 객체
    movies = list(reader)      # CSV 파일 내용을 리스트로 읽어들인다

pprint.pprint(movies)  # 읽어들인 내용을 화면에 출력

csv.reader() 함수는 전달받은 파일 객체의 CSV 파일을 읽어들이는 읽기 객체를 반환한다. 이 리더 객체를 리스트로 변환하면 CSV 파일을 중첩 리스트로 읽어들일 수 있다. 코드 11-64를 실행하면 다음과 같이 출력된다.

[['title', 'genre', 'year'],
 ['Interstella', 'SF', '2014'],
 ['Braveheart', 'Drama', '1995'],
 ['Mary Poppins', 'Fantasy', '1964'],
 ['Gloomy Sunday', 'Drama', '2000']]

출력 결과에서 보듯, 파이썬에서 표 데이터를 저장할 때는 표의 각 행을 리스트로 담는 중첩 리스트가 사용된다.

이번에는 반대로, 파이썬에서 중첩 리스트로 저장한 표 데이터를 CSV 형식 파일로 출력해보자. csv 모듈의 csv.writer() 함수로 쓰기 객체를 생성하고, 쓰기 객체의 writerows() 메서드를 사용하면 된다. 다음 예를 따라해 보자.

코드 11-65 중첩 리스트를 CSV 파일로 작성하기

import csv

# 표 데이터를 담은 중첩 리스트
table = [
    ['title', 'genre', 'year'],
    ['Interstella', 'SF', '2014'],
    ['Braveheart', 'Drama', '1995'],
    ['Mary Poppins', 'Fantasy', '1964'],
    ['Gloomy Sunday', 'Drama', '2000'],
]

# movies_output.csv 파일을 쓰기 모드로 열기
with open('movies_output.csv', 'w') as file:
    writer = csv.writer(file)  # CSV 파일을 작성하는 쓰기 객체
    writer.writerows(table)    # 표를 전체 행을 CSV 파일에 써넣는다

새로 만들어진 파일을 열어 보면 CSV 형식으로 작성된 표를 볼 수 있을 것이다.

CSV 형식의 파일은 마이크로소프트 엑셀 등의 스프레드시트 프로그램으로도 읽어들일 수 있다. 뿐만 아니라 스프레드시트 프로그램에서 작성한 표를 CSV 형식으로 저장하는 것도 가능하다. 이를 통해 스프레드시트 프로그램과 파이썬 사이에서 데이터를 주고받을 수 있다. CSV는 그 외에도 다양한 프로그램에서 활용된다.

JSON: 계층적 데이터 나타내기

CSV 형식은 표 형식의 평면적인 데이터를 나타내기에 좋다. 하지만 리스트와 사전이 여러 층 중첩된 입체적인 데이터를 나타내기는 쉽지 않다. 이런 데이터를 파일로 저장하거나 네트워크로 주고받으려면 더 복잡한 구조를 표현할 수 있는 형식이 필요하다. 가장 많이 사용되는 형식은 XML(Extensible Markup Language)과 JSON(JavaScript Object Notation, ‘제이슨’으로 읽는다)이다. 그 중에서도 JSON은 웹 브라우저에서 사용되는 언어인 자바스크립트(JavaScript)로 처리하기가 용이하며, XML보다 간결하여 인기가 높다. JSON으로 표기된 데이터를 파이썬에서 읽어들이는 방법을 알아보자.

입체적인 데이터도 표 형태로 나타낼 수 있다

표 만으로도 입체적인 데이터를 나타낼 수 있다. 중복 데이터를 별도의 표로 잘라내고 표들의 관계를 정의하는 정규화를 적용하면 된다. 이런 방식으로 구성된 데이터 체계를 관계형 데이터베이스라고 한다. 대규모 데이터를 관리하는 원리에 관심이 있다면 데이터베이스를 학습해보기를 추천한다.

JSON의 표기법은 파이썬의 리스트와 사전을 중첩한 데이터와 흡사하다. 예를 들어, 데이터를 파이썬 코드로 다음과 같이 나타냈다면,

[
    {
        'title': 'Interstella',
        'genre': 'SF',
        'year': 2014,
        'starring': ['M. McConaughey', 'A. Hathaway', 'J. Chastain'],
    },
    {
        'title': 'Mary Poppins',
        'genre': 'Fantasy',
        'year': 1964,
        'starring': ['J. Andrews', 'D. Van Dyke'],
    },
]

JSON으로는 다음과 같이 표현한다.

[
    {
        "title": "Interstella",
        "genre": "SF",
        "year": 2014,
        "starring": ["M. McConaughey", "A. Hathaway", "J. Chastain"]
    },
    {
        "title": "Mary Poppins",
        "genre": "Fantasy",
        "year": 1964,
        "starring": ["J. Andrews", "D. Van Dyke"]
    }
]

보다시피 파이썬으로 표현한 데이터 코드와 JSON으로 표현한 데이터 코드가 매우 닮았다. JSON의 문법을 자세히 설명하는 것은 이 책의 범위 밖이다. 다음 몇 가지 사항만 간단히 알아 두자.

  • 숫자와 따옴표 데이터는 각각 파이썬의 수와 문자열에 대응된다.
  • 중괄호 표현(객체)은 파이썬의 사전에 대응된다.
  • 대괄호 표현(배열)은 파이썬의 리스트에 대응된다.
  • 작은따옴표가 아니라 큰따옴표를 사용한다.
  • 중괄호와 대괄호 안의 마지막 데이터 뒤에 콤마를 붙여서는 안 된다.
  • 줄바꿈과 들여쓰기 등 공백 문자는 생략될 수 있다.

JSON과 CSV를 비교하면 다음과 같은 차이가 있다.

  • CSV의 모든 값은 텍스트이지만, JSON은 수와 텍스트를 구별할 수 있다.
  • CSV는 열과 행만으로 데이터를 구조화하지만, JSON은 객체(사전과 유사)와 배열(리스트와 유사)을 이용해 데이터를 입체적으로 구조화할 수 있다.

JSON의 문법을 당장 익히지 않더라도 상관 없다. 파이썬의 json 모듈이 변환을 대신 수행해주기 때문이다. 먼저, 컬렉션을 JSON으로 직렬화할 때는 json.dumps() 함수를 사용한다. 이 함수는 컬렉션을 입력받아 직렬화된 문자열을 반환한다.

코드 11-66 입체적인 데이터를 JSON으로 직렬화하기

import json   # json 모듈 임포트

# 직렬화하려는 데이터
data = [
    {
        'title': 'Interstella',
        'genre': 'SF',
        'year': 2014,
        'starring': ['M. McConaughey', 'A. Hathaway', 'J. Chastain'],
    },
    {
        'title': 'Mary Poppins',
        'genre': 'Fantasy',
        'year': 1964,
        'starring': ['J. Andrews', 'D. Van Dyke'],
    },
]

json_data = json.dumps(data)  # 컬렉션을 JSON으로 직렬화
print(json_data)              # 직렬화된 텍스트 확인
with open('movies.json', 'w') as file:  # 파일로 저장
    file.write(json_data)

위 코드는 리스트 속에 사전, 사전 속에 다시 리스트가 포함된 복잡한 구조를 JSON으로 직렬화하는 과정을 보여준다. 데이터를 입력하느라 코드가 길어졌지만 직렬화를 수행하는 코드는 json.dumps(data)에 불과하다. 직렬화의 결과물은 문자열이므로 화면에 출력하거나 텍스트 파일로 저장할 수도 있다.

반대로, JSON으로 직렬화된 텍스트를 읽어들여 파이썬 컬렉션으로 해석할 때는 json.loads() 함수를 사용하면 된다. 이 함수는 문자열을 입력받아 컬렉션을 반환한다. 다음 프로그램은 앞서 저장한 JSON 파일을 open() 함수로 열어 읽어들인 뒤, json.loads() 함수로 데이터를 해석한다.

코드 11-67 JSON 텍스트를 읽어들여 역직렬화하기

import json   # json 모듈 임포트
import pprint

with open('movies.json') as file:  # 텍스트 파일을 읽어들인다
    json_data = file.read()

data = json.loads(json_data)  # 읽어들인 텍스트 데이터를 역직렬화
pprint.pprint(data)  # 해석된 데이터 확인

CSV, JSON 등의 직렬화 형식을 이용해 프로그램의 데이터를 파일로 저장해 두었다가 나중에 다시 사용하는 방법을 알아보았다. 직렬화는 인터넷을 통해 데이터를 다른 컴퓨터로 전송할 때도 필요하다. 인터넷으로 데이터를 주고받는 방법은 다음 절에서 알아볼 것이다.

연습문제

연습문제 11-8 CSV 파일 읽어들이기

스프레드시트 또는 텍스트 편집기를 이용해 다음 표의 내용을 CSV 파일로 작성해라.

country population area
South Korea 48422644 98480
China 1330044000 9596960
Japan 127288000 377835
United States 310232863 9629091
Russia 140702000 17100000

이 CSV 파일을 읽어들이고 각 나라의 인구밀도를 구해 인구밀도가 높은 나라부터 낮은 나라 순으로 나라 이름과 인구밀도를 출력하는 프로그램을 작성해라.

11.4.3 파일 시스템에서 작업하기

운영 체제의 파일 시스템에서 할 수 있는 일은 단순히 데이터를 파일로 저장하고 읽는 것 외에도 많다. 예컨대 파일의 이름을 변경하거나, 파일을 옮기거나, 디렉터리를 만들거나, 어떤 디렉터리에 들어 있는 모든 파일을 구하는 것 말이다. 파일 시스템에서 다양한 작업을 수행하는 방법을 알아보고, 반복 작업도 줄여 보자.

경로 다루기

운영 체제에서 어떤 파일 또는 디렉터리를 가리킬 때는 그 대상이 위치한 경로를 이용한다. 다음은 몇 가지를 예로 든 것인데, 평소 컴퓨터를 많이 사용해 보았다면 낯설지 않을 것이다.

  • .: 현재 디렉터리
  • ..: 한 단계 위의 디렉터리
  • C:\: (윈도우) 첫번째 하드 디스크
  • C:\Users\bakyeono\Documents\: (윈도우) 사용자 문서 디렉터리
  • /: (유닉스) 파일 시스템의 루트
  • /home/bakyeono: (유닉스) 사용자 디렉터리

경로를 나타내는 방법은 운영 체제마다 약간씩 차이가 있다. 경로가 시작되는 지점이 유닉스에서는 루트(최상위)인데 반해, 윈도우에서는 디스크 운영 체제(DOS)의 전통에 따른 드라이브 문자(A:, B:, C: 등)다. 또, 상위 디렉터리와 하위 항목을 구별하는 문자가 유닉스에서는 슬래시(/)고, 윈도우에서는 역슬래시(\)다. 파이썬에서는 편의상 둘 다 슬래시(/)로 통일하여 사용한다. 그 외에는 전체적으로 비슷하다.

파이썬에서 경로를 나타내는 객체는 pathlib 모듈에 정의된 Path 클래스를 이용해 만들 수 있다. Path(경로문자열)와 같이 가리킬 경로를 문자열로 전달하여 인스턴스화하면 된다.

코드 11-68 경로를 표현하는 Path 객체 생성하기

>>> from pathlib import Path   # pathlib 모듈에서 Path 클래스 임포트
>>> Path('.')     # 현재 디렉터리의 경로
WindowsPath('.')

>>> Path('C:/')   # 하드 디스크의 경로
WindowsPath('C:/')

Path 객체를 생성한 뒤에는 객체의 속성과 메서드를 이용해 해당 경로의 대상을 조작할 수 있다. 다음 표를 참고하자. (표에서 주의라고 써 둔 메서드는 해당 경로의 파일·디렉터리를 실제로 생성·수정·삭제한다.)

속성 또는 메서드 값 또는 기능
exists() 대상이 존재하는지 검사한다
is_file() 대상이 파일인지 검사한다
is_dir() 대상이 디렉터리인지 검사한다
parent 한 단계 위의 경로
name 대상의 이름
suffix 대상의 확장자
with_name(new) 이름을 new로 변경한 경로를 반환한다
with_suffix(new) 확장자를 new로 변경한 경로를 반환한다
iterdir() 대상 디렉터리를 순회하는 반복자를 반환한다
mkdir() (주의) 대상 디렉터리를 생성한다
touch() (주의) 빈 파일을 생성한다
replace(new) (주의) 대상의 경로를 new로 바꾼다
rmdir() (주의) 대상 디렉터리를 삭제한다
unlink() (주의) 대상 파일을 삭제한다

표 11-12 Path 객체에서 자주 사용되는 속성과 메서드

다음은 C:\python-programming\ 경로를 가리키는 Path 객체에서 위 표의 속성과 메서드를 사용해 본 예다.

코드 11-69 Path 객체의 속성과 메서드 사용하기

>>> path = Path('C:/python-programming/')  # 디렉터리 경로

>>> path.exists()    # 경로의 대상이 존재하는가?
True

>>> path.is_file()   # 경로의 대상이 파일인가?
False

>>> path.is_dir()    # 경로의 대상이 디렉터리인가?
True

>>> path.parent      # 한 단계 위 경로
WindowsPath('C:/')

>>> path.name        # 대상의 이름
'python-programming'

>>> path.suffix      # 대상의 확장자
''

>>> path.with_name('lisp-programming')  # 이름을 바꾼 경로
WindowsPath('C:/lisp-programming')

>>> path.with_suffix('.backup')         # 확장자를 바꾼 경로
WindowsPath('C:/python-programming.backup')

이 예제에서 다루지 않은 iterdir() 메서드와 파일·디렉터리를 실제로 수정하는 메서드들은 곧이어 살펴본다.

디렉터리의 하위 항목 순회하기

Path 객체의 iterdir() 메서드는 경로가 가리키는 디렉터리에 포함된 파일·디렉터리를 순회하는 반복자를 반환한다. 이렇게 반환된 메서드를 리스트로 변환하거나, for 문으로 순회하면 사용하면 여러 가지 유용한 작업을 수행할 수 있다.

예를 들어, 특정 디렉터리의 모든 하위 파일·디렉터리를 화면에 출력하는 함수를 정의해 볼 수 있다.

코드 11-70 디렉터리의 모든 하위 항목 출력하기

from pathlib import Path

def ls(path):
    """path 디렉터리에 포함된 모든 하위 항목을 화면에 출력한다."""
    path_obj = Path(path)            # 전달된 경로의 Path 객체 생성
    
    for item in path_obj.iterdir():  # 모든 하위 항목을 순회하며
        print(item)                  # 화면에 출력

ls('C:/')  # C:\ 경로의 모든 하위 항목 출력

실행 결과:

...
example_11_64.py
example_11_65.py
example_11_70.py
...

iterdir() 메서드의 반복자를 통해 순회되는 각 항목 역시 Path 객체다. 반복자로 리스트를 생성해 확인해 보자.

코드 11-71 하위 항목 반복자의 각 요소도 Path 객체다

>>> from pathlib import Path
>>> path = Path('C:/')
>>> path_list = list(path.iterdir())   # 디렉터리의 하위 항목 리스트
>>> path_list
[WindowsPath('C:/Program Files/'), WindowsPath('C:/Users/'), ...]

>>> path_list[0]           # 리스트의 각 요소 역시 Path 객체다
WindowsPath('C:/Program Files/')

>>> path_list[0].isdir()   # 물론, 메서드도 사용할 수 있다
True

디렉터리의 하위 항목들도 Path 객체이므로 이 객체들의 메서드를 호출하여, 모든 하위 항목의 이름을 변경하거나 삭제할 수도 있다. 파일·디렉터리의 이름을 바꾸거나 삭제하는 메서드는 아래에서 살펴보자.

파일·디렉터리 생성하기

Path 객체의 mkdir() 메서드를 호출하여 지정한 경로의 디렉터리를 새로 생성할 수 있다. 파일은 open() 함수로 열어 작성할 수 있지만, 단순히 빈 파일을 만들고자 할 때는 Path 객체의 touch() 메서드를 사용해도 된다. 동일한 이름의 파일·디렉터리가 이미 존재할 때는 FileExistsError 예외가 발생한다.

코드 11-72 파일·디렉터리 생성하기

>>> Path('dir1').mkdir()   # dir1 디렉터리를 생성한다
>>> Path('dir2').mkdir()   # dir2 디렉터리를 생성한다
>>> Path('file1').touch()  # file1 (빈) 파일을 생성한다
>>> Path('dir2').mkdir()   # 이미 존재하는 디렉터리를 만들 때
FileExistsError: [Errno 17] File exists: 'dir2'

파일·디렉터리 이동하기

Path 객체의 replace() 메서드는 파일·디렉터리를 대상 경로로 옮긴다. 대상 경로는 Path 객체 또는 경로를 나타내는 문자열로 지정할 수 있다. 예를 들어, 다음 코드는 dir2 디렉터리를 dir1의 하위 항목으로 옮기고, 다시 되돌린다.

코드 11-73 파일·디렉터리 이동하기

>>> Path('dir2').replace(`dir1/dir2')     # dir2를 dir3 아래로 이동
>>> Path('dir1/dir2').replace(Path('dir2'))  # dir2를 원위치로 이동

또한, 파일·디렉터리의 이름을 변경할 때도 사용된다. 아래 코드는 dir2 디렉터리의 이름을 dir3으로 수정한다.

코드 11-74 파일·디렉터리 이름 수정하기

>>> Path('dir2').replace('dir3')  # dir2의 이름을 dir3으로 변경

replace() 메서드로 파일을 이동시키거나 이름을 수정할 때, 새로 지정한 경로에 이미 파일·디렉터리가 존재한다면 경로가 변경된 파일로 교체되어 버린다. 따라서 파일이 이미 존재하는지 확인하는 것이 좋다. 다음은 파일을 안전하게 이동시키는 함수를 정의해 본 것이다.

코드 11-75 대상 경로가 이미 존재할 때 예외 일으키기

from pathlib import Path

def mv(src, dst):
    """src 경로의 파일을 dst 경로로 이동한다."""
    src_obj = Path(src)
    dst_obj = Path(dst)
    
    if dst_obj.exists():       # 대상 경로의 파일이 존재할 경우
        raise FileExistsError  # FileExistsError 예외를 일으키자
    src_obj.replace(dst_obj)

mv('old', 'new')

파일·디렉터리 삭제하기

코드 11-76 파일·디렉터리 삭제하기

디렉터리를 삭제할 때는 rmdir() 메서드를 사용한다. 이 때 디렉터리는 비어있어야 하며, 그렇지 않으면 OSError 예외가 발생한다. 파일을 삭제할 때는 unlink() 메서드를 사용한다.

>>> Path('dir3').rmdir()     # dir3 디렉터리를 삭제한다
>>> Path('file1').unlink()   # file1 파일을 삭제한다