NLP Blog

이펙티브 파이썬 - 8. 리스트 컴프리헨션에서 표현식을 두 개 넘게 쓰지 말자

|

8. 리스트 컴프리헨션에서 표현식을 두 개 넘게 쓰지 말자

  • 리스트 컴프리헨션은 기본 사용법뿐만 아니라 다중 루프로 지원함
    • ex: 행렬을 모든 셀이 포함된 평평한 리스트 하나로 간략화, for를 두개 사용한 리스트 컴프리헨션 (왼쪽 > 오른쪽 순서)
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
print(flat)

$$$
[1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 입력 리스트의 레이아웃을 두 레벨로 중복해서 구성(매트릭스 제곱 구하기)
squared = [[x**2 for x in row] for row in matrix]
print(squared)

$$$
[[1, 4, 9], [16, 25, 36], [49, 64, 81]]
  • 위 표현식을 다른 루프에 넣는다면, 리스트 컴프리헨션이 여러 줄로 구분해야 할 정도로 길어진다.
my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for sublist1 in my_list
        for sublist2 in sublist1
        for x in sublist2]
  • 이번엔 일반 루프문으로 같은 결과를 만들어보자. 이 비전은 들여쓰기를 사용해서 리스트 컴프리헨션보다 이해하기 쉽다.
flat = []
for sublist1 in my_lists:
    for sublist2 in sublist1:
        flat.extend(sublist2)
  • 리스트 컴프리헨션도 다중 if 조건을 지원한다.
    • 같은 루프 레벨에 여러조건이 있으면 암시적인 and 표현식이 된다
    • ex: 숫자로 구성된 리스트에서 4보다 큰 짝수 값만 가지고 오는 것
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = [x for x in a if x > 4 if x % 2 == 0]
c = [x for x in a if x > 4 and x % 2 == 0]
  • 조건은 루프의 각 레벨에서 for 표현식 뒤에 설정 가능
    • ex: 행렬에서 row의 합이 10 이상이고 3으로 나누어 떨어지는 셀을 구하자
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
filtered = [[x for x in raw if x % 3 ==0]
            for raw in matrix if sum(row) >= 10]
print(filtered)

$$$
[[6], [9]]
  • 리스트 컴프리헨션으로 표현하면 간단하지만 이해하기가 매우 어렵다.

  • 이런 리스트 컴프리헨션은 피하라고 강력히 말하고 싶음
  • 이런 코드는 다른 사람들이 이해하기가 매우 어렵다
  • 몇 줄을 절약한 장점이 나중에 겪을 어려움보다 크지않음
  • 리스트 컴프리헨션을 사용할 때는 표현식이 두 개를 넘어가면 피하는 게 좋다.
    • 조건 두개, 루프 두개, 혹은 조건 한 개와 루프 한 개 정도면 됨
    • 이것보다 복잡해지면 일반적인 if 문과 for 문을 사용하고 헬퍼 함수를 작성

핵심정리

  • 리스트 컴프리헨션은 다중 루프와 루프 레벨별 다중 조건을 지원
  • 표현식이 두 개가 넘게 들어 있는 리스트 컴프리헨션은 이해하기 매우 어려우므로 피해야 한다.

Comment  Read more

이펙티브 파이썬 - 7. map과 filter 대신 리스트 컴프리헨션을 사용하자

|

7. map과 filter 대신 리스트 컴프리헨션을 사용하자

  • 파이썬에는 한 리스트에서 다른 리스트를 만들어내는 간결한 문법이 있다.
  • 이 문법을 사용한 표션식을 리스트 컴프리헨션(list comprehension) 이라고 함
    • ex: 리스트에 있는 각 숫자의 제곱을 계산 한다고 하자 몇가지 방법이 존재함
      • 첫번째, 리스트 컴프리헨션 사용
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
squares = [x**2 for x in a]
print(squares)

$$$
[1, 4, 9. 16, ...]
- 두번째, map 사용 (lambda 사용해야 해서 깔끔해 보이지 않음)
squares = map(lambda x: x ** 2, a)
  • map과 달리 리스트 컴프리헨션을 사용하면 입력 리스트에 있는 아이템을 간편하게 걸러내서 그에 대응하는 출력을 결과에서 삭제할 수 있음
    • ex: 2로 나누어 떨어지는 숫자의 제곱만 계산
      • 첫번쨰, 리스트 컴프리헨션 사용
