본문 바로가기

Python/자료구조 (Data Structure)

4. 구조와 모듈 (Structure and Module): 파이썬 자료구조와 알고리즘

모듈

파이썬에서 모듈 (module) 은 def 를 사용하여 정의한다. def가 실행되면, 함수의 객체와 참조가 같이 생성되며, 반환값을 정의하지 않으면 자동으로 None을 반환하도록 한다 (None은 다른 언어들의 null 과 같은 개념이다). C언어처럼 아무것도 반환하지 않는 함수는 프로시저 (procedure) 라고 부른다.

스택과 활성화 레코드

함수가 호출될 때마다 활성화 레코드 (activation record) 가 생성된다. 활성화 레코드에는 함수의 정보 (반환값, 매개변수, 지역 변수, 반환 주소 등) 가 기록되며 이를 스택 (stack) 에 저장한다. 이는 다음 순서로 처리된다.

  1. 함수의 실제 매개변수를 스택에 저장 (push) 한다.
  2. 반환 주소를 스택에 저장한다.
  3. 스택의 최상위 인덱스를 함수의 지역 변수에 필요한 총량만큼 늘린다.
  4. 함수로 건너뛴다 (jump).

활성화 레코드를 풀어내는 (unwinding) 절차는 다음과 같다.

  1. 스택의 최상위 인덱스는 함수에 소비된 총 메모리양 (지역 변수) 만큼 감소한다.
  2. 반환 주소를 스택에서 빼낸다 (pop).
  3. 스택의 최상위 인덱스는 함수의 실제 매개변수만큼 감소한다.

모듈의 기본값

모듈을 생성할 때, 함수 또는 메서드에서 가변 객체를 기본값으로 사용해선 안된다. 예를들어

>>> def append(number, number_list = []):
    number_list.append(number)
    return number_list

>>> append(5)
[5]
>>> append(6)
[5, 6]
>>> append(7)
[5, 6, 7]

위를 보면 이상하다. 기본값을 빈 배열로 해주었는데 왜 저렇게 되는 것일까? 그 이유는 빈 배열을 선언할 때 하나의 객체로써 선언이 되기 때문이다. 따라서 이를 이렇게 바꾸어 주어야 한다.

>>> def append(number, number_list = None):
    if number_list == None:
        number_list = []
    number_list.append(number)
    return number_list

>>> append(5)
[5]
>>> append(6)
[6]
>>> append(7)
[7]

__init__.py 파일

패키지 (package) 는 모듈과 __init__.py 파일이 있는 디렉터리다. 파이썬은 __init__.py 파일이 있는 디렉터리를 패키지로 취급하며, 이는 흔한 이름의 디렉터리에 유효한 모듈이 들어 있지만, 이가 검색되지 않는 것을 방지하기 위해서이다.

패키지의 모듈은 다음과 같이 불러온다.

import 폴더이름.파일모듈명

__init__.py 파일은 빈 파일일 수도 있지만, 패키지의 초기화 코드를 실행시키거나 __all__.py 변수를 정의할 수도 있다.

__all__.py = ["파일1", ...]

실제 파일 이름은 확장자가 .py지만, 여기서 작성할 때는 .py를 붙이지 않는다.

from 폴더이름 import *

위 코드는 이름이 __로 시작하는 모듈을 제외한 모든 모듈의 객체들을 불러온다. __all__.py 변수가 있는 경우, 해당 리스트의 객체를 불러온다.

__name__ 변수

파이썬은 모듈을 가져올 때마다 __name__ 이라는 변수를 만들고, 모듈 이름을 저장한다. IDLE 또는 .py 파일의 직접 실행은 모듈 이름을 __main__ 으로 설정한다. 따라서 IDLE에 이를 프린트해 보면 아래와 같이 뜬다.

>>> print(__name__)
__main__

컴파일된 파이트코드 모듈

컴파일러가 사용하는 바이트 컴파일 코드 (byte-compiled code) 는 표준 모듈을 많이 사용하는 프로그램의 시작시간 (로딩 시간) 을 줄여준다.

-0 플래그 (flag) 를 사용하여 파이썬 인터프리터를 호출하면, 최적화된 코드가 생성되며 .pyo 파일에 저장된다 (3.5 부터는 .pyc 파일). 이렇게 만든 파일은 리버스 엔지니어링이 까다로워 라이브러리로 배포하는 데에도 사용될 수 있다.

