5. 파이썬으로 코딩 시작하기(2)

김건희·2022년 12월 14일
0

Fundamental

목록 보기
6/7

5-4. 자료형

여태까지의 예제에서는 f(x) = y에서 변수 x에 들아갈 수 있는 값으로 숫자(0, 1, 2, ...)와 문자열('Hi', 'Hello')를 사용했습니다.

그러나 파이썬에는 숫자와 문자열 외에도 다양한 종류의 '값'들이 있으며, 이러한 종류 하나하나를 자료형(data type) 이라고 합니다.

또한 문자열은 반복이 가능(iterable) 하지만 숫자는 그렇지 않은 것처럼, 각 자료형마다 특징이 있습니다. 자료형 스텝 에서는 이러한 자료형들에 대해 알아볼 것입니다.

참고로 값의 자료형이 무엇인지는 type() 함수를 통해 확인할 수 있습니다. 한번 숫자 1의 자료형이 무엇인지 확인해 봅시다.

print(type(1))
>><class 'int'>

(1) 정수(integer, int)

지금까지 우리가 잘 써온 숫자 데이터에도 사실 두 가지 자료형이 있습니다.

우선 정수인 int는 양의 정수와 음의 정수를 모두 포함합니다.

print(type(1))
>><class 'int'>

단순해 보이지만 여기서 문제가 있습니다.

정수는 무한하지만 우리 컴퓨터의 메모리는 유한하다는 점입니다. 64비트 컴퓨터의 경우 기본적으로 64비트의 공간에 하나의 숫자를 저장할 수 있습니다. 이렇게 되면 2642^{64}
(=18,446,744,073,709,551,616)개의 숫자만 인식할 수 있습니다. 즉, 음과 양을 모두 생각하면 -9,223,372,036,854,775,808부터 9,223,372,036,854,775,807까지의 숫자만을 표현할 수 있다는 뜻입니다.

물론 해결법은 있습니다. 💡

우리가 어릴 때 숫자를 세다가 손가락이 부족하면 발가락까지 쓰던 것처럼, 큰 숫자를 표현할 때는 64비트 이상의 공간을 사용하면 됩니다. 다만 이렇게 숫자를 담을 메모리 크기를 수동으로 설정해 주어야 하는 프로그래밍 언어들이 있는 반면, 우리의 파이썬은 다행히 숫자가 커지면 알아서 메모리를 더 사용하기에 안심해도 됩니다.

아래 예제로 한번 실험해 볼 수 있습니다. 참고로 **는 제곱 연산자입니다.

number = 1000 ** 10
print(number)
>> 1000000000000000000000000000000

(2) 부동소수점 수(floating-point number, float)

이외에 정수를 제외한 실수를 부동소수점 수 또는 떠돌이 소수점 수인 float라고 합니다. 여기서 소수는 1과 자신으로밖에 나누어지지 않는 素數(prime number)가 아니라, 소수점 아래에 값이 있는 小數(decimal)입니다.

print(type(1.1))
>> <class 'float'>

여기서 주의할 점은, 1은 정수이지만 1.0은 소수라는 점입니다.

print(type(1.0))
>> <class 'float'>

'어차피 같은 숫자 아니야?😫' 싶지만 사실 영어에서도 둘을 구분합니다. 영어 문법에서 1은 "1 point"처럼 단수로 쓰고, 1을 제외한 모든 수는 복수로 받아야 하는데, 이는 "0 points", "-1 points"뿐만 아니라 "1.0 points"도 포함합니다. 이건 대학에서 영어 전공한 제 친구들도 몰랐습니다. 물론 파이썬은 똑똑하니까, 값을 비교할 때나 함께 연산할 때는 알아서 자료형을 변환해 줍니다.

print(1 == 1.0)
print(1 + 1.0)
>> True
   2.0