even_squares = [x**2 for x in a if x % 2==0]
print(even_squares)

$$$
[4, 16, 36,...]
  • 두번째, filter와 map 사용 (훨씬 읽기 어려움)
alt = map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, a))
assert even_squares == list(alt)

  • 딕셔너리와 세트에도 리스트 컴프리헨션에 해당하는 문법이 존재
  • 컴프리헨션 문법을 쓰면 알고리즘을 작성할 때 파생되는 자료 구조를 쉽게 생성할 수 있음
chile_ranks = {'ghost': 1, 'habanero': 2, 'cayenne': 3}
rank_dict = {rank: name for name, rank in chile_ranks.items()}
chile_len_set = {len(name) for name in rank_dict.values()}
print(rank_dict)
print(chile_len_set)

$$$
{1: 'ghost', 2: 'habanero', 3: 'cayenne'}
{8, 5, 7} # 알파벳 역순인가...? 아님 그냥 python set 자체가 unordered collection 임....

핵심정리

  • 리스트 컴프리헨션은 추가적임 lambda 표현식이 필요 없어서 내장 함수인 map 이나 filter를 사용하는 것보다 명확하다.
  • 리스트 컴프리헨션을 사용하면 입력 리스트에서 아이템을 간단히 건너뛸 수 있다. map으로는 filter를 사용하지 않고서는 이런 작업을 못한다.
  • 딕셔너리와 세트도 컴프리헨션 표현식을 지원한다.

Comment  Read more

이펙티브 파이썬 - 6. 한 슬라이스에 start, end, stride를 함께 쓰지 말자

|

6. 한 슬라이스에 start, end, stried를 함께 쓰지 말자

  • somelist[start:end:stride] 처럼 슬라이스의 스트라이드(간격)를 설정하는 문법이 있다.
    • 시퀀스를 슬라이스 할 때, 매 n번째 아이템을 가져올 수 있음
a = ['red', 'orange', 'yellow', 'green', 'blue', 'purple']
odds = a[::2]
evens = a[1::2]
print(odds)
print(evens)

$$$
['red', 'yellow', 'blue']
['orange', 'green', 'purple']
  • 문제는 stride문법이 종종 예상치 못한 동작을 해서 버그를 만들어내기도 하는 것
    • ex: 파이썬에서 바이트 문자열을 역순으로 만드는 일반적인 방법은 스트라이드 -1로 문자열을 슬라이스하는 것
x = b'mongoose'
y = x[::-1]
print(y)

$$$
b'esoognom'
  • 위의 코드는 바이트 문자열이나 아스키 문자에는 잘 동작하지만, utf-8 바이트 문자열로 인코드된 유니코드 문자에는 원하는 대로 동작하지 않음
w = '正正'
x = w.encode('utf-8')
y = x[::-1]
z = y.decode('utf-8')

