4. 숫자 다루기
3장에서 변수 만드는 방법과 Python의 기본 숫자 타입인 정수와 부동소수점 수(실수)에 대해 배웠습니다. 이제 그 숫자들을 실제로 활용해 볼 차례입니다. 이 장에서는 계산을 수행하고, 여러 연산을 조합하며, 숫자 데이터를 다룰 때 사용할 수 있는 Python의 내장 도구들을 배웁니다.
숫자는 프로그래밍에서 근본적인 요소입니다. 합계를 계산하거나, 측정값을 처리하거나, 재고를 관리하거나, 데이터를 분석할 때든 산술 연산이 필요합니다. Python은 숫자 연산을 직관적이고 간단하게 만들어 주지만, 꼭 이해해야 할 중요한 세부 내용들도 있습니다. 특히 서로 다른 종류의 나눗셈이 어떻게 동작하는지, 연산자들이 서로 어떻게 상호작용하는지, 부동소수점 수가 어떻게 동작하는지가 중요합니다.
이 장을 마치면, 계산을 수행하고 연산자의 동작을 이해하며, Python의 숫자 함수를 사용해 실제 문제를 해결하는 데 익숙해질 것입니다.
4.1) 기본 산술: 덧셈, 뺄셈, 곱셈
가장 기초적인 산술 연산부터 시작하겠습니다. Python은 기본 수학에 익숙한 기호들을 사용하며, 이 연산들은 일상적인 산수에서 기대하는 대로 동작합니다.
4.1.1) + 연산자로 덧셈하기
+ 연산자는 두 숫자를 더합니다. 정수와 부동소수점 수 모두에 사용할 수 있습니다:
# basic_addition.py
# Adding integers
total = 15 + 27
print(total) # Output: 42
# Adding floats
price = 19.99 + 5.50
print(price) # Output: 25.49
# You can add multiple numbers in one expression
sum_total = 10 + 20 + 30 + 40
print(sum_total) # Output: 100덧셈 자체는 단순하지만, 중요한 점이 하나 있습니다. 정수와 실수를 함께 더하면, Python은 결과를 소수 정밀도를 보존하기 위해 자동으로 실수로 변환합니다:
# mixed_addition.py
result = 10 + 3.5
print(result) # Output: 13.5
print(type(result)) # Output: <class 'float'>이 자동 변환이 일어나는 이유는 실수 타입은 정수와 소수를 모두 표현할 수 있지만, 정수 타입은 소수를 표현할 수 없기 때문입니다. Python은 정보가 손실되지 않는 타입을 선택합니다.
4.1.2) - 연산자로 뺄셈하기
- 연산자는 첫 번째 숫자에서 두 번째 숫자를 뺍니다:
# basic_subtraction.py
# Subtracting integers
difference = 100 - 42
print(difference) # Output: 58
# Subtracting floats
remaining = 50.75 - 12.25
print(remaining) # Output: 38.5
# Subtraction can produce negative results
balance = 20 - 35
print(balance) # Output: -15덧셈과 마찬가지로, 뺄셈에서도 서로 다른 타입을 섞으면 정수가 실수로 승격됩니다:
# mixed_subtraction.py
result = 100 - 0.01
print(result) # Output: 99.99
print(type(result)) # Output: <class 'float'>4.1.3) * 연산자로 곱셈하기
* 연산자는 두 숫자를 곱합니다:
# basic_multiplication.py
# Multiplying integers
product = 6 * 7
print(product) # Output: 42
# Multiplying floats
area = 3.5 * 2.0
print(area) # Output: 7.0
# Multiplying by zero always gives zero
result = 1000 * 0
print(result) # Output: 0곱셈도 같은 타입 변환 규칙을 따릅니다. 정수와 실수를 곱하면 결과는 실수가 됩니다:
# mixed_multiplication.py
result = 5 * 2.5
print(result) # Output: 12.5
print(type(result)) # Output: <class 'float'>
# Even if the result is a whole number
result = 4 * 2.0
print(result) # Output: 8.0 (note the .0)
print(type(result)) # Output: <class 'float'>마지막 예에서 보듯, 4 × 2.0의 수학적 결과는 8이지만, 피연산자 중 하나가 실수였기 때문에 Python은 이를 8.0으로 표현합니다. 결과 타입은 수학적 결과가 정수인지 여부가 아니라, 입력의 타입에 의해 결정됩니다.
4.1.4) 기본 산술의 실용 예시
이제 이러한 연산들이 현실적인 상황에서 어떻게 함께 사용되는지 보겠습니다:
# shopping_cart.py
# Calculate a shopping cart total
item1_price = 12.99
item2_price = 8.50
item3_price = 15.00
subtotal = item1_price + item2_price + item3_price
print(f"Subtotal: ${subtotal}") # Output: Subtotal: $36.49
tax_rate = 0.08
tax = subtotal * tax_rate
print(f"Tax: ${tax}") # Output: Tax: $2.9192
total = subtotal + tax
print(f"Total: ${total}") # Output: Total: $39.4092# temperature_change.py
# Calculate temperature change
morning_temp = 45.5
afternoon_temp = 68.2
change = afternoon_temp - morning_temp
print(f"Temperature increased by {change} degrees")
# Output: Temperature increased by 22.7 degrees이러한 기본 연산들은 더 복잡한 계산의 토대가 됩니다. 특히 Python이 타입 변환을 어떻게 처리하는지 이해하면, 프로그램이 계산을 수행할 때 예상치 못한 결과를 피하는 데 도움이 됩니다.
4.2) 나눗셈, 버림 나눗셈, 나머지: /, //, %
Python의 나눗셈은 덧셈, 뺄셈, 곱셈보다 조금 더 섬세합니다. Python은 서로 다른 목적을 가진 세 가지 나눗셈 관련 연산자를 제공합니다. 각각을 언제 사용해야 하는지 이해하는 것은 올바른 프로그램을 작성하는 데 매우 중요합니다.
4.2.1) / 연산자로 일반 나눗셈하기
/ 연산자는 "참 나눗셈(true division)"을 수행합니다. 두 피연산자가 모두 정수여도 항상 부동소수점 결과를 반환합니다:
# true_division.py
# Dividing integers
result = 10 / 2
print(result) # Output: 5.0 (note: float, not 5)
print(type(result)) # Output: <class 'float'>
# Division that doesn't result in a whole number
result = 10 / 3
print(result) # Output: 3.3333333333333335
# Dividing floats
result = 15.5 / 2.5
print(result) # Output: 6.2여기서 핵심은 / 연산자가 항상 실수를 만든다는 점입니다. 두 피연산자가 정수이고 수학적으로 결과가 정수여도 마찬가지입니다. 이는 일관성을 위해서인데, Python은 나눗셈이 항상 소수 자릿수를 보존하도록 설계했습니다.
이 동작은 두 정수를 나누면 정수 결과를 반환하는 일부 다른 언어들과는 다릅니다. Python 3에서 정수 결과가 필요하다면, 버림 나눗셈(곧 살펴보겠습니다)을 사용하거나, 직접 결과를 정수로 변환해야 합니다.
4.2.2) // 연산자로 버림 나눗셈하기
// 연산자는 "버림 나눗셈(floor division)"을 수행합니다. 나눈 뒤, 결과를 가장 가까운 아래쪽 정수로 내립니다. 두 피연산자가 정수이면 결과도 정수입니다. 어느 한 쪽이 실수라면 결과는 실수지만, 여전히 아래로 내립니다:
# floor_division.py
# Floor division with integers
result = 10 // 3
print(result) # Output: 3 (not 3.333...)
print(type(result)) # Output: <class 'int'>
# Even division still gives an integer
result = 10 // 2
print(result) # Output: 5 (integer, not 5.0)
print(type(result)) # Output: <class 'int'>
# Floor division with floats gives a float
result = 10.0 // 3
print(result) # Output: 3.0
print(type(result)) # Output: <class 'float'>"아래로 내린다"는 것은 단순히 소수 부분을 잘라내는 것이 아니라, 음의 무한대 방향으로 이동한다는 뜻입니다. 이는 음수에서 특히 중요합니다:
# floor_division_negative.py
# Positive numbers: rounds down (toward negative infinity)
result = 7 // 2
print(result) # Output: 3
# Negative numbers: still rounds toward negative infinity
result = -7 // 2
print(result) # Output: -4 (not -3!)
# Why -4? Because -3.5 rounded down (toward negative infinity) is -4
# Think of the number line: ... -4, -3.5, -3, -2, -1, 0, 1, 2, 3, 3.5, 4 ...버림 나눗셈은 항목을 묶음으로 나누거나, 어떤 수량 안에 완전한 단위가 몇 개 들어가는지 계산할 때 유용합니다:
# floor_division_practical.py
# How many complete dozens in 50 eggs?
eggs = 50
eggs_per_dozen = 12
complete_dozens = eggs // eggs_per_dozen
print(f"Complete dozens: {complete_dozens}") # Output: Complete dozens: 4
# How many full hours in 150 minutes?
minutes = 150
full_hours = minutes // 60
print(f"Full hours: {full_hours}") # Output: Full hours: 24.2.3) % 연산자로 나머지(modulo) 구하기
% 연산자는 "나머지 연산자(modulo, mod)"라고 부르며, 나눗셈 후에 남는 나머지를 반환합니다. 즉, "나누고 난 뒤에 얼마가 남는가?"를 알려줍니다.
# modulo_basic.py
# 10 divided by 3 is 3 with remainder 1
result = 10 % 3
print(result) # Output: 1
# 10 divided by 2 is 5 with remainder 0 (even division)
result = 10 % 2
print(result) # Output: 0
# Works with floats too
result = 10.5 % 3
print(result) # Output: 1.5나머지 연산자는 여러 흔한 프로그래밍 패턴에서 매우 유용합니다.
숫자가 짝수인지 홀수인지 검사하기:
# even_odd_check.py
number = 17
if number % 2 == 0:
print(f"{number} is even")
else:
print(f"{number} is odd")
# Output: 17 is odd남는 항목 개수 구하기:
# leftover_items.py
eggs = 50
eggs_per_dozen = 12
leftover = eggs % eggs_per_dozen
print(f"Leftover eggs: {leftover}") # Output: Leftover eggs: 2값을 범위 안에서 순환시키기(시계 산술처럼):
# clock_arithmetic.py
# What hour is it 25 hours from now? (on a 12-hour clock)
current_hour = 10
hours_later = 25
future_hour = (current_hour + hours_later) % 12
print(f"Hour: {future_hour}") # Output: Hour: 114.2.4) //, %, / 사이의 관계
버림 나눗셈과 나머지 연산은 서로 밀접하게 관련되어 있습니다. 둘을 함께 사용하면 나눗셈의 전체 결과를 알 수 있습니다:
# division_relationship.py
dividend = 17
divisor = 5
quotient = dividend // divisor # How many times 5 goes into 17
remainder = dividend % divisor # What's left over
print(f"{dividend} ÷ {divisor} = {quotient} remainder {remainder}")
# Output: 17 ÷ 5 = 3 remainder 2
# You can verify: quotient * divisor + remainder should equal dividend
verification = quotient * divisor + remainder
print(f"Verification: {quotient} × {divisor} + {remainder} = {verification}")
# Output: Verification: 3 × 5 + 2 = 17이 관계는 항상 성립합니다. 0이 아닌 어떤 수 b에 대해, 임의의 숫자 a에 대해 a == (a // b) * b + (a % b)입니다.
4.2.5) 적절한 나눗셈 연산자 선택하기
어떤 연산자를 사용할지 결정하기 위한 간단한 가이드는 다음과 같습니다:
- 소수까지 포함한 정확한 수학적 결과가 필요할 때는
/사용 (일반적인 계산에서 가장 자주 사용) - 몇 개의 완전한 그룹이 들어가는지 세고 싶을 때는
//사용 (12개씩 묶음, 시간, 페이지 수 등) - 나머지나 남는 양이 필요할 때는
%사용 (짝수/홀수 검사, 값 순환, 남는 개수 찾기 등)
# choosing_operators.py
# Scenario: Distributing 47 candies among 6 children
candies = 47
children = 6
# How many candies per child? (use //)
per_child = candies // children
print(f"Each child gets {per_child} candies") # Output: Each child gets 7 candies
# How many candies are left over? (use %)
leftover = candies % children
print(f"Leftover candies: {leftover}") # Output: Leftover candies: 5
# What's the exact average? (use /)
average = candies / children
print(f"Average per child: {average}") # Output: Average per child: 7.8333333333333334.3) 복합 대입 연산자
프로그래밍을 하다 보면, 변수의 현재 값에 어떤 연산을 적용해 값을 갱신해야 할 일이 자주 생깁니다. 예를 들어, 누적 합계에 값을 더하거나, 잔액에서 금액을 빼거나, 값에 어떤 비율을 곱하는 경우입니다. Python은 이런 작업을 간결하게 표현할 수 있도록 복합 대입 연산자를 제공합니다.
4.3.1) 복합 대입 연산자란 무엇인가
복합 대입 연산자는 산술 연산과 대입을 결합한 것입니다. 다음처럼 쓰는 대신:
# traditional_update.py
count = 10
count = count + 5 # Add 5 to count
print(count) # Output: 15이렇게 쓸 수 있습니다:
# augmented_update.py
count = 10
count += 5 # Same as: count = count + 5
print(count) # Output: 15두 버전은 완전히 같은 일을 하지만, 복합 대입 버전이 더 간결하고, "count를 5만큼 증가시킨다"라는 의도가 더 분명하게 드러납니다.
4.3.2) 모든 복합 대입 연산자
Python은 모든 산술 연산에 대해 복합 대입 연산자를 제공합니다:
# all_augmented_operators.py
# Addition
x = 10
x += 5 # x = x + 5
print(f"After += 5: {x}") # Output: After += 5: 15
# Subtraction
x = 10
x -= 3 # x = x - 3
print(f"After -= 3: {x}") # Output: After -= 3: 7
# Multiplication
x = 10
x *= 4 # x = x * 4
print(f"After *= 4: {x}") # Output: After *= 4: 40
# Division
x = 10
x /= 2 # x = x / 2
print(f"After /= 2: {x}") # Output: After /= 2: 5.0
# Floor division
x = 10
x //= 3 # x = x // 3
print(f"After //= 3: {x}") # Output: After //= 3: 3
# Modulo
x = 10
x %= 3 # x = x % 3
print(f"After %= 3: {x}") # Output: After %= 3: 1
# Exponentiation (we'll see more about ** in section 4.6)
x = 2
x **= 3 # x = x ** 3 (2 to the power of 3)
print(f"After **= 3: {x}") # Output: After **= 3: 84.3.3) 복합 대입 연산자의 흔한 사용 사례
복합 대입 연산자는 여러 흔한 프로그래밍 패턴에서 빛을 발합니다.
합계 누적하기:
# accumulating_total.py
total = 0
total += 10 # Add first item
total += 25 # Add second item
total += 15 # Add third item
print(f"Total: {total}") # Output: Total: 50발생 횟수 세기:
# counting.py
count = 0
# Imagine these happen as we process data
count += 1 # Found one
count += 1 # Found another
count += 1 # Found another
print(f"Count: {count}") # Output: Count: 3잔액 갱신하기:
# balance_updates.py
balance = 100.00
balance -= 25.50 # Purchase
balance += 50.00 # Deposit
balance -= 10.00 # Purchase
print(f"Balance: ${balance}") # Output: Balance: $114.5반복적으로 연산 적용하기:
# repeated_operations.py
value = 100
value *= 1.1 # Increase by 10%
value *= 1.1 # Increase by 10% again
value *= 1.1 # Increase by 10% again
print(f"Value after three 10% increases: {value}")
# Output: Value after three 10% increases: 133.100000000000024.3.4) 복합 대입 연산자의 중요한 세부사항
복합 대입은 변경 불가능(immutable) 타입에 대해 새 객체를 만듭니다:
3장에서 숫자는 변경 불가능(immutable)하다고 배웠습니다. 숫자 자체의 값을 바꿀 수 없고, 새로운 숫자를 만들 수 있을 뿐입니다. x += 5라고 쓰면, Python은 새 숫자 객체를 만들고 그것을 x에 대입합니다. 이전 숫자 객체는 버려집니다(이 개념은 17장에서 Python의 객체 모델을 다룰 때 더 깊이 살펴봅니다):
# augmented_with_immutables.py
x = 10
print(id(x)) # Output: (some memory address)
x += 5
print(id(x)) # Output: (different memory address)지금은 x += 5가 x = x + 5와 완전히 동등하다는 점만 이해하면 됩니다. 편리한 축약 표기일 뿐, 근본적으로 다른 연산은 아닙니다.
변수가 존재하기 전에 복합 대입을 쓸 수는 없습니다:
# augmented_requires_existing.py
# This will cause an error:
# count += 1 # NameError: name 'count' is not defined
# You must initialize the variable first:
count = 0
count += 1 # Now this works
print(count) # Output: 1타입 변환은 동일한 규칙을 따릅니다:
# augmented_type_conversion.py
x = 10 # integer
x += 2.5 # Add a float
print(x) # Output: 12.5
print(type(x)) # Output: <class 'float'>결과 타입은 일반 연산자와 같은 타입 변환 규칙을 따릅니다. 정수와 실수를 섞으면 결과는 실수입니다.
4.3.5) 복합 대입 연산자를 언제 사용할까
변수의 현재 값을 기반으로 변수를 갱신할 때마다 복합 대입 연산자를 사용하는 것이 좋습니다. 이렇게 하면 코드가 다음과 같이 됩니다.
- 더 간결합니다:
x += 5는x = x + 5보다 짧습니다. - 더 읽기 쉽습니다: 의도가 즉시 드러납니다.
- 오타를 줄여 줍니다: 변수 이름을 두 번 쓰다가 틀릴 위험이 줄어듭니다.
다음 두 버전을 비교해 보겠습니다:
# comparison.py
# Without augmented assignment
accumulated_distance_in_kilometers = 0
accumulated_distance_in_kilometers = accumulated_distance_in_kilometers + 10
accumulated_distance_in_kilometers = accumulated_distance_in_kilometers + 25
# With augmented assignment
accumulated_distance_in_kilometers = 0
accumulated_distance_in_kilometers += 10
accumulated_distance_in_kilometers += 25복합 대입 버전이 더 명확하고, 오타 가능성도 줄어듭니다. 복합 대입 연산자는 작은 기능처럼 보일 수 있지만, 실제 Python 코드에서 매우 자주 사용됩니다.
4.4) 연산자 우선순위와 괄호
여러 연산을 하나의 식(expression)에 결합하면, Python은 어떤 것부터 계산할지에 대한 규칙이 필요합니다. 예를 들어 2 + 3 * 4는 (2 + 3) * 4 = 20으로 계산해야 할까요, 아니면 2 + (3 * 4) = 14로 계산해야 할까요? 답은 연산자 우선순위에 달려 있습니다. 연산자 우선순위란 어떤 연산이 먼저 수행되는지를 결정하는 규칙입니다.
4.4.1) 연산자 우선순위 이해하기
Python은 수학에서 배운 것과 같은 우선순위를 따릅니다. 곱셈과 나눗셈이 덧셈과 뺄셈보다 먼저 수행됩니다. 이는 PEMDAS (Parentheses, Exponents, Multiplication/Division, Addition/Subtraction)라는 약어로 기억하기도 하는데, 지수(제곱)는 4.6절에서 다룹니다.
# precedence_basic.py
# Multiplication happens before addition
result = 2 + 3 * 4
print(result) # Output: 14 (not 20)
# Python calculates: 2 + (3 * 4) = 2 + 12 = 14
# Division happens before subtraction
result = 10 - 8 / 2
print(result) # Output: 6.0 (not 1.0)
# Python calculates: 10 - (8 / 2) = 10 - 4.0 = 6.0지금까지 배운 연산자들의 우선순위는 (위에서 아래로, 우선순위가 높은 것부터) 다음과 같습니다.
- 괄호
()— 가장 높은 우선순위, 항상 먼저 계산 - 지수
**— (4.6절에서 다룹니다) - 곱셈, 나눗셈, 버림 나눗셈, 나머지
*,/,//,%— 같은 수준, 왼쪽에서 오른쪽 순으로 평가 - 덧셈, 뺄셈
+,-— 같은 수준, 왼쪽에서 오른쪽 순으로 평가
우선순위가 어떻게 작동하는지 더 많은 예시를 보겠습니다:
# precedence_examples.py
# Multiplication before addition
result = 5 + 2 * 3
print(result) # Output: 11 (5 + 6)
# Division before subtraction
result = 20 - 10 / 2
print(result) # Output: 15.0 (20 - 5.0)
# Multiple operations at the same level: left to right
result = 10 - 3 + 2
print(result) # Output: 9 ((10 - 3) + 2)
result = 20 / 4 * 2
print(result) # Output: 10.0 ((20 / 4) * 2)4.4.2) 괄호로 연산 순서 제어하기
괄호는 기본 우선순위를 무시하고, 괄호 안의 연산을 먼저 수행하도록 합니다. 괄호 안의 식은 연산자 종류와 상관없이 항상 먼저 계산됩니다:
# parentheses_override.py
# Without parentheses: multiplication first
result = 2 + 3 * 4
print(result) # Output: 14
# With parentheses: addition first
result = (2 + 3) * 4
print(result) # Output: 20
# Another example
result = 10 - 8 / 2
print(result) # Output: 6.0
result = (10 - 8) / 2
print(result) # Output: 1.0괄호는 중첩해서 사용할 수 있고, Python은 가장 안쪽 괄호부터 바깥쪽으로 차례대로 평가합니다:
# nested_parentheses.py
result = ((2 + 3) * 4) - 1
print(result) # Output: 19
# Step 1: (2 + 3) = 5
# Step 2: (5 * 4) = 20
# Step 3: 20 - 1 = 19
result = 2 * (3 + (4 - 1))
print(result) # Output: 12
# Step 1: (4 - 1) = 3
# Step 2: (3 + 3) = 6
# Step 3: 2 * 6 = 124.4.3) 같은 우선순위를 가진 연산자들
같은 우선순위 수준의 연산자가 하나의 식에 여러 개 있을 때, Python은 왼쪽에서 오른쪽 순서대로 평가합니다(이를 "왼쪽 결합성(left associativity)"이라고 합니다):
# left_to_right.py
# Addition and subtraction: left to right
result = 10 - 3 + 2 - 1
print(result) # Output: 8
# Evaluation: ((10 - 3) + 2) - 1 = (7 + 2) - 1 = 9 - 1 = 8
# Multiplication and division: left to right
result = 20 / 4 * 2
print(result) # Output: 10.0
# Evaluation: (20 / 4) * 2 = 5.0 * 2 = 10.0
# This matters! Different order gives different result:
result = 20 / (4 * 2)
print(result) # Output: 2.54.4.4) 우선순위가 중요한 실제 예시
현실적인 상황에서 우선순위를 이해하는 것이 중요한 경우를 살펴보겠습니다:
# temperature_conversion.py
# Convert Fahrenheit to Celsius: C = (F - 32) * 5 / 9
fahrenheit = 98.6
# Correct: parentheses ensure subtraction happens first
celsius = (fahrenheit - 32) * 5 / 9
print(f"{fahrenheit}°F = {celsius}°C")
# Output: 98.6°F = 37.0°C
# Wrong: without parentheses, multiplication happens first
# celsius = fahrenheit - 32 * 5 / 9 # This would be wrong!
# This would calculate: fahrenheit - ((32 * 5) / 9)# calculate_average.py
# Calculate average of three numbers
num1 = 85
num2 = 92
num3 = 78
# Correct: parentheses ensure addition happens before division
average = (num1 + num2 + num3) / 3
print(f"Average: {average}") # Output: Average: 85.0
# Wrong: without parentheses, only num3 is divided by 3
# average = num1 + num2 + num3 / 3 # This would be wrong!
# This would calculate: 85 + 92 + (78 / 3) = 85 + 92 + 26.0 = 203.0# discount_calculation.py
# Calculate price after discount and tax
original_price = 100.00
discount_rate = 0.20
tax_rate = 0.08
# Calculate discount amount
discount = original_price * discount_rate
# Calculate price after discount
discounted_price = original_price - discount
# Calculate final price with tax
final_price = discounted_price * (1 + tax_rate)
print(f"Final price: ${final_price}") # Output: Final price: $86.4
# Or in one expression (using parentheses to be clear):
final_price = (original_price * (1 - discount_rate)) * (1 + tax_rate)
print(f"Final price: ${final_price}") # Output: Final price: $86.44.4.5) 연산자 우선순위에 대한 모범 사례
꼭 필요하지 않더라도 괄호를 사용해 의도를 명확히 하십시오:
괄호를 추가해도 결과는 같지만, 의도가 더 잘 드러나는 경우가 있습니다:
# clarity_with_parentheses.py
# These are equivalent, but the second is clearer:
result = 2 + 3 * 4
result = 2 + (3 * 4) # Clearer: shows you know multiplication happens first
# Complex expressions benefit from parentheses:
result = (subtotal * tax_rate) + (subtotal * tip_rate) # Clear intent복잡한 식은 여러 단계로 나누십시오:
식이 너무 복잡해지면, 여러 줄로 나누는 것이 좋습니다:
# breaking_into_steps.py
# Instead of this complex one-liner:
result = ((price * quantity) * (1 - discount)) * (1 + tax)
# Consider breaking it into steps:
subtotal = price * quantity
discounted = subtotal * (1 - discount)
final = discounted * (1 + tax)
result = final여러 단계로 나눈 버전이 더 읽기 쉽고, 디버그와 검증도 수월합니다. 간결함 때문에 가독성을 희생하지 마십시오.
헷갈릴 때는 항상 괄호를 사용하십시오:
우선순위가 확실하지 않다면 괄호를 사용하십시오. Python 인터프리터는 전혀 불평하지 않고, 미래의 여러분(혹은 여러분의 코드를 읽을 다른 프로그래머)이 고마워할 것입니다.
연산자 우선순위를 이해하면 올바른 식을 작성하고, 다른 사람이 작성한 코드를 읽는 데 큰 도움이 됩니다. 핵심 원칙은: 여러 연산을 결합할 때, 평가 순서를 의식하고, 괄호를 사용해 의도를 명시적으로 드러내는 것입니다.
4.5) 식에서 정수와 실수 섞어 쓰기
Python이 정수와 실수를 단순한 연산에서 혼합하는 방법을 이미 살펴보았습니다. 이제 이 동작을 조금 더 체계적으로 살펴보고, 숫자 식에서 타입 변환을 결정하는 규칙을 이해해 보겠습니다.
4.5.1) 타입 승격(Type Promotion) 규칙
Python은 정수와 실수가 함께 등장하는 산술 연산을 수행할 때, 먼저 정수를 자동으로 실수로 변환(또는 "승격")한 후 연산을 수행합니다. 결과는 항상 실수입니다:
# type_promotion.py
# Integer + Float = Float
result = 10 + 3.5
print(result) # Output: 13.5
print(type(result)) # Output: <class 'float'>
# Float + Integer = Float (order doesn't matter)
result = 3.5 + 10
print(result) # Output: 13.5
print(type(result)) # Output: <class 'float'>
# This applies to all arithmetic operators
result = 5 * 2.0
print(result) # Output: 10.0 (float, not int)
print(type(result)) # Output: <class 'float'>Python이 이렇게 하는 이유는, 실수가 정수와 소수를 모두 표현할 수 있지만 정수는 소수를 표현할 수 없기 때문입니다. 실수로 변환하면 정보 손실 없이 연산을 수행할 수 있습니다.
Python이 결과 타입을 결정하는 과정을 도식으로 표현하면 다음과 같습니다:
4.5.2) 복잡한 식에서의 타입 승격
여러 연산과 혼합 타입이 함께 있는 식에서는, Python이 각 단계에서 승격 규칙을 적용합니다:
# complex_mixed_types.py
# Multiple operations with mixed types
result = 10 + 3.5 * 2
print(result) # Output: 17.0
print(type(result)) # Output: <class 'float'>
# What happens step by step:
# 1. 3.5 * 2 → 3.5 * 2.0 (2 promoted to float) → 7.0 (float)
# 2. 10 + 7.0 → 10.0 + 7.0 (10 promoted to float) → 17.0 (float)
# Another example
result = 5 / 2 + 3
print(result) # Output: 5.5
print(type(result)) # Output: <class 'float'>
# Step by step:
# 1. 5 / 2 → 2.5 (division always produces float)
# 2. 2.5 + 3 → 2.5 + 3.0 (3 promoted to float) → 5.5 (float)식 안의 어느 단계에서든 실수가 한 번이라도 생성되면, 이후 이 결과를 포함하는 연산들도 모두 실수 결과를 만들게 됩니다.
4.5.3) 특수한 경우: 일반 나눗셈은 항상 실수
4.2절에서 봤듯, / 연산자는 두 정수를 나누더라도 항상 실수를 만듭니다:
# division_always_float.py
# Even when the result is a whole number
result = 10 / 2
print(result) # Output: 5.0 (not 5)
print(type(result)) # Output: <class 'float'>
# This means any expression with / will have a float result
result = 10 / 2 + 3 # 5.0 + 3 → 5.0 + 3.0 → 8.0
print(result) # Output: 8.0
print(type(result)) # Output: <class 'float'>나눗셈 결과를 정수로 얻고 싶다면, 대신 버림 나눗셈 //을 사용해야 합니다(단, 이 연산은 아래 방향으로 반올림한다는 점을 기억해야 합니다):
# floor_division_integer.py
# Floor division with integers produces an integer
result = 10 // 2
print(result) # Output: 5 (integer)
print(type(result)) # Output: <class 'int'>
# But floor division with any float produces a float
result = 10.0 // 2
print(result) # Output: 5.0 (float)
print(type(result)) # Output: <class 'float'>4.5.4) 타입 혼합의 실용적인 영향
타입 승격을 이해하면 결과 타입을 예측하고 제어하는 데 도움이 됩니다:
# practical_type_mixing.py
# Calculating price per item
total_cost = 47.50
num_items = 5
price_per_item = total_cost / num_items # Float / int → float
print(f"Price per item: ${price_per_item}")
# Output: Price per item: $4.75
# Calculating average (will be float even if inputs are integers)
total_points = 450
num_tests = 5
average = total_points / num_tests # Int / int → float
print(f"Average: {average}") # Output: Average: 90.0
# If you need an integer result, convert explicitly
average_rounded = int(total_points / num_tests)
print(f"Average (as integer): {average_rounded}")
# Output: Average (as integer): 904.5.5) 정수 결과가 중요한 경우
세기, 인덱싱, 이산적인 연산처럼 정수 결과가 특히 중요한 경우가 있습니다. 이럴 때는 다음처럼 정수 결과를 보장할 수 있습니다:
# ensuring_integer_results.py
# Using floor division when you need integer results
items = 47
items_per_box = 12
# How many complete boxes?
complete_boxes = items // items_per_box # Integer result
print(f"Complete boxes: {complete_boxes}")
# Output: Complete boxes: 3
# If you use regular division, you get a float
boxes_float = items / items_per_box
print(f"Boxes (float): {boxes_float}")
# Output: Boxes (float): 3.9166666666666665
# Converting float to integer (truncates toward zero)
boxes_truncated = int(boxes_float)
print(f"Boxes (truncated): {boxes_truncated}")
# Output: Boxes (truncated): 3여기서 차이에 주목해야 합니다. //는 아래로 반올림(음의 무한대 방향)하고, int()는 0을 향해 잘라냅니다. 양수에 대해서는 결과가 같지만, 음수에서는 다릅니다:
# truncation_vs_floor.py
# For positive numbers: same result
print(7 // 2) # Output: 3
print(int(7/2)) # Output: 3
# For negative numbers: different results
print(-7 // 2) # Output: -4 (rounds down toward negative infinity)
print(int(-7/2)) # Output: -3 (truncates toward zero)4.5.6) 예상치 못한 실수 결과 피하기
정수 결과를 기대했는데 실수가 나와서 놀랄 때가 있습니다. 대부분 나눗셈을 사용했거나, 타입을 혼합했기 때문에 그렇습니다:
# unexpected_floats.py
# Calculating average - result is always float because of division
count = 10
total = 100
average = total / count
print(average) # Output: 10.0 (float, even though it's a whole number)
# If you need an integer and you know it divides evenly:
average_int = total // count
print(average_int) # Output: 10 (integer)
# Or convert explicitly:
average_int = int(total / count)
print(average_int) # Output: 10 (integer)핵심 요점은, Python의 타입 승격 규칙은 정밀도 보존과 데이터 손실 방지에 맞춰져 있다는 것입니다. 정수와 실수를 섞거나 일반 나눗셈을 사용하면, 실수 결과를 기대해야 합니다. 정수가 필요하다면 버림 나눗셈이나 명시적 변환을 사용하되, 이들이 반올림을 어떻게 처리하는지 이해해야 합니다.
4.6) 유용한 숫자 내장 함수: abs(), min(), max(), pow()
Python은 흔한 숫자 연산을 수행할 수 있는 여러 내장 함수를 제공합니다. 이 함수들은 정수와 실수 모두에 대해 동작하며, 일상적인 프로그래밍에서 매우 중요한 도구입니다. 여기서는 가장 자주 쓰이는 것들을 살펴봅니다.
4.6.1) abs()로 절댓값 구하기
abs() 함수는 숫자의 절댓값, 즉 부호를 무시한 0으로부터의 거리를 반환합니다:
# absolute_value.py
# Absolute value of negative numbers
result = abs(-42)
print(result) # Output: 42
# Absolute value of positive numbers (unchanged)
result = abs(42)
print(result) # Output: 42
# Works with floats too
result = abs(-3.14)
print(result) # Output: 3.14
# Absolute value of zero is zero
result = abs(0)
print(result) # Output: 0절댓값 함수는 방향(부호)은 중요하지 않고 크기만 중요할 때 유용합니다:
# practical_abs.py
# Calculate temperature difference (magnitude only)
morning_temp = 45.5
evening_temp = 38.2
difference = abs(evening_temp - morning_temp)
print(f"Temperature changed by {difference} degrees")
# Output: Temperature changed by 7.3 degrees
# Calculate distance between two points (always positive)
position1 = 10
position2 = 25
distance = abs(position2 - position1)
print(f"Distance: {distance}") # Output: Distance: 15
# Works the same regardless of order
distance = abs(position1 - position2)
print(f"Distance: {distance}") # Output: Distance: 154.6.2) min()과 max()로 최소값/최댓값 찾기
min() 함수는 인수들 중 가장 작은 값을, max()는 가장 큰 값을 반환합니다:
# min_max_basic.py
# Find minimum of two numbers
smallest = min(10, 25)
print(smallest) # Output: 10
# Find maximum of two numbers
largest = max(10, 25)
print(largest) # Output: 25
# Works with more than two arguments
smallest = min(5, 12, 3, 18, 7)
print(smallest) # Output: 3
largest = max(5, 12, 3, 18, 7)
print(largest) # Output: 18
# Works with floats and mixed types
smallest = min(3.5, 2, 4.1, 1.9)
print(smallest) # Output: 1.9이 함수들은 데이터에서 극값(최댓값, 최솟값)을 찾을 때 매우 유용합니다:
# practical_min_max.py
# Find the best and worst test scores
test1 = 85
test2 = 92
test3 = 78
test4 = 95
highest_score = max(test1, test2, test3, test4)
lowest_score = min(test1, test2, test3, test4)
print(f"Highest score: {highest_score}") # Output: Highest score: 95
print(f"Lowest score: {lowest_score}") # Output: Lowest score: 78
# Clamp a value within a range
value = 150
minimum = 0
maximum = 100
# Ensure value is not below minimum
value = max(value, minimum)
# Ensure value is not above maximum
value = min(value, maximum)
print(f"Clamped value: {value}") # Output: Clamped value: 100특정 범위 안으로 값을 제한하는 이 "클램핑(clamping)" 패턴은, max()와 min()을 함께 사용할 때 자주 등장합니다:
# clamping_pattern.py
def clamp(value, min_value, max_value):
"""Constrain value to be within [min_value, max_value]"""
return max(min_value, min(value, max_value))
# Test the clamp function
print(clamp(150, 0, 100)) # Output: 100 (too high, clamped to max)
print(clamp(-10, 0, 100)) # Output: 0 (too low, clamped to min)
print(clamp(50, 0, 100)) # Output: 50 (within range, unchanged)4.6.3) pow()로 거듭제곱 계산하기
pow() 함수는 숫자의 거듭제곱을 계산합니다. Python에는 거듭제곱을 위한 ** 연산자도 있으며, 이는 더 자주 사용됩니다. 하지만 pow()에는 몇 가지 추가 기능이 있습니다:
# exponentiation.py
# Using the ** operator (most common)
result = 2 ** 3 # 2 to the power of 3
print(result) # Output: 8
result = 5 ** 2 # 5 squared
print(result) # Output: 25
# Using pow() function (equivalent)
result = pow(2, 3)
print(result) # Output: 8
result = pow(5, 2)
print(result) # Output: 25
# Negative exponents give fractions
result = 2 ** -3 # 1 / (2^3) = 1/8
print(result) # Output: 0.125
# Fractional exponents give roots
result = 9 ** 0.5 # Square root of 9
print(result) # Output: 3.0
result = 8 ** (1/3) # Cube root of 8
print(result) # Output: 2.0pow() 함수는 선택적 세 번째 인수로 모듈러 거듭제곱(modular exponentiation)을 지원합니다(암호학이나 수론에서 유용하지만, 기본 산술의 범위를 벗어납니다):
# modular_exponentiation.py
# pow(base, exponent, modulus) computes (base ** exponent) % modulus efficiently
result = pow(2, 10, 100) # (2^10) % 100
print(result) # Output: 24
# This is more efficient than computing separately for large numbers:
# result = (2 ** 10) % 100 # Same result, but less efficient for large numbers일반적인 일상 사용에서는 pow()보다 ** 연산자가 더 편리합니다:
# practical_exponentiation.py
# Calculate compound interest: A = P(1 + r)^t
principal = 1000 # Initial amount
rate = 0.05 # 5% interest rate
years = 10
amount = principal * (1 + rate) ** years
print(f"Amount after {years} years: ${amount:.2f}")
# Output: Amount after 10 years: $1628.89
# Calculate area of a square
side_length = 5
area = side_length ** 2
print(f"Area: {area}") # Output: Area: 25
# Calculate volume of a cube
side_length = 3
volume = side_length ** 3
print(f"Volume: {volume}") # Output: Volume: 274.6.4) 내장 함수들을 조합해 사용하기
이 함수들은 서로 조합해 흔한 문제들을 해결할 때도 잘 작동합니다:
# combining_functions.py
# Find the range (difference between max and min)
values = [15, 42, 8, 23, 37]
value_range = max(values) - min(values)
print(f"Range: {value_range}") # Output: Range: 34
# Note: We're using a list here (we'll learn about lists in detail in Chapter 13)
# For now, just understand that max() and min() can work with a list of values
# Calculate distance between two points in 2D space
x1, y1 = 3, 4
x2, y2 = 6, 8
# Distance formula: sqrt((x2-x1)^2 + (y2-y1)^2)
# We'll use ** for squaring (square root comes in section 4.11)
distance_squared = (x2 - x1) ** 2 + (y2 - y1) ** 2
distance = distance_squared ** 0.5 # Square root via fractional exponent
print(f"Distance: {distance}") # Output: Distance: 5.0이러한 내장 함수들은 Python 프로그래밍의 기본 도구입니다. 효율적이고, 잘 검증되어 있으며, 서로 다른 숫자 타입에서 일관되게 동작합니다. 이런 연산이 필요할 때마다 직접 구현하려 하지 말고, 내장 함수들을 사용하십시오.
4.7) round()로 숫자 반올림하기
부동소수점 수를 다루다 보면, 결과를 표시하거나, 계산하거나, 저장할 때 특정 소수 자릿수까지 반올림해야 하는 경우가 많습니다. Python의 round() 함수는 이런 기능을 제공합니다.
4.7.1) round()로 기본 반올림하기
round() 함수는 숫자를 가장 가까운 정수로 반올림합니다:
# basic_rounding.py
# Round to nearest integer
result = round(3.7)
print(result) # Output: 4
result = round(3.2)
print(result) # Output: 3
# Exactly halfway rounds to nearest even number (banker's rounding)
result = round(2.5)
print(result) # Output: 2 (rounds to even)
result = round(3.5)
print(result) # Output: 4 (rounds to even)
# Negative numbers work too
result = round(-3.7)
print(result) # Output: -4
result = round(-3.2)
print(result) # Output: -3정수의 정확히 중간(예: 2.5, 3.5)에 있는 수를 반올림할 때의 동작에 주목하십시오. Python은 "반올림 오차를 줄이기 위한 은행가 반올림(banker's rounding)"인 "가장 가까운 짝수로 반올림(round half to even)"을 사용합니다. 이는 반복적인 반올림에서 편향(bias)을 줄입니다. 대부분의 일상적인 프로그래밍에서는 크게 신경 쓸 일이 없지만, 알고 있는 것이 좋습니다.
4.7.2) 특정 소수 자릿수까지 반올림하기
round() 함수는 몇 번째 소수 자리까지 남길지 지정하는 선택적 두 번째 인수를 받을 수 있습니다:
# decimal_places.py
# Round to 2 decimal places
result = round(3.14159, 2)
print(result) # Output: 3.14
# Round to 1 decimal place
result = round(3.14159, 1)
print(result) # Output: 3.1
# Round to 3 decimal places
result = round(2.71828, 3)
print(result) # Output: 2.718
# You can round to 0 decimal places (same as omitting the argument)
result = round(3.7, 0)
print(result) # Output: 4.0 (note: returns float, not int)소수 자릿수를 지정하면 round()는 항상 실수를 반환합니다(소수 자릿수를 0으로 지정했을 때도 마찬가지입니다). 두 번째 인수를 생략하면 정수를 반환합니다.
4.7.3) 반올림의 실용적인 활용
반올림은 금액, 측정값 등에서 너무 많은 자릿수를 보여 주는 것이 불필요하거나 혼란스러운 경우에 필수적입니다:
# practical_rounding.py
# Display prices with 2 decimal places
price = 19.99
tax_rate = 0.08
total = price * (1 + tax_rate)
print(f"Total (unrounded): ${total}") # Output: Total (unrounded): $21.5892
print(f"Total (rounded): ${round(total, 2)}") # Output: Total (rounded): $21.59
# Calculate and display average
total_score = 456
num_tests = 7
average = total_score / num_tests
print(f"Average (unrounded): {average}") # Output: Average (unrounded): 65.14285714285714
print(f"Average (rounded): {round(average, 2)}") # Output: Average (rounded): 65.14
# Round measurements to reasonable precision
distance_meters = 123.456789
distance_rounded = round(distance_meters, 1)
print(f"Distance: {distance_rounded} meters") # Output: Distance: 123.5 meters4.7.4) 반올림 vs 잘라내기 vs 바닥/천장
반올림은 소수 부분을 버리는 잘라내기(truncating)나, 바닥/천장(floor/ceiling) 연산과 다르다는 점을 이해하는 것이 중요합니다:
# rounding_vs_others.py
value = 3.7
# Rounding: nearest integer
rounded = round(value)
print(f"Rounded: {rounded}") # Output: Rounded: 4
# Truncating: remove decimal part (convert to int)
truncated = int(value)
print(f"Truncated: {truncated}") # Output: Truncated: 3
# We'll see floor and ceil in section 4.11, but briefly:
# Floor: largest integer <= value (always rounds down)
# Ceiling: smallest integer >= value (always rounds up)음수에 대해서는 차이가 더 뚜렷합니다:
# negative_rounding.py
value = -3.7
# Rounding: nearest integer
rounded = round(value)
print(f"Rounded: {rounded}") # Output: Rounded: -4
# Truncating: toward zero
truncated = int(value)
print(f"Truncated: {truncated}") # Output: Truncated: -3
# Floor (rounds down toward negative infinity): -4
# Ceiling (rounds up toward positive infinity): -34.7.5) 반올림 사용 시 주의할 점
부동소수점 정밀도 때문에 예상과 다른 결과가 나올 수 있습니다:
컴퓨터가 부동소수점 수를 표현하는 방식(4.10절에서 설명합니다) 때문에, 반올림이 항상 기대한 정확한 10진수를 만들어 내는 것은 아닙니다:
# rounding_surprises.py
# Sometimes rounding doesn't give the exact decimal you expect
value = 2.675
rounded = round(value, 2)
print(rounded) # Output: 2.67 (not 2.68 as you might expect)
# This happens because 2.675 can't be represented exactly in binary floating-point
# The actual stored value is slightly less than 2.675대부분의 실용적인 상황에서는 문제가 되지 않습니다. 하지만 금융 계산처럼 정확한 소수 연산이 특히 중요한 경우에는 Python의 decimal 모듈이 필요할 수 있습니다(이 책에서는 다루지 않지만, 존재한다는 것을 알고 있으면 좋습니다).
표시용 반올림과 계산용 반올림을 구분하십시오:
종종 계산에는 전체 정밀도를 유지하면서, 표시할 때만 반올림하고 싶을 때가 많습니다:
# rounding_for_display.py
price1 = 19.99
price2 = 15.49
tax_rate = 0.08
# Calculate with full precision
subtotal = price1 + price2
tax = subtotal * tax_rate
total = subtotal + tax
# Round only for display
print(f"Subtotal: ${round(subtotal, 2)}") # Output: Subtotal: $35.48
print(f"Tax: ${round(tax, 2)}") # Output: Tax: $2.84
print(f"Total: ${round(total, 2)}") # Output: Total: $38.32
# The variables still contain full precision
print(f"Total (full precision): ${total}")
# Output: Total (full precision): $38.3184round() 함수는 Python에서 가장 자주 사용하는 내장 함수 중 하나입니다. 읽기 좋은 형식으로 숫자 결과를 표시해야 할 때나, 특정 목적을 위해 정밀도를 제한해야 할 때마다 사용하십시오.
4.8) 흔한 숫자 패턴 (카운터, 합계, 평균)
이제 Python의 숫자 연산과 함수들을 이해했으니, 실제 프로그램에서 반복적으로 사용하게 될 흔한 패턴들을 살펴보겠습니다. 이 패턴들은 숫자 데이터를 처리하는 기본 빌딩 블록입니다.
4.8.1) 카운터(counter): 개수 세기
카운터는 어떤 일이 몇 번 발생했는지 추적하는 변수입니다. 0으로 초기화하고, 사건이 발생할 때마다 1씩 증가시킵니다:
# basic_counter.py
# Count how many numbers we've processed
count = 0 # Initialize counter
# Process first number
count += 1
print(f"Processed {count} number(s)") # Output: Processed 1 number(s)
# Process second number
count += 1
print(f"Processed {count} number(s)") # Output: Processed 2 number(s)
# Process third number
count += 1
print(f"Processed {count} number(s)") # Output: Processed 3 number(s)실제 프로그램에서는 이런 카운터를 반복문(loop) 안에서 사용하는 경우가 대부분입니다(10장과 11장에서 배웁니다). 지금은 패턴만 이해하면 됩니다: 0에서 시작해, 매번 1을 더합니다.
카운터는 서로 다른 종류의 사건을 추적하는 데도 사용할 수 있습니다:
# multiple_counters.py
# Track different categories
even_count = 0
odd_count = 0
# Check number 1
number = 4
if number % 2 == 0:
even_count += 1
else:
odd_count += 1
# Check number 2
number = 7
if number % 2 == 0:
even_count += 1
else:
odd_count += 1
# Check number 3
number = 10
if number % 2 == 0:
even_count += 1
else:
odd_count += 1
print(f"Even numbers: {even_count}") # Output: Even numbers: 2
print(f"Odd numbers: {odd_count}") # Output: Odd numbers: 14.8.2) 누산기(accumulator): 합계 누적하기
누산기는 합계를 누적하는 변수입니다. 카운터처럼 0으로 초기화하지만, 매번 1을 더하는 대신, 그때그때 다른 값을 더합니다:
# basic_accumulator.py
# Calculate total sales
total_sales = 0 # Initialize accumulator
# First sale
sale = 19.99
total_sales += sale
print(f"Total so far: ${total_sales}") # Output: Total so far: $19.99
# Second sale
sale = 34.50
total_sales += sale
print(f"Total so far: ${total_sales}") # Output: Total so far: $54.49
# Third sale
sale = 12.00
total_sales += sale
print(f"Total so far: ${total_sales}") # Output: Total so far: $66.49누산기는 데이터 처리의 핵심 패턴입니다. 데이터를 처리하면서 값을 점진적으로 합산할 수 있게 해줍니다:
# multiple_accumulators.py
# Track both total and count to calculate average later
total_score = 0
count = 0
# Process score 1
score = 85
total_score += score
count += 1
# Process score 2
score = 92
total_score += score
count += 1
# Process score 3
score = 78
total_score += score
count += 1
print(f"Total score: {total_score}") # Output: Total score: 255
print(f"Number of scores: {count}") # Output: Number of scores: 34.8.3) 평균 계산하기
평균은 합계(누산기)와 개수(카운터)를 결합한 패턴입니다. 합계와 개수를 모두 알고 있어야, 합계를 개수로 나눌 수 있습니다:
# calculating_average.py
# Calculate average of test scores
total_score = 0
count = 0
# Add scores
total_score += 85
count += 1
total_score += 92
count += 1
total_score += 78
count += 1
total_score += 88
count += 1
# Calculate average
average = total_score / count
print(f"Average score: {average}") # Output: Average score: 85.75
# Often you'll want to round the average
average_rounded = round(average, 2)
print(f"Average (rounded): {average_rounded}") # Output: Average (rounded): 85.75중요: 0으로 나누지 않도록 항상 확인해야 합니다:
평균을 계산할 때는, 개수(count)가 0이 아닌지 꼭 확인해야 합니다:
# safe_average.py
total_score = 0
count = 0
# If no scores were added, count is still 0
# Dividing by zero causes an error!
if count > 0:
average = total_score / count
print(f"Average: {average}")
else:
print("No scores to average")
# Output: No scores to average이런 조건 처리에 대해서는 8장에서 더 자세히 배웁니다.
4.8.4) 현재까지의 최댓값/최솟값 찾기
지금까지 본 값들 중에서 가장 큰 값이나 가장 작은 값을 추적해야 할 때가 자주 있습니다.
max()와 min() 함수를 사용하여 쉽게 이를 구현할 수 있습니다:
# running_max_min_simplified.py
# Track highest and lowest using max() and min()
highest_temp = 72
lowest_temp = 72
# Update with new temperature
current_temp = 85
highest_temp = max(highest_temp, current_temp)
lowest_temp = min(lowest_temp, current_temp)
# Update with another temperature
current_temp = 68
highest_temp = max(highest_temp, current_temp)
lowest_temp = min(lowest_temp, current_temp)
print(f"High: {highest_temp}, Low: {lowest_temp}")
# Output: High: 85, Low: 684.9) 정수를 위한 비트 연산자: &, |, ^, <<, >> (간략 소개)
비트 연산자(bitwise operator)는 정수의 개별 비트(0과 1)에 대해 동작합니다. 이런 연산자들은 일상적인 프로그래밍에서 산술 연산자만큼 자주 사용되지는 않지만, 플래그(flags), 권한(permissions), 저수준 데이터 조작, 성능 최적화 등 특정 작업에서 매우 중요합니다.
이 절에서는 간단한 개요만 제공합니다. 비트 연산을 제대로 이해하려면 이진법 표현(binary representation)을 이해해야 하는데, 이는 이 입문 장에서 완전히 다루기에는 다소 깊은 주제입니다.
4.9.1) 이진 표현 이해하기 (간단 소개)
컴퓨터는 정수를 비트(0과 1)의 나열로 저장합니다. 예를 들어:
- 10진수 5는 2진수로
101입니다 (1×4 + 0×2 + 1×1) - 10진수 12는 2진수로
1100입니다 (1×8 + 1×4 + 0×2 + 0×1)
Python의 bin() 함수는 정수의 이진 표현을 보여 줍니다:
# binary_representation.py
# See binary representation of integers
print(bin(5)) # Output: 0b101
print(bin(12)) # Output: 0b1100
print(bin(255)) # Output: 0b11111111
# The 0b prefix indicates binary notation4.9.2) 비트 AND (&)
& 연산자는 비트 AND를 수행합니다. 결과의 각 비트는, 두 피연산자의 해당 비트가 모두 1일 때만 1이 됩니다:
# bitwise_and.py
# 12 is 1100 in binary
# 10 is 1010 in binary
result = 12 & 10
print(result) # Output: 8
print(bin(result)) # Output: 0b1000
# How it works:
# 1100 (12)
# & 1010 (10)
# ------
# 1000 (8)비트 AND는 특정 비트가 켜져 있는지(플래그 검사용) 확인할 때 자주 사용됩니다:
# checking_flags.py
# File permissions example (simplified)
READ = 4 # 100 in binary
WRITE = 2 # 010 in binary
EXECUTE = 1 # 001 in binary
permissions = 6 # 110 in binary (READ + WRITE)
# Check if READ permission is set
has_read = (permissions & READ) != 0
print(f"Has read: {has_read}") # Output: Has read: True
# Check if EXECUTE permission is set
has_execute = (permissions & EXECUTE) != 0
print(f"Has execute: {has_execute}") # Output: Has execute: False4.9.3) 비트 OR (|)
| 연산자는 비트 OR를 수행합니다. 결과의 각 비트는, 두 피연산자의 해당 비트 중 하나라도 1이면 1이 됩니다:
# bitwise_or.py
# 12 is 1100 in binary
# 10 is 1010 in binary
result = 12 | 10
print(result) # Output: 14
print(bin(result)) # Output: 0b1110
# How it works:
# 1100 (12)
# | 1010 (10)
# ------
# 1110 (14)비트 OR는 플래그를 결합할 때 사용됩니다:
# combining_flags.py
READ = 4 # 100 in binary
WRITE = 2 # 010 in binary
EXECUTE = 1 # 001 in binary
# Grant READ and WRITE permissions
permissions = READ | WRITE
print(f"Permissions: {permissions}") # Output: Permissions: 6
print(bin(permissions)) # Output: 0b110
# Add EXECUTE permission
permissions = permissions | EXECUTE
print(f"Permissions: {permissions}") # Output: Permissions: 7
print(bin(permissions)) # Output: 0b1114.9.4) 비트 XOR (^)
^ 연산자는 비트 XOR(배타적 OR)을 수행합니다. 결과의 각 비트는 두 피연산자의 해당 비트가 서로 다를 때 1이 됩니다:
# bitwise_xor.py
# 12 is 1100 in binary
# 10 is 1010 in binary
result = 12 ^ 10
print(result) # Output: 6
print(bin(result)) # Output: 0b110
# How it works:
# 1100 (12)
# ^ 1010 (10)
# ------
# 0110 (6)XOR에는 비트를 토글하거나 값을 되돌리는 등의 흥미로운 성질이 있습니다:
# xor_properties.py
# XOR with itself gives 0
result = 5 ^ 5
print(result) # Output: 0
# XOR with 0 gives the original number
result = 5 ^ 0
print(result) # Output: 5
# XOR is its own inverse (useful for simple encryption)
original = 42
key = 123
encrypted = original ^ key
decrypted = encrypted ^ key
print(f"Original: {original}, Encrypted: {encrypted}, Decrypted: {decrypted}")
# Output: Original: 42, Encrypted: 81, Decrypted: 424.9.5) 왼쪽 시프트 (<<)와 오른쪽 시프트 (>>)
<< 연산자는 비트를 왼쪽으로 시프트하고, >> 연산자는 오른쪽으로 시프트합니다:
# bit_shifting.py
# Left shift: multiply by powers of 2
result = 5 << 1 # Shift left by 1 bit
print(result) # Output: 10
# 5 is 101 in binary, shifted left becomes 1010 (10)
result = 5 << 2 # Shift left by 2 bits
print(result) # Output: 20
# 5 is 101 in binary, shifted left becomes 10100 (20)
# Right shift: divide by powers of 2 (floor division)
result = 20 >> 1 # Shift right by 1 bit
print(result) # Output: 10
# 20 is 10100 in binary, shifted right becomes 1010 (10)
result = 20 >> 2 # Shift right by 2 bits
print(result) # Output: 5
# 20 is 10100 in binary, shifted right becomes 101 (5)왼쪽으로 n 비트 시프트하는 것은 2^n을 곱하는 것과 같고, 오른쪽으로 시프트하는 것은 2^n으로 나누는 것(버림 나눗셈)과 같습니다:
# shift_as_multiplication.py
# Left shift multiplies by powers of 2
print(3 << 1) # Output: 6 (3 * 2^1 = 3 * 2)
print(3 << 2) # Output: 12 (3 * 2^2 = 3 * 4)
print(3 << 3) # Output: 24 (3 * 2^3 = 3 * 8)
# Right shift divides by powers of 2
print(24 >> 1) # Output: 12 (24 // 2^1 = 24 // 2)
print(24 >> 2) # Output: 6 (24 // 2^2 = 24 // 4)
print(24 >> 3) # Output: 3 (24 // 2^3 = 24 // 8)4.9.6) 비트 연산자를 언제 사용할까
비트 연산자는 다음과 같은 상황에서 유용합니다:
- 이진 플래그와 권한을 다룰 때 (예: Unix/Linux 파일 권한)
- 저수준 데이터 조작 (여러 값을 하나의 정수에 압축해서 저장하기 등)
- 네트워크 프로그래밍 (IP 주소, 프로토콜 플래그 조작 등)
- 그래픽스/게임 프로그래밍 (색상 조작, 충돌 감지 등)
- 성능 최적화 (2의 거듭제곱으로 곱셈/나눗셈할 때 시프트가 더 빠를 수 있음)
대부분의 일상적인 프로그래밍 작업에서는 비트 연산자를 사용할 일이 많지 않습니다. 하지만 시스템 프로그래밍, 네트워킹, 성능이 중요한 코드에서 작업할 때는 매우 귀중한 도구입니다.
참고: 여기서는 기본적인 내용만 다루었습니다. 음수에 대한 비트 연산은 2의 보수(two's complement) 표현이 필요하기 때문에 더 복잡하며, 이 입문 설명의 범위를 벗어납니다. 지금은 이러한 연산자들이 존재하고, 대략 어떤 일을 하는지만 이해하고 있으면 충분합니다.
4.10) 부동소수점 정밀도와 반올림 오류 (간단 설명)
Python(그리고 대부분의 프로그래밍 언어)에서 부동소수점 수는 아주 작은 수에서부터 매우 큰 수까지 다양한 값을 표현할 수 있습니다. 하지만 초보자가 당황할 수 있는 한 가지 제한이 있습니다. 바로 모든 10진 소수를 정확하게 표현할 수 있는 것은 아니라는 점입니다. 이 절에서는 왜 이런 일이 발생하는지, 그리고 이것이 프로그램에 어떤 의미를 가지는지 설명합니다.
4.10.1) 부동소수점 수가 항상 정확하지 않은 이유
컴퓨터는 부동소수점 수를 10진수(기수 10)가 아니라 2진수(기수 2)로 저장합니다. 우리가 보기에는 간단해 보이는 어떤 10진수 소수는 2진수로는 정확하게 표현할 수 없습니다. 마치 1/3을 10진수로 정확히 표현할 수 없는 것(0.333333...이 끝없이 이어짐)과 비슷합니다.
놀라운 예를 하나 보겠습니다:
# floating_point_surprise.py
# This seems like it should be exactly 0.3
result = 0.1 + 0.2
print(result) # Output: 0.30000000000000004 (not exactly 0.3!)
# Check if it equals 0.3
print(result == 0.3) # Output: False이는 Python의 버그가 아니라, 컴퓨터가 부동소수점 수를 표현하는 방식에서 오는 근본적인 제한입니다. 10진수 0.1은 2진 부동소수점으로 정확히 표현할 수 없고, 10진수에서 1/3을 정확히 표현할 수 없는 것과 비슷한 상황입니다.
4.10.2) 표현상의 문제 이해하기
이 동작을 좀 더 보여주는 예들을 보겠습니다:
# more_precision_examples.py
# Simple decimal values that aren't exact in binary
print(0.1) # Output: 0.1 (Python rounds for display)
print(repr(0.1)) # Output: 0.1 (still rounded)
# But the actual stored value has tiny errors
print(0.1 + 0.1 + 0.1) # Output: 0.30000000000000004
# Multiplication can accumulate these errors
result = 0.1 * 3
print(result) # Output: 0.30000000000000004
# Some numbers are exact (powers of 2)
print(0.5) # Output: 0.5 (exact)
print(0.25) # Output: 0.25 (exact)
print(0.125) # Output: 0.125 (exact)0.5, 0.25, 0.125 처럼 2의 거듭제곱 또는 그 합으로 표현 가능한 숫자들은 정확하게 표현됩니다. 하지만 대부분의 10진 소수는 그렇지 않습니다.
4.10.3) 실용적인 영향
대부분의 일상적인 프로그래밍에서는 이러한 작은 오류가 크게 중요하지 않습니다. 하지만 몇 가지 상황에서는 이를 알고 있어야 합니다.
부동소수점 수 비교하기:
# comparing_floats.py
# Direct equality comparison can fail
a = 0.1 + 0.2
b = 0.3
print(a == b) # Output: False (due to tiny difference)
# Better: check if they're close enough
difference = abs(a - b)
tolerance = 0.0001 # How close is "close enough"?
print(difference < tolerance) # Output: True (they're close enough)
# Python 3.5+ provides math.isclose() for this (we'll see in section 4.11)반복 계산에서 누적되는 오류:
# accumulated_errors.py
# Adding 0.1 ten times
total = 0.0
for i in range(10):
total += 0.1
print(total) # Output: 0.9999999999999999 (not exactly 1.0)
print(total == 1.0) # Output: False
# The error accumulates with each addition금융 계산:
# financial_calculations.py
# Money calculations can have surprising results
price = 0.10
quantity = 3
total = price * quantity
print(total) # Output: 0.30000000000000004
# For financial calculations, consider rounding to cents
total_cents = round(total * 100) # Convert to cents
total_dollars = total_cents / 100
print(total_dollars) # Output: 0.3
# Or use Python's decimal module for exact decimal arithmetic
# (beyond the scope of this chapter, but worth knowing about)4.10.4) 부동소수점 수를 다루는 전략
표시할 때 결과를 반올림하십시오:
# rounding_for_display.py
result = 0.1 + 0.2
print(f"Result: {round(result, 2)}") # Output: Result: 0.3
# Or use formatted output (we'll learn more in Chapter 6)
print(f"Result: {result:.2f}") # Output: Result: 0.30부동소수점 수를 정확히 같은지 비교하지 마십시오:
# safe_float_comparison.py
a = 0.1 + 0.2
b = 0.3
# Instead of: if a == b:
# Use: if they're close enough
if abs(a - b) < 0.0001:
print("Close enough to equal")
# Output: Close enough to equal정밀도가 중요할 때는 주의하십시오:
과학 계산, 금융 계산, 또는 정확한 10진 연산이 중요한 다른 분야에서는 다음을 고려해야 할 수 있습니다:
- 정확한 10진 연산을 위한 Python의
decimal모듈 - 정확한 유리수 연산을 위한
fractions모듈 - 계산의 적절한 지점에서 신중한 반올림
이 모듈들은 이 장의 범위를 벗어나지만, 존재를 알고 있는 것만으로도 가치가 있습니다.
4.10.5) 부동소수점 정밀도가 중요하지 않은 경우
대부분의 일상적인 프로그래밍 작업에서는 부동소수점의 정밀도가 충분합니다:
# when_precision_is_fine.py
# Calculating area
length = 5.5
width = 3.2
area = length * width
print(f"Area: {area:.2f} square meters") # Output: Area: 17.60 square meters
# Converting temperature
fahrenheit = 98.6
celsius = (fahrenheit - 32) * 5 / 9
print(f"Temperature: {celsius:.1f}°C") # Output: Temperature: 37.0°C
# Calculating average
total = 456.78
count = 7
average = total / count
print(f"Average: {average:.2f}") # Output: Average: 65.25표시할 때 적절히 반올림하거나, 측정 정밀도에 비해 이런 작은 오류들이 무시할 수 있는 수준이라면, 부동소수점 연산은 충분히 잘 동작합니다.
4.10.6) 핵심 요약
부동소수점 수는 실제 수의 근사값입니다. 대부분의 프로그래밍 작업에서는 이 정도 정밀도로 충분합니다. 다만 다음을 기억하십시오.
- 부동소수점 수를 정확히 같은지 비교하지 말고, 충분히 가까운지를 검사하십시오.
- 결과를 표시할 때는 반올림해 의미 없는 소수 자릿수를 숨기십시오.
- 반복 계산에서 누적 오류에 주의하십시오.
- 금융 계산에서는 적절한 정밀도로 반올림하거나,
decimal모듈 사용을 고려하십시오.
이러한 제한을 이해하면 더 견고한 프로그램을 작성하고, 놀라운 버그를 피할 수 있습니다. 좋은 소식은, 위의 간단한 지침들을 따르면 대부분의 프로그래밍 작업에서 부동소수점 연산은 "그냥 잘 동작"한다는 것입니다.
4.11) (선택) math 모듈의 기본 수학 함수 (sqrt, floor, ceil, pi)
Python의 내장 연산자와 함수만으로도 기본 산술 연산은 충분히 할 수 있습니다. 그러나 더 고급 수학 연산을 위해 Python은 math 모듈을 제공합니다. 모듈(module)은 관련된 함수와 값들을 모아 놓은 묶음입니다. 22장에서 모듈에 대해 더 자세히 배우겠지만, 여기서는 math 모듈을 사용하는 데 필요한 기초만 소개합니다.
4.11.1) math 모듈 가져오기(import)
math 모듈을 사용하려면 프로그램 시작 부분에서 import 해야 합니다:
# importing_math.py
import math
# Now you can use functions from the math module
# by writing math.function_name()모듈을 import 하면, 그 안에 있는 모든 함수와 값에 접근할 수 있습니다. 사용할 때는 모듈 이름, 점(.), 그리고 함수나 값 이름을 순서대로 씁니다.
4.11.2) 수학 상수: pi와 e
math 모듈은 중요한 수학 상수들의 정확한 값을 제공합니다:
# math_constants.py
import math
# Pi (π): ratio of circle's circumference to diameter
print(math.pi) # Output: 3.141592653589793
# Euler's number (e): base of natural logarithms
print(math.e) # Output: 2.718281828459045
# Using pi to calculate circle properties
radius = 5
circumference = 2 * math.pi * radius
area = math.pi * radius ** 2
print(f"Circumference: {circumference:.2f}") # Output: Circumference: 31.42
print(f"Area: {area:.2f}") # Output: Area: 78.544.11.3) sqrt()로 제곱근 구하기
sqrt() 함수는 숫자의 제곱근을 계산합니다:
# square_root.py
import math
# Square root of perfect squares
result = math.sqrt(16)
print(result) # Output: 4.0
result = math.sqrt(25)
print(result) # Output: 5.0
# Square root of non-perfect squares
result = math.sqrt(2)
print(result) # Output: 1.4142135623730951
# Using sqrt in calculations
# Distance formula: sqrt((x2-x1)^2 + (y2-y1)^2)
x1, y1 = 3, 4
x2, y2 = 6, 8
distance = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
print(f"Distance: {distance}") # Output: Distance: 5.0sqrt()는 완전제곱수에 대해서도 항상 실수를 반환합니다. 또한, sqrt()로 음수의 제곱근을 구할 수는 없습니다(에러가 발생합니다).
4.11.4) floor()와 ceil()로 바닥/천장 값 구하기
floor() 함수는 값을 가장 가까운 아래쪽 정수(음의 무한대 방향)로 내리고, ceil() 함수는 가장 가까운 위쪽 정수(양의 무한대 방향)로 올립니다:
# floor_and_ceil.py
import math
# Floor: rounds down
result = math.floor(3.7)
print(result) # Output: 3
result = math.floor(3.2)
print(result) # Output: 3
# Ceiling: rounds up
result = math.ceil(3.7)
print(result) # Output: 4
result = math.ceil(3.2)
print(result) # Output: 4
# With negative numbers
result = math.floor(-3.7)
print(result) # Output: -4 (rounds toward negative infinity)
result = math.ceil(-3.7)
print(result) # Output: -3 (rounds toward positive infinity)이 함수들은 값을 특정 범위 안에 맞추거나, 특정한 반올림 방식으로 정수 결과가 필요할 때 유용합니다:
# practical_floor_ceil.py
import math
# How many boxes needed to pack all items?
items = 47
items_per_box = 12
# Use ceil to round up (need a full box even if not completely filled)
boxes_needed = math.ceil(items / items_per_box)
print(f"Boxes needed: {boxes_needed}") # Output: Boxes needed: 4
# How many complete pages for a document?
total_lines = 250
lines_per_page = 30
# Use floor to count only complete pages
complete_pages = math.floor(total_lines / lines_per_page)
print(f"Complete pages: {complete_pages}") # Output: Complete pages: 84.11.5) floor(), ceil(), round(), int() 비교하기
이 다양한 반올림 방식들이 어떻게 다른지 이해하는 것이 도움이 됩니다:
# comparing_rounding_methods.py
import math
value = 3.7
print(f"Original: {value}")
print(f"floor(): {math.floor(value)}") # Output: floor(): 3
print(f"ceil(): {math.ceil(value)}") # Output: ceil(): 4
print(f"round(): {round(value)}") # Output: round(): 4
print(f"int(): {int(value)}") # Output: int(): 3
# With negative numbers, the differences are more apparent
value = -3.7
print(f"\nOriginal: {value}")
print(f"floor(): {math.floor(value)}") # Output: floor(): -4
print(f"ceil(): {math.ceil(value)}") # Output: ceil(): -3
print(f"round(): {round(value)}") # Output: round(): -4
print(f"int(): {int(value)}") # Output: int(): -3이 함수들이 어떻게 동작하는지 그림으로 나타내면 다음과 같습니다:
4.11.6) 기타 유용한 math 모듈 함수들
math 모듈에는 이외에도 많은 함수가 있습니다. 그 중 자주 사용되는 몇 가지를 더 살펴보겠습니다:
# other_math_functions.py
import math
# Absolute value (also available as built-in abs())
result = math.fabs(-5.5)
print(result) # Output: 5.5
# Power function (also available as ** operator)
result = math.pow(2, 3)
print(result) # Output: 8.0
# Trigonometric functions (angles in radians)
result = math.sin(math.pi / 2) # sin(90 degrees)
print(result) # Output: 1.0
result = math.cos(0) # cos(0 degrees)
print(result) # Output: 1.0
# Logarithms
result = math.log(math.e) # Natural log (base e)
print(result) # Output: 1.0
result = math.log10(100) # Log base 10
print(result) # Output: 2.0
# Check if a float is close to another (Python 3.5+)
a = 0.1 + 0.2
b = 0.3
result = math.isclose(a, b)
print(result) # Output: Trueisclose() 함수는 4.10절에서 이야기했던 부동소수점 비교에 특히 유용합니다:
# isclose_example.py
import math
# Instead of direct equality comparison
a = 0.1 + 0.2
b = 0.3
# Don't do this:
# if a == b: # This would be False
# Do this instead:
if math.isclose(a, b):
print("Values are close enough")
# Output: Values are close enough
# You can specify the tolerance
if math.isclose(a, b, rel_tol=1e-9): # Very strict tolerance
print("Values are very close")
# Output: Values are very close4.11.7) math 모듈을 언제 사용할까
math 모듈은 다음이 필요할 때 사용하십시오:
- 수학 상수 (π, e)
- 제곱근 및 기타 근호 연산
- 삼각 함수 (sin, cos, tan)
- 로그 및 지수 함수
- 정밀한 반올림 제어 (floor, ceil)
- 부동소수점 비교 (
isclose)
기본 산술 연산(+,-,*,/,//,%,**)에는 Python의 내장 연산자를 사용하고, 더 고급 수학 연산이 필요할 때는 math 모듈을 import 해서 사용하면 됩니다.
math 모듈은 Python 표준 라이브러리의 일부이므로, 항상 사용할 수 있습니다. 단지 import만 해 주면 됩니다. 22장에서 더 많은 모듈을 탐색하고, import 시스템에 대해 자세히 배울 것입니다.
이 장에서는 Python에서 숫자를 다루는 방법을 배웠습니다. 산술 연산 수행, 다양한 종류의 나눗셈 이해, 연산자 우선순위 사용, 정수와 실수를 섞는 법, 숫자 내장 함수 활용, 흔한 숫자 패턴 사용, 비트 연산자 다루기, 부동소수점 정밀도 이해, 그리고 math 모듈을 사용한 고급 연산까지 살펴보았습니다.
이러한 숫자 연산들은 간단한 계산에서 복잡한 데이터 분석까지, 수많은 프로그래밍 작업의 기초를 이룹니다. Python을 계속 공부하면서, 앞으로 배울 제어 흐름 구조와 데이터 컬렉션과 함께 이 연산들을 끊임없이 사용하게 될 것입니다.
이 연산들이 몸에 배도록 연습해 보십시오. 온도 변환, 면적과 부피 계산, 금융 데이터 처리, 측정값 분석 등, 실제 세계의 양을 계산하는 작은 프로그램들을 직접 작성해 보십시오. 연습을 많이 할수록, Python의 숫자 기능을 더 편안하게 다루게 될 것입니다.