다만 1 + 1.0의 결과와 같이, 정수와 소수를 더하면 그 결과가 정수로 표현될 수 있다고 하더라도 소수 자료형을 가지게 됩니다. 만일 자료형의 국경을 넘어 정수를 소수로, 소수를 정수로 변환하고 싶다면 각각 자료형 이름을 함수처럼 써주면 됩니다.

print(float(1))
print(type(float(1)))
print(int(1.0))
print(type(int(1.0)))
>> 1.0
   <class 'float'>
   1
   <class 'int'>

그럼 1이 정수형이고 1.0이 소수형인 것은 알 수 있을 것 같습니다. 그런데 부동소수점 수(floating point number, 줄여서 float)라는 것은 뭘까요?
이것을 이해하려면 고정소수점 수(fixed point number)라는 것과 함께 이해하면 좋습니다. 고정소수점이든 부동소수점이든 모두 소수를 표현하는 방식일 뿐입니다.

우리는 파이썬이 1.0을 기본적으로 float으로 처리하는 것으로 이미 보았습니다. 파이썬은 소수를 표현하는 방식으로 부동소수점 방식을 채택했습니다.
아래 링크를 통해 두 방식의 차이와, 왜 부동소수점 방식으로 소수를 표현하는 것이 더 유리한지 이해해 봅시다.

부동소수점 코딩 설명

(3) NoneType

print(type(None))
>> <class 'NoneType'>

앞의 함정들과 다르게 NoneType 자료형에 속하는 값은 유한하며, 그마저도 딱 하나입니다. 아무것도 없음을 뜻하는 None이 바로 그 값입니다. 0도 아무것도 없다는 뜻 아니야?라고 생각하시는 분들은 아래 도표를 참조해 주시기 바랍니다.

그렇습니다. 휴지심조차 없는 상태가 None입니다.

불교의 공(空) 개념처럼 심오하게 이해할 필요 없이, 그냥 아무것도 없음을 표시하기 위해 만들어놓은 값입니다. 이전 예시들 중 함수의 반환값으로 아무것도 정의하지 않았을 경우 NoneType과 관련된 오류가 나왔던 것이 바로 이런 상황입니다. 이런 상황에서는 0과 같은 숫자도, 그 무엇도 반환하지 않았다는 것을 알려주기 위해 None이 반환된 것으로 표현합니다. 참고로 다른 프로그래밍 언어에서는 같은 개념을 null이라고 부르기도 합니다.

왜 이런 게 필요할까요?
그 어떤 것으로도 정의되지 않는 상태라는 개념은 프로그래밍 시점에 필요한 것이 아닙니다. 디자인 타임(Design Time)이라고 부르는 설계 타이밍에는 이런 개념이 필요하지 않습니다. 모든 개념을 다 정의할 수 있는 시점이니까요.
하지만, 프로그램이 수행하는 도중에 입력값에 따라 전혀 의도하지 않았던 유형의 출력이 발생한다면? 런타임(Runtime)이라고 부르는 프로그램 실행 타이밍에 정의되지 않은 어떤 것이 발생했다고 할 때, 그 어떤 것으로도 정의되지 않는 유형을 정의하기 위해 NoneType이라는 것이 필요하게 됩니다.

예를 들어 볼까요?

def get_mobile_phonenum(value):
    if len(value) >= 11:     # 010XXXXYYYY 의 길이는 11 이상
        return new PhoneNumber(value)

여러분들이 어떤 스트링 value로부터 모바일 전화번호 유형의 객체를 구하는 메서드를 위와 같이 짰다고 합시다. 그런데 저 코드를 돌리는 도중에 value에 10자리 숫자가 입력되었다고 합시다. 그 순간 에러를 내면서 프로그램이 죽어야 할까요? 아니면 NoneType 객체라도 리턴하는 게 좋을까요?

이것은 인터프리터 언어인 파이썬의 설계 사상과 관련이 있습니다. 컴파일 타임에 모든 유형이 다 정의되어야 하는 C 같은 언어는 NoneType을 필요로 하지 않습니다. 정의될 수 없는 어떤 것이 나온다면 컴파일 오류를 내면 되니까요. 하지만 어떤 것의 유형이 런타임에 정의되어도 무방하다거나, 심지어 늦게 정의될수록 좋다고 생각하는 파이썬 같은 언어는 NoneType 유형을 필요로 하게 됩니다.

