또르르's 개발 Story

[03-1] Pythonic code 본문

부스트캠프 AI 테크 U stage/이론

[03-1] Pythonic code

또르르21 2021. 1. 20. 23:52

파이썬 스타일의 코드 Pythonic code는 파이썬 특유의 문법을 활용하여 효율적으로 코드를 표현하는 방법입니다.

다른 언어보다 훨씬 짧게 코드를 작성할 수 있는 이유도 Python이 효율적인 문법을 제공하기 때문이죠.

이번 시간에는 이러한 파이썬만이 가지고 있는 문법에 대해서 배워봅니다.

 

1️⃣ split & join

 

C++에서 문자 자르기를 하면서 정말 많은 고생을 했던 것이 기억이 납니다. token으로 자르고, 함수를 호출하고... 문장을 자르기 위한 코드를 작성하는데 몇 줄이 소비됩니다. 하지만 Python은 split() 한 함수로 문장을 쪼갤 수 있습니다. 원하는 문자를 split의 parameter로 넣어주면 됩니다.

>>> items = 'zero one two three'.split() # 빈칸을 기준으로 문자열 나누기
>>> print (items)
['zero', 'one', 'two', 'three']


>>> example = 'python,java,javascript' # ","을 기준으로 문자열 나누기
>>> example.split(",")
['python', ‘java', 'javascript']

또한 join을 사용하면 string으로 구성된 list를 합쳐 하나의 string으로 반환해줍니다.

>>> colors = ['red', 'blue', 'green', 'yellow']
>>> result = ''.join(colors)
>>> result
'redbluegreenyellow'


>>> result = ' '.join(colors) # 연결 시 빈칸 1칸으로 연결
>>> result
'red blue green yellow'


>>> result = ', '.join(colors) # 연결 시 ", "으로 연결
>>> result
'red, blue, green, yellow'


>>> result = '-'.join(colors) # 연결 시 "-"으로 연결
>>> result
'red-blue-green-yellow'

 

2️⃣ list comprehension

 

C/C++, Java에서는 list(또는 배열)에 값을 넣어주기 위해서는 push_back, Indexing을 사용해서 값을 넣어줘야 합니다. 최소 두 줄 이상의 코드가 작성되는데요. 하지만 Python은 list comprehension을 통해 코드 한 줄에 작성할 수 있습니다.

 

또한 list comprehension의 좋은 점은 list의 for+append 조합보다 속도가 빠르다는 점입니다.

 

아래 코드는 기본적인 list comprehension Filter(if문)가 추가된 list comprehension입니다.

기본적인 list comprehension은 "[ ]"안에 for문을 넣어주고 변수 i를 빼는 형태입니다. 

Filter(if문) 추가된 list comprehension은 기본적인 list comprehension 뒤에 if문을 추가한 형태입니다. 변수 i가 짝수일 때만 list안에 들어가게 됩니다.

>>> result = [i for i in range(10)]				# 기본적인 list comprehension style

>>> result

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]



>>> result = [i for i in range(10) if i % 2 == 0]		# Filter(if문)가 추가된 list comprehension

>>> result

[0, 2, 4, 6, 8]

이중 for문을 사용할 수도 있습니다. 아래 코드는 이중 for문을 사용해서 list comprehension을 구현한 모습입니다.

>>> word_1 = "Hello"

>>> word_2 = "World"

>>> result = [i+j for i in word_1 for j in word_2]

# for i in word_1:
#	for j in word_2:
#		result.append(i+j)

>>> result

['HW', 'Ho', 'Hr', 'Hl', 'Hd', 'eW', 'eo', 'er',
'el', 'ed', 'lW', 'lo', 'lr', 'll', 'ld', 'lW',
'lo', 'lr', 'll', 'ld', 'oW', 'oo', 'or', 'ol', 'od']

이중 for문을 사용한 list comprehension에는 마찬가지로 Filter(if문)을 사용할 수 있습니다.

>>> result = [i+j for i in case_1 for j in case_2 if not(i==j)]

# Filter: i랑 j과 같다면 List에 추가하지 않음

>>> result