$$$
UnicodeDecodeError: 'utf-8' codec can`t decode byte 0x9d in
position 0: invalid start byte

  • stride를 사용하면 리스트 내의 인수를 다양하게 뽑아낼 수 있지만 문제가 있음
  • 슬라이싱 문법의 stride 부분이 매우 혼란스러울 수 있음
    • 대괄호 안에 숫자가 세 개나 있으면 빽빽해서 읽기 어려움, 그래서 start와 end 인덱스가 stride와 연계되어 어떤 작용을하는지 불분명
    • 특히 stride가 음수인 경우는 더 심함
  • stride를 사용해야 한다면 양수 값을 사용하고, start와 end 인덱스는 생략하자
  • stride를 꼭 start나 end 인덱스와 함께 사용해야 한다면
    • stride를 적용한 결과를 변수에 할당하고, 이 변수를 슬라이스한 결과를 다른 변수에 할당해서 사용
b = a[::2]
c = b[1:-1]
  • 슬라이싱부터 하고 스트라이딩을 하면 데이터의 얕은 복사본이 추가로 생김
    • 첫 번째 연산은 결과로 나오는 슬라이스의 크기를 최대한 줄여야함 (보통 stride가 1/n 크기로 리스트를 줄이므로 먼저 쓰자)
    • 시간과 메모리가 충부하지 않다면 내장 모듈 itertoolsislice 메서드를 사용
    • islice 메서드는 start, end, stride에 음수값을 허용하지 않음

핵심정리

  • 한 슬라이스에 start, end, stride를 지정하면 매우 혼란스러울 수 있다.
  • 슬라이스에 start와 end 인덱스 없이 양수 stride값을 사용하자. 음수 stride 값은 가능하면 피하는게 좋다.
  • 한 슬라이스에 start, end, stride를 함께 사용하는 상황은 피하자. 파라미터 세 개를 사용해야 한다면 할당 두개 (하나는 슬라이스, 다른 하나는 스트라이드)를 사용하거나 내장모듈 itertoolsislice를 사용하자

Comment  Read more

이펙티브 파이썬 - 5. 시퀀스를 슬라이스하는 방법을 알자

|

5. 시퀀스를 슬라이스하는 방법을 알자

  • 파이썬은 시퀀스를 슬라이스해서 조각으로 만드는 문법을 제공
    • 슬라이싱 대상 : list, str, bytes …
    • __getitem__, __setitem__ 매직메서드를 구현하는 클래스에서도 슬라이싱 적용 가능
  • 기본 형태 : somelist[start:end]
    • start 인덱스는 포함, end 인덱스는 제외
a = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print('First four:', a[:4])
print('Last four: ', a[4:])
print('Middle two:', a[3:-3])

$$$
Fisrt four: ['a', 'b', 'c', 'd']
Last four : ['e', 'f', 'g', 'h']
Middle two: ['d', 'e']
  • 리스트 처음부터 슬라이스 할 때는 인덱스 0 생략
    • assert a[:5] == a[0:5]
  • 리스트 끝까지 슬라이스 할 때도 마지막 인덱스 생략
    • assert a[5:] == a[5:len(a)]
  • 리스트 끝을 기준으로 오프셋을 계산할 때는 음수로 계산 하는 것이 편함
    • note: 리스트의 인덱스를 음수 변수로 지정하면 슬라이싱으로 뜻밖의 결과를 얻는 몇 가지 상황 중 하나가 발생하니 주의해야 함. (ex: somelist[-n:]이라는 구문은 n>1일때는 정상 작동하지만, n=0일때 somelist[-0:] 이되면 원본 리스트의 복사본을 만듬)
  • 슬라이싱은 startend 인덱스가 리스트의 경계를 벗어나도 적절하게 처리
    • 하지만 리스트의 경계를 벗어난 인덱스를 직접 접근하면 예외가 발생

  • 슬라이싱의 결과는 완전히 새로운 리스트
    • 원본 리스트에 들어 있는 객체에 대한 참조는 유지
    • 하지만 슬라이스한 결과를 수정해도 원본 리스트에 아무런 영향을 미치지 않음
b = a[4:]
print('Before:    ', b)
b[1] = 99
print('After:     ', b)
print('No Change: ', a)

$$$
Before:   ['e', 'f', 'g', 'h']
After:    ['e', 99, 'g', 'h']
No Change:['a', 'b,' 'c,' 'd', 'e', 'f', 'g', 'h']
  • 할당에 사용하면 슬라이스는 원본 리스트에서 지정한 범위를 대체
    • a, b = c[:2]같은 튜플 할당과 달리 슬라이스 할다으이 길이는 달라도 된다.
    • 할당 받은 슬라이스의 앞뒤 값은 유지
    • 리스트는 새로 들어온 값에 맞춰 늘어나거나 줄어듦
print('Before ', a)
a[2:7] = [99, 22, 14]
print('After  ', a)

$$$
Before ['a', 'b,' 'c,' 'd', 'e', 'f', 'g', 'h']
After  ['a', 'b', 99, 22, 14, 'h']
  • 시작과 끝 인덱스를 모두 생략하고 슬라이스 하면 원본 리스트의 복사본을 얻는다
b = a[:]
assert b == a and b is not a
  • 새 리스트를 할당하지 않고 슬라이스 시작과 끝 인덱스를 지정하지 않고 할당하면 슬라이스의 전체 내용을 참조 대상의 복사본으로 대체
b = a
print('Before', a)
a[:] = [101, 102, 103]
assert a is b # 여전히 같은 리스트 객체임
print('After', a) # 이제 다른 내용을 담음

$$$
Before ['a', 'b', 99, 22, 14, 'h']
After [101, 102, 103]

핵심정리

  • 너무 장황하게 ㄴㄴ, start 인덱스에 0을 설정하거나 end 인덱스에 len(a) 이런거 ㄴㄴ
  • 슬라이싱은 범위를 벗어난 start나 end를 허용하므로 편함
  • list 슬라이스에 할당하면 원본 시퀀스에 지정한 범위를 참조 대상의 내용으로 대체 (길이가 달라도 동작)

Comment  Read more

이펙티브 파이썬 - 4. 복잡한 표현식 대신 헬퍼 함수를 작성하자

|

4. 복잡한 표현식 대신 헬퍼 함수를 작성하자

헬퍼함수를 사용하지 않는 경우

  • 파이썬의 간결한 문법을 이용하면 많은 로직을 표현식 한 줄로 쉽게 작성 가능
from urllib.parse import parse_qs
my_values = parse_qs('red=5&blue=0&green=', keep_blank_values=True)
print(repr(my_values))

$$$
{'red': ['5'], 'green': [''], 'blue': ['0']}
  • 쿼리 문자열 파라미터에 따라 값이 여러개, 한개일 수 있고, 파라미터는 존재하지만 값x 혹은 파라미터 x
  • 결과 딕셔너리에 get 메서드를 사용하면 각 상황에 따라 다른 값을 반환 할 것이다.
print('Red:       ', my_values.get('red'))
print('Green:     ', my_values.get('green'))
print('Opacity:   ', my_values.get('opacity'))


$$$
Red:      ['5']
Green:    ['']
Opacity:  None
  • 이때 파라미터가 없거나 비어 있으면 기본값으로 0을 할당하게 하면 좋을 것 같다
  • 사용하는 트릭은 빈 문자열, 빈 리스트, 0이 모두 암시적으로 False 평가되는 것
red = my_values.get('red', [''])[0] or 0
green = my_values.get('green', [''])[0] or 0
opacity = my_values.get('opacity', [''])[0] or 0
print('Red:       %r' % red)
print('Green:     %r' % green)
print('Opacitry:  %r' % opacity)


$$$
Red:    '5'
Green:   0
Opacity: 0
  • 위 코드의 red, green, opacity 표현식에서 첫 번째 서브 표현식이 False일 때 or 연산자 뒤에 오는 서브 표현식을 평가한 값이 결과가 됨
    • red : 키값이 my_values 딕셔너리에 존재 -> 값은 멤버 하나만 있는 list -> 이 문자열은 True -> redor 의 첫 번째 부분을 할당받음
    • green : 키값이 my_values 딕셔너리에 존재 -> 빈 문자열인 list -> 암시적으로 ‘False’ -> or 의 결과는 0
    • opacity : 키 값이 없음 -> get 메서드는 키가 딕셔너리에 없으면 두 번째 인수를 반환(기본값은 빈 문자열) -> green과 똑같이 동작
  • 하지만 이 표현식의 문제점은
    • 읽기 어렵다
    • 필요한 작업을 다 수행하지도 않음 : 모든 파라미터 값이 정수가 되도록 해야함, int 사용 필요
red = int(my_values.get('red', [''])[0] or 0)
- 이것도 읽기 너무 어려움....
- if/else 조건 삼항 표현식 사용해보자 !
red = my_values.get('red', [''])
red = int(red[0]) if red[0] else 0
- 훨씬 낫지만 if/else 삼항 표현식 자체가 어렵다....
- 여러 줄 if/else 문으로 표현한다면?
green = my_values.get('green', [''])
  if green[0]:
      green = int(green[0])
  else:
      green = 0

헬퍼함수를 사용할 때!

  • 위와 같은 로직을 반복해서 써야한다면 헬퍼 함수를 만들자!
def get_first_int(values, ket, default=0):
    found = values.get(key, [''])
    if found[0]:
        found = int(found[0])
    else:
        found = default
    return found

green = get_first_int(my_values, 'green')
  • 표현식이 복잡해지기 시작하면
    • 최대한 빨리 해당 표현식을 작은 조각으로 분할
    • 로직을 헬퍼 함수로 옮기는 방안 고려
  • 무조건 짧은 코드를 만들기 보다는 가독성을 선택하는 편이 나음

핵심 정리

  • 파이썬의 문법을 이용하면 한 줄짜리 표현식을 쉽게 작성할 수 있지만 코드가 복잡해지고 읽기 어려워진다
  • 잡한 표현식은 헬퍼 함수로 옮기는 게 좋다. 특히, 같은 로직을 반복해서 사용해야 한다면 헬퍼 함수를 사용하자.
  • if/else 표현식을 이용하면 or나 and 같은 bool 연산자를 사용할 때보다 읽기 수월한 코드 작성 가능

Comment  Read more