어쨌거나 휴지심조차 없는 일은 종종 일상생활 런타임에서는 발생하니까요.

(4) 불리언(boolean, bool)

이름부터 어렵지만 사실 뭐 별거 없습니다. 불리언 자료형에는 딱 두 가지 값만 존재합니다. 참(True)과 거짓(False).

이렇게 단순한 자료형에 이런 이름이 붙은 이유는 수학자 겸 논리학자인 불 아저씨(George Boole)가 불 대수라는 개념을 만들었기 때문입니다. 즉, 불리언 자료형은 수학에서 시작되었습니다.

int()나 float()처럼 bool()을 통해 다른 값을 참과 거짓으로 변환할 수도 있습니다.

None이거나 0 일 때는 False로 변환되며, 그 외 나머지 경우에는 True가 됩니다.

(5) 문자열(string, str)

지금까지 하나의 값만 다뤄왔지만, 이제부터는 내부에 여러 값을 포함할 수 있는 자료형들을 살펴보겠습니다.

그중 첫 번째는 우리가 지금까지 잘 써온 문자열(string) 입니다. 문자열은 따옴표 또는 쌍따옴표로 감싸야 합니다. 만약 문자열 안에 쌍따옴표를 그대로 쓰고 싶다면, 아래처럼 따옴표로 감싸면 됩니다.

print('He said, "spam."')
>> He said, "spam."

문자열 안에 쌍따옴표랑 따옴표를 둘 다 쓰고 싶다면? 욕심이 많으시군요.🤭

문자열 안에서 따옴표 앞에 \를 붙여서 이건 코드 상의 기호가 아닌, 문자 그대로로 해석하라고 표시(escape) 할 수 있습니다.

여러 줄을 쓰고 싶다면 따옴표나 쌍따옴표 대신, 따옴표나 쌍따옴표를 세 개씩 연달아 써서 열고 닫아주면 됩니다.

특히 값들이 순서대로 들어있는 반복 가능한 객체 중에서도 유한한 길이를 갖는 자료형들을 파이썬에서는 컨테이너(container) 자료형이라고 부릅니다. 이런 컨테이너 자료형들은 뒤에 [숫자]를 붙여서 해당 순서 인덱스(index) 에 해당하는 값만 쏙 뽑아올 수 있습니다.

주의할 점은, 순서가 0부터 시작한다는 점입니다. ❕

message = 'Hello'
print(message[1])
>> e

뒤에서의 순서로 뽑고 싶다면 음수를 주면 됩니다.

message = 'Hello'
print(message[-1])
>> o

:를 써서 [n:m]처럼 n번째부터 m번 직전까지를 한 번에 뽑아올 수도 있습니다.

이를 치즈를 잘라먹는 것처럼 슬라이스(slice)라고 합니다.

두 칸씩 띄어서 보고 싶으면 어떻게 할까요?

(6) 튜플과 리스트(tuple, list)

'글자 외에 숫자와 다른 객체들도 여러 개 줄줄이 꿰고 싶다!' 하시는 분들을 위해 준비된 또 다른 컨테이너 자료형이 있습니다.😎

튜플(tuple)과 리스트(list)입니다.

튜플의 경우에는 괄호() 안에 원하는 값들을 쉼표(,)로 구분해서 써주면 됩니다.

numbers = (1, 2, 3)
for number in numbers:
    print(number)
>> 1
   2
   3

물론 숫자만 들어가라는 법은 없고, 문자든 어떤 값이든 넣어도 됩니다.

tuple_123 = (1, 2, 3)
print(tuple_123[0])
>> 1

하지만 값을 변경할 수는 없습니다. 아래 코드는 에러가 납니다.

