파이썬 특강 3부

정렬(sorting)

리스트의 소트(sort) 메서드를 이용하여 항목을 크기 순으로 정렬할 수 있다.

기존의 리스트를 건드리지 않으면서 항목을 정렬하여 활용하고 싶으면 소티드(sorted) 함수를 활용한다.

주의: sorted 함수는 리스트 메서드가 아니라 일반 함수이다. 따라서 메서드와 일반 함수와의 적용법 차이를 확인해야 한다.

sort 메서드와 sorted 함수 모두 오름차순 정렬을 기본으로 한다. 내림차순으로 정렬하려면 reverse 매개변수의 키워드 인자값을 True로 지정하면 된다.

정렬 기준

정렬 기준은 기본적으로 알려진 크기이다.

정렬 기준 변경: key 옵션변수 활용

하지만 key 옵션변수에 대한 인자를 지정하여 정렬 기준을 변경할 수 있다. key 옵션변수의 인자로 하나의 인자를 받는 함수가 사용된다.

예를 들어, 숫자들의 절댓값을 계산하는 abs 함수는 숫자 하나를 입력받는다.

따라서 숫자들의 절대닶의 크기를 기준으로 정렬하려면 abs 함수를 key 옵션변수의 인자로 사용하면 된다.

물론 절대값을 기준으로 내림차순 정렬도 가능하다.

예제: 빈도수 기준 정렬

파이썬 특강 2부에서 살펴보았던 word_counts 변수에 저장된 단어 빈도수 목록을 살펴보자.

word_counts의 항목들을 빈도수 기준 내림차순으로 정렬하고자 한다면 어떻게 해야 하는가? 가장 단순한 방법은 Counter 자료형의 most_common 메서드를 활용하면되다.

즉, 먼저 word_countsCounter 자료형으로 변환시킨 후 most_common 메서드를 적용한다.

하지만 Counter 자료형을 사용하지 않고도 가능하다. sorted 함수와 sort 메서드의 옵션변수인 key의 인자로 적절한 함수를 사용하면 된다.

먼저, 정렬기준으로 사용할 함수를 살펴보자.

이제 sortingKey를 기준으로 내림차순으로 정렬하면 된다.

참조: wc 는 보통 워드 카운트(word count, 단어 빈도수)의 줄임말이다.

조건제시법

집합을 정의하기 위해 사용하는 조건제시법을 리스트, 집합, 사전(dict)에도 적용할 수 있다.

예를 들어 0 ~ 9 까지의 정수 중에서 짝수만으로 이루어진 집합을 다음과 같이 조건제시법으로 정의할 수 있다.

{$x$ | $0 \le x < 10$, 단 $x$는 짝수}

리스트 조건제시법

동일한 조건으로 리스트를 생성하려면 다음과 같이 for ... in ... if ...문을 활용한다. 형식은 다음과 같다.

[x for x in range(10) if x % 2 == 0]

집합에 대한 조건제시법 적용은 다음과 같다.

아래 squares는 다음 집합에 대응한다.

{$x^2$ | $0 \le x < 10$ 이고 $x$는 정수} = {0, 1, 4, 9, 16, 25, 36, 49, 64, 81}

아래 even_squares는 다음 집합에 대응한다.

{$x^2$ | $x \in$ evensUnder10} = {0, 4, 16, 36, 64}

사전 조건제시법

사전(dict) 자료형에 대해서도 조건제시법을 적용할 수 있다.

밑줄 활용

조건제시법에서 불필요한 것은 밑줄(_)로 처리한다. 예를 들어, 동일한 항복을 반복 생성하고자 할 때 아래와 같이 할 수 있다.

참조: 밑줄은 영어로 언더스코어(underscore)라고 한다. 언더바(underbar) 표현을 사용하는 경우가 있는데 틀린 표현이다.

주의: 위와 같이 하면 evensUnder10와 동일한 길이의 리스트가 생성되며, 모든 항목은 0으로 동일하다. 따라서 for ... in ...에 사용되어야 하는 변수가 아무런 역할도 수행하지 않는다. 따라서 밑줄로 처리하는 것이다.

실제로 아래와 같이 해도 동일한 결과를 얻는다. 이유는 x가 리스트의 항목을 생성하는데 아무런 역할도 수행하지 않기 때문이다.

중첩 조건제시법

조건제시법에 여러 개의 for ... in ... 문을 사용할 수 있다.

참고: 중첩 for 반복문에 대응한다.

pairs는 아래 집합에 대응한다.

{$(x, y)$ | $0 \le x, y < 10$ 이고 $x$, $y$ 는 정수} }

따라서 pairs에는 총 100개의 순서쌍이 들어 있다. 첫 10개의 항목을 확인해보자.

