프로그램은 혼자 동작하지 않는다.
사용자에게 값을 입력받고, 파일을 읽고 저장하며, 예상하지 못한 상황에서는 오류를 처리해야 한다.
이 글에서는 Python 3 기준으로 다음 내용을 정리한다.
input()과 print()pickletry-exceptelse, finallyraisewithinput()터미널에서 사용자에게 값을 입력받을 때 input()을 사용할 수 있다.
name = input(
"이름을 입력하세요: "
)
print(
f"안녕하세요, "
f"{name}님."
)
input()의 반환값은 항상 문자열이다.
age = input("나이: ")
print(type(age))
<class 'str'>
숫자로 사용하려면 변환이 필요하다.
age = int(
input("나이: ")
)
print(age + 1)
숫자가 아닌 값을 입력하면 ValueError가 발생할 수 있다.
print()name = "kim"
score = 92
print(name)
print(score)
여러 값:
print(
name,
score,
)
구분자 변경:
print(
"python",
"sql",
"api",
sep=" | ",
)
줄바꿈 대신 다른 문자열:
print(
"loading",
end="...",
)
print("done")
user = "kim"
score = 92.345
print(
f"{user}: "
f"{score:.1f}점"
)
천 단위 쉼표:
price = 1234567
print(
f"{price:,}원"
)
기본적인 파일 쓰기는 open()으로 할 수 있다.
file = open(
"result.txt",
"w",
encoding="utf-8",
)
file.write(
"Python 파일 저장 테스트"
)
file.close()
하지만 직접 close()를 호출하는 방식보다는 with 문을 사용하는 편이 안전하다.
with open()을 사용하는 이유with open(
"result.txt",
"w",
encoding="utf-8",
) as file:
file.write(
"Python 파일 저장 테스트"
)
블록을 빠져나오면 파일이 자동으로 닫힌다.
예외가 발생하더라도 자원 정리가 보장되기 쉬워진다.
| 모드 | 의미 |
|---|---|
"r" | 읽기 |
"w" | 새로 쓰기, 기존 내용 덮어쓰기 |
"a" | 기존 내용 뒤에 추가 |
"x" | 파일이 없을 때 새 파일 생성 |
"b" | 바이너리 모드 |
"t" | 텍스트 모드 |
전체 읽기:
with open(
"result.txt",
"r",
encoding="utf-8",
) as file:
content = file.read()
print(content)
큰 파일은 전체를 한꺼번에 읽지 않고 줄 단위로 처리하는 편이 좋다.
with open(
"access.log",
"r",
encoding="utf-8",
) as file:
for line in file:
print(
line.rstrip()
)
로그 분석에서 자주 볼 수 있는 패턴이다.
pathlib로 파일 다루기경로를 문자열로만 다루기보다 pathlib.Path를 활용하면 편한 경우가 많다.
from pathlib import Path
path = (
Path("data")
/ "users.txt"
)
파일 읽기:
content = path.read_text(
encoding="utf-8"
)
파일 쓰기:
path.write_text(
"hello",
encoding="utf-8",
)
Python 3의 str은 Unicode 문자열이다.
message = "안녕하세요 👋"
print(type(message))
<class 'str'>
파일이나 네트워크 경계에서는 문자열을 어떤 바이트 형식으로 저장할지 결정해야 한다.
텍스트 파일을 읽고 쓸 때는 인코딩을 명시하는 습관이 좋다.
with open(
"message.txt",
"w",
encoding="utf-8",
) as file:
file.write(
"안녕하세요"
)
문자열:
text = "Python"
인코딩:
data = text.encode(
"utf-8"
)
print(data)
b'Python'
디코딩:
restored = data.decode(
"utf-8"
)
네트워크 통신과 바이너리 파일을 다룰 때 str과 bytes의 차이를 이해해야 한다.
웹 서비스와 데이터 처리에서는 JSON을 자주 사용한다.
import json
user = {
"name": "kim",
"age": 30,
}
쓰기:
with open(
"user.json",
"w",
encoding="utf-8",
) as file:
json.dump(
user,
file,
ensure_ascii=False,
indent=4,
)
읽기:
with open(
"user.json",
"r",
encoding="utf-8",
) as file:
user = json.load(file)
ensure_ascii=False를 사용하면 한글이 읽기 좋은 형태로 저장된다.
picklePython 객체를 그대로 직렬화하고 복원할 때 pickle을 사용할 수 있다.
import pickle
data = {
"name": "kim",
"skills": [
"python",
"sql",
],
}
쓰기:
with open(
"user.pkl",
"wb",
) as file:
pickle.dump(
data,
file,
)
읽기:
with open(
"user.pkl",
"rb",
) as file:
restored = (
pickle.load(file)
)
pickle 파일은 열지 말자pickle은 단순 데이터 포맷이 아니다.
역직렬화 과정에서 코드 실행으로 이어질 수 있으므로 인터넷에서 받은 파일이나 사용자 업로드처럼 출처를 신뢰할 수 없는 pickle 데이터를 pickle.load()로 열면 안 된다.
외부 시스템과 데이터를 교환할 때는 JSON 같은 형식을 우선 고려하는 편이 안전하다.
문법 자체가 잘못된 경우:
if True
print("hello")
SyntaxError가 발생한다.
문법은 맞지만 실행 중 문제가 생길 수도 있다.
number = int("hello")
이 경우 ValueError가 발생한다.
이런 실행 중 예외 상황을 try-except로 처리할 수 있다.
try-excepttry:
age = int(
input("나이: ")
)
except ValueError:
print(
"숫자를 입력해야 합니다."
)
오류 가능성이 있는 코드는 try에 두고, 처리 가능한 예외는 except에서 지정한다.
try:
number = int("abc")
except ValueError as error:
print(error)
예외 객체에는 문제를 파악하는 데 도움이 되는 정보가 들어 있다.
try:
value = input(
"숫자: "
)
number = int(value)
print(
100 / number
)
except ValueError:
print(
"정수를 입력해 주세요."
)
except ZeroDivisionError:
print(
"0으로 나눌 수 없습니다."
)
처리할 수 있는 구체적인 예외를 지정하는 것이 좋다.
try:
...
except (
ValueError,
TypeError,
) as error:
print(error)
else예외가 발생하지 않았을 때만 실행된다.
try:
number = int(
input("숫자: ")
)
except ValueError:
print("잘못된 입력")
else:
print(
f"정상 입력: "
f"{number}"
)
성공 경로를 try 블록 밖으로 분리할 수 있다.
finally예외 발생 여부와 관계없이 실행해야 할 코드를 둔다.
try:
print("작업 시작")
except Exception:
print("오류 처리")
finally:
print("정리 작업")
파일 같은 자원은 보통 with를 사용하는 편이 더 간결하다.
raise프로그램이 직접 예외를 발생시켜야 할 때 사용한다.
def withdraw(
balance,
amount,
):
if amount <= 0:
raise ValueError(
"출금 금액은 "
"0보다 커야 합니다."
)
if amount > balance:
raise ValueError(
"잔액이 부족합니다."
)
return (
balance
- amount
)
호출:
try:
balance = withdraw(
10000,
15000,
)
except ValueError as error:
print(error)
도메인 의미를 더 명확하게 표현할 수 있다.
class InsufficientBalanceError(
Exception
):
pass
def withdraw(
balance,
amount,
):
if amount > balance:
raise (
InsufficientBalanceError(
"잔액이 부족합니다."
)
)
return (
balance
- amount
)
로그만 남기고 상위 코드가 계속 처리하도록 할 수도 있다.
try:
...
except ValueError:
print(
"입력 처리 실패"
)
raise
raise만 작성하면 현재 예외를 다시 발생시킨다.
from pathlib import Path
path = Path(
"config.json"
)
try:
content = path.read_text(
encoding="utf-8"
)
except FileNotFoundError:
print(
"설정 파일이 없습니다."
)
파일 작업에서 자주 만나는 예외:
FileNotFoundErrorPermissionErrorUnicodeDecodeErrorOSErrorPython에서는 먼저 모든 조건을 검사하기보다 일단 실행하고 문제가 생기면 예외를 처리하는 스타일을 자주 볼 수 있다.
예:
try:
name = user["name"]
except KeyError:
name = "unknown"
반대로 먼저 검사할 수도 있다.
if "name" in user:
name = user["name"]
else:
name = "unknown"
상황에 따라 더 읽기 쉬운 방식을 선택하면 된다.
import json
from pathlib import Path
CONFIG_PATH = Path(
"config.json"
)
def load_config():
try:
content = (
CONFIG_PATH
.read_text(
encoding="utf-8"
)
)
return json.loads(
content
)
except FileNotFoundError:
print(
"config.json 파일이 "
"없습니다."
)
return {}
except (
json.JSONDecodeError
) as error:
print(
f"JSON 형식 오류: "
f"{error}"
)
return {}
사용:
config = load_config()
api_url = config.get(
"api_url",
"http://localhost:8000",
)
print(api_url)
이 예제에는 파일 읽기, UTF-8, JSON 파싱, 예외 처리, 기본값 사용이 함께 들어 있다.
try:
...
except:
pass
문제가 발생했다는 사실 자체가 사라져 디버깅이 어려워진다.
trytry:
# 서로 다른 작업 수십 줄
...
except ValueError:
...
어디서 문제가 발생했는지 파악하기 어려울 수 있다.
가능하면 예외 가능성이 있는 범위를 좁게 잡는 편이 좋다.
input()으로 받고 결과는 문자열이다.with open(..., encoding="utf-8") 패턴이 안전하고 읽기 쉽다.str은 Unicode 문자열이며 파일과 네트워크 경계에서는 인코딩을 이해해야 한다.pickle은 편리하지만 신뢰할 수 없는 데이터를 역직렬화하면 위험할 수 있다.try-except는 실행 중 발생할 수 있는 예외를 처리한다.else는 예외가 없을 때, finally는 예외 여부와 관계없이 실행된다.raise를 사용하면 프로그램 규칙 위반을 명시적인 예외로 표현할 수 있다.