https://dojang.io/mod/page/view.php?id=2366


33.3 클로저 사용하기

이제 함수를 클로저 형태로 만드는 방법을 알아보겠습니다. 다음은 함수 바깥쪽에 있는 지역 변수 ab를 사용하여 a * x + b를 계산하는 함수 mul_add를 만든 뒤에 함수 mul_add 자체를 반환합니다.

closure.py

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산
    return mul_add          # mul_add 함수를 반환
 
c = calc()
print(c(1), c(2), c(3), c(4), c(5))

실행 결과

8 11 14 17 20

먼저 calc에 지역 변수 a와 b를 만들고 3과 5를 저장했습니다. 그다음에 함수 mul_add에서 a와 b를 사용하여 a * x + b를 계산한 뒤 반환합니다.

def calc():
    a = 3
    b = 5
    def mul_add(x):
        return a * x + b    # 함수 바깥쪽에 있는 지역 변수 a, b를 사용하여 계산

함수 mul_add를 만든 뒤에는 이 함수를 바로 호출하지 않고 return으로 함수 자체를 반환합니다(함수를 반환할 때는 함수 이름만 반환해야 하며 ( )(괄호)를 붙이면 안 됩니다).

    return mul_add          # mul_add 함수를 반환

이제 클로저를 사용해보겠습니다. 다음과 같이 함수 calc를 호출한 뒤 반환값을 c에 저장합니다. calc에서 mul_add를 반환했으므로 c에는 함수 mul_add가 들어갑니다. 그리고 c에 숫자를 넣어서 호출해보면 a * x + b 계산식에 따라 값이 출력됩니다.

c = calc()
print(c(1), c(2), c(3), c(4), c(5))    # 8 11 14 17 20

잘 보면 함수 calc가 끝났는데도 c는 calc의 지역 변수 ab를 사용해서 계산을 하고 있습니다. 이렇게 함수를 둘러싼 환경(지역 변수, 코드 등)을 계속 유지하다가, 함수를 호출할 때 다시 꺼내서 사용하는 함수를 클로저(closure)라고 합니다. 여기서는 c에 저장된 함수가 클로저입니다.

▼ 그림 33-4 클로저의 개념

이처럼 클로저를 사용하면 프로그램의 흐름을 변수에 저장할 수 있습니다. 즉, 클로저는 지역 변수와 코드를 묶어서 사용하고 싶을 때 활용합니다. 또한, 클로저에 속한 지역 변수는 바깥에서 직접 접근할 수 없으므로 데이터를 숨기고 싶을 때 활용합니다.

33.3.1  lambda로 클로저 만들기

클로저는 다음과 같이 lambda로도 만들 수 있습니다.

closure_lambda.py

def calc():
    a = 3
    b = 5
    return lambda x: a * x + b    # 람다 표현식을 반환
 
c = calc()
print(c(1), c(2), c(3), c(4), c(5))

실행 결과

8 11 14 17 20

return lambda x: a * x + b처럼 람다 표현식을 만든 뒤 람다 표현식 자체를 반환했습니다. 이렇게 람다를 사용하면 클로저를 좀 더 간단하게 만들 수 있습니다.

보통 클로저는 람다 표현식과 함께 사용하는 경우가 많아 둘을 혼동하기 쉽습니다. 람다는 이름이 없는 익명 함수를 뜻하고, 클로저는 함수를 둘러싼 환경을 유지했다가 나중에 다시 사용하는 함수를 뜻합니다.

33.3.2  클로저의 지역 변수 변경하기

지금까지 클로저의 지역 변수를 가져오기만 했는데, 클로저의 지역 변수를 변경하고 싶다면 nonlocal을 사용하면 됩니다. 다음은 a * x + b의 결과를 함수 calc의 지역 변수 total에 누적합니다.

closure_nonlocal.py

def calc():
    a = 3
    b = 5
    total = 0
    def mul_add(x):
        nonlocal total
        total = total + a * x + b
        print(total)
    return mul_add
 
c = calc()
c(1)
c(2)
c(3)

실행 결과

8
19
33

지금까지 전역 변수, 지역 변수, 변수의 범위, 클로저에 대해 알아보았습니다. 클로저는 다소 어려운 개념이므로 지금 당장 완벽하게 이해하지 않아도 상관없습니다. 나중에 파이썬에 익숙해지면 자연스럽게 익히게 됩니다.




33.2 함수 안에서 함수 만들기

이번에는 함수 안에서 함수를 만드는 방법을 알아보겠습니다. 다음과 같이 
def로 함수를 만들고 그 안에서 다시 def로 함수를 만들면 됩니다.

def 함수이름1():
    코드
    def 함수이름2():
        코드

간단하게 함수 안에서 문자열을 출력하는 함수를 만들고 호출해봅니다.

function_in_function.py

def print_hello():
    hello = 'Hello, world!'
    def print_message():
        print(hello)
    print_message()
 
print_hello()

실행 결과

Hello, world!

함수 print_hello 안에서 다시 def로 함수 print_message를 만들었습니다. 그리고 print_hello 안에서 print_message()처럼 함수를 호출했습니다. 하지만 아직 함수를 정의만 한 상태이므로 아무것도 출력되지 않습니다.

두 함수가 실제로 동작하려면 바깥쪽에 있는 print_hello를 호출해주어야 합니다. 즉, print_hello > print_message 순으로 실행됩니다.

33.2.1  지역 변수의 범위

그럼 print_hello 함수와 print_message 함수에서 지역 변수의 범위를 살펴보겠습니다. 안쪽 함수 print_message에서는 바깥쪽 함수 print_hello의 지역 변수 hello를 사용할 수 있습니다.

def print_hello():
    hello = 'Hello, world!'
    def print_message():
        print(hello)    # 바깥쪽 함수의 지역 변수를 사용

즉, 바깥쪽 함수의 지역 변수는 그 안에 속한 모든 함수에서 접근할 수 있습니다.

이 지역 변수의 접근 범위를 그림으로 나타내면 다음과 같은 모양이 됩니다.

▼ 그림 33-3 함수의 지역 변수에 접근할 수 있는 범위

33.2.2  지역 변수 변경하기

지금까지 바깥쪽 함수의 지역 변수를 안쪽 함수에서 사용해봤습니다. 그럼 바깥쪽 함수의 지역 변수를 안쪽 함수에서 변경하면 어떻게 될까요?

다음과 같이 안쪽 함수 B에서 바깥쪽 함수 A의 지역 변수 x를 변경해봅니다.

function_local_error.py

def A():
    x = 10        # A의 지역 변수 x
    def B():
        x = 20    # x에 20 할당
 
    B()
    print(x)      # A의 지역 변수 x 출력
 
A()

실행 결과

10

실행을 해보면 20이 나와야 할 것 같은데 10이 나왔습니다. 왜냐하면 겉으로 보기에는 바깥쪽 함수 A의 지역 변수 x를 변경하는 것 같지만, 실제로는 안쪽 함수 B에서 이름이 같은 지역 변수 x를 새로 만들게 됩니다. 즉, 파이썬에서는 함수에서 변수를 만들면 항상 현재 함수의 지역 변수가 됩니다.

def A():
    x = 10        # A의 지역 변수 x
    def B():
        x = 20    # B의 지역 변수 x를 새로 만듦

현재 함수의 바깥쪽에 있는 지역 변수의 값을 변경하려면 nonlocal 키워드를 사용해야 합니다. 다음과 같이 함수 안에서 nonlocal에 지역 변수의 이름을 지정해줍니다.

  • nonlocal 지역변수

function_nonlocal_keyword.py

def A():
    x = 10        # A의 지역 변수 x
    def B():
        nonlocal x    # 현재 함수의 바깥쪽에 있는 지역 변수 사용
        x = 20        # A의 지역 변수 x에 20 할당
 
    B()
    print(x)      # A의 지역 변수 x 출력
 
A()

실행 결과

20

이제 함수 B에서 함수 A의 지역 변수 x를 변경할 수 있습니다. 즉, nonlocal은 현재 함수의 지역 변수가 아니라는 뜻이며 바깥쪽 함수의 지역 변수를 사용합니다.

33.2.3  nonlocal이 변수를 찾는 순서

