이 영역을 누르면 첫 페이지로 이동
StudyDiary 블로그의 첫 페이지로 이동

StudyDiary

페이지 맨 위로 올라가기

StudyDiary

Python assignment expression

  • 2022.01.18 05:38
  • 카테고리 없음

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

  1. assignment expression을 다른 operation들과 함께 쓰기 위해서는 parenthesis가 필요함
  2. 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을 사용한 것과 같이 동작한다.
다음 두가지 이유로 위와 같은 예외사항을 사용한다.

  1. any 나 all expression을 편하게 사용
  2. 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

  1. 왜 그냥 assignment ( = )로 표현하지 않음?
    if (x ==y) 와 if (x=y) 가 다르다는 것을 명확히 표현하기 위해서
  2. 왜 scope을 sublocal로 제한하지 않음 (single expression)
    그렇게 해봤는데, 오히려 복잡도가 올라가 득보다 실이 더 컸음.
저작자표시 (새창열림)

댓글

이 글 공유하기

  • 구독하기

    구독하기

  • 카카오톡

    카카오톡

  • 라인

    라인

  • 트위터

    트위터

  • Facebook

    Facebook

  • 카카오스토리

    카카오스토리

  • 밴드

    밴드

  • 네이버 블로그

    네이버 블로그

  • Pocket

    Pocket

  • Evernote

    Evernote

다른 글

다른 글 더 둘러보기

정보

StudyDiary 블로그의 첫 페이지로 이동

StudyDiary

  • StudyDiary의 첫 페이지로 이동

검색

메뉴

  • 홈
  • 태그
  • 방명록

카테고리

  • 분류 전체보기 (10)
    • Machine Learning (2)
      • PR113 (1)
    • 회고 (3)
      • 일주일 회고 (0)
      • Today I Learned (3)

최근 글

인기 글

댓글

공지사항

아카이브

태그

  • 리눅스
  • 도커
  • til
  • combinatorial optimization
  • gnn
  • PR113
  • ml4co
  • TSP

나의 외부 링크

정보

Understand의 StudyDiary

StudyDiary

Understand

블로그 구독하기

  • 구독하기
  • RSS 피드

방문자

  • 전체 방문자
  • 오늘
  • 어제

티스토리

  • 티스토리 홈
  • 이 블로그 관리하기
  • 글쓰기
Powered by Tistory / Kakao. © Understand. Designed by Fraccino.

티스토리툴바