앎을 경계하기

[가짜연구소3기] Data Engineer

[가짜연구소 3기] 데이터 엔지니어링 40 - Foundations for efficiencies

양갱맨 2021. 9. 13. 09:53

이 과정에서는...

  • 깨끗하고 빠르고 효율적인 파이썬 코드를 작성하는 방법을 배운다.
  • 병목 현상을 찾기위해 코드 프로파일링 방법을 배운다.
  • 병목 현상과 안 좋은 코드 패턴을 제거하는 방법을 배운다.
  • Python, Numpy, Pandas를 사용한다.

효율적인 코드?

  • 효율적인 코드는 빠르고 실행 시간이 짧다.
    • 즉, 실행부터 결과 반환까지 대기 시간이 짧다.
  • 리소스를 효율적으로 할당하고 메모리 오버헤드가 크지 않다.
    • 즉, 리소스 사용을 최소화한다.
오버헤드? 처리를 위해 직접적으로 필요한 자원보다 간접적으로 사용되는 자원이 더 많은 경우→ 배보다 배꼽이 더 크다

파이써닉하게 코드를 짜보자

Python의 핵심은 가독성

파이썬 코드 작성의 모범 사례를 따라 작성된 코드를 Pythonic하다고 표현함.

# 파이써닉하지 않은 코드
doubled_numbers = []

for i in range(len(numbers)):
	doubled_numbers.append(numbers[i] * 2)

# 파이써닉하게
doubled_numbers = [x * 2 for x in numbers] # list comprehension

파이써닉하지 않은 코드는 길고 실행도 오래걸림.

실제로 해봄

import time

a = []
start = time.time()

for i in range(10000000):
    a.append(i*2)
print(time.time()-start)

start = time.time()
a = [i*2 for i in range(10000000)]
print(time.time()-start)
1.8296644687652588
0.8037307262420654

이런 방법은 어디서 확인? "Zen of Python"

Zen of Python은 Python 프로그래밍 언어의 디자인에 영향을 미치는 컴퓨터 프로그램 작성을 위한 19가지 "지침 원칙"의 모음입니다.

연습문제

# Print the list created using the Non-Pythonic approach
i = 0
new_list= []
while i < len(names):
    if len(names[i]) >= 6:
        new_list.append(names[i])
    i += 1
print(new_list)

# Print the list created by looping over the contents of names
better_list = []
for name in names:
    if len(name) >= 6:
        better_list.append(name)
print(better_list)

# Print the list created by using list comprehension
best_list = [name for name in names if len(name) >= 6]
print(best_list)

파이썬 표준 라이브러리

Python을 설치하면 같이 설치되는 표준 라이브러리

  • 빌트인 타입들
    • list, tuple, set, dict 등
  • 빌트인 함수들
    • print(), len(), range(), round(), enumerate(), map(), zip() 등
  • 빌트인 모듈들
    • os, sys, itertools, coolections, math 등

주로 사용하는 것들

1. range()

일련의 숫자 집합을 만들고 싶을 때 사용함.

range(start, end, step)

nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
nums_list = list(range(11)) # 0부터 10까지

2. enumerate()

객체의 각 항목에 대한 인덱스 쌍을 만들고 싶을 때 사용함.

iterable object는 다 가능. string, list, tuple, dictionary, range ... enumerate(반복 가능한 객체, start)

letters = ['a', 'b', 'c', 'd']

indexed_letters = enumerate(letters)
indexed_letters_list = list(indexed_letters) # [(0, 'a'),(1, 'b'),(2, 'c'),(3, 'd')]
s = range(10)
print(list(enumerate(s, 5)))
[(5, 0), (6, 1), (7, 2), (8, 3), (9, 4), (10, 5), (11, 6), (12, 7), (13, 8), (14, 9)]

3. map()

객체의 각 요소에 함수를 적용한다. 매핑시키고 싶을 때 사용함. map(함수, 객체)

nums = [1.5, 2.3, 3.2, 4.7, 5.0]
rnd_nums = list(map(round, nums)) # [2, 2, 3, 5, 5] int를 하게 되면 버림 수행
nums = [1,2,3,4,5]
sqrd_nums = list(map(lambda x : x**2, nums)) # [1, 4, 9, 16, 25]

연습문제

# Create a range object that goes from 0 to 5
nums = range(6)
print(type(nums))

# Convert nums to a list
nums_list = list(nums)
print(nums_list)

# Create a new list of odd numbers from 1 to 11 by unpacking a range object
nums_list2 = [*range(1,12,2)] # unpacking - 남은 요소를 전체 리스트에 담아 대입
print(nums_list2)
<class 'range'>
[0, 1, 2, 3, 4, 5]
[1, 3, 5, 7, 9, 11]
# Rewrite the for loop to use enumerate
indexed_names = []
for i,name in enumerate(names):
    index_name = (i,name)
    indexed_names.append(index_name) 
print(indexed_names)

# Rewrite the above for loop using list comprehension
indexed_names_comp = [(i, name) for i,name in enumerate(names)]
print(indexed_names_comp)

# Unpack an enumerate object with a starting index of one
indexed_names_unpack = [*enumerate(names, 1)]
print(indexed_names_unpack)
[(0, 'Jerry'), (1, 'Kramer'), (2, 'Elaine'), (3, 'George'), (4, 'Newman')]
[(0, 'Jerry'), (1, 'Kramer'), (2, 'Elaine'), (3, 'George'), (4, 'Newman')]
[(1, 'Jerry'), (2, 'Kramer'), (3, 'Elaine'), (4, 'George'), (5, 'Newman')]
# Use map to apply str.upper to each element in names
names_map  = map(str.upper, names)