nonlocal은 현재 함수의 바깥쪽에 있는 지역 변수를 찾을 때 가장 가까운 함수부터 먼저 찾습니다. 이번에는 함수의 단계를 ABC로 만들었습니다.

function_in_function_nonlocal_keyword.py

def A():
    x = 10
    y = 100
    def B():
        x = 20
        def C():
            nonlocal x
            nonlocal y
            x = x + 30
            y = y + 300
            print(x)
            print(y)
        C()
    B()
 
A()

실행 결과

50
400

함수 C에서 nonlocal x를 사용하면 바깥쪽에 있는 함수 B의 지역 변수 x = 20을 사용하게 됩니다. 따라서 x = x + 30은 50이 나옵니다. 그리고 함수 C에서 nonlocal y를 사용하면 바깥쪽에 있는 함수의 지역 변수 y를 사용해야 하는데 함수 B에는 y가 없습니다. 이때는 한 단계 더 바깥으로 나가서 함수 A의 지역 변수 y를 사용하게 됩니다. 즉, 가까운 함수부터 지역 변수를 찾고, 지역 변수가 없으면 계속 바깥쪽으로 나가서 찾습니다.

실무에서는 이렇게 여러 단계로 함수를 만들 일은 거의 없습니다. 그리고 함수마다 이름이 같은 변수를 사용하기 보다는 변수 이름을 다르게 짓는 것이 좋습니다.

33.2.4  global로 전역 변수 사용하기

특히, 함수가 몇 단계든 상관없이 global 키워드를 사용하면 무조건 전역 변수를 사용하게 됩니다.

function_in_function_global_keyword.py

x = 1
def A():
    x = 10
    def B():
        x = 20
        def C():
            global x
            x = x + 30
            print(x)
        C()
    B()
 
A()

실행 결과

31

함수 C에서 global x를 사용하면 전역 변수 x = 1을 사용하게 됩니다. 따라서 x = x + 30은 31이 나옵니다.

파이썬에서 global을 제공하지만 함수에서 값을 주고받을 때는 매개변수와 반환값을 사용하는 것이 좋습니다. 특히 전역 변수는 코드가 복잡해졌을 때 변수의 값을 어디서 바꾸는지 알기가 힘듭니다. 따라서 전역 변수는 가급적이면 사용하지 않는 것을 권장합니다.


https://dojang.io/mod/page/view.php?id=2364


Unit 33. 클로저 사용하기

이번에는 변수의 사용 범위와 함수를 클로저 형태로 만드는 방법을 알아보겠습니다.

참고로 클로저는 개념이 다소 어려울 수 있으므로 변수의 사용 범위부터 알아본 뒤에 설명하겠습니다.

33.1 변수의 사용 범위 알아보기

파이썬 스크립트에서 변수를 만들면 다음과 같이 함수 안에서도 사용할 수 있습니다.

global_variable.py

x = 10          # 전역 변수
def foo():
    print(x)    # 전역 변수 출력
 
foo()
print(x)        # 전역 변수 출력

실행 결과

10
10

foo 함수에서 함수 바깥에 있는 변수 x의 값을 출력했습니다. 물론 함수 바깥에서도 x의 값을 출력할 수 있습니다. 이처럼 함수를 포함하여 스크립트 전체에서 접근할 수 있는 변수를 전역 변수(global variable)라고 부릅니다. 특히 전역 변수에 접근할 수 있는 범위를 전역 범위(global scope)라고 합니다.

▼ 그림 33-1 전역 변수와 전역 범위

그럼 변수 x를 함수 foo 안에서 만들면 어떻게 될까요?

local_variable.py

def foo():
    x = 10      # foo의 지역 변수
    print(x)    # foo의 지역 변수 출력
 
foo()
print(x)        # 에러. foo의 지역 변수는 출력할 수 없음


실행 결과

10
Traceback (most recent call last):
  File "C:\project\local_variable.py", line 6, in <module>
    print(x)        # 에러. foo의 지역 변수는 출력할 수 없음
NameError: name 'x' is not defined

실행을 해보면 x가 정의되지 않았다는 에러가 발생합니다. 왜냐하면 변수 x는 함수 foo 안에서 만들었기 때문에 foo의 지역 변수(local variable)입니다. 따라서 지역 변수는 변수를 만든 함수 안에서만 접근할 수 있고, 함수 바깥에서는 접근할 수 없습니다. 특히 지역 변수를 접근할 수 있는 범위를 지역 범위(local scope)라고 합니다.

▼ 그림 33-2 지역 변수와 지역 범위

이렇게 전역 변수와 지역 변수를 사용해보았습니다.

33.1.1  함수 안에서 전역 변수 변경하기

만약 함수 안에서 전역 변수의 값을 변경하면 어떻게 될까요?

global_local_variable.py

x = 10          # 전역 변수
def foo():
    x = 20      # x는 foo의 지역 변수
    print(x)    # foo의 지역 변수 출력
 
foo()
print(x)        # 전역 변수 출력

실행 결과

20
10

분명 함수 foo 안에서 x = 20처럼 x의 값을 20으로 변경했습니다. 하지만 함수 바깥에서 print로 x의 값을 출력해보면 10이 나옵니다. 겉으로 보기에는 foo 안의 x는 전역 변수인 것 같지만 실제로는 foo의 지역 변수입니다. 즉, 전역 변수 x가 있고, foo에서 지역 변수 x를 새로 만들게 됩니다. 이 둘은 이름만 같을 뿐 서로 다른 변수입니다.

함수 안에서 전역 변수의 값을 변경하려면 global 키워드를 사용해야 합니다. 다음과 같이 함수 안에서 global에 전역 변수의 이름을 지정해줍니다.

  • global 전역변수

function_global_keyword.py

x = 10          # 전역 변수
def foo():
    global x    # 전역 변수 x를 사용하겠다고 설정
    x = 20      # x는 전역 변수
    print(x)    # 전역 변수 출력
 
foo()
print(x)        # 전역 변수 출력

실행 결과

20
20

이제 함수 안에서 x를 20으로 변경하면 함수 바깥에서 x를 출력했을 때 20이 나옵니다. 이렇게 함수 안에서 변수를 global로 지정하면 전역 변수를 사용하게 됩니다.

만약 전역 변수가 없을 때 함수 안에서 global을 사용하면 해당 변수는 전역 변수가 됩니다.

# 전역 변수 x가 없는 상태
def foo():
    global x    # x를 전역 변수로 만듦
    x = 20      # x는 전역 변수
    print(x)    # 전역 변수 출력
 
foo()
print(x)        # 전역 변수 출력

참고 | 네임스페이스

파이썬에서 변수는 네임스페이스(namespace, 이름공간)에 저장됩니다. 다음과 같이 locals 함수를 사용하면 현재 네임스페이스를 딕셔너리 형태로 출력할 수 있습니다.

>>> x = 10
>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'x': 10}

출력된 네임스페이스를 보면 'x': 10처럼 변수 x와 값 10이 저장되어 있습니다. 여기서는 전역 범위에서 네임스페이스를 출력했으므로 전역 네임스페이스를 가져옵니다.

마찬가지로 함수 안에서 locals를 사용할 수도 있습니다.

>>> def foo():
...     x = 10
...     print(locals())
...
>>> foo()
{'x': 10}

네임스페이스를 보면 'x': 10만 저장되어 있습니다. 이때는 지역 범위에서 네임스페이스를 출력했으므로 지역 네임스페이스를 가져옵니다.


https://medium.com/@charliesharding/learning-python-no-template-literals-da7cbd77e3ba


Learning Python — Template Literals?

Charles Harding
Mar 11, 2017 · 1 min read

Coming from Javascript, python is a fairly approachable language. I’m going to try to keep up these posts as a reference for others learning python as well. One of the first things that I’ve encountered is the absence of template literals in the language.

In Javascript, one can type:

let age = 27;
console.log(`my age is ${age}!`);

And the output will be 'my age is 27!' . This is useful when you have a lot of embedded variables in a string so that you don’t have to keep concatenating with ending quotes and plus signs.

'lol ' + adversary.name + ' you suk at ' + adversary.favoriteThing + '!!!!111!11!'

I found an active proposal for this feature but it was posted two years ago. https://www.python.org/dev/peps/pep-0498/

Turns out they totally do exist, just under a different name and syntax.