['AD', 'AE', 'BD', 'BE', 'BA', 'CD', 'CE', 'CA']

아래 코드는 이차원 배열 list comprehension입니다.

>>> case_1 = ["A","B","C"]

>>> case_2 = ["D","E","A"]

>>> result = [ [i+j for i in case_1] for j in case_2]		# case_2 for문이 먼저 실행됨

>>> result

[['AD', 'BD', 'CD'], ['AE', 'BE', 'CE'], ['AA', 'BA', 'CA']]

 

 

3️⃣ enumerate & zip

 

enumerate는 list의 index와 value를 같이 추출하는 함수입니다.

>>> for i, v in enumerate(['tic', 'tac', 'toe']):	# list의 있는 index와 값을 unpacking

... 	print (i, v)

...

0 tic
1 tac
2 toe

zip은 두 개의 list의 값을 병렬적으로 추출합니다.

>>> alist = ['a1', 'a2', 'a3']

>>> blist = ['b1', 'b2', 'b3']

>>> for a, b in zip(alist, blist): # 병렬적으로 값을 추출

... 	print (a,b)

...

a1 b1
a2 b2
a3 b3

물론 enumerate와 zip을 동시에 사용할 수 있습니다.

>>> alist = ['a1', 'a2', 'a3']

>>> blist = ['b1', 'b2', 'b3']

>>> for i, (a, b) in enumerate(zip(alist, blist)):

... 	print (i, a, b) 		# index alist[index] blist[index] 표시

...

0 a1 b1
1 a2 b2
2 a3 b3

 

4️⃣ lambda & map & reduce

 

1) lambda

 

lambda는 함수 이름 없이, 함수처럼 쓸 수 있는 익명 함수입니다.

익명 함수이기 때문에 일회성으로만 사용할 수 있으며, lambda 함수에는 parameter와 return만 존재합니다.

아래와 같이 def을 사용해서 하는 방법(왼쪽)과 Lambda를 사용하는 방법이 있습니다.

 

Lambda의 표기법은 lambda를 쓰고 " : " 앞에는 parameter를 넣고 뒤에는 return값을 나타냅니다.

하지만 Python 3부터는 코드 해석의 어려움, 테스트의 어려움 등으로 인해 Lambda를 권장하지는 않는다 합니다.

그래도 아직도 많이 사용하고 있는 함수입니다.

>>> print((lambda x: x+1)(5))	# lambda 옆 괄호는 argument를 나타냅니다.

6

 

2) map

 

map은 list의 요소들을 지정된 함수로 처리해주는 함수입니다.

map의 첫 번째 parameter는 변환하고 싶은 함수를 넣어주고, 두 번째 parameter는 list를 넣어줍니다.

>>> ex = [1,2,3,4,5]

>>> list(map(lambda x: x ** 2 if x % 2 == 0 else x,ex))		

[1, 4, 3, 16, 5]

# map의 첫 번째 parameter는 함수(lambda), 두 번쨰 parameter는 list(ex)

# map은 python 3.0 이상부터 앞에 list를 감싸줘야 list로 출력할 수 있습니다.

# lambda : x가 짝수일 때 제곱을 하고 아니면 그냥 출력

map은 또한 list 두 개 이상을 받을 수 있습니다. (대신 함수(map의 첫 번째 parameter)에서 parameter의 개수가 map의 parameter 개수와 같아야 합니다.)

>>> ex = [1,2,3,4,5]

>>> f = lambda x, y: x + y

>>> print(list(map(f, ex, ex)))

[2, 4, 6, 8, 10]

 

3) reduce

 

reduce는 map과 달리 list에 똑같은 함수를 적용해서 통합하는 방법입니다. 

아래 코드를 보면 한 번에 이해가 되실 겁니다.

>>> from functools import reduce

>>> print(reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]))		# x + y 함수를 사용해서 reduce

15

 

5️⃣ Iterable object

 

Iterable object는 Sequence형 자료형(list, 문자열 등)에서 데이터를 순서대로 추출하는 object입니다.

object의 내부적 구현으로는 __iter__과 __next__가 사용됩니다.