sys 모듈

sys.path는 인터프리터가 모듈을 검색할 경로를 담은 문자열 리스트이다. sys.path 변수는 PYTHONPATH 환경변수 또는 내장된 기본값 경로로 초기화된다. 환경변수를 수정하면 모듈 경로를 추가하거나 임시로 모듈 경로를 추가할 수 있다.

참고로 sys.ps1과 sys.ps2 는 각각 파아씬 대화식 인터프리터의 기본 및 보조 프롬프트 문자열을 정의한다 (알다시피 기본값은 >>>과 ...이다).

또한, sys.argv 변수는 명령줄에 전달된 인수를 프로그램 내에서도 사용할 수 있다.

dir(sys) 함수를 사용하면 모듈이 정의하는 모든 유형의 이름 (모듈, 변수, 함수) 을 찾는다. 이름 기준으로 정렬된 문자열 리스트를 반환한다.

제어문

if, elif, else 문

조건문이다. 다른 언어의 switch, case문 또는 if, else if, else 를 대체한다. 안에 참, 거짓 이외에도 문자, 숫자 등등이 들어갈 수 있다. None, 숫자 0, 문자 '', 빈 배열 [], 등등은 False 값이며, 이외의 값들은 Truth 값이다.

>>> def iffy(x):
    if x:
        print(True)

>>> iffy(True)
True
>>> iffy('hello')
True
>>> iffy('')
>>> iffy(1)
True
>>> iffy(0)
>>> iffy([])
>>> iffy([1])
True
>>> iffy({})
>>> iffy(())

for 문

반복문이다. 그러나 다른 언어와 살짝 다르다. 파이썬의 for문은 반복 가능한 (iterable) 시퀀스 (sequence) 항목 (리스트, 문자열 등)을 순서대로 순회한다.

>>> a = [1, 2, 3, 4]
>>> for num in a:
    print(num)

1
2
3
4

참과 거짓

거짓(False) 은 불리언 False, 숫자 0, 특수 객체 None, 빈 컬렉션 또는 시퀀스 (문자열 '', 리스트 [], 튜플 (), 딕셔너리 {}) 로 정의된다. 참 (True) 은 이 이외의 모든 객체이다. 비교 또는 다른 불리언 식의 결과를 변수에 할당할 수 있다.

and, or은 다른 언어들의 && 와 || 의 파이썬 버전이다. 아래 예제를 보자.

>>> 'hello' and True
True
>>> True and 'hello'
'hello'
>>> False and 'hello'
False
>>> '' and True
''
>>> 'hello' or True
'hello'
>>> True or 'hello'
True
>>> False or 'hello'
'hello'
>>> False or ''
''

and 와 or 는 왼쪽과 오른쪽 두 객체의 참과 거짓이 비교된다. 먼저 and 는 왼쪽이 참이라면, 오른쪽을 본다. 만약 오른쪽도 참이라면, 오른쪽을 반환한다. 만약 왼쪽이 거짓이라면, 왼쪽의 거짓을 먼저 반환한다. or 는 반대이다. 왼쪽이 참이라면, 왼쪽을 반환한다. 만약 왼쪽이 거짓이라면, 오른쪽을 본다. 오른쪽이 참이라면, 오른쪽을 반환한다. 당연히도, 둘다 거짓이라면 오른쪽의 거짓을 반환한다.

구글의 파이썬 스타일 가이드에서는 암묵적 (implicit) False 사용에 대한 다음과 같은 기준을 세워뒀다. 참고하자.

  • == 또는 != 연산자를 사용해 내장 변수 None 같은 싱글턴 (singleton) 을 비교하지 않는다. 대신 is 나 is not 을 사용한다.
  • if x is not None 과 if x 를 잘 구분하여 사용한다.
  • == 를 사용하여 불리언 변수를 False 와 비교하지 않는다. 대신 if not x 를 사용한다. None 과 False 를 구분할 필요가 있을 때는 if not x and is not None 과 같은 연결 표현식을 사용한다.
  • 시퀀스 (문자열, 튜플, 리스트) 의 경우, 빈 시퀀스는 False 이다. 따라서 굳이 if len(시퀀스) 또는 if not len(시퀀스) 를 쓰기 보단, 그냥 if 시퀀스 또는 if not 시퀀스를 사용해 준다.
  • 정수를 처리할 때 뜻하지 않게 None 을 0 으로 잘못 처리하는 것처럼, 암묵적인 False 의 사용은 위험하다.

