2장: 루프 및 함수

장 구성


학습 목표


  • Python에서 ‘for’ 및 ‘while’ 루프를 작성합니다.
  • ‘for’ 루프에 사용할 수 있는 반복 가능한 데이터 유형을 식별합니다.
  • 컴프리헨션을 이용하여 목록, 사전, 집합을 만듭니다.
  • try/제외 문을 작성하세요.
  • Python에서 함수와 익명 함수를 정의합니다.
  • 위치 인수와 키워드 인수의 차이점을 설명합니다.
  • 로컬 인수와 전역 인수의 차이점을 설명하세요.
  • ’DRY 원리’를 적용하여 모듈식 코드를 작성합니다.
  • 기능에 부작용이 있는지 평가합니다.
  • 매개변수, 반환 값, 동작 및 사용법을 설명하는 함수에 대한 독스트링을 작성합니다.

1. for 루프


For 루프를 사용하면 특정 횟수만큼 코드를 실행할 수 있습니다.

for n in [2, 7, -1, 5]:
    print(f"The number is {n} and its square is {n**2}")
print("I'm outside the loop!")
The number is 2 and its square is 4
The number is 7 and its square is 49
The number is -1 and its square is 1
The number is 5 and its square is 25
I'm outside the loop!

주목해야 할 주요 사항:

  • for 키워드는 루프를 시작합니다. 콜론:은 루프의 첫 번째 줄을 끝냅니다.
  • 들여쓰기된 코드 블록은 목록의 각 값에 대해 실행됩니다(따라서 “for” 루프라는 이름이 붙음).
  • 변수 n이 목록의 모든 값을 가져온 후에 루프가 종료됩니다.
  • list, tuple, range, set, string 등 모든 종류의 “반복 가능”을 반복할 수 있습니다.
  • iterable은 실제로 반복될 수 있는 일련의 값을 가진 모든 객체입니다. 이 경우 목록의 값을 반복합니다.
word = "Python"
for letter in word:
    print("Gimme a " + letter + "!")

print(f"What's that spell?!! {word}!")
Gimme a P!
Gimme a y!
Gimme a t!
Gimme a h!
Gimme a o!
Gimme a n!
What's that spell?!! Python!

매우 일반적인 패턴은 range()와 함께 for를 사용하는 것입니다. range()는 특정 값(최종 값을 포함하지 않음)까지의 정수 시퀀스를 제공하며 일반적으로 루핑에 사용됩니다.

range(10)
range(0, 10)
list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for i in range(10):
    print(i)
0
1
2
3
4
5
6
7
8
9

’range’를 사용하여 시작 값과 건너뛰기 값을 지정할 수도 있습니다.

for i in range(1, 101, 10):
    print(i)
1
11
21
31
41
51
61
71
81
91

여러 차원의 데이터를 반복하기 위해 다른 루프 안에 루프를 작성할 수 있습니다.

for x in [1, 2, 3]:
    for y in ["a", "b", "c"]:
        print((x, y))
(1, 'a')
(1, 'b')
(1, 'c')
(2, 'a')
(2, 'b')
(2, 'c')
(3, 'a')
(3, 'b')
(3, 'c')
list_1 = [0, 1, 2]
list_2 = ["a", "b", "c"]
for i in range(3):
    print(list_1[i], list_2[i])
0 a
1 b
2 c

Python에는 이러한 종류의 작업을 수행하는 영리한 방법이 많이 있습니다. 객체를 반복할 때 나는 작업에서 zip()enumerate()를 꽤 많이 사용하는 경향이 있습니다. zip()은 튜플의 반복 가능한 zip 객체를 반환합니다.

for i in zip(list_1, list_2):
    print(i)
(0, 'a')
(1, 'b')
(2, 'c')

for 루프에서 이러한 튜플을 직접 “압축해제”할 수도 있습니다.

for i, j in zip(list_1, list_2):
    print(i, j)
0 a
1 b
2 c

enumerate()는 루프 내에서 사용할 수 있는 반복 가능 항목에 카운터를 추가합니다.

for i in enumerate(list_2):
    print(i)
(0, 'a')
(1, 'b')
(2, 'c')
for n, i in enumerate(list_2):
    print(f"index {n}, value {i}")
index 0, value a
index 1, value b
index 2, value c

.items()를 사용하여 사전의 키-값 쌍을 반복할 수 있습니다. 일반적인 구문은 for key, value in Dictionary.items()입니다.

courses = {521 : "awesome",
           551 : "riveting",
           511 : "naptime!"}

for course_num, description in courses.items():
    print(f"DSCI {course_num}, is {description}")
DSCI 521, is awesome
DSCI 551, is riveting
DSCI 511, is naptime!

더 복잡한 언패킹을 수행하기 위해 enumerate()를 사용할 수도 있습니다:

for n, (course_num, description) in enumerate(courses.items()):
    print(f"Item {n}: DSCI {course_num}, is {description}")
Item 0: DSCI 521, is awesome
Item 1: DSCI 551, is riveting
Item 2: DSCI 511, is naptime!

2. while 루프


또한 while 루프를 사용하여 코드 블록을 여러 번 실행할 수도 있습니다. 하지만 조심하세요! 조건식이 항상 ’True’이면 무한 루프가 발생하는 것입니다!

n = 10
while n > 0:
    print(n)
    n -= 1

print("Blast off!")
10
9
8
7
6
5
4
3
2
1
Blast off!

위의 while 문을 영어인 것처럼 읽어보겠습니다. 이는 “n이 0보다 큰 동안 n의 값을 표시한 다음 n을 1씩 감소시킵니다. 0에 도달하면 Blast off라는 단어를 표시합니다.”라는 의미입니다.

일부 루프의 경우 언제 중지할지 또는 중지할지 알기가 어렵습니다! 콜라츠 추측을 살펴보세요. 추측에 따르면 우리가 어떤 양의 정수 ’n’으로 시작하더라도 시퀀스는 결국 항상 1에 도달할 것입니다. 우리는 얼마나 많은 반복이 필요할지 알 수 없습니다.

n = 11
while n != 1:
    print(int(n))
    if n % 2 == 0: # n is even
        n = n / 2
    else: # n is odd
        n = n * 3 + 1
print(int(n))
11
34
17
52
26
13
40
20
10
5
16
8
4
2
1

따라서 어떤 경우에는 ‘break’ 키워드를 사용하여 일부 기준에 따라 ‘while’ 루프를 강제로 중지할 수 있습니다.

n = 123
i = 0
while n != 1:
    print(int(n))
    if n % 2 == 0: # n is even
        n = n / 2
    else: # n is odd
        n = n * 3 + 1
    i += 1
    if i == 10:
        print(f"Ugh, too many iterations!")
        break
123
370
185
556
278
139
418
209
628
314
Ugh, too many iterations!

‘continue’ 키워드는 ’break’와 유사하지만 루프를 중지하지 않습니다. 대신, 위에서부터 루프를 다시 시작합니다.

n = 10
while n > 0:
    if n % 2 != 0: # n is odd
        n = n - 1
        continue
        break  # this line is never executed because continue restarts the loop from the top
    print(n)
    n = n - 1

print("Blast off!")
10
8
6
4
2
Blast off!

3. 이해


컴프리헨션을 사용하면 편리하고 간결한 코드 줄로 목록/튜플/집합/사전을 작성할 수 있습니다. 저는 이것들을 꽤 많이 사용하고 있어요! 다음은 반복 가능한 항목을 반복하고 목록을 만드는 데 사용할 수 있는 표준 for 루프입니다.

subliminal = ['Tom', 'ingests', 'many', 'eggs', 'to', 'outrun', 'large', 'eagles', 'after', 'running', 'near', '!']
first_letters = []
for word in subliminal:
    first_letters.append(word[0])
print(first_letters)
['T', 'i', 'm', 'e', 't', 'o', 'l', 'e', 'a', 'r', 'n', '!']

List Comprehension을 사용하면 이 작업을 하나의 간결한 줄로 수행할 수 있습니다.

letters = [word[0] for word in subliminal]  # list comprehension
letters
['T', 'i', 'm', 'e', 't', 'o', 'l', 'e', 'a', 'r', 'n', '!']

여러 번 반복하거나 조건부 반복을 수행하면 상황을 더 복잡하게 만들 수 있습니다.

[(i, j) for i in range(3) for j in range(4)]
[(0, 0),
 (0, 1),
 (0, 2),
 (0, 3),
 (1, 0),
 (1, 1),
 (1, 2),
 (1, 3),
 (2, 0),
 (2, 1),
 (2, 2),
 (2, 3)]
[i for i in range(11) if i % 2 == 0]  # condition the iterator, select only even numbers
[0, 2, 4, 6, 8, 10]
[-i if i % 2 else i for i in range(11)]  # condition the value, -ve odd and +ve even numbers
[0, -1, 2, -3, 4, -5, 6, -7, 8, -9, 10]

설정된 이해력도 있습니다.

words = ['hello', 'goodbye', 'the', 'antidisestablishmentarianism']
y = {word[-1] for word in words}  # set comprehension
y  # only has 3 elements because a set contains only unique items and there would have been two e's
{'e', 'm', 'o'}

사전 이해:

word_lengths = {word:len(word) for word in words} # dictionary comprehension
word_lengths
{'hello': 5, 'goodbye': 7, 'the': 3, 'antidisestablishmentarianism': 28}

튜플 이해력은 예상한 대로 작동하지 않습니다. 대신 “생성기”를 얻습니다(나중에 자세히 설명).

