Python assignment expression
python 3.8 버전 부터 추가된 assignment expression 에 대한 다음 문서를 정리했다.
https://www.python.org/dev/peps/pep-0572
제안된 이유
기존 개발자들은 실제 코드를 작성할 때 line 수를 줄이기 위해 성능을 포기하고 sub-expression을 반복해서 사용하는 경우가 있다.
Examples
# Original match = re.match(data) group = match.group(1) if match else None # Repeated sub-expression but one-line code group = re.match(data).group(1) if re.match(data) else None # Less efficient but looks good match1 = pattern1.match(data) match2 = pattern2.match(data) if match1: result = match1.group(1) elif match2: result = match2.group(2) else: result = None match1 = pattern1.match(data) if match1: result = match1.group(1) else: match2 = pattern2.match(data) if match2: result = match2.group(2) else: result = None # More efficient but deeper indent level match1 = pattern1.match(data) if match1: result = match1.group(1) else: match2 = pattern2.match(data) if match2: result = match2.group(2) else: result = None
이는 expression의 중간 결과물을 저장할 수 없기 때문에 발생한 것이다.
만약 expression의 중간 결과물을 저장할 수 있다면 코드가 간소화 될 것이다. 또한 추가적으로 디버깅할 때도 subpart에 대한 정보를 얻을 수 있으며 수정할 코드가 줄어들어 버그가 줄어들 수 있다.
이런 이유로 중간 결과물을 저장하는 assignment expression이 제안되었다.
Syntax and Semantics
Syntax
- NAME := expression
- expression: unparenthesized tuple을 제외한 any valid expression
- NAME: identifier
Semantics
Assignment exrpression은 다음 두가지 기능을 동시에 수행한다.
- expression 값 반환
- NAME에 expression 값 할당
# Handle a matched regex
if (match := pattern.search(data)) is not None:
# Do something with match
# A loop that can't be trivially rewritten using 2-arg iter()
while chunk := file.read(8192):
process(chunk)
# Reuse a value that's expensive to compute
[y := f(x), y**2, y**3]
# Share a subexpression between a comprehension filter clause and its output
filtered_data = [y for x in data if (y := f(x)) is not None]
Exceptional cases
- assignment expression을 다른 operation들과 함께 쓰기 위해서는 parenthesis가 필요함
- assignment expression이 expression의 마지막에 계산되어야 할 때 parenthesis가 필요함
그러나 위 두 예외상황을 처리하기 위한 아래의 examples 들은 추천되지 않음
# Unparenthesized assignment expressions at the top level of an expression statement
y := f(x) # INVALID
(y := f(x)) # Valid, though not recommended
# Unparenthesized assignment expressions are prohibited for the value of a keyword argument in a call
foo(x = y := f(x)) # INVALID
foo(x=(y := f(x))) # Valid, though probably confusing
# Unparenthesized assignment expressions are prohibited at the top level of a function default value
def foo(answer = p := 42): # INVALID
...
def foo(answer=(p := 42)): # Valid, though not great style
# Unparenthesized assignment expressions are prohibited as annotations for arguments, return values and assignments
def foo(answer: p := 42 = 5): # INVALID
...
def foo(answer: (p := 42) = 5): # Valid, but probably never useful
# Unparenthesized assignment expressions are prohibited in lambda functions
(lambda: x := 1) # INVALID
lambda: (x := 1) # Valid, but unlikely to be useful
(x := lambda: 1) # Valid
lambda line: (m := re.match(pattern, line)) and m.group(1) # Valid
comprehension iterable expression (comprehension expression 중 iterate되는 iterable) 에 사용할 수 없다. 이는 symbol table analyzer가 iterable과 나머지 부분에서 같은 변수가 재사용된다는 것을 알 수 없기 때문이다.
[i+1 for i in (j := stuff)] # INVALID
[i+1 for i in range(2) for j in (k := stuff)] # INVALID
[i+1 for i in [j for j in (k := stuff)]] # INVALID
[i+1 for i in (lambda: (j := stuff))()] # INVALID
Scope
기본적으로 단순 assignment와 동일한 scope을 갖는다.
하지만 list/dict/set/generator comprehension 에 대해서는 한가지 예외가 있다.
scope 외부에 있는 변수를 사용한다면, 자동으로 nonlocal이나 global을 사용한 것과 같이 동작한다.
다음 두가지 이유로 위와 같은 예외사항을 사용한다.
any
나all
expression을 편하게 사용- State를 쉽게 저장하기 위해서
# Example 1. For convenient usage of any and all expression
if any((comment := line).startswith('#') for line in lines):
print("First comment:", comment)
else:
print("There are no comments")
if all((nonblank := line).strip() == '' for line in lines):
print("All lines are blank")
else:
print("First non-blank line:", nonblank)
# Example 2. For compact updating mutable state from comprehension
values = [0,1,2,3,4,5]
total = 0
partial_sums = [total := total + v for v in values]
print("Total:", total)
추가적으로 for 문을 사용할 때 target 변수 이름과 for문의 iterate하는 변수의 이름이 같지 않게 해야 한다.
두 변수의 이름이 같다면 target 변수는 global이고, for문의 변수는 local이여서 scope이 정의되지 않는다.
lambda는 새로운 scope을 지정하는 것이므로 사용할 수 있다.
[[(i := i + 1) for i in range(5)] # SyntaxError
[[(j := j) for i in range(5)] for j in range(5)] # SyntaxError
[i := 0 for i, j in stuff] # SyntaxError
[i+1 for i in (i := stuff)] # SyntaxError
[False and (i := 0) for i, j in stuff] # SyntaxError
[i for i, j in stuff if True or(j:=1)] # SyntaxError
Precedence
comma < := operator < or, not, conditional expressions, =
엄밀히 말하면 assignment expression은 = 보다 낮은 level이어야 한다.
x = 1, 2 #Sets x to (1, 2) (x := 1, 2) #Sets x to 1 # INVALID x := 0 # Valid alternative (x := 0) # INVALID x = y := 0 # Valid alternative x = (y := 0) # Valid len(lines := f.readlines()) # Valid foo(x := 3, cat='vector') # INVALID foo(cat=category := 'vector') # Valid alternative foo(cat=(category := 'vector'))
Examples
site.py
# Original env_base = os.environ.get("PYTHONUSERBASE", None) if env_base: return env_base # Assignment Expression if env_base := os.environ.get("PYTHONUSERBASE",None): return env_base
_pydecimal.py
# Original if self._is_special: ans = self._check_nans(context=context) if ans: return ans # Assignment Expression if self._is_special and (ans := self._check_nans(context=context)): return ans
copy.py
# Original reductor = dispatch_table.get(cls) if reductor: rv = reductor(x) else: reductor = getattr(x, "__reduce_ex__", None) if reductor: rv = reductor(4) else: reductor = getattr(x, "__reduce__", None) if reductor: rv = reductor() else: raise Error( "un(deep)copyable object of type %s" % cls) # Assignment Expression if reductor := dispatch_table.get(cls): rv = reductor(x) elif reductor := getattr(x, "__reduce_ex__", None): rv = reductor(4) elif reductor := getattr(x, "__reduce__", None): rv = reductor() else: raise Error("un(deep)copyable object of type %s" % cls)
datetime.py
# Original s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) tz = self._tzstr() if tz: s += tz return s # Assignment Expression s = _format_time(self._hour, self._minute, self._second, self._microsecond, timespec) if tz := self._tzstr(): s += tz return s
sysconfig.py
# Original while True: line = fp.readline() if not line: break m = define_rx.match(line) if m: n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v else: m = undef_rx.match(line) if m: vars[m.group(1)] = 0 # Assignment Expression while line := fp.readline(): if m := define_rx.match(line): n, v = m.group(1, 2) try: v = int(v) except ValueError: pass vars[n] = v elif m := undef_rx.match(line): vars[m.group(1)] = 0
simplifying list comprehension
# Original results = [(x, f(x), x/f(x)) for x in input_data if f(x) > 0] # Assignment Expression results = [(x, y, x/y) for x in input_data if (y := f(x)) > 0]
Capturing condition values
# Loop-and-a-half while (command := input("> ")) != "quit": print("You entered:", command) # Capturing regular expression match objects # See, for instance, Lib/pydoc.py, which uses a multiline spelling # of this effect if match := re.search(pat, text): print("Found:", match.group(0)) # The same syntax chains nicely into 'elif' statements, unlike the # equivalent using assignment statements. elif match := re.search(otherpat, text): print("Alternate found:", match.group(0)) elif match := re.search(third, text): print("Fallback found:", match.group(0)) # Reading socket data until an empty string is returned while data := sock.recv(8192): print("Received data:", data)
Fork
if pid := os.fork(): # Parent code else: # Child code
Combining unrelated logic
# Original mylast = mylast[1] yield mylast[0] # Assignment Expression yield (mylast := mylast[1])[0]
Combining complex related logic made it harder to understand
# Original while True: old = total total += term if old == total: return total term *= mx2 / (i*(i+1)) i += 2 # Assignment Expression while total != (total := total + term): term *= mx2 / (i*(i+1)) i += 2 return total
Reduce indent level
# Original diff = x - x_base if diff: g = gcd(diff, n) if g > 1: return g # Assignment Expression if (diff := x - x_base) and (g := gcd(diff, n)) > 1: return g
Numeric Example
# Original while True: d = x // a**(n-1) if a <= d: break a = ((n-1)*a + d) // n return a # Assignment Expression while a > (d := x // a**(n-1)): a = ((n-1)*a + d) // n return a
Frequently Raised Objection
- 왜 그냥 assignment ( = )로 표현하지 않음?
if (x ==y) 와 if (x=y) 가 다르다는 것을 명확히 표현하기 위해서 - 왜 scope을 sublocal로 제한하지 않음 (single expression)
그렇게 해봤는데, 오히려 복잡도가 올라가 득보다 실이 더 컸음.