return 과 yield

파이썬에서 제너레이터 (generator) 는 이터레이터 (iterator) 를 작성하는 편리한 방법이다. 객체에 __iter__() 와 __next__() 메서드를 둘 다 정의하면 이터레이터 프로토콜을 구현한 것이다. 이 때 yield 키워드를 사용하면 편리하다.

호출자가 메서드를 호출할 때, return 키워드는 반환값을 반환하고 메서드를 종료한 후, 호출자에게 제어를 반환한다. 반면 yield 키워드는 각 반환값을 호출자에게 반환하며 반환값이 모두 소진된 후에야 메서드가 종료된다.

yield 키워드는 제너레이터 맥락에서 이터레이터를 만드는 아주 강력한 도구다. 제너레이터는 최종값을 반환하지만, 이터레이터는 yield 키워드를 사용하여 코드 실행 중 값을 반환한다. 즉 __next__() 메서드를 호출할 때마다 어떤 값 하나를 추출한 후 해당 yield 표현식의 값을 반환한다. 그리고 이는 StopIteration 에러가 발생할 때까지 값을 반환한다.

제너레이터는 매우 강력하고 효율적이다. 시퀀스를 반환하거나 반복문을 사용하는 함수를 다루는 경우에 제너레이터를 고려해 볼 수 있다. 아래는 피보나치 수열 제너레이터이다.

>>> def fib_generator():
    a,b = 0,1
    while True:
        yield b
        a,b = b,a+b

>>> fib = fib_generator()
>>> next(fib)
1
>>> next(fib)
1
>>> next(fib)
2
>>> next(fib)
3
>>> next(fib)
5

break 와 continue

반복문 (for과 while) 에서 break 키워드는 반복문을 빠져나가도록 한다. continue 는 해당 단계를 건너 뛰고 다음 단계로 전환한다.

반복문에는 else 문을 사용할 수 있다. 이는 반복문이 종료되었을 때 실행되지만, break 로 반복문이 종료되는 경우에는 실행되지 않는다.

>>> for i in range(0,3):
    print(i)
else:
    print('for loop finished!')

0
1
2
for loop finished!
>>> for i in range(0,3):
    print(i)
    if i == 2:
        break
else:
    print('for loop finished!')

0
1
2

range( )

range(low, up, step) 메서드는 숫자 low 와 숫자 up 사이의 step 간격의 숫자 리스트를 생성한다.

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(2,5))
[2, 3, 4]
>>> list(range(0,10,2))
[0, 2, 4, 6, 8]

enumerate( )

enumerate(iterable, start) 메서드는 반복 가능한 객체 iterable 의 인덱스 값과 항목 값의 튜플을 인덱스 start 부터 시작하여 반환한다. start의 기본값은 0이다.

>>> list(enumerate(['hello','world!']))
[(0, 'hello'), (1, 'world!')]

zip( )

zip(iterables...) 메서드는 2개 이상의 시퀀스를 인수로 취하여 짧은 길이의 시퀀스를 기준으로 각 항목이 순서대로 1:1 대응하는 새로운 튜플 시퀀스를 만든다.

>>> a = ['one','two','three','four','five']
>>> b = '1234'
>>> list(zip(a,b))
[('one', '1'), ('two', '2'), ('three', '3'), ('four', '4')]

filter( )

filter() 메서드는 시퀀스의 항목들 중 함수 조건이 참인 항목만 추출한 시퀀스를 반환한다.

>>> def hello(x):
    if x == 'hello':
        return True
    return False

>>> a = ['hello','hello','hello','bye','hello','bye','bye']
>>> list(filter(hello, a))
['hello', 'hello', 'hello', 'hello']

map( )

map(func, list) 메서드는 시퀀스의 모든 항목에 함수를 적용한 결과 리스트를 반환한다.

>>> def square(x):
    return x**2