x = "There are %d types of people." % 10
binary = "binary"
do_not = "don't"
y = "Those who know %s and those who %s." % (binary, do_not)

The %s and % (varnames) is the way of inserting the values of those variables into the string. %s is used for strings whereas %d is used for numbers.

As pointed out in the comments by Phil Owens — a better functionality actually has been added in to python 3.6

“As of 3.6 you can use the syntax:

date = '22nd'txt = f"Today is March {date}."

Thanks Phil!

https://wikidocs.net/16038


1. mutable과 immutable 객체

객체에는 mutable과 immutable 객체가 있습니다.

❈ 객체 구분 표


class설명구분
listmutable 한 순서가 있는 객체 집합mutable
setmutable 한 순서가 없는 고유한 객체 집합mutable
dictkey와 value가 맵핑된 객체, 순서 없음mutable
bool참,거짓immutable
int정수immutable
float실수immutable
tupleimmutable 한 순서가 있는 객체 집합immutable
str문자열immutable
frozensetimmutable한 setimmutable

일반 user가 작성한 class도 대부분 mutable 한 객체 입니다.
immutable한 클래를 만들기 위해서는 특별한 방법이 필요합니다.

https://stackoverflow.com/questions/4828080/how-to-make-an-immutable-object-in-python

  • REPL에서 mutable과 immutable에서 구분해봅시다. 몇가지만 해봅니다.

  • list 는 mutable 입니다.

  • 변수 a 에 1, 2, 3을 원소로 가지는 리스트를 할당하였습니다.
  • id는 변수의 메모리 주소값을 리턴해줍니다.
  • a의 첫번째 원소를 변경한 후에도 id값은 변경없이 a의 변수가 변경되었습니다.
>>> a = [1, 2, 3]
>>> id(a)
4393788808
>>> a[0] = 5
>>> a
[5, 2, 3]
>>> id(a)
4393788808
  • set도 mutable입니다.
  • |= set에서 or 연산입니다. 합집합이 됩니다.
  • 값은 변경되었으나 id는 변함없습니다.
>>> x = {1, 2, 3}
>>> x
{1, 2, 3}
>>> id(x)
4396095304
>>> x|={4,5,6}
>>> x
{1, 2, 3, 4, 5, 6}
>>> id(x)
4396095304
  • str은 immutable 입니다.
  • s 변수에 첫번째 글자를 변경 시도하면 에러가 발생합니다.
  • s에 다른 값을 할당하면, id가 변경됩니다. 재할당은 애초에 변수를 다시할당하는 것이므로 mutable과 immutable과는 다른 문제입니다. list또한 값을 재할당하면 id가 변경됩니다.
>>> s= "abc"
>>> s
'abc'
>>> id(s)
4387454680
>>> s[0]
'a'
>>> s[0] = 's'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> s = 'def'
>>> s
'def'
>>> id(s)
4388970768

2. 변수 간 대입(단순 복사)

2-1 mutable한 객체의 변수 간 대입

  • list의 얕은 복사를 확인 해봅니다.
  • b 에 a를 할당하면 값이 할당되는 것이 아니라 같은 메모리 주소를 바라봅니다.
  • b를 변경하면 같이 a도 바뀝니다.
  • mutable한 다른 객체 또한 똑같은 현상이 나타납니다.
>>> a = [1, 2, 3]
>>> b = a # shallow copy
>>> b[0]= 5
>>> a
[5, 2, 3]
>>> b
[5, 2, 3]
>>> id(a)
4396179528
>>> id(b)
4396179528

2-2 immutable한 객체의 변수간 대입

  • str 문자열의 얕은 복사를 확인해봅니다.
  • list와 똑같이 b를 a에 할당하면 같은 메모리 주소를 바라보게 됩니다.
  • 하지만 b에 다른 값을 할당하면 재할당이 이루어지며 메모리 주소가 변경됩니다.
  • 고로 a와 b는 다른 값을 가집니다.
>>> a = "abc"
>>> b = a
>>> a
'abc'
>>> b
'abc'
>>> id(a)
4387454680
>>> id(b)
4387454680
>>> b = "abcd"
>>> a
'abc'
>>> b
'abcd'
>>> id(a)
4387454680
>>> id(b)
4396456400

3. 얕은 복사(shallow copy)

  • list의 슬라이싱을 통한 새로운 값을 할당해봅니다.
  • 아래의 결과와 같이 슬라이싱을 통해서 값을 할당하면 새로운 id가 부여되며, 서로 영향을 받지 않습니다.
>>> a = [1,2,3]
>>> b = a[:] # copy.copy(a)와 동일
>>> id(a)
4396179528
>>> id(b)
4393788808
>>> a == b
True
>>> a is b
False
>>> b[0] = 5
>>> a
[1, 2, 3]
>>> b
[5, 2, 3]
  • 하지만, 이러한 슬라이싱 또한 얕은 복사에 해당합니다.
  • 리스트안에 리스트 mutable객체 안에 mutable객체인 경우 문제가 됩니다.
  • id(a) 값과 id(b) 값은 다르게 되었지만, 그 내부의 객체 id(a[0])과 id(b[0])같은 주소를 바라보고 있습니다.
>>> a = [[1,2], [3,4]]
>>> b = a[:]
>>> id(a)
4395624328
>>> id(b)
4396179592
>>> id(a[0])
4396116040
>>> id(b[0])
4396116040
  • 재할당하는 경우는 문제가 없습니다. 메모리 주소도 변경되었습니다.
>>> a[0] = [8,9]
>>> a
[[8, 9], [3, 4]]
>>> b
[[1, 2], [3, 4]]
>>> id(a[0])
4393788808
>>> id(b[0])
4396116040
  • 하지만, a[1] 에 값을 변경하면 b[1]도 따라 변경됩니다.
>>> a[1].append(5)
>>> a
[[8, 9], [3, 4, 5]]
>>> b
[[1, 2], [3, 4, 5]]
>>> id(a[1])
4396389896
>>> id(b[1])
4396389896
  • copy 모듈의 copy 메소드 또한 얕은 복사입니다.
>>> import copy
>>> a = [[1,2],[3,4]]
>>> b = copy.copy(a)
>>> a[1].append(5)
>>> a
[[1, 2], [3, 4, 5]]
>>> b
[[1, 2], [3, 4, 5]]

4. 깊은 복사(deep copy)

  • 깊은 복사는 내부에 객체들까지 모두 새롭게 copy 되는 것입니다.
  • copy.deepcopy메소드가 해결해줍니다.
>>> import copy
>>> a = [[1,2],[3,4]]
>>> b = copy.deepcopy(a)
>>> a[1].append(5)
>>> a
[[1, 2], [3, 4, 5]]
>>> b
[[1, 2], [3, 4]]



https://docs.python.org/3/library/itertools.html#itertools.product


itertools — Functions creating iterators for efficient looping


This module implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML. Each has been recast in a form suitable for Python.

The module standardizes a core set of fast, memory efficient tools that are useful by themselves or in combination. Together, they form an “iterator algebra” making it possible to construct specialized tools succinctly and efficiently in pure Python.

For instance, SML provides a tabulation tool: tabulate(f) which produces a sequence f(0), f(1), .... The same effect can be achieved in Python by combining map() and count() to form map(f, count()).

These tools and their built-in counterparts also work well with the high-speed functions in the operator module. For example, the multiplication operator can be mapped across two vectors to form an efficient dot-product: sum(map(operator.mul, vector1, vector2)).

Infinite iterators:

Iterator

Arguments

Results

Example

count()

start, [step]

start, start+step, start+2*step, …

count(10) --> 10 11 12 13 14 ...

cycle()

p

p0, p1, … plast, p0, p1, …

cycle('ABCD') --> A B C D A B C D ...

repeat()

elem [,n]

elem, elem, elem, … endlessly or up to n times

repeat(10, 3) --> 10 10 10

Iterators terminating on the shortest input sequence:

Iterator

Arguments

Results

Example

accumulate()

p [,func]

p0, p0+p1, p0+p1+p2, …

accumulate([1,2,3,4,5]) --> 1 3 6 10 15

chain()

p, q, …

p0, p1, … plast, q0, q1, …

chain('ABC', 'DEF') --> A B C D E F

chain.from_iterable()

iterable

p0, p1, … plast, q0, q1, …