y = (word[-1] for word in words)  # this is NOT a tuple comprehension - more on generators later
print(y)
<generator object <genexpr> at 0x15e469c50>

4. 시도 / 제외


위: 나인 인치 네일스 콘서트의 죽음의 블루 스크린! 출처: cnet.com.

문제가 발생하더라도 코드가 충돌하는 것을 원하지 않습니다. 정상적으로 실패하기를 원합니다. Python에서는 try/Exception을 사용하여 이를 수행할 수 있습니다. 다음은 기본 예입니다.

this_variable_does_not_exist
print("Another line")  # code fails before getting to this line
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-30-dd878f68d557> in <module>
----> 1 this_variable_does_not_exist
      2 print("Another line")  # code fails before getting to this line

NameError: name 'this_variable_does_not_exist' is not defined
try:
    this_variable_does_not_exist
except:
    pass # do nothing
    print("You did something bad! But I won't raise an error.") # print something
print("Another line")
You did something bad! But I won't raise an error.
Another line

Python은 try 블록의 코드를 실행하려고 시도합니다. 오류가 발생하면 ‘제외’ 블록(다른 언어에서는 ‘try’/’catch’라고도 함)에서 이를 “잡습니다”. 다양한 오류 유형 또는 예외가 있습니다. 위에서 NameError를 보았습니다.

5/0  # ZeroDivisionError
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-32-9866726f0353> in <module>
----> 1 5/0  # ZeroDivisionError

ZeroDivisionError: division by zero
my_list = [1,2,3]
my_list[5]  # IndexError
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-33-8f0c4b3b2ce1> in <module>
      1 my_list = [1,2,3]
----> 2 my_list[5]  # IndexError

IndexError: list index out of range
my_tuple = (1,2,3)
my_tuple[0] = 0  # TypeError
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-34-90cd0bd9ddec> in <module>
      1 my_tuple = (1,2,3)
----> 2 my_tuple[0] = 0  # TypeError

TypeError: 'tuple' object does not support item assignment

좋습니다. 분명히 여러 가지 오류가 발생할 수 있습니다. try/Exception을 사용하면 예외 자체를 잡을 수도 있습니다:

try:
    this_variable_does_not_exist
except Exception as ex:
    print("You did something bad!")
    print(ex)
    print(type(ex))
You did something bad!
name 'this_variable_does_not_exist' is not defined
<class 'NameError'>

위에서는 예외를 포착하고 이를 인쇄할 수 있도록 ex 변수에 할당했습니다. 이는 프로그램을 중단시키지 않고도 오류 메시지가 무엇인지 확인할 수 있기 때문에 유용합니다. 특정 예외 유형을 포착할 수도 있습니다. 이는 일반적으로 오류를 포착하는 데 권장되는 방법입니다. 코드가 실패한 위치와 이유를 정확히 알 수 있도록 오류를 구체적으로 포착해야 합니다.

try:
    this_variable_does_not_exist  # name error
#     (1, 2, 3)[0] = 1  # type error
#     5/0  # ZeroDivisionError
except TypeError:
    print("You made a type error!")
except NameError:
    print("You made a name error!")
except:
    print("You made some other sort of error")
You made a name error!

오류가 위의 유형 중 어느 것도 아닌 경우 마지막 제외는 트리거되므로 이러한 종류는 if/elif/else 느낌을 갖습니다. 선택적 elsefinally 키워드(저는 거의 사용하지 않음)도 있지만 여기에 대한 자세한 내용을 읽을 수 있습니다.

try:
    this_variable_does_not_exist
except:
    print("The variable does not exist!")
finally:
    print("I'm printing anyway!")
The variable does not exist!
I'm printing anyway!

’raise’를 사용하여 의도적으로 예외를 발생시키는 코드를 작성할 수도 있습니다.

def add_one(x):  # we'll get to functions in the next section
    return x + 1
add_one("blah")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-39-96e0142692a3> in <module>
----> 1 add_one("blah")

<ipython-input-38-eabf290fc405> in add_one(x)
      1 def add_one(x):  # we'll get to functions in the next section
----> 2     return x + 1

TypeError: can only concatenate str (not "int") to str
def add_one(x):
    if not isinstance(x, float) and not isinstance(x, int):
        raise TypeError(f"Sorry, x must be numeric, you entered a {type(x)}.")
        
    return x + 1
add_one("blah")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-41-96e0142692a3> in <module>
----> 1 add_one("blah")

<ipython-input-40-3a3a8b564774> in add_one(x)
      1 def add_one(x):
      2     if not isinstance(x, float) and not isinstance(x, int):
----> 3         raise TypeError(f"Sorry, x must be numeric, you entered a {type(x)}.")
      4 
      5     return x + 1

TypeError: Sorry, x must be numeric, you entered a <class 'str'>.

