파이썬으로 코딩 문제 풀이 시 알아야 할 것들

개발자 강세영·2023년 5월 6일
0

TIL

목록 보기
54/70
post-thumbnail
post-custom-banner

lambda

lambda args: expression

  • lambda는 파이썬에서 이름이 없는 함수(익명 함수)를 만드는 키워드다.
  • 이름이 없는 함수를 람다 식(lambda expression) 또는 람다 함수(lambda function)이라 한다.
  • 람다라는 이름은 람다 대수(lambda calculus)로부터 나왔다.
  • 람다 식을 함수형 프로그래밍을 위해 활용할 수 있다.
  • 람담 식의 매개변수는 ,으로 구분하여 여러 개를 정의할 수 있다.
  • 파이썬의 람다 함수는 반드시 유효하고 단일한 표현식(single expression)이어야 한다.
    따라서 람다 식에 if, try, for, while문과 같은 표현식이 아닌 문장(statement)은 쓸 수 없으며 람다 식이 너무 길면 가독성이 떨어지기 때문에 한 줄 정도의 간단한 코드만 쓰는 게 좋다.
  • x, y를 인수로 받아 x + y를 반환하는 람다 함수:
lambda x, y: x + y
  • 함수의 인수로 간단한 처리를 하는 함수를 넣을 때 람다 식을 쓸 수 있다.
    예시로는 map()filter()function 인수, sorted(), sort(), max(), min()key 인수 등이 있다.
  • 람다 식 때문에 가독성이 나빠질 수 있지만, 코딩 문제 풀이 시엔 코드를 간결하게 만들어 주기 때문에 유용하다.
  • PEP 8에서는 람다 식을 식별자에 바인딩(변수로 저장)하려면 람다를 쓰지 말고 그냥 def문을 사용하라고 권장한다.
# 둘 다 같은 방식으로 작동하지만 식별자에 람다를 넣으면 람다를 사용하는 의미가 퇴색될 수 있으므로, 주의할 것.
my_func = lambda x, y: x + y
my_func(1, 2)

def my_func(x, y):
	return x + y
my_func(1, 2)
  • 람다 활용 예시
my_dict = {"Z": 10000, "C": 500, "D": 100, "A": 200}

# sorted() 함수의 정렬 기준(key=)을 아이템의 값(item[1])으로 하여 정렬
# 이렇게 하면 딕셔너리의 값(value) 기준 오름차순으로 정렬된다.
dict(sorted(my_dict.items(), key=lambda item: item[1])) # {'D': 100, 'A': 200, 'C': 500, 'Z': 10000}

# 람다를 쓰지 않으면 이런식으로 써야한다.
def sort_by_value(item):
	return item[1]
dict(sorted(my_dict.items(), key=sort_by_value)) 

# 단어 리스트를 고유한 문자의 수로 정렬하기
words = ["aaaa", "bbbb", "asdf", "fffooo", "barbar", "qwertyuiop"]
sorted(words, key=lambda word: len(set(word))) # ['aaaa', 'bbbb', 'fffooo', 'barbar', 'asdf', 'qwertyuiop']
  • 람다 표현식에 자유 변수(매개변수로 지정되지 않은 변수)가 있으면 주의해야 한다.
    람다 함수를 정의했을 때의 변숫값을 담으려면 기본값이 있는 인수를 사용하면 된다.
x = 2
f = lambda y: x * y
x = 3
g = lambda y: x * y

# f(10)의 평가 시점에 변수 x가 갖는 값을 사용한다.
print(f(10)) # 따라서 20이 아니라 30 출력
print(g(10)) # 30 출력

# 기본값이 있는 인수 사용하기
x = 2
f = lambda y, x=x: x * y
x = 3
g = lambda y, x=x: x * y
print(f(10)) # 20 출력
print(g(10)) # 30 출력
print(f(10)) # 20 출력

map()

