※포스트의 모든 내용은 박상길 저자의 "파이썬 알고리즘 인터뷰" 에서 참고한 내용이다.
제너레이터(Generator)는 루프 반복 동작을 제어할 수 있는 루틴 형태를 말한다. 이를테면 숫자 100만개를 만들어낸다 하면 메모리 어딘가에 100만개의 숫자를 보관해야한다. 하지만 제너레이터를 이용한다면, 숫자 100만개에 해당하는 메모리를 사용할 필요 없이 제너레이터만 생성한 뒤 필요할 때 숫자를 사용할 수 있다. 제너레이터에서는 yeild
구문을 사용하는데, 이는 기존 함수에서 사용되는 return
과 다르게 함수를 종료하는 것이 아닌 그 전까지의 값을 내보내고 이어서 함수의 나머지 부분이 실행된다. 다음은 예시 코드이다.
>>> def get_numbers(): ... n = 0 ... while n <= 1000000: ... n += 1 ... yeild n
이 경우 함수의 리턴 값은 제너레이더가 된다. 여기서 다음 값을 생성하려면 next()
함수로 추출할 수 있다. 또 다음 예시 코드처럼 여러 타입의 값을 하나의 함수에서 생성하는 것도 가능하다.
>>> def generator(): ... yeild 1 ... yeild 'string' ... yeild True >>> g = generate() >>> g <generator object generator at 0x10a47c678> >>> next(g) 1 >>> next(g) 'string' >>> next(g) True
range()
함수는 앞서 말했던 제너레이터의 방식을 을 활용한다. 먼저 예시 코드를 보자
>>> list(range(4)) [0, 1, 2, 3] >>>range(4) range(0, 4) >>> type(range(4)) <class 'range'> >>> for i in range(4): ... print(i, end=" ") ... 0 1 2 3
이를 살펴보면 range()
함수는 range
클래스를 리턴하고 반복문에서는 제너레이터가 next()
함수로 하나씩 다음 값을 호출하듯 작동한다.
만약 생성하고 싶은 수가 100만개라면 이를 생성하는 방법은 두 가지 방법이 있다.
>>> a = [n for n in range(1000000)] >>> b = range(1000000)
이 때 a
와 b
를 len()
함수로 길이를 측정했을 때, 그 값은 둘 다 1000000으로 같다. 하지만 다른 점은 a는 1000000개의 숫자에 해당하는 값이 담겨있는 것이고, b는 생성해야 한다는 조건만 존재한다. 이제 메모리 점유율을 비교해보자
>>> sys.getsizeof(a) 8697464 >>> sys.getsizeof(b) 48
같은 1000000개의 숫자를 가지고 있지만 두 변수의 메모리 점유율은 b가 압도적으로 작다. 숫자가 얼마로 커지던 b는 생성 조건만 저장하고 있기 때문에 b의 메모리 점유율은 항상 같을 것이다. 또 이러한 range()
는 인덱스로 접근도 가능하다.
>>> b[14] 14
이 때문에 리스트와 거의 동일하게 사용할 수 있는 반면에, 메모리 측면에서는 리스트에 비해 엄청난 효율성을 가진다.
enumerate()
는 list, set, tuple 같은 다양한 자료형을 인덱스를 포함한 enumerate 객체로 리턴한다. 다음 예제 코드를 보자.
>>> ex1 = [1, 2, 3, 14, 23, 91, 7] >>> ex1 [1, 2, 3, 14, 23, 91, 7] >>> enumerate(ex1) <enumerate object at 0x1001a24c7> >>> list(enumerate(ex1)) [(0,1), (1, 2), (2, 3), (3, 14), (4, 23), (5, 91), (6, 7)]
enumerate()
를 사용하면 이와 같이 인덱스를 부여하여 각 값을 표현한다. 그럼 이를 이용하는 방법은 무엇일까. 다음과 같이 활용할 수 있다.
for i, v in enumerate(ex1): print(i, v)
print()
는 코딩 테스트 시 디버깅을 위해 제공되는 유일한 기능으로 볼 수 있다. 따라서 이를 잘 활용하면 유용할 것이다.
먼저 여러 값을 한번에 출력함에 있어 각 값들을 구분지어 주는 디폴트 값은 공백이다.
>>> print("a", "b") a b
구분자를 다른 값으로 설정하기 위해서는 sep
파라미터를 활용해줄 수 있다
>>> print("a", "b", sep = ",") a,b >>> print("a", "b", sep = " / ") a / b
print()
함수는 기본적으로 항상 줄바꿈을 한다. 긴 루프를 이용하여 디버깅을 할 때 불편함을 겪을 수 있는데 이는 end
파라미터를 이용하여 다음과 같이 해결할 수 있다.
>>> for i in range(7): ... print(i, end = " ") # 출력 후 마지막을 공백으로 처리 ... 0 1 2 3 4 5 6
리스트 출력은 join()
을 활용하여 한꺼번에 묶어서 처리할 수 있다.
>>> list = [0, 1, 2, 3, "apple"] >>> print(' '.join(list)) 0 1 2 3 "apple"
코딩을 할 때, 큰 프레임을 만들고 세부적인 내용을 작성하고 구현하려는 생각으로 다음과 같이 코딩하는 경우가 있다.
class ExClass(): def method_ex1: def method_ex2: print("hello") a = ExClass()
하지만 이와 같은 경우 인덴트 오류로 작동하지 않을 것이다. method_ex1
를 구현하지 않고 그냥 넘어가 발생한 오류이다. 이런 번거로운 오류를 막을 때 사용할 수 있는 것이 바로 pass
이다. 다음과 같이 사용하면 된다.
class ExClass(): def method_ex1: pass # pass 추가하여 오류 발생 조치 def method_ex2: print("hello") a = ExClass()
여기서 pass
는 널 연산으로 아무 기능도 하지 않는다. 이를 이용해서 원래 의도했던 큰 프레임을 먼저 작성한 뒤 세부 내용을 구현할 수 있다. 이는 온라인 코딩 테스트 시에도 유용하게 활용할 수 있다고 한다.