이는 함수가 복잡하고 이상한 오류 메시지와 함께 복잡한 방식으로 실패할 때 유용합니다. 함수의 _user_에게 오류의 원인을 훨씬 더 명확하게 만들 수 있습니다. 이렇게 하는 경우 함수 문서에 이러한 예외를 이상적으로 설명해야 합니다. 그러면 사용자가 함수를 호출할 때 무엇을 기대할 수 있는지 알 수 있습니다.

마지막으로 자체 예외 유형을 정의할 수도 있습니다. 우리는 Exception 클래스를 상속함으로써 이를 수행합니다. 다음 장에서 클래스와 상속에 대해 더 자세히 살펴보겠습니다!

class CustomAdditionError(Exception):
    pass
def add_one(x):
    if not isinstance(x, float) and not isinstance(x, int):
        raise CustomAdditionError("Sorry, x must be numeric")
        
    return x + 1
add_one("blah")
---------------------------------------------------------------------------
CustomAdditionError                       Traceback (most recent call last)
<ipython-input-3-96e0142692a3> in <module>
----> 1 add_one("blah")

<ipython-input-2-25db54189b4f> in add_one(x)
      1 def add_one(x):
      2     if not isinstance(x, float) and not isinstance(x, int):
----> 3         raise CustomAdditionError("Sorry, x must be numeric")
      4 
      5     return x + 1

CustomAdditionError: Sorry, x must be numeric

5. 기능


함수는 ’인수’라고도 알려진 입력 매개변수를 허용할 수 있는 재사용 가능한 코드 조각입니다. 예를 들어, 하나의 입력 매개변수 n을 취하고 정사각형 n**2를 반환하는 square라는 함수를 정의해 보겠습니다.

def square(n):
    n_squared = n**2
    return n_squared
square(2)
4
square(100)
10000
square(12345)
152399025

함수는 def 키워드로 시작하고 함수 이름, 괄호 안의 인수, 콜론(:) 순입니다. 함수에 의해 실행되는 코드는 들여쓰기로 정의됩니다. 함수의 출력 또는 ‘반환’ 값은 ‘return’ 키워드를 사용하여 지정됩니다.

부작용 및 지역 변수

함수 내에서 변수를 생성하면 로컬 변수가 됩니다. 즉, 함수 내부에만 존재한다는 의미입니다. 예를 들어:

def cat_string(str1, str2):
    string = str1 + str2
    return string
cat_string('My name is ', 'Tom')
'My name is Tom'
string
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-10-edbf08a562d5> in <module>
----> 1 string

NameError: name 'string' is not defined

함수가 전달된 변수를 변경하는 경우 부작용이 있다고 합니다. 예를 들어:

def silly_sum(my_list):
    my_list.append(0)
    return sum(my_list)
l = [1, 2, 3, 4]
out = silly_sum(l)
out
10

위의 내용은 우리가 원했던 것과 같나요? 그런데 잠깐만요… ‘l’ 객체가 바뀌었어요…

l
[1, 2, 3, 4, 0]

함수에 이와 같은 부작용이 있는 경우 문서에서 이를 언급해야 합니다(이 장의 뒷부분에서 다루겠습니다).

Null 반환 유형

반환 값을 지정하지 않으면 함수가 종료될 때 None을 반환합니다.

def f(x):
    x + 1 # no return!
    if x == 999:
        return
print(f(0))
None

선택 및 필수 인수

때로는 함수의 일부 인수에 대해 _기본값_을 갖는 것이 편리합니다. 기본값이 있기 때문에 이러한 인수는 선택 사항이므로 “선택적 인수”라고 합니다. 예를 들어:

def repeat_string(s, n=2):
    return s*n
repeat_string("mds", 2)
'mdsmds'
repeat_string("mds", 5)
'mdsmdsmdsmdsmds'
repeat_string("mds") # do not specify `n`; it is optional
'mdsmds'

이상적으로는 선택적 인수의 기본값을 신중하게 선택해야 합니다. 위 함수에서 무언가를 “반복”한다는 생각은 2개의 복사본을 갖는 것을 생각하게 하므로 ’n=2’가 합리적인 기본값처럼 느껴집니다.

필수 인수와 선택적 인수를 원하는 만큼 가질 수 있습니다. 모든 선택적 인수는 필수 인수 뒤에 와야 합니다. 필수 인수는 나타나는 순서에 따라 매핑됩니다. 함수를 사용할 때 선택적 인수를 순서 없이 지정할 수 있습니다.

def example(a, b, c="DEFAULT", d="DEFAULT"):
    print(a, b, c, d)
    
example(1, 2, 3, 4)
1 2 3 4

cd에 대한 기본값 사용:

example(1, 2)
1 2 DEFAULT DEFAULT

cd키워드 인수(예: 이름으로)로 지정:

example(1, 2, c=3, d=4)
1 2 3 4

키워드별로 선택적 인수 중 하나만 지정:

example(1, 2, c=3)
1 2 3 DEFAULT

cd만 선택사항이더라도 모든 인수를 키워드 인수로 지정:

example(a=1, b=2, c=3, d=4)
1 2 3 4

3번째라는 사실로 c를 지정합니다(혼란스럽기 때문에 권장하지 않습니다):

example(1, 2, 3)
1 2 3 DEFAULT

키워드로 선택적 인수를 지정하지만 순서가 잘못되었습니다(혼란스러울 수도 있지만 그렇게 나쁘지는 않습니다. 괜찮습니다).

example(1, 2, d=4, c=3)
1 2 3 4

선택사항이 아닌 인수를 키워드로 지정하기(이렇게 해도 괜찮습니다):

example(a=1, b=2)
1 2 DEFAULT DEFAULT

선택사항이 아닌 인수를 키워드로 지정하지만 순서가 잘못되었습니다(권장하지 않으므로 혼란스럽습니다).

example(b=2, a=1)
1 2 DEFAULT DEFAULT

키워드가 아닌 인수 앞에 키워드 인수를 지정하면 오류가 발생합니다.

example(a=2, 1)
  File "<ipython-input-23-a37b920e8205>", line 1
    example(a=2, 1)
                ^
SyntaxError: positional argument follows keyword argument

다중 반환 값

많은 프로그래밍 언어에서 함수는 하나의 객체만 반환할 수 있습니다. 이는 Python에서도 기술적으로는 사실이지만, 튜플을 반환하는 “해결 방법”이 있습니다.

def sum_and_product(x, y):
    return (x + y, x * y)
sum_and_product(5, 6)
(11, 30)

괄호는 생략될 수 있으며(종종 생략됨), ’튜플’은 쉼표를 사용하여 정의된 대로 암시적으로 반환됩니다.

def sum_and_product(x, y):
    return x + y, x * y
sum_and_product(5, 6)
(11, 30)

반환된 튜플을 별도의 변수로 즉시 압축 해제하는 것이 일반적이므로 실제로 함수가 여러 값을 반환하는 것처럼 느껴집니다.

s, p = sum_and_product(5, 6)
s
11
p
30

여담이지만, Python에서는 원하지 않는 값에 _를 사용하는 것이 관례입니다.

s, _ = sum_and_product(5, 6)
s
11
_
30

임의 개수의 인수를 갖는 함수

*args**kwargs를 사용하여 임의 개수의 위치 또는 키워드 인수를 허용하는 함수를 호출/정의할 수도 있습니다.

def add(*args):
    print(args)
    return sum(args)
add(1, 2, 3, 4, 5, 6)
(1, 2, 3, 4, 5, 6)
21
def add(**kwargs):
    print(kwargs)
    return sum(kwargs.values())
add(a=3, b=4, c=5)
{'a': 3, 'b': 4, 'c': 5}
12

6. 데이터 유형으로서의 기능


Python에서 함수는 실제로 데이터 유형입니다.

def do_nothing(x):
    return x
type(do_nothing)
function
print(do_nothing)
<function do_nothing at 0x1102450e0>

이는 함수를 다른 함수에 인수로 전달할 수 있음을 의미합니다.

def square(y):
    return y**2

def evaluate_function_on_x_plus_1(fun, x):
    return fun(x+1)
evaluate_function_on_x_plus_1(square, 5)
36

그럼 위에서 무슨 일이 일어났나요? - fun(x+1)square(5+1)이 됩니다. - square(6)36이 됩니다.

7. 익명 함수


Python에서 함수를 정의하는 방법에는 두 가지가 있습니다. 지금까지 우리가 사용해 온 방식은 다음과 같습니다.

def add_one(x):
    return x+1
add_one(7.2)
8.2

또는 lambda 키워드를 사용하여:

add_one = lambda x: x+1 
type(add_one)
function
add_one(7.2)
8.2

위의 두 가지 접근 방식은 동일합니다. ’lambda’가 있는 함수를 익명 함수라고 합니다. 익명 함수는 코드 한 줄만 차지하므로 대부분의 경우에는 적합하지 않지만 소규모 작업에는 유용할 수 있습니다.

evaluate_function_on_x_plus_1(lambda x: x ** 2, 5)
36

위:

  • 첫째, lambda x: x**2function 유형의 값으로 평가됩니다(이 함수에는 이름이 지정되지 않으므로 “익명 함수”라는 점에 유의하세요).
  • 그런 다음 함수와 정수 5evaluate_function_on_x_plus_1에 전달됩니다.
  • 이 시점에서 익명 함수는 ’5+1’로 평가되고 ’36’을 얻습니다.

8. DRY 원칙, 좋은 기능을 디자인하다


DRY는 반복하지 마세요를 의미합니다. 이 원칙에 대한 자세한 내용은 관련 Wikipedia 기사를 참조하세요.