그 다음 10개를 아래와 같다.

마지막 10개를 확인해보자.

두 개의 for ... in ... 문이 어떻게 움직이는지 감잡았을 것이다.

다음 예제는 조금 다르게 작동한다.

increasing_pairs의 길이는 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1, 즉, 45이다.

또한 만약 (x, y) $\in$ increasing_pairs 이면 xy 보다 작다.

테스팅: assert 활용

작성된 코드가 의도한 대로 작동하는지를 확인하는 여러 방법이 있다. 이 강좌에서는 가장 단순한 assert 테스팅 기법만 사용한다.

어서트(assert, 주장) 이후에 나오는 문장의 참/거짓을 판단하여 참이면 그냥 넘어가고, 거짓이면 오류를 발생시킨다.

예외처리 기능을 활용할 수도 있다.

assert 테스팅은 주로 함수가 제대로 구현되었는가를 테스트할 때 활용한다.

예를 들어, 숫자들의 리스트를 인자로 받아 최소값을 반환하는 함수를 아래와 같이 구현했다고 하자.

이제 위 구현이 제대로 작동하는지 아래처럼 테스팅할 수 있다.

함수의 인자로 제대로된 값들이 입력되는지 여부를 확인하기 위해서도 assert가 활용될 수 있다.

이제 빈 리스트([])를 인자로 사용하면 오류가 발생하는 대신에 바로 예외처리가 진행되어 AssertionError가 발생하고 발생 이유를 설명한다.

반면에 smallest_item 함수의 경우 빈 리스트의 최소값이 존재하지 않는다는 ValueError가 발생한다.

테스팅의 한계

테스팅을 몇 개를 한 결과 오류가 발생하지 않않다고 해서 구현이 제대로 되었다고 보장할 수는 없다. 테스팅은 몇 가지 경우에서 확인하니 잘 된다는 의미일 뿐이며, 모든 경우에 올바르게 작동한다는 보장은 하지 못한다.

예를 들어, 아래 코드는 두 문자열이 서로 순서가 뒤집어진 관계인지를 테스트한다.

그런데 테스팅 몇 개를 해보니 잘 구현된 것 같아 보인다.

그런데 아래의 경우엔 문제가 있다.

이와 같이 테스팅을 통과했다고 해서 일반적으로 구현이 완전하다라고 보장할 수는 없다.

객체 지향 프로그래밍

파이썬은 소위 객체 지향 프로그래밍을 지원하는 언어이다. 이 강좌에서는 객체 지행 프로그래밍의 정체를 논하지 않는다. 다만, 객체 지향 프로그래밍의 핵심 요소인 클래스(class)와 인스턴스(instance)를 어떻게 정의하고 활용하는지 예제 두 개를 이용하여 보여준다.

앞으로 클래스와 인스턴스를 수 없이 보고 사용할 것이다. 사실, 지금까지도 많은 클래스와 인스턴스를 살펴 보았다.

클래스의 활용

클래스는 크게 세 가지 방식으로 사용된다.

  1. 자료형 및 메서드 정의
    • 여기에서 주로 사용되는 방식이며, 앞서 언급한 클래스들이 여기에 해당한다.
    • 아래 첫째 예제가 자료형 및 메서드를 정의하는 과정을 잘 보여준다.
  2. 서로 관련된 기능을 갖는 변수와 함수들의 모둠, 일종의 도구 상자
    • 아래 둘째 예제가 여기에 해당함.
  3. 동일한 기능을 갖는 객체를 쉽고 다양한 방식으로 생성할 수 있도록 도와주는 기계틀 역할을 수행하는 도구
    • 게임 프로그래밍 등에서 게임 캐릭터, 배경, 도구 등을 쉽게 생성할 때 기본적으로 사용되는 방식임.
    • 여기서는 거의 다루지 않음.

예제 1: 집합 클래스 구현하기

집합 자료형인 set이 없다고 가정하고 직접 집합 자료형을 MySet 클래스를 이용하여 정의해보자.

매직 메서드

이닛(__init__), 레퍼(__repr__) 메서드처럼 두 개의 밑줄(underscores)로 감싸인 메서드를 매직 메서드(magic methods)라 부르며, 모든 파이썬 클래스에 동일한 이름으로 포함되어 있다. 기타 많은 매직 메서드가 존재하며 명시적으로 선언되지 않은 매직 메서드는 모든 클래스에서 동일하게 사용되는 기본 함수가 자동으로 지정된다. (대부분은 아무 것도 하지 않는 함수로 지정됨.)

클래스와 인스턴스

1, 2, 3을 원소로 갖는 MySet을 다음처럼 생성한다.