chain.from_iterable(['ABC', 'DEF']) --> A B C D E F

compress()

data, selectors

(d[0] if s[0]), (d[1] if s[1]), …

compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F

dropwhile()

pred, seq

seq[n], seq[n+1], starting when pred fails

dropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1

filterfalse()

pred, seq

elements of seq where pred(elem) is false

filterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8

groupby()

iterable[, key]

sub-iterators grouped by value of key(v)

islice()

seq, [start,] stop [, step]

elements from seq[start:stop:step]

islice('ABCDEFG', 2, None) --> C D E F G

starmap()

func, seq

func(*seq[0]), func(*seq[1]), …

starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000

takewhile()

pred, seq

seq[0], seq[1], until pred fails

takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4

tee()

it, n

it1, it2, … itn splits one iterator into n

zip_longest()

p, q, …

(p[0], q[0]), (p[1], q[1]), …

zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-

Combinatoric iterators:

Iterator

Arguments

Results

product()

p, q, … [repeat=1]

cartesian product, equivalent to a nested for-loop

permutations()

p[, r]

r-length tuples, all possible orderings, no repeated elements

combinations()

p, r

r-length tuples, in sorted order, no repeated elements

combinations_with_replacement()

p, r

r-length tuples, in sorted order, with repeated elements

Examples

Results

product('ABCD', repeat=2)

AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD

permutations('ABCD', 2)

AB AC AD BA BC BD CA CB CD DA DB DC

combinations('ABCD', 2)

AB AC AD BC BD CD

combinations_with_replacement('ABCD',2)

AA AB AC AD BB BC BD CC CD DD

Itertool functions

The following module functions all construct and return iterators. Some provide streams of infinite length, so they should only be accessed by functions or loops that truncate the stream.

itertools.accumulate(iterable[func*initial=None])

Make an iterator that returns accumulated sums, or accumulated results of other binary functions (specified via the optional func argument).

If func is supplied, it should be a function of two arguments. Elements of the input iterable may be any type that can be accepted as arguments to func. (For example, with the default operation of addition, elements may be any addable type including Decimal or Fraction.)

Usually, the number of elements output matches the input iterable. However, if the keyword argument initial is provided, the accumulation leads off with the initial value so that the output has one more element than the input iterable.

Roughly equivalent to:

def accumulate(iterable, func=operator.add, *, initial=None):
    'Return running totals'
    # accumulate([1,2,3,4,5]) --> 1 3 6 10 15
    # accumulate([1,2,3,4,5], initial=100) --> 100 101 103 106 110 115
    # accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120
    it = iter(iterable)
    total = initial
    if initial is None:
        try:
            total = next(it)
        except StopIteration:
            return
    yield total
    for element in it:
        total = func(total, element)
        yield total

There are a number of uses for the func argument. It can be set to min() for a running minimum, max() for a running maximum, or operator.mul() for a running product. Amortization tables can be built by accumulating interest and applying payments. First-order recurrence relations can be modeled by supplying the initial value in the iterable and using only the accumulated total in func argument:

>>>
>>> data = [3, 4, 6, 2, 1, 9, 0, 7, 5, 8]
>>> list(accumulate(data, operator.mul))     # running product
[3, 12, 72, 144, 144, 1296, 0, 0, 0, 0]
>>> list(accumulate(data, max))              # running maximum
[3, 4, 6, 6, 6, 9, 9, 9, 9, 9]

# Amortize a 5% loan of 1000 with 4 annual payments of 90
>>> cashflows = [1000, -90, -90, -90, -90]
>>> list(accumulate(cashflows, lambda bal, pmt: bal*1.05 + pmt))
[1000, 960.0, 918.0, 873.9000000000001, 827.5950000000001]

# Chaotic recurrence relation https://en.wikipedia.org/wiki/Logistic_map
>>> logistic_map = lambda x, _:  r * x * (1 - x)
>>> r = 3.8
>>> x0 = 0.4
>>> inputs = repeat(x0, 36)     # only the initial value is used
>>> [format(x, '.2f') for x in accumulate(inputs, logistic_map)]
['0.40', '0.91', '0.30', '0.81', '0.60', '0.92', '0.29', '0.79', '0.63',
 '0.88', '0.39', '0.90', '0.33', '0.84', '0.52', '0.95', '0.18', '0.57',
 '0.93', '0.25', '0.71', '0.79', '0.63', '0.88', '0.39', '0.91', '0.32',
 '0.83', '0.54', '0.95', '0.20', '0.60', '0.91', '0.30', '0.80', '0.60']

See functools.reduce() for a similar function that returns only the final accumulated value.

New in version 3.2.

Changed in version 3.3: Added the optional func parameter.

Changed in version 3.8: Added the optional initial parameter.

itertools.chain(*iterables)

Make an iterator that returns elements from the first iterable until it is exhausted, then proceeds to the next iterable, until all of the iterables are exhausted. Used for treating consecutive sequences as a single sequence. Roughly equivalent to:

def chain(*iterables):
    # chain('ABC', 'DEF') --> A B C D E F
    for it in iterables:
        for element in it:
            yield element
classmethod chain.from_iterable(iterable)

Alternate constructor for chain(). Gets chained inputs from a single iterable argument that is evaluated lazily. Roughly equivalent to:

def from_iterable(iterables):
    # chain.from_iterable(['ABC', 'DEF']) --> A B C D E F
    for it in iterables:
        for element in it:
            yield element
itertools.combinations(iterabler)

Return r length subsequences of elements from the input iterable.

Combinations are emitted in lexicographic sort order. So, if the input iterable is sorted, the combination tuples will be produced in sorted order.

Elements are treated as unique based on their position, not on their value. So if the input elements are unique, there will be no repeat values in each combination.

Roughly equivalent to:

def combinations(iterable, r):
    # combinations('ABCD', 2) --> AB AC AD BC BD CD
    # combinations(range(4), 3) --> 012 013 023 123
    pool = tuple(iterable)
    n = len(pool)
    if r > n:
        return
    indices = list(range(r))
    yield tuple(pool[i] for i in indices)
    while True:
        for i in reversed(range(r)):
            if indices[i] != i + n - r:
                break
        else:
            return
        indices[i] += 1
        for j in range(i+1, r):
            indices[j] = indices[j-1] + 1
        yield tuple(pool[i] for i in indices)

The code for combinations() can be also expressed as a subsequence of permutations() after filtering entries where the elements are not in sorted order (according to their position in the input pool):

def combinations(iterable, r):
    pool = tuple(iterable)
    n = len(pool)
    for indices in permutations(range(n), r):
        if sorted(indices) == list(indices):
            yield tuple(pool[i] for i in indices)

The number of items returned is n! / r! / (n-r)! when 0 <= r <= n or zero when r > n.

itertools.combinations_with_replacement(iterabler)

Return r length subsequences of elements from the input iterable allowing individual elements to be repeated more than once.

Combinations are emitted in lexicographic sort order. So, if the input iterable is sorted, the combination tuples will be produced in sorted order.

Elements are treated as unique based on their position, not on their value. So if the input elements are unique, the generated combinations will also be unique.

Roughly equivalent to:

def combinations_with_replacement(iterable, r):
    # combinations_with_replacement('ABC', 2) --> AA AB AC BB BC CC
    pool = tuple(iterable)
    n = len(pool)
    if not n and r:
        return
    indices = [0] * r
    yield tuple(pool[i] for i in indices)
    while True:
        for i in reversed(range(r)):
            if indices[i] != n - 1:
                break
        else:
            return
        indices[i:] = [indices[i] + 1] * (r - i)
        yield tuple(pool[i] for i in indices)

The code for combinations_with_replacement() can be also expressed as a subsequence of product() after filtering entries where the elements are not in sorted order (according to their position in the input pool):

def combinations_with_replacement(iterable, r):
    pool = tuple(iterable)
    n = len(pool)
    for indices in product(range(n), repeat=r):
        if sorted(indices) == list(indices):
            yield tuple(pool[i] for i in indices)

The number of items returned is (n+r-1)! / r! / (n-1)! when n > 0.

New in version 3.1.

itertools.compress(dataselectors)

Make an iterator that filters elements from data returning only those that have a corresponding element in selectors that evaluates to True. Stops when either the data or selectors iterables has been exhausted. Roughly equivalent to:

def compress(data, selectors):
    # compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F
    return (d for d, s in zip(data, selectors) if s)

New in version 3.1.

itertools.count(start=0step=1)

Make an iterator that returns evenly spaced values starting with number start. Often used as an argument to map() to generate consecutive data points. Also, used with zip() to add sequence numbers. Roughly equivalent to:

def count(start=0, step=1):
    # count(10) --> 10 11 12 13 14 ...
    # count(2.5, 0.5) -> 2.5 3.0 3.5 ...
    n = start
    while True:
        yield n
        n += step

When counting with floating point numbers, better accuracy can sometimes be achieved by substituting multiplicative code such as: (start + step * i for i in count()).

Changed in version 3.1: Added step argument and allowed non-integer arguments.

itertools.cycle(iterable)

Make an iterator returning elements from the iterable and saving a copy of each. When the iterable is exhausted, return elements from the saved copy. Repeats indefinitely. Roughly equivalent to:

def cycle(iterable):
    # cycle('ABCD') --> A B C D A B C D A B C D ...
    saved = []
    for element in iterable:
        yield element
        saved.append(element)
    while saved:
        for element in saved:
              yield element

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

itertools.dropwhile(predicateiterable)

Make an iterator that drops elements from the iterable as long as the predicate is true; afterwards, returns every element. Note, the iterator does not produce any output until the predicate first becomes false, so it may have a lengthy start-up time. Roughly equivalent to:

def dropwhile(predicate, iterable):
    # dropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1
    iterable = iter(iterable)
    for x in iterable:
        if not predicate(x):
            yield x
            break
    for x in iterable:
        yield x
itertools.filterfalse(predicateiterable)

Make an iterator that filters elements from iterable returning only those for which the predicate is False. If predicate is None, return the items that are false. Roughly equivalent to:

def filterfalse(predicate, iterable):
    # filterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
    if predicate is None:
        predicate = bool
    for x in iterable:
        if not predicate(x):
            yield x
itertools.groupby(iterablekey=None)

Make an iterator that returns consecutive keys and groups from the iterable. The key is a function computing a key value for each element. If not specified or is Nonekey defaults to an identity function and returns the element unchanged. Generally, the iterable needs to already be sorted on the same key function.

The operation of groupby() is similar to the uniq filter in Unix. It generates a break or new group every time the value of the key function changes (which is why it is usually necessary to have sorted the data using the same key function). That behavior differs from SQL’s GROUP BY which aggregates common elements regardless of their input order.

The returned group is itself an iterator that shares the underlying iterable with groupby(). Because the source is shared, when the groupby() object is advanced, the previous group is no longer visible. So, if that data is needed later, it should be stored as a list:

groups = []
uniquekeys = []
data = sorted(data, key=keyfunc)
for k, g in groupby(data, keyfunc):
    groups.append(list(g))      # Store group iterator as a list
    uniquekeys.append(k)

groupby() is roughly equivalent to:

class groupby:
    # [k for k, g in groupby('AAAABBBCCDAABBB')] --> A B C D A B
    # [list(g) for k, g in groupby('AAAABBBCCD')] --> AAAA BBB CC D
    def __init__(self, iterable, key=None):
        if key is None:
            key = lambda x: x
        self.keyfunc = key
        self.it = iter(iterable)
        self.tgtkey = self.currkey = self.currvalue = object()
    def __iter__(self):
        return self
    def __next__(self):
        self.id = object()
        while self.currkey == self.tgtkey:
            self.currvalue = next(self.it)    # Exit on StopIteration
            self.currkey = self.keyfunc(self.currvalue)
        self.tgtkey = self.currkey
        return (self.currkey, self._grouper(self.tgtkey, self.id))
    def _grouper(self, tgtkey, id):
        while self.id is id and self.currkey == tgtkey:
            yield self.currvalue
            try:
                self.currvalue = next(self.it)
            except StopIteration:
                return
            self.currkey = self.keyfunc(self.currvalue)
itertools.islice(iterablestop)
itertools.islice(iterablestartstop[step])

Make an iterator that returns selected elements from the iterable. If start is non-zero, then elements from the iterable are skipped until start is reached. Afterward, elements are returned consecutively unless step is set higher than one which results in items being skipped. If stop is None, then iteration continues until the iterator is exhausted, if at all; otherwise, it stops at the specified position. Unlike regular slicing, islice() does not support negative values for startstop, or step. Can be used to extract related fields from data where the internal structure has been flattened (for example, a multi-line report may list a name field on every third line). Roughly equivalent to:

def islice(iterable, *args):
    # islice('ABCDEFG', 2) --> A B
    # islice('ABCDEFG', 2, 4) --> C D
    # islice('ABCDEFG', 2, None) --> C D E F G
    # islice('ABCDEFG', 0, None, 2) --> A C E G
    s = slice(*args)
    start, stop, step = s.start or 0, s.stop or sys.maxsize, s.step or 1
    it = iter(range(start, stop, step))
    try:
        nexti = next(it)
    except StopIteration:
        # Consume *iterable* up to the *start* position.
        for i, element in zip(range(start), iterable):
            pass
        return
    try:
        for i, element in enumerate(iterable):
            if i == nexti:
                yield element
                nexti = next(it)
    except StopIteration:
        # Consume to *stop*.
        for i, element in zip(range(i + 1, stop), iterable):
            pass

If start is None, then iteration starts at zero. If step is None, then the step defaults to one.

itertools.permutations(iterabler=None)

Return successive r length permutations of elements in the iterable.

If r is not specified or is None, then r defaults to the length of the iterable and all possible full-length permutations are generated.

Permutations are emitted in lexicographic sort order. So, if the input iterable is sorted, the permutation tuples will be produced in sorted order.

Elements are treated as unique based on their position, not on their value. So if the input elements are unique, there will be no repeat values in each permutation.

Roughly equivalent to:

def permutations(iterable, r=None):
    # permutations('ABCD', 2) --> AB AC AD BA BC BD CA CB CD DA DB DC
    # permutations(range(3)) --> 012 021 102 120 201 210
    pool = tuple(iterable)
    n = len(pool)
    r = n if r is None else r
    if r > n:
        return
    indices = list(range(n))
    cycles = list(range(n, n-r, -1))
    yield tuple(pool[i] for i in indices[:r])
    while n:
        for i in reversed(range(r)):
            cycles[i] -= 1
            if cycles[i] == 0:
                indices[i:] = indices[i+1:] + indices[i:i+1]
                cycles[i] = n - i
            else:
                j = cycles[i]
                indices[i], indices[-j] = indices[-j], indices[i]
                yield tuple(pool[i] for i in indices[:r])
                break
        else:
            return

The code for permutations() can be also expressed as a subsequence of product(), filtered to exclude entries with repeated elements (those from the same position in the input pool):

def permutations(iterable, r=None):
    pool = tuple(iterable)
    n = len(pool)
    r = n if r is None else r
    for indices in product(range(n), repeat=r):
        if len(set(indices)) == r:
            yield tuple(pool[i] for i in indices)

The number of items returned is n! / (n-r)! when 0 <= r <= n or zero when r > n.

itertools.product(*iterablesrepeat=1)

Cartesian product of input iterables.

Roughly equivalent to nested for-loops in a generator expression. For example, product(A, B) returns the same as ((x,y) for x in A for y in B).

The nested loops cycle like an odometer with the rightmost element advancing on every iteration. This pattern creates a lexicographic ordering so that if the input’s iterables are sorted, the product tuples are emitted in sorted order.

To compute the product of an iterable with itself, specify the number of repetitions with the optional repeat keyword argument. For example, product(A, repeat=4) means the same as product(A, A, A, A).

This function is roughly equivalent to the following code, except that the actual implementation does not build up intermediate results in memory:

def product(*args, repeat=1):
    # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
    # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
    pools = [tuple(pool) for pool in args] * repeat
    result = [[]]
    for pool in pools:
        result = [x+[y] for x in result for y in pool]
    for prod in result:
        yield tuple(prod)
itertools.repeat(object[times])

Make an iterator that returns object over and over again. Runs indefinitely unless the times argument is specified. Used as argument to map() for invariant parameters to the called function. Also used with zip() to create an invariant part of a tuple record.

Roughly equivalent to:

def repeat(object, times=None):
    # repeat(10, 3) --> 10 10 10
    if times is None:
        while True:
            yield object
    else:
        for i in range(times):
            yield object

A common use for repeat is to supply a stream of constant values to map or zip:

>>>
>>> list(map(pow, range(10), repeat(2)))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
itertools.starmap(functioniterable)

Make an iterator that computes the function using arguments obtained from the iterable. Used instead of map() when argument parameters are already grouped in tuples from a single iterable (the data has been “pre-zipped”). The difference between map() and starmap() parallels the distinction between function(a,b) and function(*c). Roughly equivalent to:

def starmap(function, iterable):
    # starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000
    for args in iterable:
        yield function(*args)
itertools.takewhile(predicateiterable)

Make an iterator that returns elements from the iterable as long as the predicate is true. Roughly equivalent to:

def takewhile(predicate, iterable):
    # takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4
    for x in iterable:
        if predicate(x):
            yield x
        else:
            break
itertools.tee(iterablen=2)

Return n independent iterators from a single iterable.

The following Python code helps explain what tee does (although the actual implementation is more complex and uses only a single underlying FIFO queue).

Roughly equivalent to:

def tee(iterable, n=2):
    it = iter(iterable)
    deques = [collections.deque() for i in range(n)]
    def gen(mydeque):
        while True:
            if not mydeque:             # when the local deque is empty
                try:
                    newval = next(it)   # fetch a new value and
                except StopIteration:
                    return
                for d in deques:        # load it to all the deques
                    d.append(newval)
            yield mydeque.popleft()
    return tuple(gen(d) for d in deques)

Once tee() has made a split, the original iterable should not be used anywhere else; otherwise, the iterable could get advanced without the tee objects being informed.

tee iterators are not threadsafe. A RuntimeError may be raised when using simultaneously iterators returned by the same tee() call, even if the original iterable is threadsafe.

This itertool may require significant auxiliary storage (depending on how much temporary data needs to be stored). In general, if one iterator uses most or all of the data before another iterator starts, it is faster to use list() instead of tee().

itertools.zip_longest(*iterablesfillvalue=None)

Make an iterator that aggregates elements from each of the iterables. If the iterables are of uneven length, missing values are filled-in with fillvalue. Iteration continues until the longest iterable is exhausted. Roughly equivalent to:

def zip_longest(*args, fillvalue=None):
    # zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-
    iterators = [iter(it) for it in args]
    num_active = len(iterators)
    if not num_active:
        return
    while True:
        values = []
        for i, it in enumerate(iterators):
            try:
                value = next(it)
            except StopIteration:
                num_active -= 1
                if not num_active:
                    return
                iterators[i] = repeat(fillvalue)
                value = fillvalue
            values.append(value)
        yield tuple(values)

If one of the iterables is potentially infinite, then the zip_longest() function should be wrapped with something that limits the number of calls (for example islice() or takewhile()). If not specified, fillvalue defaults to None.

Itertools Recipes

This section shows recipes for creating an extended toolset using the existing itertools as building blocks.

Substantially all of these recipes and many, many others can be installed from the more-itertools project found on the Python Package Index:

pip install more-itertools

The extended tools offer the same high performance as the underlying toolset. The superior memory performance is kept by processing elements one at a time rather than bringing the whole iterable into memory all at once. Code volume is kept small by linking the tools together in a functional style which helps eliminate temporary variables. High speed is retained by preferring “vectorized” building blocks over the use of for-loops and generators which incur interpreter overhead.

def take(n, iterable):
    "Return first n items of the iterable as a list"
    return list(islice(iterable, n))

def prepend(value, iterator):
    "Prepend a single value in front of an iterator"
    # prepend(1, [2, 3, 4]) -> 1 2 3 4
    return chain([value], iterator)

def tabulate(function, start=0):
    "Return function(0), function(1), ..."
    return map(function, count(start))

def tail(n, iterable):
    "Return an iterator over the last n items"
    # tail(3, 'ABCDEFG') --> E F G
    return iter(collections.deque(iterable, maxlen=n))

def consume(iterator, n=None):
    "Advance the iterator n-steps ahead. If n is None, consume entirely."
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        collections.deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)

def nth(iterable, n, default=None):
    "Returns the nth item or a default value"
    return next(islice(iterable, n, None), default)

def all_equal(iterable):
    "Returns True if all the elements are equal to each other"
    g = groupby(iterable)
    return next(g, True) and not next(g, False)

def quantify(iterable, pred=bool):
    "Count how many times the predicate is true"
    return sum(map(pred, iterable))

def padnone(iterable):
    """Returns the sequence elements and then returns None indefinitely.

    Useful for emulating the behavior of the built-in map() function.
    """
    return chain(iterable, repeat(None))

def ncycles(iterable, n):
    "Returns the sequence elements n times"
    return chain.from_iterable(repeat(tuple(iterable), n))

def dotproduct(vec1, vec2):
    return sum(map(operator.mul, vec1, vec2))

def flatten(listOfLists):
    "Flatten one level of nesting"
    return chain.from_iterable(listOfLists)

def repeatfunc(func, times=None, *args):
    """Repeat calls to func with specified arguments.

    Example:  repeatfunc(random.random)
    """
    if times is None:
        return starmap(func, repeat(args))
    return starmap(func, repeat(args, times))