map(function, iterable, ...)

  • map()은 파이썬 내장함수이다.
  • 이 함수는 iterable의 모든 항목에 function 함수를 적용한 후 그 결과를 산출하는(yielding) 이터레이터(map object)를 반환한다.
  • function 인수에는 함수명만 쓴다
  • 이터레이터를 반환하기 때문에 느긋한 연산(lazy evaluation)이 가능하다.
  • 즉, 연산의 결과가 필요한 순간(반복문에서의 연산, map 객체를 리스트로 변환하는 등의 작업)이 오기 전까지는 연산이 수행되지 않는다.
  • map()의 반환 값을 여러 개의 변수에 나눠서 저장하는 언패킹이 가능하다.
  • 코딩 문제에서 요구하는 입력 방식이나 자료형 변환을 위해 다양하게 활용할 수 있다.
  • 흔한 방식인 두 개의 정수를 빈칸으로 구분하여 입력 받아서 변수 a, b에 저장하기:
    a, b = map(int, input().split())
  • 위 코드는 input()으로 입력받은 값을 빈칸으로 구분하기 위해 split()을 써서 리스트로 만들고, 이 리스트의 모든 값을 int로 형변환하여 맵 객체를 반환한다. 마지막으로 맵 객체가 변수 a, b에 언패킹되면서 입력한 값이 저장된다.
  • 아래 코드와 같이 map()을 사용하면 여러 개의 입력을 코드 한 줄로 처리할 수 있기 때문에 코드를 줄일 수 있다.
# 첫 줄에서 리스트의 총 개수를 입력받고 그 개수만큼의 요소를 포함하는 중첩된 리스트를 만드는 코드이다
# 리스트 컴프리헨션으로 여러 개의 input()을 쓰면 for 반복문의 input()부터 입력을 받는다
nested_list = [list(map(int, input().split())) for _ in range(int(input()))]

# 풀어쓴 코드
nested_list = []
for _ in range(int(input())):
	input_list = list(map(int, input().split()))
    nested_list.append(input_list)
  • map()을 사용하면 중첩된 함수가 많아져서 보통 가독성은 떨어지지만 풀어쓴 코드보다 타이핑해야 되는 양이 적으므로 익숙해지면 코딩 문제풀이 시 매우 유용하다.
# 입력받은 첫번째 정수 만큼의 크기를 가지는 리스트 만들기
num, *num_list = map(int, input().split()) # 첫번째 정수만 num에 저장되고, 나머지는 num_list에 패킹 된다
# 5 1 3 5 7 9 입력하면
# num_list은 [1, 3, 5, 7, 9]
  • 리스트의 모든 요소를 문자열로 변환하고 연결해서 출력하기
arr = [1, 2, 3, 4] 
print("".join(map(str, arr))) # 1234

zip()

zip(*iterables, strict=False)

  • zip()은 파이썬 내장함수이다.
  • zip()은 여러 개의 이터러블 객체를 인자로 받고, 각 객체가 담고 있는 원소를 튜플의 형태로 차례로 접근할 수 있는 이터레이터를 반환한다.
  • 이터레이터를 반환하기 때문에 느긋한 연산이 가능하다.
  • 여기서 n번째 튜플은 각 인수의 이터레이터의 n번째 요소를 포함한다.
for item in zip([1, 2, 3], ['sugar', 'spice', 'everything nice']):
    print(item)
# 출력
(1, 'sugar')
(2, 'spice')
(3, 'everything nice')
  • zip()으로 딕셔너리 만들기
keys = [1, 2, 3]
values = ["A", "B", "C"]
dict(zip(keys, values)) # {1: 'A', 2: 'B', 3: 'C'}
  • zip()에 인수로 넣은 이터러블 객체들의 길이가 서로 다른 경우, 기본적으로 zip()은 가장 짧은 이터러블 객체의 요소가 모두 사용되면 중단된다. 더 긴 이터러블의 나머지 요소는 무시하고 결과를 가장 짧은 이터러블 객체의 길이로 잘라낸다.
list(zip(range(3), ['fee', 'fi', 'fo', 'fum'])) # 'fum'은 무시됨: [(0, 'fee'), (1, 'fi'), (2, 'fo')] 
  • zip()은 이터러블 인수들의 길이가 동일한 경우에 자주 사용되는데 이 경우 옵션strict=True를 사용할 수 있다.
    이 옵션을 쓰고 이터러블 간의 길이가 같으면 그냥 zip()을 쓰는 것과 같다.
list(zip(('a', 'b', 'c'), (1, 2, 3), strict=True))
[('a', 1), ('b', 2), ('c', 3)]
  • strict=True인데 이터러블 간의 길이가 맞지 않는 경우, ValueError가 발생한다.
for item in zip(range(3), ['fee', 'fi', 'fo', 'fum'], strict=True):  
    print(item)

(0, 'fee')
(1, 'fi')
(2, 'fo')
Traceback (most recent call last):
  ...
ValueError: zip() argument 2 is longer than argument 1
  • strict=False(기본값)이어도 zip()은 잘 작동하긴 하지만, 서로 다른 길이의 이터러블들을 zip으로 묶으면서 나오는 버그들을 알 수 없어지므로 길이가 엄격하게 맞아야 하는 상황에서는 strict=True를 이용하는게 좋다.
  • strict 매개변수는 파이썬 3.10 버전에서 추가됐다.
  • itertools.zip_longest()를 사용하여 더 짧은 이터러블에 상수값을 추가함으로써 이터러블 간의 길이를 맞출 수 있다.