예를 들어, 목록의 각 요소를 회문으로 바꾸는 작업을 생각해 보세요.

names = ["milad", "tom", "tiffany"]
name = "tom"
name[::-1]  # creates a slice that starts at the end and moves backwards, syntax is [begin:end:step]
'mot'
names_backwards = list()

names_backwards.append(names[0] + names[0][::-1])
names_backwards.append(names[1] + names[1][::-1])
names_backwards.append(names[2] + names[2][::-1])
names_backwards
['miladdalim', 'tommot', 'tiffanyynaffit']

위의 코드는 여러 가지 이유로 역겹고, 끔찍하고, 이상한 코드입니다. 1. 3개의 요소가 있는 목록에서만 작동합니다. 2. names라는 이름의 목록에서만 작동합니다. 3. 기능을 변경하려면 유사한 코드 줄 3개를 변경해야 합니다(반복하지 마세요!!). 4. 겉모습만으로는 무슨 일을 하는지 이해하기 어렵습니다.

이것을 다른 방식으로 시도해 봅시다:

names_backwards = list()

for name in names:
    names_backwards.append(name + name[::-1])
    
names_backwards
['miladdalim', 'tommot', 'tiffanyynaffit']

위의 내용이 약간 나아졌으며 문제 (1)과 (3)을 해결했습니다. 하지만 우리의 삶을 더 쉽게 만들어주는 함수를 만들어 봅시다:

def make_palindromes(names):
    names_backwards = list()
    
    for name in names:
        names_backwards.append(name + name[::-1])
    
    return names_backwards

make_palindromes(names)
['miladdalim', 'tommot', 'tiffanyynaffit']

좋아요, 이게 더 낫네요. 이제 문제 (2)도 해결했습니다. ’이름’뿐만 아니라 어떤 목록으로도 함수를 호출할 수 있기 때문입니다. 예를 들어, 여러 개의 _lists_가 있다면 어떻게 될까요?

names1 = ["milad", "tom", "tiffany"]
names2 = ["apple", "orange", "banana"]
make_palindromes(names1)
['miladdalim', 'tommot', 'tiffanyynaffit']
make_palindromes(names2)
['appleelppa', 'orangeegnaro', 'bananaananab']

좋은 기능 디자인하기

DRY 원칙을 적용하는 방법과 선택 방법은 사용자와 프로그래밍 컨텍스트에 달려 있습니다. 이러한 결정은 종종 모호합니다. make_palindromes()를 한 번만 수행한다면 함수여야 합니까? 두 배? 루프가 함수 내부에 있어야 할까요, 아니면 외부에 있어야 할까요? 두 개의 함수가 있어야 하며, 하나는 다른 하나를 반복해야 합니까?

내 개인적인 의견으로는 make_palindromes()는 이해하기 어려울 만큼 너무 많은 일을 합니다. 나는 이것을 선호한다:

def make_palindrome(name):
    return name + name[::-1]

make_palindrome("milad")
'miladdalim'

여기에서 “목록의 모든 요소에 make_palindrome을 적용”하려면 목록 이해를 사용할 수 있습니다.

[make_palindrome(name) for name in names]
['miladdalim', 'tommot', 'tiffanyynaffit']

이 작업을 정확하게 수행하는 내장 map() 함수도 있으며 시퀀스의 모든 요소에 함수를 적용합니다.

list(map(make_palindrome, names))
['miladdalim', 'tommot', 'tiffanyynaffit']

9. 발전기


이 장의 앞부분에서 목록 이해를 상기해 보세요.