def pairwise(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
    a, b = tee(iterable)
    next(b, None)
    return zip(a, b)

def grouper(iterable, n, fillvalue=None):
    "Collect data into fixed-length chunks or blocks"
    # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    # Recipe credited to George Sakkis
    num_active = len(iterables)
    nexts = cycle(iter(it).__next__ for it in iterables)
    while num_active:
        try:
            for next in nexts:
                yield next()
        except StopIteration:
            # Remove the iterator we just exhausted from the cycle.
            num_active -= 1
            nexts = cycle(islice(nexts, num_active))

def partition(pred, iterable):
    'Use a predicate to partition entries into false entries and true entries'
    # partition(is_odd, range(10)) --> 0 2 4 6 8   and  1 3 5 7 9
    t1, t2 = tee(iterable)
    return filterfalse(pred, t1), filter(pred, t2)

def powerset(iterable):
    "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

def unique_everseen(iterable, key=None):
    "List unique elements, preserving order. Remember all elements ever seen."
    # unique_everseen('AAAABBBCCDAABBB') --> A B C D
    # unique_everseen('ABBCcAD', str.lower) --> A B C D
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in filterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element

def unique_justseen(iterable, key=None):
    "List unique elements, preserving order. Remember only the element just seen."
    # unique_justseen('AAAABBBCCDAABBB') --> A B C D A B
    # unique_justseen('ABBCcAD', str.lower) --> A B C A D
    return map(next, map(operator.itemgetter(1), groupby(iterable, key)))

def iter_except(func, exception, first=None):
    """ Call a function repeatedly until an exception is raised.

    Converts a call-until-exception interface to an iterator interface.
    Like builtins.iter(func, sentinel) but uses an exception instead
    of a sentinel to end the loop.

    Examples:
        iter_except(functools.partial(heappop, h), IndexError)   # priority queue iterator
        iter_except(d.popitem, KeyError)                         # non-blocking dict iterator
        iter_except(d.popleft, IndexError)                       # non-blocking deque iterator
        iter_except(q.get_nowait, Queue.Empty)                   # loop over a producer Queue
        iter_except(s.pop, KeyError)                             # non-blocking set iterator

    """
    try:
        if first is not None:
            yield first()            # For database APIs needing an initial cast to db.first()
        while True:
            yield func()
    except exception:
        pass

def first_true(iterable, default=False, pred=None):
    """Returns the first true value in the iterable.

    If no true value is found, returns *default*

    If *pred* is not None, returns the first item
    for which pred(item) is true.

    """
    # first_true([a,b,c], x) --> a or b or c or x
    # first_true([a,b], x, f) --> a if f(a) else b if f(b) else x
    return next(filter(pred, iterable), default)

def random_product(*args, repeat=1):
    "Random selection from itertools.product(*args, **kwds)"
    pools = [tuple(pool) for pool in args] * repeat
    return tuple(random.choice(pool) for pool in pools)

def random_permutation(iterable, r=None):
    "Random selection from itertools.permutations(iterable, r)"
    pool = tuple(iterable)
    r = len(pool) if r is None else r
    return tuple(random.sample(pool, r))

def random_combination(iterable, r):
    "Random selection from itertools.combinations(iterable, r)"
    pool = tuple(iterable)
    n = len(pool)
    indices = sorted(random.sample(range(n), r))
    return tuple(pool[i] for i in indices)

def random_combination_with_replacement(iterable, r):
    "Random selection from itertools.combinations_with_replacement(iterable, r)"
    pool = tuple(iterable)
    n = len(pool)
    indices = sorted(random.randrange(n) for i in range(r))
    return tuple(pool[i] for i in indices)

def nth_combination(iterable, r, index):
    'Equivalent to list(combinations(iterable, r))[index]'
    pool = tuple(iterable)
    n = len(pool)
    if r < 0 or r > n:
        raise ValueError
    c = 1
    k = min(r, n-r)
    for i in range(1, k+1):
        c = c * (n - k + i) // i
    if index < 0:
        index += c
    if index < 0 or index >= c:
        raise IndexError
    result = []
    while r:
        c, n, r = c*r//n, n-1, r-1
        while index >= c:
            index -= c
            c, n = c*(n-r)//n, n-1
        result.append(pool[-1-n])
    return tuple(result)


https://wikidocs.net/22801



packing, unpacking

print함수는 출력하고자하는 객체가 몇개던지, 즉 몇개의 인자를 받던지 상관하지 않고 출력해줍니다.

print("가나다 abc 123")
print("가나다", "abc 123")
print("가나다", "abc", "123")

출력결과

가나다 abc 123
가나다 abc 123
가나다 abc 123

이런 경우처럼 함수가 받을 인자의 갯수를 유연하게 지정할 수 있다면 보다 유연하게 코드를 작성할 수 있습니다.

파이썬은 이런 경우를 지원하기 위해 packing을 지원합니다.

패킹은 인자로 받은 여러개의 값을 하나의 객체로 합쳐서 받을 수 있도록 합니다.

위치인자 패킹은 *한개를 매개변수 앞에 붙임으로 사용합니다.

def func(*args):
      print(args)
      print(type(args))

이런식으로 매개변수 이름 앞에 *을 붙여준다면, 위치인자로 보낸 모든 객체들을 하나의 객체로 관리해줍니다.

func(1, 2, 3, 4, 5, 6, 'a', 'b')

결과

(1, 2, 3, 4, 5, 6, 'a', 'b')
<class 'tuple'>

이러한 packing을 통해 받은 모든 숫자들의 합을 구하는 연산도 구할 수 있습니다. 몇개든 상관없이 가능합니다.

def sum_all(*numbers):
      result = 0
      for number in numbers:
           result += number
      return result

print(sum_all(1, 2, 3)) # 6
print(sum_all(1, 2, 3, 4, 5, 6)) # 21

packing을 이용해서 반드시 받아야하는 매개변수와 여러개를 받을수있는 매개변수를 구분해서 작성할 수 있습니다.

def print_family_name(father, mother, *sibling):
      print("아버지 :", father)
      print("어머니 :", mother)
      if sibling:
           print("호적 메이트..")
           for name in sibling:
                 print(name)

print_family_name("홍길동", '심사임당', '김태희', '윤아')

결과값

아버지 : 홍길동
어머니 : 심사임당
호적 메이트..
김태희
윤아

위치인자가 패킹하는 매개변수를 만나면 그 이후에 위치인자가 몇개이던지, tuple로 하나의 객체가되어서 관리됩니다.

동일하게 키워드 인자에 패킹은 **을 통해 작성할 수 있습니다.

def kwpacking(**kwargs):
     print(kwargs)
     print(type(kwargs)

kwpacking(a=1, b=2, c=3)

결과값

{'a': 1, 'b': 2, 'c': 3}
<class 'dict'>

키워드 인자는 패킹한 인자들을 키워드와 인자 쌍으로 이뤄진 딕셔너리로 관리합니다.

이를 이용해서 print_family_name함수에 호적메이트들에 사회적명칭도 출력하도록 변경할 수 있습니다.

def print_family_name(father, mother, **sibling):
      print("아버지 :", father)
      print("어머니 :", mother)
      if sibling:
           print("호적 메이트..")
           for title, name in sibling.items():
                 print('{} : {}'.format(title, name))

print_family_name("홍길동", '심사임당', 누나='김태희', 여동생='윤아')

결과값

아버지 : 홍길동
어머니 : 심사임당
호적 메이트..
누나 : 김태희
여동생 : 윤아

위치 인자와 키워드 인자 packing을 동시에 사용할 수 있습니다.

def print_family_name(*parents, **sibling):
      print("아버지 :", parents[0])
      print("어머니 :", parents[1])
      if sibling:
           print("호적 메이트..")
           for title, name in sibling.items():
                 print('{} : {}'.format(title, name))

print_family_name("홍길동", '심사임당', 누나='김태희', 여동생='윤아')

unpacking

packing과 반대되는 개념인 unpacking이라는 개념이 있습니다.

packing은 여러개의 객체를 하나의 객체로 합쳐주었습니다. unpacking은 여러개의 객체를 포함하고 있는 하나의 객체를 풀어줍니다.

함수에서 unpacking을 할때는, 매개변수에서 *을 붙이는게 아니라 인자 앞에 *을 붙여서 사용합니다.

동일하게 위치인자를 unpacking 하는 경우는 *를, 키워드인자를 unpacking하는 경우 **를 사용합니다.

def sum(a, b, c):
    return a + b + c

numbers = [1, 2, 3]
sum(numbers) # error

print(sum(*numbers)) # 출력 : 6

[1, 2, 3]을 인자로 보낼때, *을 붙이면 unpacking이 발생합니다.

unpacking은 아래와 같은 순서로 변경되어 실행됩니다.

1. sum(*numbers)
2. sum(*[1, 2, 3])
3. sum(1, 2, 3)

unpacking은 함수를 호출할때 인자를 해체하는 개념이기 때문에, 해채된 결과가 함수의 매개변수에 갯수와 다르다면 에러가 발생합니다.

sum(*[1, 2, 3, 4])

실행결과

TypeError: sum() takes 3 positional arguments but 4 were given

위치인자를 unpacking할때는 위에 예에서는 list타입이였지만, Container객체라면 다 가능합니다.

sum(*'abc') # 'abc'
sum(*(4, 5, 6)) # 15
sum(*{'가', '나', '다'}) # '나다가'
sum(*{'치킨': 3, '피자': 12, '음료수': 10}) # '치킨피자음료수'

set타입과 dict타입은 순서정보를 가지고 있지 않기 때문에 결과가 다를 수 있습니다.

동일한 방식으로 키워드인자로 unpacking할 수 있습니다. unpacking하기 위해선 인자가 key와 인자로 구성되어 있는 mapping타입, 즉 dict가 필요합니다.

def cal(first, op, second):
    if op == '+':
        return first + second
    if op == '/':
        return first / second
    if op == '-':
        return first - second
    if op == '*':
        return first * second

prob = {
  'first': 12,
  'second': 34,
  'op': '*'
}

cal(**prob) # 결과 : 408

위 예제에 키워드 인자의 unpacking은 다음과 같이 작동합니다.

1. cal(**prob)
2. cal(prob = {
  'first': 12,
  'second': 34,
  'op': '*'
})
3. cal(first=12, second=34, op='*')

위치인자의 unpacking처럼 unpacking되는 인자는 매개변수의 키워드 매개변수와 일치해야합니다.

만약 비어있는 인자를 unpacking를 하면 무시합니다. 이러한 특성이 있기 때문에 함수의 packing과 unpacking을 이용하여, 다음과 같이 어떠한 함수에도 반응하는 함수를 작성할 수 있습니다.

함수와 함수에 인자들을 받아서 시작전 알림과 함께 함수를 실행시켜주는 함수입니다.

def start(func, *args, **kwargs):
    print("함수를 시작합니다.")
    return func(*args, **kwargs)

실행할 함수를 인자로 받고, 그 함수를 실행할때 넣을 위치인자와 키워드인자를 갯수와 상관없이 넣어줍니다.

start(print, '안녕하세요', '파이썬 꿀잼!', sep='~~ ')

실행결과

함수를 시작합니다.
안녕하세요~~ 파이썬 꿀잼!
def sum_a_b(a, b):
     return a + b

result = start(sum_a_b, 1, 2) # 함수를 시작합니다.
print(result) # 3


https://wikidocs.net/23109



네임스페이스의 종류


파이썬의 네임스페이스는 3가지로 분류할 수 있다:

  • 전역 네임스페이스: 모듈별로 존재하며, 모듈 전체에서 통용될 수 있는 이름들이 소속된다.
  • 지역 네임스페이스: 함수 및 메서드 별로 존재하며, 함수 내의 지역 변수들의 이름들이 소속된다.
  • 빌트인 네임스페이스: 기본 내장 함수 및 기본 예외들의 이름들이 소속된다. 파이썬으로 작성된 모든 코드 범위가 포함된다.

(출처: https://www.programiz.com/python-programming/namespace)

파이썬의 네임스페이스는 다음과 같은 특징들을 가지고 있다.

  • 네임스페이스는 딕셔너리 형태로 구현된다.
  • 모든 이름 자체는 문자열로 되어있고 각각은 해당 네임스페이스의 범위에서 실제 객체를 가리킨다.
  • 이름과 실제 객체 사이의 매핑은 가변적(Mutable)이므로 런타임동안 새로운 이름이 추가될 수 있다.
  • 다만, 빌트인 네임스페이스는 함부로 추가하거나 삭제할 수 없다.

(출처: https://blog.confirm.ch/python-namespaces/)

위의 그림은 파이썬의 네임스페이스가 어떤 것인지 잘 표현해준다. 파이썬 공식 홈페이지의 튜토리얼에서는 네임스페이스를 이름들과 실제 객체들 사이의 매핑이라고 정의한다.




네임스페이스

변수에 객체를 할당하면, 변수와 변수에 연결된 객체는 딕셔너리형태(key, value)로 연결되어 네임스페이스 안에 저장됩니다.

a = 3
print(globals())

실행결과

{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x10af19400>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'test3.py', '__cached__': None, 'a': 3}

전역(globals) 네임스페이스를 확인하는 globals함수를 실행하면 함수의 네임스페이스를 볼 수 있습니다. 정의한적 없는 변수들도 여럿 보입니다.
a라는 이름에 객체 3이 지정되있는걸 확인할 수 있습니다.

함수는 전역과 별개의 네임스페이스를 가집니다.

a = 3

def func():
    a = 100
    b = 'python'

func()
print(globals())

함수안에서 변수 a의 값을 바꾸고, 새로운 변수 b를 생성했지만, globals의 네임스페이스에는 바뀐 값과, 새로 할당한 변수이름이 보이지 않습니다.

함수는 함수만에 별도 구분된 네임스페이스 공간을 가집니다.

a = 3
c = 'Hello'

def func():
    a = 100
    b = 'python'
    print('함수 내부 namespace', locals())

func()

locals() 함수는 실행하는 공간에 namespace를 출력해줍니다. 함수안에서 locals함수를 실행했기 때문에, 함수안에 namespace를 출력해줍니다.

만약 함수 밖에서, 즉 전역에서 locals함수를 실행한다면 globals함수를 실행한 결과와 동일한 값을 출력합니다.

현재 코드기 실행되는 공간의 namespace를 리턴하는 locals함수를 실행시켜서 확인합니다.

실행결과

함수 내부 namespace {'b': 'python', 'a': 100}

이처럼 함수는 별도의 namespace를 가집니다. 만약 함수안에 함수를 또 생성한다면 어떻게 될까요?


a = 3
c = 'Hello'

def func():
    a = 100
    b = 'python'
    print('외부 함수 내부 namespace', locals())
    def inner_func():
        print('내부 함수 내부 namespace', locals())
    inner_func()

func()

실행결과

외부 함수 내부 namespace {'b': 'python', 'a': 100}
내부 함수 내부 namespace {}

함수는 다른 함수안에 있을지라도, 별도의 namespace를 가지는걸 확인할 수 있습니다.

네임스페이스 참조

네임스페이스가 다르다면, 값을 참조할 수 없습니다.

def func():
      a = 3

print(a)

실행결과

NameError: name 'a' is not defined

1

하지만 함수는 globals의 변수를 참조할 수 있습니다.

a = 100

def func():
      def inner():
            b = 'python'
      inner()
      print(a)
      print(b)

func()

실행결과

100
NameError: name 'b' is not defined

2

함수는 globals 네임스페이스뿐 아니라 상위 함수의 변수도 참조할 수 있습니다.

a = 'fastcampus'

def func1():
      b = 'python'
      def func2():
             print(a)
             print(b)
      func2()

func1()

실행결과

fastcampus
python

3

네임스페이스가 분리된 이유

함수별로 네임스페이스가 분리되어 있지 않고 전역과 함수내부에 네임스페이스가 하나로 관리된다면, 함수를 만드는데 복잡한 고려사항이 생기게됩니다.

네임스페이스가 변경되지 않으면, 함수내부에서 외부에 있는 변수가 참조하고 있는 객체를 변경할 수 있습니다.

a = 'python'
def func():
      a = 3

func()

print(a)

네임스페이스가 하나여서 함수내부에서 외부에 (전역 또는 외부함수)에 변수를 수정할 수 있게되면, 위 코드의 출력결과가 3이 됩니다.

네임스페이스가 하나라면 프로그래머는 복잡한 고려사항을 항상 생각하고 함수를 작성해야합니다. 바로 변수이름을 겹치지 않게 지어야 한다는 점입니다.

이러한 복잡한 고려사항을 생각해야하는 상황이라고 가정하고 다음의 코드를 실행시켜보겠습니다.

한변의 길이가 리스트로 주어져있을때, 그 변을 한변으로 하는 정사각형의 넓이를 구해서, 가장 큰 사각형의 넓이를 출력하는 코드입니다.

반복되는 연산인 정사각형의 넓이는 구하는 연산은 함수로 지정했습니다.

def square(n):
    result = n ** 2
    return result


lines = [4, 3, 9, 8]
result = 0
for line in lines:
    area = square(line)
    if area > result:
        result = area

print(result)

이러한 코드에서 square함수가 실행될때 마다 함수안에서 전역에 result변수의 값을 계속 바꾸고 있기 때문에 즉 함수내부에서 전역에 있는 result변수의 참조를 변경시킨다면 의도한 결과가 나오지 않게 됩니다.

모든 함수의 변수가 하나의 네임스페이스에서 관리된다면, 모든 함수를 만들때 변수를 겹치지 않게 만들어야하는 노력을 해야합니다.

파이썬에서 함수는, 함수의 로직에만 신경쓸수 있도록, 함수내부에서 외부에 변수를 참조는 가능하지만, 새로운 변수를 만든다면 변수를 별도의 공간에 저장하도록 네임스페이스를 분리하였습니다.

외부 네임스페이스 변수 값 변경

함수별로 변수를 저장하는 네임스페이스는 분리되어 있습니다. 하지만, 함수에서 외부에 있는 네임스페이스에 값은 참조가능합니다.

만약 함수내부에서 외부에 변수를 변경하고 싶다면 어떻게 해야할까요?

함수에서 외부에 변수이름과 동일한 이름의 새로운 변수를 만들 수 있습니다. 이때 외부에 변수는 영향을 받지 않습니다.

lang = 'jython'
def func():
      lang = 'python'
      print(lang)

func()
print(lang)

실행결과

python
jython

새로운 변수를 함수 local 네임스페이스에 생성하는게 아니라, 외부에 있는 변수를 변경하고 싶다면 global또는 nonlocal키워드를 사용해야 합니다.

lang = 'jython'
def func():
      global lang 
      lang = 'python'
      print(lang)

func()
print(lang)

실행결과

python
python

함수 내부에 선언한 global lang 부분은, 이름 lang을 global에 선언한 lang으로만 사용하겠다는 뜻입니다.

이후에 lang변수의 값을 변경하면, global에 있는 변수가 참조하고 있는 객체가 바뀌게 됩니다.

외부함수에 있는 값을 변경할때는 nonlocal키워드를 사용하면 됩니다.

def func():
      var = 1
      def func2():
            nonlocal var
            var = 3
      func2()
      print(var)

func()

실행결과

3


+ Recent posts