정식 이름은 Assignment expression operator인데 walrus operator라고도 불린다.
walrus는 “바다코끼리”라는 뜻으로 operator가 바다 코끼리의 눈과 이빨을 닮아서 이렇게 부른다.
때론 colon(:) equals(=) operator라고도 한다.
Python 3.8버전부터 새로 등장했다.
Statement vs Expression in Python
바다코끼리 연산자의 정식 이름을 보면 Assignment expression operator로, expression이라는 단어가 나온다.
Python에서 statement와 expression이라는 표현이 비슷해 혼동스러운데 간단히 정리하면, 아래와 같다.
- statement: 코드를 구성할 수 있는 단위 혹은 모든 것
- expression: 값을 평가하는 statement로 연산자와 피연산자의 조합으로 구성됨
예시
x = 25 # a statement
x = x + 10 # an expression
- statement는 변수를 생성하는데 사용된다.
- expression은 x값에 10을 더하는 연산이 수행된 후 결과가 x에 할당되었다.
>>> walrus = False # (1)
>>> walrus
False
>>> (walrus := True) # (2)
True
>>> walrus
True
- walrus = False는 값 False가 walrus에 할당된다. (traditional statement)
- (walrus := True) 는 assignment expression으로 walrus에 값 True를 할당한다.
둘의 미묘한 차이중 하나는 walrus = False는 값을 반환하지 않지만 (walrus := True)는 값을 반환한다는 것이다!
>>> walrus = False
>>> (walrus := True)
True
등장한 이유
PEP 572에 Abstract에 아래와 expression 내에서 변수에 할당하는 방법을 제안하고 있다.
creating a way to assign to variables within an expression using the notation NAME := expr.
C언어에서는 변수에 값을 할당하는 statement도 expression인데 강력하지만 찾기 힘든 버그를 생산하기도 한다.
int main(){
int x = 3, y = 8;
if (x = y) {
printf("x and y are equal (x = %d, y = %d)", x, y);
}
return 0;
}
x와 y값을 비교후 값이 같으면 두 값을 출력하는 코드지만 x와 y값이 다르기 때문에 아무것도 출력 안되길 기대되지만 실제 코드 실행 결과는 아래와 같이 print 문이 출력된다. 왜일까?
x and y are equal (x = 8, y = 8)
문제는 위 코드 세번째 줄 if (x = y) 에서 equality comparison operator(==) 대신 assignment operator(=) 를 사용하고 있기 때문이다. if 문의 조건에는 expression이 와야하는데 C언어에서는 x = y를 expression으로 x값이 8로 할당되고 1이상의 값으로 True로 판단되서 print문이 출력된다.
그럼 Python에서는?
x, y = 3, 8
if x = y:
print(f"x and y are equal ({x = }, {y = })")
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
- Syntax Error를 내뱉는데 expression이 아닌 statement이기 때문이다. 파이썬은 이를 분명히 구분하고 walrus operator에도 이러한 설계 원칙이 반영되었다. 그래서 walrus operator를 이용해서 일반적인 assignment를 할 수 없다.
>>> walrus := True
File "<stdin>", line 1
walrus := True
^
SyntaxError: invalid syntax
이를 해결하기 위해 많은 경우에 assignment expression 에 괄호를 추가해 python에서 syntax error를 피할 수 있다.
>>> (walrus := True) # Valid, but regular assignments are preferred
True
사용 예시
walrus operator는 반복적으로 사용되는 코드를 간단히 하는데 유용하게 사용될 수 있다.
(1) 수식 검증
예로 복잡한 수식을 코드로 작성하고 이름 검증하고 debugging할 때 walrus operator가 유용할 수 있다.
아래와 같은 수식이 있다고 하자 (참고: haversine formula, 지구 표면의 2점 사이의 거리를 구하는 식)
$$
2 \cdot \text{r} \cdot \arcsin\left(
\sqrt{
\sin^2\left(\frac{\phi_2 - \phi_1}{2}\right)
+ \cos(\phi_1) \cdot \cos(\phi_2) \cdot \sin^2\left(\frac{\lambda_2 - \lambda_1}{2}\right)
}
\right)
$$
- ϕ: 위도(latitude), λ: 경도(longitude)
위 수식을 이용해 오슬로(59.9°N 10.8°E) 와 밴쿠버(49.3°N 123.1°W) 사이의 거리를 구하면,
from math import asin, cos, radians, sin, sqrt
# Approximate radius of Earth in kilometers
rad = 6371
# Locations of Oslo and Vancouver
ϕ1, λ1 = radians(59.9), radians(10.8)
ϕ2, λ2 = radians(49.3), radians(-123.1)
# Distance between Oslo and Vancouver
print(2 * rad * asin(
sqrt(
sin((ϕ2 - ϕ1) / 2) ** 2
+ cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
)
))
# 7181.7841229421165 (km)
- 위 수식을 검증하기 위해서 수식의 일부 값을 확인해야할 수 있는데 수식의 일부를 복&붙으로 확인할 수 있다.
- 이때 walrus operator를 이용하면,
2 * rad * asin(
sqrt(
**(ϕ_hav := sin((ϕ2 - ϕ1) / 2) ** 2)**
+ cos(ϕ1) * cos(ϕ2) * sin((λ2 - λ1) / 2) ** 2
)
)
# 7181.7841229421165
ϕ_hav
# 0.008532325425222883
- 전체 expression의 값을 계산하면서 동시에 ϕ_hav값을 계속 확인할 수 있어서 copy & paste로 인해 발생할 수 있는 오류의 가능성을 줄일 수 있다.
(2) Lists 에서 활용될 수 있는 walrus operator
numbers = [2, 8, 0, 1, 1, 9, 7, 7]
위 list에서 길이, 합계, 평균 값을 dictionary에 저장한다고 가정해보자
description = {
"length": len(numbers),
"sum": sum(numbers),
"mean": sum(numbers) / len(numbers),
}
print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
- description에서 numbers의 len과 sum이 각각 두번씩 호출된다
- 짧은 list에서는 큰 문제가 되지 않지만 길이가 더 긴 list나 연산이 복잡할 경우에는 최적화할 필요가 있다
물론 아래처럼 len_numbers, sum_numbers 변수를 dictionary 밖에서 선언 후 사용할 수도 있다
numbers = [2, 8, 0, 1, 1, 9, 7, 7]
len_numbers = len(numbers)
sum_numbers = sum(numbers)
description = {
"length": len_numbers,
"sum": sum_numbers,
"mean": sum_numbers / len_numbers,
}
print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
하지만 walrus operator를 이용해 len_numbers, sum_numbers 변수를 dictionary 내부에서만 사용하여 code를 최적화할 수 있다
numbers = [2, 8, 0, 1, 1, 9, 7, 7]
description = {
"length": (len_numbers := len(numbers)),
"sum": (sum_numbers := sum(numbers)),
"mean": sum_numbers / len_numbers,
}
print(description) # {'length': 8, 'sum': 35, 'mean': 4.375}
- 이 경우 코드를 읽는 사람들에게 len_numbers와 sum_numbers 변수는 계산을 최적화하기 위해 dictionary 내부에서만 사용했고 다시 사용되지 않음을 명확히 전달 할 수 있다
(3) Text 파일에서 lines, words, character 수 세는 예시
# wc.py
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
counts = (
path.read_text().count("\\n"), # Number of lines
len(path.read_text().split()), # Number of words
len(path.read_text()), # Number of characters
)
print(*counts, path) # 11 32 307 wc.py
- wc.py 파일은 11줄, 32단어, 307 character로 구성되어있다
- 위 코드를 보면 path.read_text() 가 반복적으로 호출되는걸 알 수 있다 ⇒ walrus operator를 이용해 개선해보면,
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
counts = (
**(text := path.read_text()).count("\\n"), # Number of lines**
len(text.split()), # Number of words
len(text), # Number of characters
)
print(*counts, path)
물론 아래처럼 text 변수를 이용하면 코드는 한줄 늘어나지만 readability를 훨신 높일 수 있다.
import pathlib
import sys
for filename in sys.argv[1:]:
path = pathlib.Path(filename)
text = path.read_text()
counts = (
text.count("\\n"), # Number of lines
len(text.split()), # Number of words
len(text), # Number of characters
)
print(*counts, path)
그러므로 walrus operator가 코드를 간결하게 해주더라도 readability를 고려해야 한다.
(4) List Comprehensions
- List comprehension과 함께 연산이 많은 함수를 사용하게 될 때, walrus operator의 사용은 효과적일 수 있다.
import time
t_start = time.time()
def slow(num):
time.sleep(5)
return num
numbers = [4, 3, 1, 2, 5]
results = [slow(num) for num in numbers if slow(num) > 4]
t_end = time.time()
print("elapsed time: ", t_end - t_start)
elapsed time: 30.01522707939148
- numbers 리스트의 각 element에 slow 함수를 적용 후 3보다 큰 경우에만 results에 slow 호출 결과를 저장하는 코드
- 문제는 slow 함수가 2번 호출됨
- slow 호출 후 반환 결과가 3보다 큰지 확인할 때
- results 리스트에 저장하기 위해 slow 호출할 때
가장 일반적인 해결책은 list comprehension 대신 for loop을 사용하는 것이다.
import time
t_start = time.time()
def slow(num):
time.sleep(5)
return num
numbers = [4, 3, 1, 2, 5]
results = []
for num in numbers:
slow_num = slow(num)
if slow_num > 4:
results.append(slow_num)
t_end = time.time()
print("elapsed time: ", t_end - t_start)
elapsed time: 25.021725063323975
- slow 함수가 모든 경우에 한번씩만 호출됨
- 하지만 코드 양이 늘어나고 가독성이 떨어짐
walrus operator를 사용하면 list comprehension을 유지하면서 가독성을 높일 수 있음
import time
t_start = time.time()
def slow(num):
time.sleep(5)
return num
numbers = [4, 3, 1, 2, 5]
results = [slow_num for num in numbers if (slow_num := slow(num)) > 4]
print(results)
t_end = time.time()
print("elapsed time: ", t_end - t_start)
elapsed time: 25.018176908493042
(5) While Loop
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}
user_answer = input(f"\n{question} ")
while user_answer not in valid_answers:
print(f"Please answer one of {', '.join(valid_answers)}")
user_answer = input(f"\n{question} ")
- 위 코드는 사용자의 입력을 받는 input 함수가 두번 반복됨
- 이를 개선하기 위해 While True 와 break를 사용하여 코드를 다시 작성하는 것이 일반적임
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}
while True:
user_answer = input(f"\n{question} ")
if user_answer in valid_answers:
break
print(f"Please answer one of {', '.join(valid_answers)}")
walrus operator를 이용해서 while loop을 간결하게 할 수 있음
question = "Do you use the walrus operator?"
valid_answers = {"yes", "Yes", "y", "Y", "no", "No", "n", "N"}
while (user_answer := input(f"\n{question} ")) not in valid_answers:
print(f"Please answer one of {', '.join(valid_answers)}")
- 사용자로부터 받은 input 입력을 user_answer 변수에 저장하고 동시에 valid_answers 내에 포함되어있는지를 체크하여 가독성을 높일 수 있음
Reference
https://realpython.com/python-walrus-operator/
'Python' 카테고리의 다른 글
[책리뷰] 파이썬 클린 코드 Chapter 2. Pythonic 코드 (1) (0) | 2024.09.29 |
---|---|
super() (0) | 2024.09.15 |
URL 다루기 위한 python의 built-in 패키지: urllib (0) | 2024.08.25 |
Pillow로 Image를 열 때 자동회전되는 현상 (0) | 2024.01.15 |
Python을 이용한 Crawling (Feat. arm64, graviton) (0) | 2024.01.06 |