>>> cities = ["Seoul", "Busan","Jeju"]

>>> iter_obj = iter(cities)		# iter을 sequence 자료형에 씌워서 iterable object로 만듭니다.

>>> print(next(iter_obj))

Seoul

>>> print(next(iter_obj))

Busan

>>> print(next(iter_obj))

Jeju

>>> next(iter_obj)		# 다음 값은 없기 때문에 error가 발생합니다.

----> 6 next(iter_obj)
StopIteration: 

 

그렇다면 왜 Sequence형 자료형이 있는데 iterable object가 필요할까요?

이 부분은 아래 generator에서 설명드리겠습니다.

 

1) generator

 

generator은 iterable object를 특수한 형태로 사용해주는 함수입니다.

특징은 element가 사용되는 시점에 값을 메모리에 반환한다는 건데요.

 

이게 무슨 소린가 하니..

원래 general 한 list는 다음과 같은 방식으로 진행합니다.

def general_list(value):

    result = []
    
    for i in range(value):
    
        result.append(i)
        
    return result

따라서 general_list에 value를 50을 넣게 되면 list에는 0부터 49까지의 값이 들어갑니다.

여기서 우리가 생각해보아야 할 점은

 

" 그러면 50개의 element가 있는 list는 얼마나 메모리를 차지하고 있을까? "

 

입니다. 

이 방법은 sys 모듈을 사용하면 알아볼 수 있습니다.

>>> import sys

>>> sys.getsizeof(generalList)

520

list에는 520 bytes의 크기를 차지하고 있습니다. 그렇다면 value가 5000인 list의 크기는 어떻게 될까요?

>>> sys.getsizeof(generalList)

43032

크기가 어마어마하게 늘어났습니다...

 

이 "메모리 크기"의 문제는 적은 양의 데이터를 처리할 때는 괜찮습니다. 하지만 Data mining과 같은 대용량의 데이터를 처리하게 된다면!! list의 크기가 너무 커져 문제가 발생할 수 있습니다.

 

이러한 문제 때문에 나온 것이 generator입니다. yield를 사용해 한번에 하나의 element만 반환하게 됩니다.

즉, 데이터를 저장하는 것이 아닌 메모리 주소만 저장을 하고 있다가 호출한 경우에만 값을 반환한다는 건데요. 이 부분은 모든 데이터 값을 저장하고 있는 list와 다르게 필요한 그때그때마다 값을 가지고 와서 큰 용량의 메모리가 필요하지 않습니다. 

 

그렇다면 yield를 사용해서 한 번에 하나의 element만 반환한 경우입니다.

def geneartor_list(value):

  result = []
  
  for i in range(value):
  
	yield i

value를 50을 주었을 때는 112 bytes가 나옵니다.

>>> gen_ex = geneartor_list(50)

>>> sys.getsizeof(gen_ex)		# 크기는 112

112

그렇다면 value 5000일 때는 어떻게 될까요?

>>> gen_ex = geneartor_list(5000)

>>> sys.getsizeof(gen_ex)		# 크기는 112

112

똑같은 메모리 크기가 112 bytes로 나옵니다. value가 엄청나게 커져도 메모리 주소를 기억하고 있기 때문에 크기는 변하지 않습니다.

 

generator는 앞에서 봤던 iterable object이며 next() 함수로 움직입니다.

또한 필요할 때 list형태로 출력할 수 있습니다.

>>> type(gen_ex)

generator

>>> print(next(gen_ex))

0

>>> print(next(gen_ex))

1

>>> print(next(gen_ex))

2

>>> print(list(gen_ex))

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49]

 

generator는 list comprehension과 유사한 형태로 generator형태의 list를 생성합니다. 대신 "[ ]"을 사용하지 않고 "( )"를 사용합니다.

>>> gen_ex = (n*n for n in range(500))		#generator는 []대신 ()를 사용합니다.

>>> print(type(gen_ex))

generator

 

Peer Session

 

조원들과의 회의 중에 재밌었던 부분은 for문에 많이 사용하는 range() 함수는 generator인가 궁금했습니다. 당연히 sequence처럼 저장하지 않기 때문이죠. 그렇다고 generator라고 하기에는 아래와 같이 type이 generator로 나오는 것이 아니고, index방식도 가능합니다. 