튜플 안에 튜플을 넣을 수도 있습니다.

이런 경우에는 [n] 문법을 연이어 써서 튜플 안의 값을 가져올 수 있습니다.

리스트는 튜플과 거의 같습니다. 괄호만 대괄호([, ])로 바꿔주면 됩니다.

리스트만이 가진 또 다른 기능이 있습니다.

변수를 한 번 정의해놓고 변수명 뒤에 .append()라는 특별한 함수를 뒤에 붙여서 값을 하나씩 추가하거나, .remove()를 통해 특정 값을 빼거나, .pop()을 통해 특정 순서의 값을 뺄 수 있다는 점입니다.

여기까지 보면 리스트가 조금 더 편리한 것 같은데, 그럼 튜플의 쓸모는 무엇일까요?

이 질문에 대한 답은 1.2 - 1.0의 경우처럼 생각보다 복잡한 주제입니다. 다른 기회에 좀 더 찬찬히 알아보겠습니다.

(7) 딕셔너리(dictionary, dict)

딕셔너리(dictionary)도 값을 여러 개 포함하는 컨테이너 자료형의 일종입니다.

리스트와 차이점은 숫자로 된 인덱스를 쓰는 대신, 직접 인덱스를 원하는 대로 지정할 수 있다는 점입니다.

제거하고 싶다면 리스트와 마찬가지로 .pop()을 사용하면 됩니다.

5-5. 마무리

이것으로 <파이썬으로 코딩 시작하기>의 여정이 끝이 났습니다. ⛺

앞으로 여러 가지 프로젝트를 탐험하는 데 꼭 필요하고 유용한 도구들을 살펴보았는데요, 어떠셨나요?

다음 여정으로 넘어가기 전에 아래 키워드들을 살펴보며 스스로 정리해 보는 시간을 가지시길 추천드립니다.

────────────────────────────────────

  • 함수(function): 불려진 시점에 특정한 작업을 수행하며, 입력값과 출력값(반환값)을 가질 수 있습니다.

    • 인자(argument): 함수를 호출할 때 전달하는 입력값입니다.
    • 매개변수(parameter): 함수가 실행될 때 입력값이 들어올 변수입니다.
    • 반환값(return value): 함수가 종료될 때 호출 지점으로 전달할 출력값입니다.
  • 변수(variable): 값을 가리키는 이름입니다.

    • 스코프(scope): 변수가 유효한 범위입니다.
  • 연산자(operator): 주어진 값들에 대해 정해진 연산을 수행합니다.

    • 수리 연산자(mathematical operator): +, -, *, /, //, **
    • 비교 연산자(comparison operator): ==, !=, <, >, <=, >=, is
    • 논리 연산자(logical operator): and, or
    • 소속 연산자(membership operator): in
  • 제어문(control statements): 코드 블록의 흐름(실행 여부, 반복)을 제어합니다.

    • if: 명제가 참이면 실행합니다.
    • else: if 명제 이외의 경우에 실행합니다.
    • elif: if 명제 이외의 경우에 또 다른 명제가 참일 경우에 실행합니다.
    • while: 명제가 참일 동안 반복합니다.
    • for: 주어진 값들 하나씩 반복합니다.
  • 자료형(data types): 값들의 종류를 나타냅니다.

    • 정수(int), 부동소수점 수(float), 불리언(bool), 문자열(str), 튜플(tuple), 리스트(list), 딕셔너리(dict)

5-6. 심화

(1) 재귀함수와 치킨

여기까지 배운 내용을 토대로 이제 재귀함수(recursive function) 라는 고오급 스킬을 구사할 수 있습니다. 재귀 함수란, 함수 내에서 그 함수 스스로를 사용하는 인셉션 같은 함수 종류입니다. 즉, def f(): 안의 코드 블럭에서 f()를 호출한다면, 그 함수는 재귀 함수입니다.

