개인적으로 python을 사용하면서 활용도 높게 pythonic하게 코딩이 가능한 문법이나 tip을 정리했다. if-else/for one line 이나 dunder나 함수형 method (filter, map...)은 제외 했다.
가장 기본적인, 짤에서 볼 수 있는 걸 살펴보자.
사실 get
은 __get__
dunder method를 호출하는 것이다. dict는 기본적으로 get
아주 잘 사용하는게 중요하다. python 초심자일때 생각보다 많이 놓치는 것 같다. 물론 나도 그랬다 :')
key를 include로 check를 하고 다른 값을 넣어야 한다면, dict["key"]
로 접근해서 attribute error를 맛보고 코드를 include (if attr in dict: ... else)
로 늘리지 말자!! get(attribute, 없으면 사용할 value)
를 사용하자!
물론 key include를 check할때 if attr in dict
쓰는게 당연하다,, 하지만 특정 API 호출 request의 response.json() dict에서 Optional할 수 있는 key 접근할 땐 제발, 더욱이, get(attribute, 없으면 사용할 value)
를 사용하자!!
python 3.8 이상부터 사용가능하며 PEP 572에 Assignment Expressions이란 이름으로 정의되어 있다.
바다 코끼리 연산자는 get을 쓸때 유용하다. 표현식의 결과를 변수에 할당하고, 동시에 반환 하는 연산자다. 예시를 보면 바로 캐치가 가능하다.
if map.get("won"):
result = a + b + map.get("won")
unit = map.get("won")
# 바다코끼리 연산자 활용
if c := map.get("won"):
result = a + b + c
unit = c
# 반복문과 컨프리헨션에서도 유리하다.
while True:
chunk = file.read(128)
if not chunk:
break
process(chunk)
while chunk := file.read(128):
process(chunk)
list
, set
, dict
등을 만들때 많이 사용한다. arr = []
for i in range(0, 10):
arr.append(i)
print(arr) # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 대신 아래와 같이 one-line으로 가능하다.
arr = [i for i in range(0, 10)]
print(arr) # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# 역시 one-line if - else 와 묶어서 아래와 같이 사용가능하다.
arr = [i if i % 2 == 0 else 0 for i in range(0, 10)]
print(arr) # Output: [0, 0, 2, 0, 4, 0, 6, 0, 8, 0]
# 2차원 배열도 간단하게 가능하다.
arr = [[0 for _ in range(5)] for _ in range(5)]
print(arr)
# Output: [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
제네레이터는 "iterator를 생성하는 함수" 이다. 그리고 메모리 관점에서 효율이 높다. 쓸모없는 변수 사용이나 메모리 사용을 줄일 수 있기 때문이다. 이 얘기에 대한 자세한 사항은 해당 글로 대체하겠다.
만드는 방법은 yeild
키워드를 사용하면 자연스럽게 generator가 된다. 처음 접하면 굉장히 생소하게 다가올 수 있다. 정확한 이해를 하려면, iterator
와 iter
, next
를 알고 있어야 한다.
def test():
for i in range(7):
yield i
arr = test()
print(arr)
for val in arr:
print(val)
# Output
<generator object test at 0x0362EFB0>
0
1
2
3
4
5
6
간단하게 yield
를 return
이라고 생각해보자. arr 자체는 test() function을 excute하고 있고, for - loop 에서 arr을 순회하면서 val을 출력할때 마다 내부 i 값의 진행 상태를 기억하고 있는 것 같다.
generator는 return
을 해도 그때의 상태가 함수 내부변수에 기록된다. 첫번째 yield i 를 한 순간 0을 반환하고 멈춰있는 상태라고 볼 수 있다. 이렇게 generator는 완성된 리스트가 아닌 필요에 따라 값을 계산해서 반환하는 함수라고 볼 수 있다.
그리고 이러한 특성때문에 끝이 없는, 무한한 순서가 있는 객체를 모델링할 때 많이 사용된다. 아래 2가지 예시를 살펴보자!
def number_sequence(n):
i = 0
while i < n:
yield i
i += 1
sequence = number_sequence(5)
for number in sequence:
print(number)
# Output:
0
1
2
3
4
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Generate the first 10 Fibonacci numbers
fib = fibonacci()
for i in range(10):
print(next(fib))
# Output:
0
1
1
2
3
5
8
13
21
34
# Define a generator function to generate Fibonacci numbers
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# Use a lambda function with filter() to filter the Fibonacci sequence
filtered_fibonacci = filter(lambda x: x % 2 == 0, fibonacci())
# Print the first 10 even Fibonacci numbers
for i in range(10):
print(next(filtered_fibonacci))
# Output:
0
2
8
34
144
610
2584
10946
46368
196418
int a = 1;
int b = 3;
int tmp = a;
a = b;
b = tmp;
a, b = b, a
함수를 정의할때 파라미터값을 정의하는게 애매할때가 있다. 특히 범용성일 가지거나 library 성격을 가지는 function (or method) 일 경우 더욱 그렇다.
특히 함수를 호출할때 전달하는 "인자"만 보고 해당 값이 왜 필요한지 추측하게, 사용할때 바로 의도를 알아차리게 하고 싶을때가 있다.
그래서 positional argument와 keyword argument를 많이 혼용을 하는데, *args
는 순서가 중요하다. 그래서 순서와 상관없이 fun(변수=값)
형태의 **kward
를 많이 쓰는데, 이때 필요한 key=value
keyword argument를 강제할 수 있다.
def fun_print(*, a, b):
print(a, b)
>>> fun_print(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: fun_print() takes 0 positional arguments but 2 were given
>>> fun_print(a=1, b=2)
1 2
특히 API를 설계하다보면 request의 url query string
또는 request payload(body)
값을 validate 또는 정형화 목적으로 당연하게 modeling을 하게 된다. 그게 python 진영에서는 주로 serializer
, pydantic
또는 dataclass
가 그런 역할을 한다.
하지만 해당 model(class) instance를 만들때 굉장히 많은, 다양한 값이 필요하게 된다.
from pydantic import BaseModel
class SalesSummary(BaseModel):
total_tran_cnt: int
total_tran_amt: int
total_app_cnt: int
total_app_amt: int
total_cnl_cnt: int
total_cnl_amt: int
my_sales = SalesSummary(
total_tran_cnt=10,
total_tran_cnt=1000,
...
)
sumary_dict = {
"total_tran_cnt": 10,
"total_tran_amt": 2000
...
}
# 비즈니스 로직
res = request.get(...).json()
sumary_dict["total_cnl_amt"] = res.get("cancled")
...
SalesSummary
instance를 만들때 sumary_dict
에서 하나씩 key 값을 가져와야 하는가? 이때 언패킹을 가장많이 사용한다. 아래 한 줄로 끝이다!my_sales = SalesSummary(**sumary_dict)
def fun_print(a, b, c):
print(a, b)
>>> l = [1, 2, 3]
>>> fun_print(*l)
1 2 3
조금 더 단어에 대한 정확한 정의를 하자면, 언패킹은 "시퀀스 객체의 각 요소들을 개별 변수로 풀어서, 각각의 변수로 함수의 인자로 전달하는 기능" 이며, 패킹은 "고정되지 않은 여러개의 인자(parameter)를 묶은 하나의 시퀀스 인자" 이다.
패킹의 가장 살펴보기 좋은 예시는 print
이다!
클로저(Closure) 와 데코레이터(Decorator) 에 대한 고찰을 담은 글을 선행하고 오면 매우 좋다.
위 글을 다 보면다면, 클로저의 정확한 의미와 데코레이터가 어떻게 액션되는지 캐치할 수 있을 것 이다. 그러면 우리는 아주 simple한 몇가지를 할 수 있다. info logging을 해주는 또는 print 해주는 데코레이터를 만들어 보자.
def print_argument(func):
def wrapper(the_number):
print("Argument for", func.__name__, "is", the_number)
return func(the_number)
return wrapper
@print_argument
def add_one(x):
return x + 1
print(add_one(1))
# Output:
"""
Argument for add_one is 1
2
"""
fibonacci
를 활용한 excute time logging을 해주는 데코레이터를 만들어보자!import logging
import time
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
def log_execution_time(func):
def wrapper(*args, **kwargs):
start_time = time.monotonic()
result = func(*args, **kwargs)
end_time = time.monotonic()
elapsed_time = end_time - start_time
logging.debug(f'{func.__name__} executed in {elapsed_time:.4f} seconds')
return result
return wrapper
@log_execution_time
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
fibonacci(10)
"람다"란, 프로그래밍 언어에서 사용되는 개념으로 익명의 함수, 이름 없는 함수를 지칭하는 용어다. 보통 함수형 프로그래밍에서 "람다"라는 개념을 많이 차용한다.
너무 depth있는 람다 함수 보다 pydantic하게 사용하기 좋은 형태의 lambda식을 살펴보자.
# 기본 표현 형식
lambda 인자 리스트 : 표현식(반환값)
# if을 사용한다면 콜론없이 표현한다.
lambda x : exp1 if 조건 else exp2
s = ['banana', 'kiwi', 'apple', 'cucumber']
s = sorted(s, key=lambda x : len(x))
print(s) # ['kiwi', 'apple', 'banana', 'cucumber']
max = lambda x, y : x if x > y else y
max(10, 200) # 200
max(200, max(10, 20)) # 200
max(200, max(10, max(50, 1000))) # 1000
filter(조건 함수, 순회 가능한 데이터)
# Define a list of numbers
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Use a lambda function with filter() to filter the list
filtered_numbers = list(filter(lambda x: x % 2 == 0, numbers))
# Print the filtered list
print(filtered_numbers) # Output: [2, 4, 6, 8]
List[Dict]
형태의 데이터 대상으로 특정 attribute값이 충족하는 값만 뽑아낼때 좋다.>>> users = [{'mail': 'gregorythomas@gmail.com', 'name': 'Brett Holland', 'sex': 'M'},
... {'mail': 'hintoncynthia@hotmail.com', 'name': 'Madison Martinez', 'sex': 'F'},
... {'mail': 'wwagner@gmail.com', 'name': 'Michael Jenkins', 'sex': 'M'},
... {'mail': 'daniel79@gmail.com', 'name': 'Karen Rodriguez', 'sex': 'F'},
... {'mail': 'ujackson@gmail.com', 'name': 'Amber Rhodes', 'sex': 'F'}]
>>> def is_man(user):
... return user["sex"] == "M"
...
>>> for man in filter(is_man, users):
... print(man)
...
{'mail': 'gregorythomas@gmail.com', 'name': 'Brett Holland', 'sex': 'M'}
{'mail': 'wwagner@gmail.com', 'name': 'Michael Jenkins', 'sex': 'M'}
>>> for woman in filter(lambda u: u["sex"] != "M", users):
... print(woman)
...
{'mail': 'hintoncynthia@hotmail.com', 'name': 'Madison Martinez', 'sex': 'F'}
{'mail': 'daniel79@gmail.com', 'name': 'Karen Rodriguez', 'sex': 'F'}
{'mail': 'ujackson@gmail.com', 'name': 'Amber Rhodes', 'sex': 'F'}
break
가 있을 때 많이 사용한다. 기본 개념은 "for가 모두 루프하고 끝난다면" 이다. # break 없는 for loop
for i in range(5):
print(i, end=' ')
else:
print("for문이 끝까지 실행됬습니다!")
# Output: 0 1 2 3 4 for문이 끝까지 실행됬습니다!
# if와 break가 있을 때, break를 만난다면
for i in range(5):
if i == 2:
break
print(i, end=' ')
else:
print("for문이 끝까지 실행됬습니다!")
# Output: 0 1
# if와 break가 있을 때, break를 만나지 않는다면
for i in range(3, 5):
if i == 2:
break
print(i, end=' ')
else:
print("for문이 끝까지 실행됬습니다!")
# Output: 3 4 for문이 끝까지 실행됬습니다!
기본적으로 heapq
의 성질을 아는데, 사용을 잘 안하는 경우가 많다.
n 번째를 빠르게, 용도에 따라 빈번하게 찾아야 할 때 가장 쉽고 빠르게 접근할 수 있는 방법은 "완전이진트리"를 떠올리게 되고, 자연스럽게 "우선순위큐"를 떠오르게 된다.
기본적으로 파이썬 heapq
모듈은 priority queue 알고리즘을 제공한다. 모든 부모 노드는 그의 자식 노드보다 값이 작거나 큰 이진트리(binary tree) 구조인데, 내부적으로는 인덱스 0에서 시작해 k번째 원소가 항상 자식 원소들(2k+1, 2k+2) 보다 작거나 같은 최소 힙의 형태로 정렬된다.
역시, 당연하게 list에서 메번 sorting을 해가면서 찾는 것 보다 훨씬 빠르다. 하지만 당연히, 삽입과 삭제에는 추가 연산이 따른다. 즉 이미 모두 메모리에 올리고 n 번째를 찾을 때 가장 유리하다는 것이다.
import heapq
scores = [51, 33, 64, 87, 91, 75, 15, 49, 33, 82]
print(heapq.nlargest(3, scores)) # [91, 87, 82]
print(heapq.nsmallest(5, scores)) # [15, 33, 33, 49, 51]
import heapq
from django.db.models import F, Sum
from myapp.models import Sales
N = 10 # get top 10 sales amounts
# get the top N sales amounts using heapq.nlargest
top_sales = heapq.nlargest(
N,
Sales.objects.filter(...).annotate(
total_sales=Sum('amount')
).values_list('total_sales', flat=True)
)
order by
로 접근해서 slice하는게 더 좋고 빠를 수 있다. 3.9 version 이상에서 가능하다.
두개의 dict
를 하나의 key:value
쌍으로 합치되 중복되는 key의 value는 하나의 string 덩어리로 합칠 수 있다.
first_dictionary = {'name': 'Fatos', 'location': 'Munich'}
second_dictionary = {'name': 'Fatos', 'surname': 'Morina',
'location': 'Bavaria, Munich'}
result = first_dictionary | second_dictionary
print(result)
# {'name': 'Fatos', 'location': 'Bavaria, Munich', 'surname': 'Morina'}
dictionary_one = {"a": 1, "b": 2}
dictionary_two = {"c": 3, "d": 4}
merged = {**dictionary_one, **dictionary_two}
print(merged) # {'a': 1, 'b': 2, 'c': 3, 'd': 4}
dictionary_one = {"a": 1, "b": 2}
dictionary_two = {"a": 3, "d": 4}
merged = {**dictionary_one, **dictionary_two}
print(merged) # {'a': 3, 'b': 2, 'd': 4}
사실 python의 이런 축약형, pydantic한 technic은 100가지를 정리해 놓은 글도 있고, 창의적인 방법으로 접근한 글도 많다.
우리가 "협업을 하는 개발자" 라면 항상 "과유불급"을 생각하게 된다. pydantic도 좋고, 퍼포먼스 및 메모리 향상을 위함을 추구하는 것도 좋지만 결국 "우리가 다 같이 쉽고 빠르게 이해하고, 규칙을 기반으로 두고 협업할 수 있는가" 가 핵심가치다.
그런면에서 적어도 이 12가지는 현업에서도 큰 부담없이 자유롭게 사용이 가능할 것이라고 생각한다. 과한 잔기술은 나 혼자 작업하는 토이프로젝트에서 하자! python이 duck typing 언어에 자유도가 너무 높으니 항상 이런 부분이 가장 큰 장점이자 단점이라고 생각한다.
그래서 type hint를 계속해서 신경쓰도록 하자! 적어도 function parameter의 type, return type 만큼은 말이다!
좋은글 감사합니다.