>>> list(map(square, [1,2,3,4]))
[1, 4, 9, 16]

람다 함수

람다 (lambda) 함수를 사용하면 코드 내에서 함수를 간결하게 (compact) 동적으로 사용할 수 있다.

>>> def area(b, h):
    return b*h

>>> area(2,5)
10
>>> triangle_area = lambda b, h: 0.5 * b * h
>>> triangle_area(2,5)
5.0

파일 처리

파일 처리 메서드

open( )

open(filename, mode, encoding) 메서드는 파일 객체를 반환한다. 모드 (mode) 와 인코딩 (encoding) 은 옵션이며, 생략하면 텍스트 읽기 모드와 시스템 기본 형식 인코딩이 적용된다. 모드는 문자열로 지정하며, 종류는 다음과 같다.

  • r: read, 읽기 모드
  • w: write, 쓰기 모드 (같은 이름의 파일이 존재한다면 그 파일을 지우고 내용을 새로 쓴다.)
  • a: append,추가 모드 (같은 이름의 파일이 존재한다면 그 파일의 끝에 내용을 추가한다.)
  • r+: 읽기와 쓰기 모드
  • t: 텍스트 모드
  • b: 바이너리 (binary) 모드

read( )

f.read(size) 메서드는 f 파일에서 size 만큼의 내용을 읽고 문자열로 반환한다. size는 정수로 지정하며, 선택적 인수다. 인수가 생략되거나 음수이면, 전체 파일의 내용을 읽고 반환한다.

readline( )

f.readline() 메서드는 파일에서 한 줄을 읽는다. 개행 문자는 문자열의 끝에 남으며, 파일의 마지막 행에서만 생략된다. 이 때문에 반환 값이 모호해지는 문제가 있다.

readlines( )

f.readlines(size) 메서드는 파일의 모든 데이터 행을 포함한 리스트를 반환한다. size 는 파일에서 해당 바이트 수만큼을 읽고, 한행을 완성하는 데 필요한 만큼 더 읽어서 반환한다. readlines() 메서드는 메모리에 전체 파일을 불러올 필요 없이 줄 단위로 효율적으로 읽을 수 있으며, 완전한 행을 반환한다.

write( )

데이터를 파일에 쓴다. 바이너리 모드에서는 바이트 또는 바이트 배열 객체를 쓰고, 텍스트 모드에서는 문자열 객체를 쓴다.

tell( ), seek( )

tell() 메서드는 파일의 현재 위치를 나타내는 정수를 반환한다.

seek(offset, from-what) 메서드는 파일 내 탐색 위치를 변경할 때 사용한다. 파일 위치는 기준이 되는 참조 포인트, from-what에 오프셋 offset을 더한 값으로 계산된다. from-what 인수를 0으로 지정하면 기준이 파일의 처음 위치가 되고, 1은 파일의 현재 위치, 2는 파일의 마지막 위치를 기준으로 삼게 된다.

close( )

파일을 닫고, 열린 파일이 차지하는 시스템 자원을 해체한다 (free up). 파일을 성공적으로 닫으면 True를 반환한다.

input( )

input() 함수는 사용자의 입력을 받는다. 콘솔에 출력될 문자열을 선택적으로 지정할 수 있다. 사용자가 입력을 하고 엔터를 누르면 해당 문자열을 반환한다.

peek( )

peek(n) 메서드는 파일 포인터 위치를 이동하지 않고 n 바이트를 반환한다.

fileno( )

파일 서술자 (descriptor) 를 반환한다.

shitil 모듈

shutil 모듈은 시스템에서 파일을 조작할 때 유용하게 쓰인다.

pickle 모듈

pickle 모듈은 파이썬 객체를 불러와 문자열 표현으로 변환한다. 이 과정을 피클링 (pickling) 또는 직렬화 (serialization) 라고 한다. 반대로, 문자열 표현을 객체로 재구성하는 것을 언피클링 (unpickling) 또는 역직렬화 (deserialization) 라고 한다.

>>> import pickle
>>> x = {}
>>> x["name"] = 'beom'
>>> x["age"] = 23
>>> with open("name.pkl", "wb") as f:
    pickle.dump(x,f)

>>> with open("name.pkl", "rb") as f:
    name = pickle.load(f)