>>> type(range(50))			# range의 type은 range입니다..?!

range

>>> value = range(50)

>>> value				# 변수에 저장해도 range()로 나옵니다.

range(0, 50)

>>> value[2]				# indexing도 됩니다.

2

궁금해서 stackoverflow에 찾아보니까..

 

If range() is a generator in Python 3.3, why can I not call next() on a range?

Perhaps I've fallen victim to misinformation on the web, but I think it's more likely just that I've misunderstood something. Based on what I've learned so far, range() is a generator, and generat...

stackoverflow.com

range is a class of immutable iterable objects. 즉, range는 변경 불가능한 iterable object라고 나오네요. next는 바로 못 부르고 iter() 함수를 씌우면 next() 함수를 사용할 수 있다고 합니다.

 

이거 찾아보는 것도 재밌는 시간이었습니다.

 

 

정리하자면 아래와 같습니다.

 

  • list 타입의 데이터를 반환해주는 함수는 generator로 만들기

  • 큰 데이터를 처리할 때는 generator expression을 고려

  • 파일 데이터를 처리할 때도 generator를 쓰기 (용량이 크기 때문)

 

6️⃣ function passing arguments (기본)

 

1) keyword arguments

 

keyword arguments는 arguments에 key 값을 명시해서 작성하는 방법입니다.

keyword arguments를 사용하면 parameters 순서에 상관없이 넣을 수 있다는 장점이 있습니다.

 

하지만 첫 번째 parameter에서 keyword arguments를 사용한 경우 그 뒤에 parameter는 모두 keyword arguments로 써주어야 합니다.

def print_somthing(my_name, your_name):

    print("Hello {0}, My name is {1}".format(your_name, my_name))
    
    

>>> print_somthing(your_name="two", my_name="one")		# arguments에 parameter key를 명시 

Hello two, My name is one

 

2) default arguments

 

default arguments는 parameter의 기본 값을 사용하는 경우입니다.

이러할 경우 사용자가 arguments를 입력하지 않아도 기본값이 출력됩니다.

def print_somthing_2(my_name, your_name="two"):

    print("Hello {0}, My name is {1}".format(your_name, my_name))
    

>>> print_somthing_2("one")		# 두번째 값은 default값인 "two"로 들어갑니다.

Hello two, My name is one

 

7️⃣ function passing arguments (variable-length asterisk)

 

keyword arguments와 default arguments는 모두 함수의 parameter가 정해져 있어야 합니다. 하지만 현실의 문제들은 그렇지가 않죠. 다항 방정식이나 마트의 물건 계산 함수처럼 함수의 parameter가 정해지지 않은 경우가 있습니다.

 

1) variable-length asterisk (*args)

 

이러한 경우에는 variable-length asterisk를 사용해야 합니다.

 

variable-length (가변 인자)는 개수가 정해지지 않은 변수를 함수의 parameter로 사용하는 방법입니다. 이 방법은 keyword arguments와 함께, arguments 추가가 가능합니다. variable-length는 Asterisk(*) 기호를 사용하여 함수의 parameter를 표시합니다.

 

중요한 특징은 입력된 값은 tuple type으로 사용할 수 있으며, 가변 인자는 오직 한 개만 맨 마지막 parameter 위치에 사용이 가능합니다.

 

variable-length는 일반적으로 *args를 변수명으로 사용합니다.

 

아래 함수와 같이 a = 1, b = 2가 입력되며 나머지 3, 4, 5는 tuple type으로 인자를 받게 됩니다.

def asterisk_test(a, b, *args):			# *args는 나머지 (3, 4, 5)를 tuple형태로 받게 됩니다.

    return [a, b, args]
    
    
>>> print(asterisk_test(1, 2, 3, 4, 5))

[1, 2, (3, 4, 5)]

 

 

2) keyword variable-length asterisk (**kwargs)

 

또 다른 variable-length asteriskasterisk(*)을 두 개 사용하여 함수의 parameter를 표시한 경우입니다. 