import itertools
a = [1, 2, 3]
b = [4]
list(itertools.zip_longest(a, b, fillvalue=0)) # [(1, 4), (2, 0), (3, 0)]
  • zip()의 인수로 이터러블 한 개만 넣을 경우 원소가 1개인 튜플의 이터레이터를 반환한다. 인수가 없으면 빈 이터레이터가 반환된다.
a = [1, 2, 3]
print(list(zip(a))) # [(1,), (2,), (3,)]

zip() # <zip at 0x2832c7dd280>

print(list(zip())) # []
  • zip()의 이터러블 인수들은 왼쪽에서 오른쪽으로 처리된다.
  • zip(*[iter(s)]*n, strict=True)를 사용하여 이터러블 객체 s에서 n개의 요소가 담긴 튜플들의 그룹으로 묶을 수 있다.
    이 코드는 이터러블 객체 s의 이터레이터를 n개 만큼 담은 리스트를 만들고, 이를 zip()의 인수로 언패킹한 것이다. 연산 결과로 n개씩의 튜플로 구성된 리스트가 반환된다.
s = [1, 2, 3, 4]
n = 2
list(zip(*[iter(s)]*n, strict=True)) # [(1, 2), (3, 4)]

a = iter([1, 2, 3, 4])
print(list(zip(a, a))) # [(1, 2), (3, 4)]
  • zip()에 *를 활용하여 unzip 할 수 있다.
foo = (1, 2, 3)
bar = ("A", "B", "C")
baz = list(zip(foo, bar))
print(baz) # [(1, 'A'), (2, 'B'), (3, 'C')]

spam, eggs = zip(*baz)
print(spam) # (1, 2, 3)
print(eggs) # ('A', 'B', 'C')

x = [1, 2, 3]
y = [4, 5, 6]
list(zip(x, y)) # [(1, 4), (2, 5), (3, 6)]

x2, y2 = zip(*zip(x, y))
x == list(x2) # True
y == list(y2) # True

filter()

filter(function, iterable)

  • filter()는 주어진 조건함수(function)에 따라 iterable의 요소들을 필터링 하는 파이썬 내장함수이다.
  • 이 함수는 function 함수가 True를 반환하는 iterable의 요소들로 이터레이터를 구축한다.
  • iterable은 시퀀스, 이터레이션을 지원하는 컨테이너 또는 이터레이터이다.
  • 특수한 사용법으로 function 인수에 일반적인 함수가 아닌 None을 넣을 수 있다.
  • function 인수가 None이면, 항등함수가 가정된다. 즉, iterable에서 False한 요소가 모두 제거된 이터레이터가 반환된다.
    filter(None, iterable)은 제너레이터 표현식 (item for item in iterable if item)와 같다.
  • function 인수가 None이 아니면 (item for item in iterable if function(item))와 같다.
# 주어진 리스트에서 False한 요소를 뺀 리스트 만들기
items = [1, "", " ", 2, None, False, 3]
list(filter(None, items))            # [1, ' ', 2, 3]
list(item for item in items if item) # [1, ' ', 2, 3]

# 주어진 문자열 리스트에서 알파벳 문자만 골라내기
items = ["1", "A", "2", "B"]
list(filter(str.isalpha, items))               # ['A', 'B']
list(item for item in items if item.isalpha()) # ['A', 'B']

# 주어진 정수 리스트에서 짝수만 골라내기
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list(filter(lambda x: x % 2 == 0, items)) # [2, 4, 6, 8, 10]

reduce()

functools.reduce(function, iterable[, initializer])
functools.reduce(집계함수, 순회 가능한 데이터[, 초기값])

  • 파이썬3 기준으로 reduce()는 빌트인 함수가 아니며 functools 모듈에 있으므로 임포트 해줘야 쓸 수 있다.
  • reduce는 집계함수 function을 iterable에 반복적으로 적용하여 그 결과로 하나의 값을 반환하는 함수이다.
  • 이 함수는 왼쪽에서 오른쪽의 순서로 iterable의 두 요소에 집계함수를 누적적으로 적용한다.
  • 예를 들면reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])는 이 수식((((1+2)+3)+4)+5)과 같다.
  • reduce()는 두 개의 요소로만 연산을 하기 때문에 function에 인수를 3개 이상 입력하는 함수를 넣으면 인수가 부족하다는 에러(TypeError)가 발생한다.
  • 초기값(initializer) 인수를 쓰면 초기값이 연산의 맨 앞에 배치되며, iterable 인수가 비어 있으면 기본값으로 사용된다. 초기값이 없고 이터러블 항목이 하나만 있으면 첫 번째 항목이 반환된다.