여기서 변수 s에 할당된 값을 MySet 클래스의 인스턴스라 부르며, 이 경우에는 특별히 MySet 자료형이라 부를 수 있다. set 클래스의 인스턴스를 집합 자료형이라 부르는 것과 동일하다.

주의:

이제 MySet 자료형인 s에 원소를 추가/삭제하는 방법과 결과를 확인해보자.

예제 2: 클릭수 세기

웹페이지의 방문자 수를 확인하는 앱을 구현하고자 할 때 아래 도구들이 필요하다.

언급한 변수 한 개와 네 개의 도구를 포함한 일종의 도구상자를 아래 CountingClicker 클래스로 구현한다.

아래 코드를 실행하면 CountingClass의 인스턴스를 하나 생성하면 앞서 언급한 변수 한 개와 네 개의 도구를 포함한 하나의 도구상자를 얻게 되며, 도구상자의 이름은 clicker이다.

clicker 도구상자에 포함된 도구, 즉 특정 메서드를 이용하려면 아래와 같이 실행한다.

clicker.메서드이름(인자,....)

먼저 클릭수가 0으로 초기화되어 있음을 확인하자. 이유는 아무도 클릭하지 않았기 때문이다. 실제로 clicker 도구상자를 생성할 때 호출되는 __init__ 메서드의 인자가 지정되지 않아서 count 매개변수의 키워드 인자로 기본값인 0이 사용되어, self.count 변수에 할당되었다.

인스턴스 변수

이제 클릭을 두 번 했다가 가정하자. 즉, click 메서드를 두 번 호출되어야 한다.

그러면 클릭수가 2가 되어 있어야 한다.

이제 클릭수를 초기화하자.

그러면 클릭수가 다시 0이 되어야 한다.

클릭수를 지정하면서 CountingClicker의 인스턴스를 생성할 수 있다.

클릭수가 50으로 설정되었음을 확인할 수 있다.

지금까지 CountingClicker의 인스턴스는 두 번 생성되었음을 아래 결과가 보여준다.

클래스 상속

부모 클래스로부터 모든 기능을 물려받을 수 있는 자식 클래스를 상속을 이용하여 선언할 수 있다.

예를 들어, 클릭수를 초기화할 수 없는 자식 클래스 NoResetClicker를 아래와 같이 선언한다. 클릭수 초기화 기능을 없애기 위해서는 reset 메서드를 재정의(overriding)해야 한다.

이제 NoResetClicker의 인스턴스를 생성한 후 클릭수 초기화가 이루어지지 않을 확인할 수 있다.

리셋이 작동하지 않습니다.

자식 클래스의 인스턴스가 만들어져도 부모 클래스의 인스턴스 카운트가 올라간다.

이터러블(iterable) 자료형과 제너레이터(generator)

리스트, 튜플, 사전 등의 모음 자료형과 for 반복문은 서로 찰떡궁합 관계이다. 이유는 특정 명령을 지정한 횟수만큼 쉽게 반복적으로 처리할 수 있도록 만들 수 있기 때문이다.

리스트, 튜플, range, 사전 자료형처럼 for ... in ... 등의 반복문에서 항목을 차례대로 읽어 활용하는 것을 가능하게 해주는 자료형을 이터러블(iterable) 자료형이다. 이터러블 자료형에는 이 외에도 더 있다. 예를 들어, 저장된 텍스트 파일의 내용을 불러와 저장한 데이터셋도 이터러블 자료형이다.

리스트와 range의 차이점

리스트는 포함된 항목이 많아지면 그만큼 사용하는 메모리 용량도 비례해서 늘어난다. 또한 매우 많은 양의 데이터를 포함하는 리스트를 다루는 일도 그만큼 오래 걸리고 어려워진다. 예를 들어, 매우 긴 리스트를 정렬하는 데에 걸리는 시간은 리스트 길이의 제곱 정도에 비례한다. 이런 이유로 한 두 번 사용하고 버릴 대용량의 리스트를 생성하면 문제가 더욱 심각해진다.

반면에 리스트의 용량 문제를 해결할 수 있는 대안이 range 자료형이다. range 함수는 리스트와 유사한 기능을 제공하면서도 메모리 저장과 관련된 리스트의 한계를 갖지 않는 값을 생성한다. 앞서 보았듯이, 예를 들어 range(5)를 실행한 결과는 range 자료형이면서 [0, 1, 2, 3, 4]와 유사한 값이다. 하지만 리스트는 이미 모든 항목을 생성한 후에 메모리에 저장되는 반면에 range(5)는 요구되는 경우에만 항목을 하나씩 생성한다.