이 재귀 함수가 유용한 한 가지 예제는 피보나치 수열의 n번째 숫자를 찾는 작업입니다. 피보나치 수열은 앞의 두 숫자를 더한 수가 다음 수가 되는 무한수열이며, 황금비율로도 잘 알려져 있습니다.

1, 1, 2, 3, 5, 8, 13, 21, ...

세상 만물에서 황금비율을 찾아볼 수 있습니다.

피보나치 수열이 그 빛을 발할 때에는 사람 수에 따른 최적의 치킨 수를 구할 때입니다. 즉, 피보나치 수열의 n번째 숫자만큼의 사람이 있다면, n-1번째 피보나치 숫자만큼 주문하면 됩니다. 즉, 1명일 때는 1마리, 2명일 때는 1마리, 3명일 때는 2마리, 5명일 때는 3마리...

우리가 궁극적으로 만들고자 하는 함수는 사람 수를 입력하면 필요한 치킨 마리 수를 반환하는 함수 fibonachicken()입니다. 우선 그전 단계로, n번째 피보나치 수 number을 반환하는 함수 fibonacci()를 한번 만들어 봅시다

# 직접 만들어보세요.
def FibonaChicken(n):
    if n <= 2:
        return 1
    else:
        return FibonaChicken(n-2) + FibonaChicken(n-1)

print(FibonaChicken(3) * 10)
>> 20

(2) fibonacci

우선 오류가 나겠지만 일단 아래와 같이 틀을 잡아봅시다.

마치 리포트 제목을 쓰고 난 뒤의 막막함이 있습니다. 하지만 시작이 반 이랬으니, 일단 우리가 알고 있는 사실을 돌아봅시다.

피보나치 수열의 n번째 수는 n-1번째 수와 n-2번째 피보나치 수의 합입니다.

그렇다고 합니다. 그대로 한 줄 추가해 줍시다.

def fibonacci(n):
    number = fibonacci(n-1) + fibonacci(n-2)
    return number

만약 코드를 이대로 돌린다면, fibonacci()가 다시 fibonacci()를 부르고, 그 fibonacci()가 다시 fibonacci()를 부르고, 무한 루프에 빠져버립니다. 그렇게 되어버렸다면 코드 블록의 정지 버튼을 눌러 실행을 취소해 줍시다. 이 무한 루프를 끝내줄 한 가지 사실이 아직 남았습니다.

피보나치 수열의 첫 번째 수와 두 번째 수는 1입니다.

우리가 조금 전에 배웠던 if문을 활용하여 분기점을 만들어주도록 합시다.

def fibonacci(n):
    if n <= 2:
        number = 1
    else:
       number = fibonacci(n-1) + fibonacci(n-2)
    return number

짠! 놀랍게도 이것으로 완성입니다.

이제 fibonacci(n-1)이 반복되다 입력값이 2가 되는 순간 더 이상 fibonacci()를 스스로 부르지 않게 됩니다. 이해를 위해 n=3로 시작하는 경우를 한번 상상으로 따라가 볼 수 있습니다. 아래 내용은 이해를 돕기 위한 가상의 코드 흐름이니 실행할 필요 없이 보고 따라오시기만 하면 됩니다.

def fibonacci(3):
    if 3 <= 2:
    else:
        number = fibonacci(2) + fibonacci(1)
    return number

위의 코드 중 2행의 3 <= 2는 거짓이기에 if문 아래 코드 블록은 무시됩니다. else문 내부의 코드를 계산하자니, fibonacci(2)를 다시 계산해야 합니다. n=2로 다시 fibonacci() 함수가 실행되는 것을 상상해 봅시다.

def fibonacci(2):
    if 2 <= 2:
        number = 1
    return number

이번에는 if문이 참이기에 else 아래 내용은 무시하고, 숫자 1을 반환합니다. 결국 처음의 fibonacci(3)으로 돌아가면, fibonacci(2)와 마찬가지로 fibonacci(1)을 각각 1로 치환할 수 있습니다.

def fibonacci(3):
    if 3 <= 2:
    else:
        number = 1 + 1
    return number

