Python 강좌 : 제 3강 - 함수 & 자료형
함수의 이해

이번 강좌에서는 Python 코드를 읽고 쓰기 위해 가장 먼저 알아야 하는 두 가지 개념인 함수와 자료형을 다룹니다. 앞으로 사용할 print(), type() 같은 기능은 모두 함수이며, 함수에 전달하고 돌려받는 값은 모두 어떤 자료형을 가집니다. 두 개념을 이해하면 이후 강좌의 예제를 “어떤 값을 넣어 어떤 값을 받는지”의 관점으로 읽을 수 있습니다.
함수란 입력한 데이터를 받아 일련의 과정을 거쳐 결과를 반환하는 것을 의미합니다.
입력 데이터를 인수(Argument), 처리하는 동작을 함수(Function), 처리한 데이터를 반환값(Return Value)라 합니다.
인수가 존재하지 않을 수도 있으며, 물론 반환값이 없는 경우도 있습니다. Python에서 반환값이 없는 함수는 None을 반환합니다.
위와 같은 함수가 있다하면 인수는 x, 함수는 x+1, 결과값은 y가 됩니다. 인수가 1일 경우 함수를 통해서 결과값은 2가 됩니다.
함수를 사용하는 이유는 같은 작업에 이름을 붙여 반복해서 사용하기 위해서입니다. 수학에서 f(x) = x + 1을 한 번 정의해 두면 f(1), f(5)처럼 값만 바꾸어 계산할 수 있는 것과 같습니다. 함수를 사용하는 쪽에서는 내부에서 어떻게 계산하는지 몰라도 무엇을 넣으면 무엇이 나오는지만 알면 됩니다.
def f(x):
return x + 1
y = f(1)
print(y)
print(len([1, 2, 3]))- 결과
- 2
3
Python에서는 def 키워드로 함수를 정의하고, return으로 반환값을 돌려줍니다.
print(), len()처럼 Python에 미리 정의되어 있는 함수를 내장 함수(Built-in Function)라 하며, 별도의 정의 없이 바로 사용할 수 있습니다.
코드는 다음 순서로 실행됩니다.
def f(x):부분은 함수를 정의만 하고 실행하지 않습니다.f라는 이름에 함수가 저장됩니다.f(1)에서 함수가 호출되며, 인수1이 매개변수x에 전달됩니다.- 함수 안의
return x + 1이2를 계산하여 호출한 위치로 돌려줍니다.f(1)자리가2로 바뀐 것처럼 동작하여y에2가 저장됩니다. len([1, 2, 3])은 목록의 원소 개수인3을 반환하고,print()가 이를 출력합니다.
함수 이름 뒤의 괄호(())가 호출을 의미합니다. 괄호 없이 f라고만 적으면 함수를 실행하지 않고 함수 자체를 가리킵니다.
- Tip : 함수를 정의할 때 괄호 안에 적는 이름(
x)을매개변수(Parameter), 함수를 호출할 때 전달하는 실제 값(1)을인수(Argument)라 구분하기도 합니다. 함수의 정의 방법은 제 20강 - 사용자 정의 함수에서 자세히 다룹니다.
반환값이 없는 함수
def greet():
print("Hello")
result = greet()
print(result)
print(print("Python"))- 결과
- Hello
None
Python
None
greet()는 인수가 없고 return도 없는 함수입니다. 호출하면 함수 안의 print("Hello")가 실행되어 Hello가 출력되지만, 돌려주는 값이 없으므로 result에는 값이 없음을 나타내는 None이 저장됩니다.
마지막 줄은 안쪽의 print("Python")이 먼저 실행되어 Python을 출력하고, 그 반환값인 None을 바깥쪽 print()가 출력합니다. 이처럼 print()는 화면에 출력하는 일을 할 뿐 값을 돌려주지 않습니다.
화면에 출력하는 것과 값을 반환하는 것은 다릅니다. 출력은 사람이 보기 위한 것이고, 반환값은 다음 계산에 사용하기 위한 것입니다. 함수의 결과를 변수에 저장하거나 다른 계산에 사용하려면 print()가 아닌 return으로 돌려주어야 합니다.
자료형