>>> name
{'name': 'beom', 'age': 23}

struct 모듈

struct 모듈을 사용하면 파이썬 객체를 이진 표현으로 변환하거나 그 반대를 할 수 있다. 객체는 특정 길이의 문자열만 처리할 수 있다.

오류 처리

파이썬 코드를 컴파일할 때, 발생할 수 있는 두가지 종류의 오류가 있다. 구문 오류 (syntax error) 와 예외 (exception) 이다. 구문 오류는 구문 분석 (parsing) 시 오류가 날 때 생기는 오류이며, 이 경우 컴파일이 아예 되지 않는다. 예외는 실행 중 발견되는 오류로, 무조건적으로 치명적인 것은 아니다. 예외는 실행 중에야 발견되기 때문에 신중하게 처리해야 한다.

예외 처리

예외가 발생했는데 이를 코드 내에서 처리하지 않으면 파이썬은 예외의 오류 메시지와 트레이스백 (역추적, traceback) 을 출력한다. 트레이스백은 처리되지 않은 예외가 발생한 지점에서 호출 스택 맨 위까지 수행된 모든 호출 목록을 포함한다. 파이썬에선 try-except-finally 문으로 예측 가능한 예외를 처리할 수 있다.

try 문의 예외 발생이 예측되는 코드가 예외를 발생시키지 않고 실행되면, except 문은 건너 뛴다. try 문 블록에서 예외가 발생했다면, 해당 예외의 except 문으로 건너뛰어 예외 처리 코드를 실행시킨다. 따라서 try문 블록에서 예외가 발생된 코드 이후의 코드는 실행되지 않는다.

>>> while True:
    try:
        x = int(input("숫자를 입력해 주세요: "))
    except ValueError:
        print("숫자가 아닙니다. 다시 입력해 주세요!")


숫자를 입력해 주세요: 2
숫자를 입력해 주세요: 4
숫자를 입력해 주세요: hello
숫자가 아닙니다. 다시 입력해 주세요!
숫자를 입력해 주세요: True
숫자가 아닙니다. 다시 입력해 주세요!
숫자를 입력해 주세요: -
숫자가 아닙니다. 다시 입력해 주세요!

구글 파이썬 스타일 가이드의 예외 처리 방식

  • raise MyError('오류 메시지') 또는 raise MyError 와 같이 예외를 발생시킨다. 두 개의 인수 형식을 사용하지 않는다. (즉, raise MyError, '오류 메시지' 와 같이 두 개의 인수 형식을 사용하지 않는다.
  • 내장 예외 클래스를 적절하게 사용한다. 예) ValueError. 여기서, 공개 API의 인수 값을 확인할 때 assert 문을 사용하지 않는다. assert 문은 정확한 사용법의 강요나 예상치 못한 이벤트가 아닌, 내부적으로 정확성을 보장하기 위해 사용한다.
  • 라이브러리 또는 패키지에 따라 자체적 예외를 정의한다. 이 경우 내장 Exception 클래스를 상속한다. 예외 이름은 Error로 끝나야하며, foo.FooError 같이 foo.가 붙으면 안된다.
  • 모든 예외를 처리하는 (catch-all) except:, except Exception, except StandardError 문을 사용하지 않는다. 예외를 다시 발생시키려 하거나 코드의 가장 바깥쪽 블록이 아니라면 말이다. 파이썬은 이 점에 대해 관용적이라 except: 문은 오탈자, sys.exit() 호출, Ctrl+C 인터럽트, 단위 테스트 실패 등 처리를 원하는 게 아닌 모든 종류의 예외까지 처리해버린다.
  • try/except 문 내의 코드 양을 최소화한다. try문에 걸리는 코드가 많을 수록 예외를 발생시키지 않을 것으로 예상한 코드 줄에서 예외가 발생할 확률도 높아져 실제 오류를 발견하기 어려워진다.
  • try 문에서 예외가 발생하는지 여부에 관계없이 finally 문을 꼭 사용한다. 이렇게 하면 파일 닫기와 같이 자원을 정리 (cleanup) 하는 데 유용하다.
  • 예외를 처리할 때는 쉼표 대신 as 를 사용한다.

출처

파이썬 자료구조와 알고리즘 - 한빛미디어, 미아 스타인