실제로도 잘 도착하는지 보기 위해 1부터 20 정도까지의 숫자를 입력하면서 결과를 확인해 봅시다.

print(fibonacci(20))
>> 6765

(3) 더 빠르게! (심화)

만들어진 피보나치 함수에 한 가지 문제가 있습니다. 최초의 n이 커질수록 속도는 기하급수적으로 느려지는 점입니다. 제 노트북은 n이 40만 되어도 힘들어하네요. 어떻게 하면 개선할 수 있을까요?

우선 왜 느려지는지 한번 생각해 봅시다. 우선 입력값이 1, 2일 경우에는 if문에 의해 fibonacci() 안에서 다시 fibonacci()를 부르는 일 없이 실행이 끝납니다. 하지만 입력값이 3이면, else문으로 빠져 fibonacci() 함수를 2번 더 불러야 합니다. 입력값이 4이면, fibonacci(3)fibonacci(2)를 불러야 하고, 여기서 fibonacci(3) 때문에 다시 fibonacci(2)fibonacci(1)을 또 부르고... 결국 우리가 짠 코드 상으로는 입력값 n에 대해 fibonacci()함수를 대략 2의 n 제곱 번 불러야 합니다.

계산 횟수 자체도 문제지만, 한 번 불린 함수/입력값 조합이 반복적으로 불리기 때문에 이미 계산한 값을 다시 계산해야 한다는 비효율성도 있습니다. 한 번 계산된 값은 또 계산하지 않도록, 예를 들어 fibonacci(40)을 부를 때 fibonacci(4)의 값을 기억해 두고 처음 한 번만 계산하는 방법은 없을까요?

변수의 스코프를 활용하면 가능합니다. 함수 밖에서 딕셔너리를 만들어서 한 번 계산된 값들을 기억하도록 만들어봅시다. 딕셔너리의 이름은 memory로 하겠습니다. 피보나치 순서 n이 키, 그 순서에 해당하는 피보나치 숫자가 값이 되도록 합니다. 여기에 일단 우리가 알고 있는 1번째 피보나치 수 1, 2번째 피보나치 수 1을 알려줍니다.

if문에 딸린 조건문과 코드 블록이 변경되었습니다. if n in memory를 통해 입력값 n이라는 키가 memory 딕셔너리에 이미 있는 경우, 그 값 memory[n]을 바로 number로써 반환하라는 이야기입니다.

기억을 읽어올 준비는 되었으니, 한 번 계산한 숫자를 기억에 쓰는 부분을 작성합시다.

memory = {1: 1, 2: 1}

def fibonacci(n):
    if n in memory:
        number = memory[n]
    else:
       number = fibonacci(n-1) + fibonacci(n-2)
       memory[n] = number
    return number

짠! 완료되었습니다. 이제 fibonacci(n-1) + fibonacci(n-2)를 통해 구한 값은 memory에 저장됩니다. memory 변수를 새로 정의하는 것이 아니라 이미 있는 딕셔너리에 새로운 값을 추가하는 것이니, 함수 내부에서 똑같은 이름의 변수가 새로 만들어지는 문제도 피할 수 있습니다. 이제 한번 큰 숫자를 돌려서 체감상 차이가 있는지 한번 봅시다. 그리고 실제로 memory에 잘 저장이 되었는지 찍어봅시다.

memory = {1: 1, 2: 1}

def fibonacci(n):
    if n in memory:
        number = memory[n]
    else:
       number = fibonacci(n-1) + fibonacci(n-2)
       memory[n] = number
    return number

print(fibonacci(100))

print(memory)

부드럽게 돌아갑니다! 프로그래밍에서 이렇게 중간 계산 값을 기억해놓고, 다시 계산하는 대신 값을 바로 읽어 쓰는 방식으로 계산 시간을 줄이는 기법을 메모이제이션(memoization) 이라고 합니다.

profile
게임광 AI 그루~~

0개의 댓글