[n for n in range(10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Comprehension은 전체 표현식을 한 번에 평가한 다음 전체 데이터 곱을 반환합니다. 때때로 우리는 한 번에 데이터의 한 부분만 작업하고 싶을 때가 있습니다. 예를 들어 모든 데이터를 메모리에 넣을 수 없는 경우입니다. 이를 위해 생성기를 사용할 수 있습니다.

(n for n in range(10))
<generator object <genexpr> at 0x110220650>

방금 ’생성기 개체’를 만들었습니다. 생성기 개체는 값을 생성하기 위한 “레시피”와 같습니다. 그들은 요청을 받을 때까지 실제로 어떤 계산도 수행하지 않습니다. 세 가지 주요 방법으로 생성기에서 값을 얻을 수 있습니다. - next() 사용 - list() 사용 - 루핑

gen = (n for n in range(10))
next(gen)
0
next(gen)
1

생성기가 소진되면 더 이상 값을 반환하지 않습니다.

gen = (n for n in range(10))
for i in range(11):
    print(next(gen))
0
1
2
3
4
5
6
7
8
9
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-67-14d35f56c593> in <module>
      1 gen = (n for n in range(10))
      2 for i in range(11):
----> 3     print(next(gen))

StopIteration: 

Error 500 (Server Error)!!1500.That’s an error.There was an error. Please try again later.That’s all we know.

gen = (n for n in range(10))
list(gen)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

마지막으로 생성기 객체도 반복할 수 있습니다.

gen = (n for n in range(10))
for i in gen:
    print(i)
0
1
2
3
4
5
6
7
8
9

위에서 우리는 이해 구문을 사용하고 괄호를 사용하여 생성기 객체를 생성하는 방법을 살펴보았습니다. 함수와 (return 키워드 대신) yield 키워드를 사용하여 생성기를 만들 수도 있습니다.

def gen():
    for n in range(10):
        yield (n, n ** 2)
g = gen()
print(next(g))
print(next(g))
print(next(g))
(0, 0)
(1, 1)
(2, 4)

다음은 생성기가 유용할 수 있는 사례에 대한 실제 동기입니다. 캐나다의 주택에 대한 정보가 포함된 사전 목록을 만들고 싶다고 가정해 보겠습니다.

import random  # we'll learn about imports in a later chapter
import time
import memory_profiler
city = ['Vancouver', 'Toronto', 'Ottawa', 'Montreal']
def house_list(n):
    houses = []
    for i in range(n):
        house = {
            'id': i,
            'city': random.choice(city),
            'bedrooms': random.randint(1, 5),
            'bathrooms': random.randint(1, 3),
            'price ($1000s)': random.randint(300, 1000)
        }
        houses.append(house)
    return houses
house_list(2)
[{'id': 0,
  'city': 'Ottawa',
  'bedrooms': 5,
  'bathrooms': 2,
  'price ($1000s)': 420},
 {'id': 1,
  'city': 'Montreal',
  'bedrooms': 5,
  'bathrooms': 1,
  'price ($1000s)': 652}]

1,000,000채의 주택 목록을 만들고 싶다면 어떻게 될까요? 얼마나 많은 시간/메모리가 소요되나요?

start = time.time()
mem = memory_profiler.memory_usage()
print(f"Memory usage before: {mem[0]:.0f} mb")
people = house_list(500000)
print(f"Memory usage after: {memory_profiler.memory_usage()[0]:.0f} mb")
print(f"Time taken: {time.time() - start:.2f}s")
Memory usage before: 86 mb
Memory usage after: 251 mb
Time taken: 2.24s
def house_generator(n):
    for i in range(n):
        house = {
            'id': i,
            'city': random.choice(city),
            'bedrooms': random.randint(1, 5),
            'bathrooms': random.randint(1, 3),
            'price ($1000s)': random.randint(300, 1000)
        }
        yield house
start = time.time()
print(f"Memory usage before: {mem[0]:.0f} mb")
people = house_generator(500000)
print(f"Memory usage after: {memory_profiler.memory_usage()[0]:.0f} mb")
print(f"Time taken: {time.time() - start:.2f}s")
Memory usage before: 86 mb
Memory usage after: 89 mb
Time taken: 0.17s

하지만 list()를 사용하여 모든 생성기 값을 추출하면 메모리 절약 효과가 손실됩니다.

print(f"Memory usage before: {mem[0]:.0f} mb")
people = list(house_generator(500000))
print(f"Memory usage after: {memory_profiler.memory_usage()[0]:.0f} mb")
Memory usage before: 36 mb
Memory usage after: 202 mb

1. 소개


좋은 함수 작성에 대해 이야기할 때 우리가 실제로 해결하지 못한 문제 중 하나는 “4. 보기만으로는 무엇을 하는지 이해하기 어렵습니다.”였습니다. 이는 “docstring”이라고 불리는 함수 문서화 아이디어를 불러일으킵니다. docstringdef 줄 바로 뒤에 오고 삼중따옴표 """로 묶입니다.

def make_palindrome(string):
    """Turns the string into a palindrome by concatenating itself with a reversed version of itself."""
    
    return string + string[::-1]

Python에서는 help() 함수를 사용하여 다른 함수의 문서를 볼 수 있습니다. IPython/Jupyter에서는 ?를 사용하여 환경에 있는 모든 함수의 문서 문자열을 볼 수 있습니다.

make_palindrome?
Signature: make_palindrome(string)
Docstring: Turns the string into a palindrome by concatenating itself with a reversed version of itself.
File:      ~/GitHub/online-courses/python-programming-for-data-science/chapters/<ipython-input-78-3399edf39112>
Type:      function

그러나 그보다 훨씬 쉬운 점은 커서가 함수 괄호 안에 있는 경우 shift + tab 단축키를 사용하여 마음대로 독스트링을 열 수 있다는 것입니다.

# make_palindrome('uncomment this line and try pressing shift+tab here.')

독스트링 구조

Python의 일반적인 독스트링 규칙은 PEP 257 - 독스트링 규칙에 설명되어 있습니다. Python에는 다양한 독스트링 스타일 규칙이 사용됩니다. 사용하는 정확한 스타일은 문서를 렌더링하거나 IDE에서 문서를 구문 분석하는 데 도움이 될 수 있습니다. 일반적인 스타일은 다음과 같습니다.

  1. 한 줄: 짧다면 함수를 설명하는 한 줄이면 됩니다(위와 같이).
  2. reST 스타일: 여기를 참조하세요.
  3. NumPy 스타일: 여기를 참조하세요. (권장! 및 MDS 권장)
  4. Google 스타일: 여기를 참조하세요.

NumPy 스타일:

def function_name(param1, param2, param3):
    """First line is a short description of the function.
    
    A paragraph describing in a bit more detail what the
    function does and what algorithms it uses and common
    use cases.
    
    Parameters
    ----------
    param1 : datatype
        A description of param1.
    param2 : datatype
        A description of param2.
    param3 : datatype
        A longer description because maybe this requires
        more explanation and we can use several lines.
    
    Returns
    -------
    datatype
        A description of the output, datatypes and behaviours.
        Describe special cases and anything the user needs to
        know to use the function.
    
    Examples
    --------
    >>> function_name(3,8,-5)
    2.0
    """
def make_palindrome(string):
    """Turns the string into a palindrome by concatenating 
    itself with a reversed version of itself.
    
    Parameters
    ----------
    string : str
        The string to turn into a palindrome.
        
    Returns
    -------
    str
        string concatenated with a reversed version of string
        
    Examples
    --------
    >>> make_palindrome('tom')
    'tommot'
    """
    return string + string[::-1]
make_palindrome?
Signature: make_palindrome(string)
Docstring:
Turns the string into a palindrome by concatenating 
itself with a reversed version of itself.
Parameters
----------
string : str
    The string to turn into a palindrome.
    
Returns
-------
str
    string concatenated with a reversed version of string
    
Examples
--------
>>> make_palindrome('tom')
'tommot'
File:      ~/GitHub/online-courses/python-programming-for-data-science/chapters/<ipython-input-1-a382cd1ad1e6>
Type:      function

선택적 인수가 있는 Docstring

함수 인수를 지정할 때 선택적 인수의 기본값을 지정합니다.

# scipy style
def repeat_string(s, n=2):
    """
    Repeat the string s, n times.
    
    Parameters
    ----------
    s : str 
        the string
    n : int, optional
        the number of times, by default = 2
        
    Returns
    -------
    str
        the repeated string
        
    Examples
    --------
    >>> repeat_string("Blah", 3)
    "BlahBlahBlah"
    """
    return s * n

이 벡터를 ‘(2, 3)’ 모양의 다음 행렬과 비교합니다.

유형 힌트는 말 그대로 함수 인수의 데이터 유형을 암시합니다. argument : dtype 구문을 사용하여 함수에서 인수 유형을 표시하고 def func() -> dtype을 사용하여 반환 값의 유형을 나타낼 수 있습니다. 예를 살펴보겠습니다:

# NumPy style
def repeat_string(s: str, n: int = 2) -> str:  # <---- note the type hinting here
    """
    Repeat the string s, n times.
    
    Parameters
    ----------
    s : str 
        the string
    n : int, optional (default = 2)
        the number of times
        
    Returns
    -------
    str
        the repeated string
        
    Examples
    --------
    >>> repeat_string("Blah", 3)
    "BlahBlahBlah"
    """
    return s * n
repeat_string?
Signature: repeat_string(s: str, n: int = 2) -> str
Docstring:
Repeat the string s, n times.
Parameters
----------
s : str 
    the string
n : int, optional (default = 2)
    the number of times
    
Returns
-------
str
    the repeated string
    
Examples
--------
>>> repeat_string("Blah", 3)
"BlahBlahBlah"
File:      ~/GitHub/online-courses/python-programming-for-data-science/chapters/<ipython-input-83-964bad9c977b>
Type:      function

유형 힌트는 사용자와 IDE가 dtype을 식별하고 버그를 식별하는 데 도움이 됩니다. 이는 또 다른 수준의 문서일 뿐입니다. 사용자에게 해당 날짜 유형을 사용하도록 강요하지 않습니다. 예를 들어, 원하는 경우 dictrepeat_string에 전달할 수 있습니다.

repeat_string({'key_1': 1, 'key_2': 2})
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-85-48f8613bcebb> in <module>
----> 1 repeat_string({'key_1': 1, 'key_2': 2})

<ipython-input-83-964bad9c977b> in repeat_string(s, n)
     21     "BlahBlahBlah"
     22     """
---> 23     return s * n

TypeError: unsupported operand type(s) for *: 'dict' and 'int'

대부분의 IDE는 유형 힌트를 읽고 함수에서 다른 dtype을 사용하는 경우 경고할 만큼 영리합니다(예: 이 VScode 스크린샷).