from functools import reduce

add = lambda x, y: x + y
reduce(add, [1, 2, 3, 4])      # 10
reduce(add, [1, 2, 3, 4], 100) # initializer가 100이라 110 반환
reduce(add, [1])               # 1
reduce(add, [], 100)           # 100

def add_digits(num: int) -> int:
	'''
    자리수가 하나만 남을 때까지 자연수의 모든 자리수를 더한 값을 출력
    '''
    print(num)
    if num < 10:
        return num
    else:
    	# int형의 num 변수는 이터러블이 아니므로 문자열로 변환
        add_digits(reduce(lambda x, y: int(x) + int(y), str(num)))

add_digits(13567)

# 출력
# 13567
# 22
# 4

입출력 속도 개선

  • 입력 크기가 크지 않거나 시간 제한이 넉넉하다면 그냥 input(), print()를 써도 문제가 없지만 입력 크기가 크거나 시간 제한이 짧으면 코드에 문제가 없어도 시간초과가 되어 통과가 안되는 경우가 있다. (백준 15552번, 1927번, 10845번 등)
  • 똑같은 코드라도 PyPy3로 제출하면 입출력 속도가 빨라져서 통과될 수도 있다.
  • 기본 파이썬 환경에서 입출력 속도를 빠르게 만드는 가장 간단한 방법은 sys모듈의 표준입출력 관련 함수들을 활용하는 것이다. 이 함수들은 input()print()보다 저수준에서 동작하며 비교적 단순하게 데이터를 처리하기 때문에 더 빠르게 입출력할 수 있다.
  • 입력: input() 대신 sys.stdin.readline() 사용
    • 이 함수는 input()과는 다르게 개행문자가 포함된 문자열을 반환한다. 개행문자는 문자열의 rstrip() 메서드로 제거할 수 있다.
  • 출력: print() 대신 sys.stdout.write() 사용
    • 이 함수는 print()와는 다르게 오직 문자열만 출력 가능하며 개행문자 없이 출력된다.
      출력 값에 빈칸 " "을 더하면 빈 칸을 넣고, 개행문자 "\n"을 더하면 줄바꿈 한다.
    • 또한 이 함수는 내장 함수 print()sep, end 등의 인수를 활용할 수 없고 인수 언패킹 또한 불가능하기 때문에 이에 대해선 다른 방식으로 처리해야 한다.
    • sys.stdout.write("\n".join(words_list)) 이런식으로 리스트의 모든 내용을 한 줄씩 출력할 수 있다.
# 백준 15552번
import sys

num = int(sys.stdin.readline().rstrip())
ret = [sum(map(int, sys.stdin.readline().split())) for _ in range(num)]
for v in ret:
    sys.stdout.write(str(v) + "\n")
    
# 내장 입출력 함수 input(), print() 대체하기
from sys import stdin, stdout
input = stdin.readline 
print = stdout.write

입력 크기를 알 수 없는 경우

  • 입력 설명이 입력은 여러 개의 테스트 케이스로 이루어져 있다. 이런식으로 쓰여있는 문제는 입력 크기를 알 수 없다고 가정하고 풀어야 한다.
  • 보통 무한 반복문 안에서 입력을 받고 종료 조건이 입력되면 종료하는 방식으로 코딩하면 된다.
  • 예시:
  • 종료 조건이 따로 없는 경우 파일의 끝(end of file, EOF)까지 입력을 받는다는 것이므로 tryexcept문으로 EOFError가 나오면 반복문을 종료하는 코드로 만들면 된다.
while True: 
    try: 
        a, b = map(int, input().split()) 
        print(a + b)
    except EOFError: 
        break

재귀 호출 제한 설정

  • 파이썬의 재귀호출 스택 제한은 기본값이 1000정도로(플랫폼에 따라 다름) 설정되어있다.
  • sys.getrecursionlimit()로 현재의 재귀호출 제한을 알 수 있고
    sys.setrecursionlimit(limit)로 재귀호출 제한을 재설정할 수 있다.
  • 재귀를 이용하여 푸는 경우 재귀 호출 횟수가 이 상한을 넘으면 RecursionError가 발생하므로 다음과 같이 재귀호출 제한을 늘려줘야 한다.
import sys

sys.setrecursionlimit(10**6)

자료 출처:

post-custom-banner

0개의 댓글