이와같이 필요한 만큼만 항목을 생성해주는 도구를 제너레이터(generator)라 부르며, range가 대표적인 제너레이터이다. 아래에서 제너레이터의 활용법을 간단한 예제를 통해 살펴본다.

range 함수

range(5)for 반복문에서 리스트 [0, 1, 2, 3, 4]와 동일한 기능을 수행한다. 하지만 리스트와는 달리 자신을 보여달라고 하면 내부를 보여주지 않는다.

즉, 0부터 4까지의 정수를 다룰 준비가 되어있다고만 알려준다. 하지만 아래와 같이 for 반복문을 이용하면 그때마다 항목을 하나씩 생성해서 보여준다.

제너레이터 구현

제너레이터 구현은 함수를 정의하는 방식과 거의 동일하다. 반환값을 지정하는 return 지정자 대신에 요청될 때 값을 생성하는 일드(yield) 지정자가 사용될 뿐이다.

예를 들어, range(n)과 동일한 기능을 수행하는 generate_range 제너레이터를 아래와 같이 정의할 수 있다.

아래 반복문을 실행한다고 해서 generate_range(5)이 0부터 9까지의 정수를 한꺼번에 생성하지는 않는다. 대신에 for 반복문이 한 번씩 반복해서 실행될 때마다 차례대로 0, 1, 2, 3, 4 를 생성해서 변수 i에 할당한다.

소극적(lazy) 계산과 적극적(eager) 계산

range, generate_range 등 처럼 모든 값을 미리 생성해서 준비해 놓는 대신에 필요할 때 필요한 항목을 생성하는 것을 소극적(lazy) 계산이라 부른다. 반면에 호출 되자마자 바로바로 실행하는 것을 적극적 계산 이라 부른다. 파이썬에서 기본적으로 적극적 계산을 사용하지만 제너레이터 경우와 같이 일부 소극적 계산을 지원한다.

소극적 계산 활용: 무한 수열 생성

제너레이터를 이용하면 무한 수열도 생성할 수 있다. 이유는 소극적 계산을 따르는 제너레이터는 항상 요구되는 만큼만 생성하고, 따라서 절대로 무한히 많은 값들을 한꺼번에 생성하지 않기 때문이다.

예를 들어, 아래 natural_numbers 함수는 모든 자연수를 생성할 준비가 된 제너레이터이다.

물론 위와 같이 무한 수열을 생성하는 제너레이터는 매우 조심스럽게 사용해야 한다. 예를 들어 아래와 같이 사용하면 무한 반복(loop)이 발생한다.

for i in natural_number():
    print(i)

무한 수열을 생성하는 제너레이터는 break 명령문과 함께 적절하게 사용할 수는 있다.

제너레이터 재활용

생성된 제너레이터는 한 번 사용하면 다시 사용할 수 없으며, 필요할 때마다 다시 제너레이터를 생성해야 한다.

예를 들어, 아래와 같이 제너레이터를 생성하여 변수에 저장하자.

그러고 나서 변수를 이용하여 반복문을 돌리자.

이제 다시 동일한 변수를 이용하여 반복문을 돌리면 아무 것도 하지 않을 것이다. 이유는 지정된 제너레이터가 이미 모든 값을 생성하였기 때문이다.

해결책은 해당 제너레이터를 다시 생성하는 것 뿐이다.

따라서 제너레이터가 반복적으로 사용되어야 한다면 차라리 리스트로 정의해서 활용하는 것이 나을 수 있다.

제너레이터 조건제시법

리스트, 집합, 사전 자료형과는 달리 튜플을 이용한 조건제시법은 제너레이이터를 생성한다. 예를 들어, 아래 튜플 모양의 값은 20보다 작은 짝수들을 필요할 때 하나씩 생성하는 제너레이터이다.

내용은 보여주지 않는다.

for 반복문을 이용하여 원소들을 확인할 수 있다.

비슷한 방식으로 아래와 같이 무한 수열들을 정의할 수 있다.

next 함수

넥스트(next) 함수는 제너레이터에서 아직 생성되지 않는 다음 항목을 계산해내는 함수이다.

enumerate 함수

리스트, 튜플, 제너레이터를 대상으로 반복문을 실행할 때 각 항목고 함께 해당 항목의 인덱스를 함께 사용해야 할 때가 있다. 이럴 때 이뉴머레이트(enumerate, 열거하기) 함수가 유용하다.

아래 두 방식도 가능하지만 enumerate 함수를 사용하는 것에 비하면 별로 좋아 보이지 않는다.

참고 문서

이터러블 객체와 제너레이터에 대한 보다 자세한 설명은 이터러블, 이터레이터, 제너레이트를 참고할 수 있다.

연습문제

  1. is_reverse 함수의 문제점을 찾아 수정하라.