자료형(Data Type)은 값의 종류를 의미합니다. 같은 1이라도 정수 1, 실수 1.0, 문자열 "1"은 서로 다른 자료형이며, 자료형에 따라 할 수 있는 연산과 사용할 수 있는 기능이 달라집니다. 예를 들어 정수끼리는 더할 수 있지만, 문자열에 정수를 더할 수는 없습니다.
Python의 자료형은 C/C++과 구성이 다릅니다.
대표적인 예로 double이라는 이름의 자료형이 없습니다. Python의 float이 C/C++의 double과 같은 배정밀도(64비트) 부동소수점이므로 별도의 double이 필요하지 않습니다.
또한, 정수형인 int는 C/C++과 달리 크기 제한이 없어 메모리가 허용하는 한 큰 정수를 표현할 수 있습니다.
C/C++의 int는 보통 32비트처럼 정해진 칸 수에 숫자를 저장하므로 표현할 수 있는 범위를 넘으면 값이 넘쳐(Overflow) 엉뚱한 값이 됩니다. Python의 int는 값이 커지면 저장 공간을 필요한 만큼 늘려 사용하므로 넘침이 발생하지 않습니다. 반면 float은 64비트라는 정해진 크기를 사용하므로 표현할 수 있는 범위와 정밀도에 한계가 있습니다. 이 차이는 제 4강에서 자세히 다룹니다.
다양한 데이터를 다루기 위한 데이터형은 수치형, 문자열, 튜플, 리스트, 사전, 집합으로 분류할 수 있습니다.
- 저장 모델
Literal: 하나의 값만 저장 (수치형, 문자열)Container: 여러 개의 값을 종류에 무관하게 저장 (튜플, 리스트, 사전, 집합)
- 변경 가능성
Immutable: 변경 불가 (수치형, 문자열, 튜플)Mutable: 변경 가능 (리스트, 사전, 집합)
- 접근 방법
Direct: 직접 할당 (수치형)Sequence: 순서(위치)로 접근 (문자열, 튜플, 리스트)Mapping: 키(Key)로 접근 (사전)Set: 순서 없음, 중복 불가 (집합)
세 가지 분류는 같은 자료형을 서로 다른 관점에서 본 것입니다. 저장 모델은 값을 몇 개 담는가, 변경 가능성은 만든 뒤 내용을 바꿀 수 있는가, 접근 방법은 원하는 값을 어떻게 꺼내는가를 기준으로 나눕니다. 예를 들어 리스트는 여러 값을 담는 Container이면서, 내용을 바꿀 수 있는 Mutable이고, 위치 번호로 값을 꺼내는 Sequence입니다.
| 자료형 | 이름 | 예시 | 변경 가능성 |
|---|---|---|---|
| int | 정수 | 3 | Immutable |
| float | 실수 | 3.14 | Immutable |
| complex | 복소수 | 3 + 2j | Immutable |
| bool | 논리 | True, False | Immutable |
| str | 문자열 | “Python” | Immutable |
| tuple | 튜플 | (1, 2) | Immutable |
| list | 리스트 | [1, 2] | Mutable |
| dict | 사전 | {“a”: 1} | Mutable |
| set | 집합 | {1, 2} | Mutable |
| NoneType | 없음 | None | Immutable |
print(type(3))
print(type(3.14))
print(type(3 + 2j))
print(type(True))
print(type("Python"))
print(type((1, 2)))
print(type([1, 2]))
print(type({"a": 1}))
print(type({1, 2}))
print(type(None))- 결과
- <class ‘int’>
<class ‘float’>
<class ‘complex’>
<class ‘bool’>
<class ‘str’>
<class ‘tuple’>
<class ‘list’>
<class ‘dict’>
<class ‘set’>
<class ‘NoneType’>
type()을 이용하여 값의 자료형을 확인할 수 있습니다.
결과의 <class 'int'>는 값이 int 클래스의 객체라는 의미입니다. Python에서는 자료형을 클래스(Class)라 부르며, 모든 값은 어떤 클래스로부터 만들어진 객체(Object)입니다. 클래스는 객체를 만드는 설계도이고, 3, 5, 100은 모두 int라는 같은 설계도로 만든 서로 다른 객체입니다.
자료형은 값을 쓰는 형태로 결정됩니다. 소수점이 있으면 float, j가 붙으면 complex, 따옴표로 감싸면 str, 괄호의 모양이 (), [], {}인지에 따라 tuple, list, dict/set이 됩니다. {} 안에 키: 값 형태가 있으면 dict, 값만 있으면 set입니다.
헷갈리기 쉬운 표기
print(type({}))
print(type(set()))
print(type((1)))
print(type((1,)))- 결과
- <class ‘dict’>
<class ‘set’>
<class ‘int’>
<class ‘tuple’>
빈 중괄호 {}는 빈 집합이 아닌 빈 사전입니다. 사전이 집합보다 먼저 Python에 도입되어 {}를 사전이 차지했기 때문이며, 빈 집합은 set()으로 만듭니다.
(1)은 튜플이 아니라 계산 순서를 정하는 괄호로 해석되어 정수 1이 됩니다. 원소가 하나인 튜플은 (1,)처럼 쉼표를 붙여야 합니다. 튜플을 만드는 것은 괄호가 아니라 쉼표입니다.
Container 자료형의 차이
튜플, 리스트, 사전, 집합은 C/C++ 등의 배열과 비슷하나 약간씩 다른 차이를 가집니다.
튜플과 리스트의 경우 둘 다 다양한 종류의 데이터를 저장하고 순서를 중요시합니다.
하지만, 튜플의 경우 최초에 입력한 데이터를 변경이 불가하며 리스트의 경우 최초에 입력한 데이터의 변경이 가능합니다.
사전은 키(Key)와 값(Value)의 쌍으로 데이터를 저장하며, 키는 중복될 수 없지만 값은 중복이 가능합니다. 같은 키에 값을 다시 대입하면 기존 값이 변경됩니다.
사전은 순서(위치)가 아닌 키로 접근하지만, Python 3.7부터는 입력한 순서가 유지되는 것이 언어 차원에서 보장됩니다.
집합은 순서가 없으며 중복이 불가능합니다. 중복된 값을 입력하면 하나만 저장됩니다.
t = (1, 2, 3)
L = [1, 2, 3]
L[0] = 100
print(L)
try:
t[0] = 100
except TypeError as err:
print(err)
d = {"b": 2, "a": 1, "c": 1}
d["a"] = 10
print(d)
print({3, 1, 2, 3, 1})- 결과
- [100, 2, 3]
‘tuple’ object does not support item assignment
{‘b’: 2, ‘a’: 10, ‘c’: 1}
{1, 2, 3}
리스트는 원소를 변경할 수 있지만, 튜플은 원소를 변경하려 하면 TypeError가 발생합니다. 문자열(str)도 튜플과 같이 변경할 수 없는 자료형입니다.
사전은 입력한 순서(b, a, c)를 유지하며, 값 1은 중복되어도 저장되지만 키 "a"는 하나만 존재하여 값이 10으로 변경됩니다.
집합은 중복된 3과 1이 제거되어 저장됩니다.
L[0]의 0은 위치 번호(Index)이며, Python의 위치 번호는 1이 아닌 0부터 시작합니다. 그래서 L[0] = 100은 첫 번째 원소를 바꿉니다.
집합의 출력이 {1, 2, 3}으로 정렬된 것처럼 보이는 것은 작은 정수의 내부 저장 방식 때문에 우연히 그렇게 보이는 것이며, 집합은 순서를 보장하지 않습니다. 집합에서 위치 번호로 원소를 꺼낼 수 없는 것도 이 때문입니다.
- Tip :
try,except는 오류가 발생했을 때 프로그램을 멈추지 않고 처리하는 구문으로, 예외 처리 강좌에서 자세히 다룹니다.
언제 무엇을 사용하는가
| 자료형 | 선택 기준 | 예시 |
|---|---|---|
| 튜플 | 순서가 있고 바뀌면 안 되는 값의 묶음 | 좌표 (x, y), 날짜 (년, 월, 일) |
| 리스트 | 순서가 있고 추가·삭제·변경이 필요한 값의 목록 | 점수 목록, 할 일 목록 |
| 사전 | 이름(키)으로 값을 찾아야 하는 경우 | 이름 → 전화번호 |
| 집합 | 중복 제거나 포함 여부 확인이 중요한 경우 | 방문한 사용자 목록 |
각 자료형의 기능은 이후 강좌에서 하나씩 자세히 다룹니다.
변경 불가능의 의미
Immutable은 객체의 내용을 바꿀 수 없다는 의미이지, 변수에 다른 값을 대입할 수 없다는 의미가 아닙니다.
s = "python"
print(s.upper())
print(s)
try:
s[0] = "P"
except TypeError as err:
print(err)- 결과
- PYTHON
python
‘str’ object does not support item assignment
s.upper()는 대문자로 바꾼 새로운 문자열을 만들어 반환할 뿐, s가 가리키는 원래 문자열은 바꾸지 않습니다. 그래서 다시 출력한 s는 여전히 python입니다.
문자열의 한 글자를 직접 바꾸는 s[0] = "P"는 TypeError가 발생합니다. 바뀐 문자열이 필요하다면 s = s.upper()처럼 새 문자열을 만들어 변수에 다시 대입해야 합니다. 이 경우에도 원래 문자열 객체가 바뀐 것이 아니라, 변수 s가 새 문자열 객체를 가리키도록 바뀐 것입니다.
해시 가능
d = {(1, 2): "tuple key"}
print(d[(1, 2)])
try:
d2 = {[1, 2]: "list key"}
except TypeError as err:
print(err)- 결과
- tuple key
cannot use ‘list’ as a dict key (unhashable type: ‘list’)
사전의 키와 집합의 원소는 변경할 수 없는 값만 사용할 수 있습니다. 그래서 튜플은 사전의 키로 사용할 수 있지만, 리스트를 키로 사용하면 TypeError가 발생합니다.
사전과 집합은 값을 해시(Hash)라는 숫자로 변환하고, 그 숫자를 이용해 저장할 위치를 바로 계산합니다. 덕분에 원소가 아무리 많아도 처음부터 하나씩 비교하지 않고 빠르게 값을 찾을 수 있습니다. 그런데 키의 내용이 저장 후에 바뀌면 해시도 바뀌어 저장한 위치를 다시 찾을 수 없게 되므로, 내용이 바뀔 수 있는 리스트, 사전, 집합은 키로 사용할 수 없습니다. 이런 값을 해시 불가능(Unhashable)하다고 합니다.
동적 타이핑
C/C++이나 Java에서는 int x = 10;처럼 변수를 만들 때 자료형을 함께 적어야 하며, 그 변수에는 정해진 자료형의 값만 저장할 수 있습니다. 이를 정적 타이핑(Static Typing)이라 합니다. Python은 이와 다르게 동작합니다.
x = 10
print(type(x))
x = "ten"
print(type(x))- 결과
- <class ‘int’>
<class ‘str’>
Python은 변수를 선언할 때 자료형을 적지 않으며, 변수에 대입된 값에 따라 자료형이 결정됩니다. 이를 동적 타이핑(Dynamic Typing)이라 합니다.
같은 변수에 다른 자료형의 값을 다시 대입할 수도 있습니다. 변수는 값 자체가 아닌 값(객체)을 가리키는 이름이기 때문입니다.
엄밀히 말하면 자료형을 가지는 것은 변수가 아니라 값(객체)입니다. x = 10은 int 객체 10을 만들고 x라는 이름표를 붙이는 동작이고, x = "ten"은 그 이름표를 떼어 str 객체 "ten"에 붙이는 동작입니다. type(x)는 x가 지금 가리키는 객체의 자료형을 알려주는 것입니다.
이름과 객체
a = [1, 2]
b = a
b.append(3)
print(a, b)
print(a is b)
x = 1
y = x
y += 1
print(x, y)- 결과
- [1, 2, 3] [1, 2, 3]
True
1 2
변수가 이름표라는 것은 이 예제에서 확인할 수 있습니다. b = a는 리스트를 복사하는 것이 아니라 같은 리스트 객체에 b라는 이름표를 하나 더 붙이는 동작입니다. 그래서 b.append(3)으로 리스트에 원소를 추가하면 a로 확인해도 같은 리스트가 바뀌어 있습니다.
a is b는 두 이름이 같은 객체를 가리키는지 확인하는 연산으로, True가 출력됩니다.
반면 정수는 변경할 수 없는 자료형이므로 y += 1은 y가 가리키는 정수를 바꾸지 못하고, 새로운 정수 2를 만들어 y가 가리키게 합니다. x는 여전히 1을 가리키므로 1 2가 출력됩니다.
이처럼 변경 가능한 객체를 여러 이름이 함께 가리키면, 한쪽에서의 변경이 다른 쪽에도 보입니다. 초보자가 자주 겪는 문제이므로 기억해 두는 것이 좋습니다.
강한 타이핑
print(1 + 2.0)
print(True + 1)
try:
print("1" + 1)
except TypeError as err:
print(err)
print(int("1") + 1, "1" + str(1))- 결과
- 3.0
2
can only concatenate str (not “int”) to str
2 11
1 + 2.0처럼 정수와 실수, True + 1처럼 논리값과 정수는 모두 숫자이므로 함께 계산할 수 있습니다. 이때 정보를 잃지 않도록 더 넓은 범위의 자료형으로 계산하여 3.0처럼 실수가 됩니다.
하지만 문자열 "1"과 정수 1은 서로 다른 종류의 값이므로 TypeError가 발생합니다. "1" + 1의 결과가 2인지 "11"인지 Python이 추측하지 않고 오류로 알려주는 것입니다. 이를 강한 타이핑(Strong Typing)이라 합니다.
원하는 결과에 맞게 int("1") + 1처럼 숫자로 바꾸어 더하거나, "1" + str(1)처럼 문자열로 바꾸어 이어 붙이는 것을 직접 명시해야 합니다.
타입 힌트
def add(a: int, b: int) -> int:
return a + b
print(add(1, 2))
print(add("1", "2"))- 결과
- 3
12
a: int, -> int처럼 자료형을 표기하는 것을 타입 힌트(Type Hint)라 합니다.
타입 힌트는 코드를 읽는 사람과 편집기, 정적 분석 도구를 위한 정보이며, 실행 시 자료형을 검사하거나 변환하지 않습니다. 그래서 문자열을 전달해도 오류 없이 "1" + "2"의 결과인 12가 출력됩니다.
결과의 12는 숫자 12가 아니라 문자열 "1"과 "2"를 이어 붙인 문자열 "12"입니다. print()는 문자열을 따옴표 없이 출력하므로 숫자처럼 보입니다.
동적 타이핑은 코드를 짧고 유연하게 만들지만, 함수에 어떤 값을 넣어야 하는지 코드만 보고 알기 어려워집니다. 타입 힌트를 작성하면 편집기가 잘못된 자료형의 값을 전달하는 코드에 실행하기 전에 경고를 표시할 수 있으므로, 규모가 큰 프로그램에서 실수를 줄이는 데 도움이 됩니다.
- Tip : Python 3.9부터
list[int]처럼 내장 자료형으로 타입 힌트를 작성할 수 있으며, Python 3.10부터int | None처럼|로 여러 자료형을 표기할 수 있습니다.
def add(a: int, b: int) -> int:
return a + b
print(add.__annotations__)- 결과
- {‘a’: <class ‘int’>, ‘b’: <class ‘int’>, ‘return’: <class ‘int’>}
작성한 타입 힌트는 함수의 __annotations__에 사전으로 저장됩니다. 매개변수 이름이 키가 되고, 반환값의 타입 힌트는 'return' 키에 저장됩니다. Python은 이 정보를 저장만 할 뿐 실행에 사용하지 않으며, 정적 분석 도구나 라이브러리가 필요할 때 읽어서 사용합니다.
정리
| 이름 | 설명 | 반환값/특징 |
|---|---|---|
| def | 함수 정의 | 호출 전까지 실행되지 않음 |
| return | 반환값 지정 | 없으면 None 반환 |
| type(x) | 값의 자료형 확인 | <class '자료형'> |
| Immutable | 변경 불가 자료형 | 수치형, 문자열, 튜플, None |
| Mutable | 변경 가능 자료형 | 리스트, 사전, 집합 |
| x is y | 같은 객체인지 확인 | True 또는 False |
| 동적 타이핑 | 값에 따라 자료형 결정 | 변수는 객체를 가리키는 이름 |
| 강한 타이핑 | 다른 자료형을 암묵적으로 섞지 않음 | "1" + 1은 TypeError |
| 타입 힌트 | 자료형 표기 | 실행 시 검사하지 않음 |
함수는 값을 받아 값을 돌려주는 이름 붙은 작업이고, 자료형은 값의 종류와 그 값으로 할 수 있는 일을 결정합니다. 특히 Immutable과 Mutable의 차이, 변수가 객체를 가리키는 이름이라는 점은 이후 강좌 전반에서 계속 사용되는 개념입니다.
댓글 남기기