Code › codeit-ai-sprint
파이썬 파일 입출력과 OOP 기본기
코드잇 스프린트 본과정 두 번째 수업에서 정리한 파이썬 시퀀스, 파일 입출력, set, OOP 기본
코드잇 스프린트 본과정 두 번째 수업에서는 파이썬 기본 문법을 살펴보였다. 문자열과 튜플 같은 시퀀스 타입, 파일 입출력, set을 활용한 중복 제거, 그리고 객체 지향 프로그래밍까지 살펴보았고 강의중에 기억에 남는 부분이나 정리하고 싶었던 부분 위주로 정리하였다.
코랩에서 변수는 계속 살아 있다
수업 중 Colab에서 실습하다가 변수가 계속 메모리에 남아 있어서 에러가 발생한 상황이 있었는데, Jupiter Notebook 환경에서는 셀 안의 내용을 한 번만 실행하는 것이 아니라, 중간 셀을 여러 번 실행하거나 순서를 바꿔 실행할 수 있다. 그래서 이전 셀에서 만든 변수나 딕셔너리, 리스트가 커널 메모리에 남아 다음 실행에 영향을 줄 수 있다.
이럴 때는 런타임을 재시작하거나, 필요한 변수를 명확히 초기화해야 한다.
문자열과 튜플도 시퀀스다
파이썬에서는 문자열도 시퀀스(sequence) 타입이다. 좀 더 찾아보니 시퀀스는 파이썬 내에서 명시적으로 정의된 추상클래스인데 Indexing, Slicing, in 연산자, len, for문 지원까지 모두 가능하다고 한다. Python 공식 문서에서도 리스트와 문자열은 시퀀스 타입이라고 설명하고, 그래서 문자열도 인덱스로 접근하거나 일부 구간을 잘라낼 수도 있다.
text = "Python"
print(text[0]) # P
print(text[1:4]) # yth
튜플도 시퀀스 타입이다. 리스트와 비슷하게 여러 값을 순서대로 담지만, 한 번 만들어진 뒤에는 요소를 바꿀 수 없다. 공식 문서에서는 튜플을 immutable이라고 설명하고, 리스트는 mutable이라고 구분한다. 즉 리스트는 값을 바꿀 수 있지만, 튜플은 같은 위치의 값을 다른 값으로 교체할 수 없다.
point = (3, 5)
# point[0] = 10 # TypeError
튜플 언패킹은 여러 값을 묶어서 튜플로 만들고, 다시 여러 변수에 나누어 담을 수 있다.
point = (3, 5)
x, y = point
파일 입출력과 with문
오늘 실습에서는 단어장을 파일에서 읽어오는 코드를 작성했다. 이때 중요한 부분은 파일을 열고 나면 반드시 닫아야 한다는 점이었다. 파일을 닫지 않으면 운영체제 자원을 계속 붙잡고 있을 수 있고, 쓰기 작업에서는 메모리 버퍼에는 내용이 써졌지만 실제 파일에는 반영되지 않을 수도 있다.
파이썬에서는 with문을 사용하면 이 처리를 안전하게 할 수 있는데, with 블록 안에서 파일을 사용하고, 블록을 빠져나가면 파일이 자동으로 정리된다.
with open("vocabulary.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())
Python 공식 문서도 파일을 다룰 때 with 키워드를 사용하는 것이 좋다고 설명한다. 블록이 끝나면 파일이 올바르게 닫히고, 예외가 발생해도 정리 과정이 실행되기 때문이다. 직접 close를 호출하는 방식보다 실수할 여지가 적다.
그리고 인코딩에 대해서, 코랩에서는 인코딩 없이 실행해도 큰 문제 없었는데 코드잇 강의에서 문제를 풀 때에는 인코딩 문제가 발생했다. 운영체제나 실행 환경마다 기본 인코딩이 다를 수 있기 때문에 유니코드가 들어간 파일을 읽고 쓸 때 encoding을 명시하지 않으면 에러가 날 수 있다.
인코딩 방식에 대해서 조금 더 살펴보면, 유니코드가 실제로 정의된 표에 있는 코드 포인트로 바뀌고 이후 컴퓨터가 읽을 수 있는 바이트로 파일에 써져야 하는데, 이 때 코드 포인트를 어떤 크기의 바이트 조각으로 조개서 저장할지, 혹은 불러올지에 대한 규칙이 UTF-8 혹은 UTF-16등이다. UTF-8은 알파벳, 숫자, 특수문자는 1바이트만 쓰고 한글, 한자, 아랍어 등은 3바이트를 쓰고 이모지는 4바이트를 쓰는 식이며 UTF-16은 알파벳과 숫자, 한글, 한자를 모두 2바이트를 쓰고 이모지는 4바이트를 쓰는 식이다. 그래서 결국 이 규칙이 다르면 제대로 된 파일 입출력이 될 수 없기 떄문에 에러가 발생하는 것이다.
아마도 코랩에서는 요즘 대부분 맞춰서 쓰는 UTF-8로 시스템 기본값이 세팅되어 있어서 에러가 안났을 수도 있고, 코드잇에서는 내부 서버의 시스템 기본 인코딩 값과 미리 주어진 vocabulary.txt 파일의 인코딩 방식이 맞지 않아서 에러가 발생했던 상황인 것이다.
이제 실습쪽으로 넘어가보자. 고급 단어장 실습 코드는 대략 이런 흐름이었다.
import random
vocab = {}
with open("vocabulary.txt", "r", encoding="utf-8") as f:
for line in f:
eng, kor = line.strip().split(":")
vocab[eng] = kor
keys = list(vocab.keys())
while True:
eng = keys[random.randint(0, len(keys) - 1)]
answer = input(f"{vocab[eng]}: ")
if answer == "q":
break
if answer == eng:
print("맞았습니다.")
else:
print(f"정답은 {eng}")
여기서 line.strip().split(”:“)로 줄 끝의 whitespace인 개행문자를 제거하고, 콜론을 기준으로 영어와 한국어를 나누었다. split을 인자 없이 쓰면 공백을 기준으로 나누는데, 이때 공백에는 일반 띄어쓰기뿐 아니라 줄바꿈(\n), 탭(\t)도 포함된다. 그래서 파일에서 읽은 문자열을 다룰 때는 strip과 split의 기준을 잘 파악하고 처리해야 된다.
set은 중복 제거에는 좋지만 정렬은 아니다
리스트 중복 제거 문제 연습문제를 풀었는데, 파이썬의 set 특징이 궁금해졌다.
nums = [1, 2, 3, 2, 4, 1, 5, 3]
num_set = set(nums)
num_list = list(num_set)
print(num_list)
여기서 리스트를 set으로 바꾸면 중복은 제거된다. 하지만 set은 정렬된 자료구조가 아니었다. 지금은 양수만 있어서 set을 순회하는 순서가 얼추 맞아떨어질 수 있는데, 음수값이 들어가거나 문자열로 들어가면 정렬이 보장이 되지 않는다. 실제로 해시값을 보면 음수는 아주 큰 숫자가 더해져서 해시테이블에 들어가게 되는데 이 경우 음수가 양수의 뒤쪽에 위치하게 되기 때문이다. 결과적으로 Python 공식 문서도 set을 중복 요소가 없는 unordered collection이라고 설명하기 떄문에 set을 다시 list로 바꾼 결과의 순서에 기대면 안되고 정렬을 해야한다.
그래서 문제에서 정렬된 새로운 리스트를 요구한다면 마지막에 sorted를 써야 한다.
nums = [1, 2, 3, 2, 4, 1, 5, 3]
result = sorted(set(nums))
print(result)
self, 인스턴스 변수, 클래스 변수
마지막으로 객체 지향 프로그래밍도 조금 다뤘다. 간략하게 self가 무엇인지와 인스턴스 변수, 클래스 변수의 차이는 따로 정리해두고 싶었다.
파이썬에서 인스턴스 메서드의 첫 번째 매개변수는 보통 self라고 쓴다. 메서드를 호출할 때 사람이 직접 self를 넘기지는 않지만, 실제로는 메서드를 호출한 객체 자신이 첫 번째 인자로 전달된다.
class User:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, {self.name}")
user = User("Sean")
user.greet()
여기서 self.name은 각 객체가 따로 들고 있는 인스턴스 변수다. user라는 객체 안에 name이 저장되는 것이다. 반대로 클래스 변수는 클래스 자체에 붙어 있고, 모든 인스턴스가 공유해서 볼 수 있다.
class User:
count = 0
def __init__(self, name):
self.name = name
User.count += 1
이 예제에서 name은 인스턴스마다 다르지만, count는 User 클래스에 속한 값이다. C++을 떠올리면 클래스 변수는 static 멤버 변수와 비슷한 역할을 한다. 다만 파이썬은 동적으로 속성을 붙일 수 있고, 메서드 호출 방식도 런타임 객체 모델 위에서 동작하기 때문에 C++의 멤버 함수 호출과 완전히 동일하게 보면 헷갈릴 수 있다. 이 부분에 대해서는 좀 길어져서 별도 포스팅을 하는게 좋을 것 같다.
self를 안 받는 메서드도 있다. 대표적으로 정적 메서드(static method)가 있다. 객체 상태나 클래스 상태를 쓰지 않고, 클래스 안에 관련 함수를 묶어두고 싶을 때 사용할 수 있다.
class Math:
@staticmethod
def add(a, b):
return a + b
이 경우 add는 self를 받지 않는다. 객체의 상태를 읽거나 바꾸는 메서드가 아니라, 클래스 안의 namespace로 묶인 함수이다.
두 번째 수업을 마치고
오늘은 파이썬 문법을 넓게 살펴보는 시간이었다. 문자열, 튜플, set, 파일 입출력, 객체 지향까지 한 번에 지나가서 내용은 많았는데, 언어는 왠만하면 비슷해서 오히려 내 기억에 남은 부분은 내부 동작에 대한 의문이었다.
기초 문법을 다시 보면서 느끼는 건, 이미 알고 있던 기능도 왜 그렇게 동작하는지까지 궁금해지기 시작하면 생각보다 알아볼 게 많다는 점이다. 특히 인스턴스 변수와 클래스 변수, 정적 메서드 등에 대한 차이를 C++과 비교하면서 포스팅을 하나 해보면 좋을 것 같다.