이러한 경우는 입력된 값을 dict type으로 사용할 수 있습니다. keyword variable-length asterisk는 오직 한 개만 기존 가변 인자 (*args) 다음에 사용할 수 있습니다.

 

keyword variable-length asterisk는 일반적으로 **kwargs로 사용합니다.

def kwargs_test_2(**kwargs):

    print(kwargs)
    
    print("First value is {first}".format(**kwargs))
    
    print("Second value is {second}".format(**kwargs))
    
    print("Third value is {third}".format(**kwargs))
    
    
    

>>> kwargs_test_2(first=1,second=2,third=3)		# keyword arguments로 표시해서 사용합니다.

{'first': 1, 'second': 2, 'third': 3}

First value is 1

Second value is 2

Third value is 3

기본적인 *args와 **kwargs의 구분은 keyword가 있는지 없는지 차이입니다.

**kwargs는 *args와 같이 사용할 수 있으며, *args 이후에 사용해야 합니다. (keyword arguments가 뒤에 와야 하기 때문에)

*args는 keyword가 없는 parameter들을 tuple형태로 받아들이고, **kwargs는 keyword가 있는 parameter들을 dict 형태로 받아들입니다.

def kwargs_test_3(one,two, *args, **kwargs):

    print(one, two)		# 첫 번째와 두 번째 값이 들어갑니다.
    
    print(args)			# keyword가 없는 parameter들이 tuple형태로 출력됩니다.
    
    print(kwargs)		# keyword가 있는 parameter들이 dict형태로 출력됩니다.
    
    
    
>>> kwargs_test_3(3,4,5,6,7,8,9, first=3, second=4, third=5)

3 4

(5, 6, 7, 8, 9)

{'first': 3, 'second': 4, 'third': 5}

 

 

3) asterisk - unpacking a container

 

asterisk(*)이

 

일반 연산에 사용되면 => 곱셈

 

함수 parameter에 사용되면 => variable-length ( *args, **kwargs)

 

으로 사용됩니다. 하지만 asterisk(*)의 사용처는 하나가 더 있습니다.

 

바로 자료형 앞에 사용하는 건데요.

 

asterisk(*)가 자료형 앞에 사용되면 unpacking 역할을 하게 됩니다.

(unpacking은 tuple이나 list형태의 '하나의 자료형'에서 '여러 개 변수'로 나누는 것을 말합니다.)

>>> test = (1,2,3,4)

>>> print(*test)		# tuple이였던 'test'앞에 asterisk(*)을 붙이면 unpacking 됩니다.

1 2 3 4

 

이 것은 위에서 사용한 *args에 사용이 가능합니다.

def asterisk_test(a, *args):

    print(a, args)
    
    print(type(args))
    
    
>>> asterisk_test(1, *(2,3,4,5,6))		# (2,3,4,5,6)의 tuple을 unpacking하면 2,3,4,5,6으로 들어갑니다.

1 (2, 3, 4, 5, 6)			# 따라서 *args는 tuple형이기 때문에 (2, 3, 4, 5, 6)은 튜플로 나오게 됩니다.

<class 'tuple'>

 

또한, 자료형 앞에 asterisk(*)을 두 번 붙이면 dict 형태의 자료형의 unpacking이 가능합니다.

def asterisk_test(a, b, c, d,):

    print(a, b, c, d)
    
    
    
data = {"b":1 , "c":2, "d":3}

>>> asterisk_test(10, **data)		# **data는 dict형태의 자료형을 unpacking해서 value를 내보냅니다.

10 1 2 3

 

재밌는 것은 zip 하고 같이 사용하면 편리하게 사용할 수 있습니다.

>>> for data in zip(*([1, 2], [3, 4], [5, 6])):		# 처음 tuple을 unpacking한 후 zip으로 값들을 출력합니다.

        print(data)
        
        
(1, 3, 5)

(2, 4, 6)

 

asterisk(*)는 사용할 수 있는 부분이 무궁무진하며, 잘 사용하면 코드 작성에 많은 도움이 될 것 같습니다.

Comments