# Print the type of the names_map
print(type(names_map))

# Unpack names_map into a list
names_uppercase = [*names_map]

# Print the list created above
print(names_uppercase)
<class 'map'>
['JERRY', 'KRAMER', 'ELAINE', 'GEORGE', 'NEWMAN']

NumPy

NumPy 패키지는 파이썬에서 데이터 사이언티스트에게 중요한 패키지이다.

딥러닝에서도 빠질 수 없음.

NumPy array

파이썬의 list는 linked list 기능이 포함되어 있다.

메모리 상 연속적으로 할당되니까 array 처럼 되어있고 스택이나 연결리스트처럼 사용할 수 있는 내장함수도 있다.

근데, 파이썬의 리스트는 사이즈가 동적으로 할당된다.

잠깐 자료구조에 대해 이야기 해보자면...

linked list의 장점은 추가, 삭제가 빠르다는 것이고 단점은 검색인데, 최악의 경우 O(n)이 걸린다.

Array는 반대로 추가와 삭제는 느리지만 index를 사용해서 빠르게 검색이 가능하다.

nums = [1,2,3,4,5]
print(list(id(nums[i]) for i in range(5)))
[140731335194400, 140731335194432, 140731335194464, 140731335194496, 140731335194528]

왜 넘파이가 더 빠른가? Numpy는 백터화하여 계산하기 때문에 루프나, 인덱싱이 없습니다. 사전에 C 코드로 작성된 코드에 의해 동작합니다. 따라서 훨씬 코드가 간결하고 읽기 쉽습니다. 더 적은 줄의 코드를 사용하므로 더 적은 확률로 버그가 발생합니다. 표준수학 표기법과 훨씬 유사하므로 일반적으로 더 쉽고 정확하게 코딩할 수 있습니다. 출처 - https://chancoding.tistory.com/10
nums_list = list(range(5)) # [0,1,2,3,4]

import numpy as np

nums_np = np.array(range(5)) # array([0,1,2,3,4])

python list는 다른 타입의 요소들을 포함할 수 있었지만 numpy array는 요소의 타입이 일치해야한다.

nums_np_ints = np.array([1,2,3])
nums_np_ints.dtype # dtype('int64')

numpy array는 요소의 타입이 일치해야한다?

→ 다른 타입의 객체들이 저장되긴 하지만 주로 연산을 사용할것이기 때문에 일치가 필요함.

import numpy as np

nums_np_ints = np.array(['string',[1,2,3],2.3])
print(type(nums_np_ints[0]),type(nums_np_ints[1]))
<class 'str'> <class 'list'>
import numpy as np

nums_np_ints = np.array([2.3, 1, 5, 100, 20.3])
print(type(nums_np_ints[0]),type(nums_np_ints[1]))
<class 'numpy.float64'> <class 'numpy.float64'>

모든 요소가 동일한 타입이 되도록 하면 타입 체크에 필요한 오버 헤드가 제거된다.

NumPy array 브로드캐스팅

넘파이 배열은 브로드캐스팅을 지원한다. (파이썬 리스트는 안됨)

nums = [-2, -1, 0, 1, 2]
nums ** 2 # TypeError: unsupported operand type(s) for ** or pow(): 'list' and 'int'
import numpy as np
nums_np = np.array([-2, -1, 0, 1, 2])
print(nums_np ** 2) # [4 1 0 1 4]

NumPy array 인덱싱

넘파이 배열과 파이썬 리스트의 인덱싱에도 차이가 존재한다.

1차원일 때는 동일하지만 2차원 이상부터 달라진다.

import numpy as np

nums2=[[1,2,3],[4,5,6]]
nums2_np=np.array(nums2)

# 원소 2가져오기
nums2[0][1]
nums2_np[0,1]

#각 행의 첫번째 원소 가져오기 [1,4]
[row[0] for row in nums2]
nums2_np[:,0]

NumPy array 부울 인덱싱

특정 조건에 대한 각 원소의 부울 값을 나타낼 수 있다.

import numpy as np
nums_np = np.array([-2, -1, 0, 1, 2])
nums_np > 0 # [False False False  True  True]
nums_np[nums_np > 0] # [1 2]

위 작업을 파이썬 리스트로 하려면 for문과 if문을 사용해서 처리해야 한다.

연습문제

# Print second row of nums
print(nums[1,:])

# Print all elements of nums that are greater than six
print(nums[nums > 6])

# Double every element of nums
nums_dbl = nums * 2
print(nums_dbl)

# Replace the third column of nums
nums[:,2] = nums[:,2] + 1
print(nums)
[ 6  7  8  9 10]
[ 7  8  9 10]
[[ 2  4  6  8 10]
 [12 14 16 18 20]]
[[ 1  2  4  4  5]
 [ 6  7  9  9 10]]
# Create a list of arrival times
arrival_times = [*range(10,60,10)]

# Convert arrival_times to an array and update the times
arrival_times_np = np.array(arrival_times)
new_times = arrival_times_np - 3

# Use list comprehension and enumerate to pair guests to new times
guest_arrivals = [(names[i],time) for i,time in enumerate(new_times)]

# Map the welcome_guest function to each (guest,time) pair
welcome_map = map(welcome_guest, guest_arrivals)

guest_welcomes = [*welcome_map]
print(*guest_welcomes, sep='\n')
Welcome to Festivus Jerry... You're 7 min late.
Welcome to Festivus Kramer... You're 17 min late.
Welcome to Festivus Elaine... You're 27 min late.
Welcome to Festivus George... You're 37 min late.
Welcome to Festivus Newman... You're 47 min late.