[Python] Advanced Python (1)

jaylight·2020년 11월 22일
0

List Comprehension

  • List Comprehension: 새로운 리스트를 만들 때 사용할 수 있는 간단한 표현식

  • 사용법: []를 사용하여 만들고자 하는 원소를 표현하는 표현식으로 시작해서
    for 루프가 뒤에 따라오는 형식이며, 필요에 따라 뒤에 if문을 추가할 수 있음

new_list = [표현식 for 원소 in 반복 가능한 객체]
new_list_if = [표현식 for 원소 in 반복 가능한 객체 if]
  • 장점: 리스트를 간결하고 직관적인 표현으로 만들어낼 수 있음
new_list = [x for x in range(1, 11)]
print(new_list) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

odd_numbers = [element for element in range(1, 11) if (element % 2) == 1]
print(odd_numbers) # [1, 3, 5, 7, 9]
  • 성능: 아래와 같이 동일한 결과 리스트를 반환하는 함수를 for루프와 리스트 컴프리헨션을 통해 구현한 결과, 일반 for루프를 통해 구현한 코드가 0.062밀리초, 리스트 컴프리헨션을 이용한 코드는 0.024초로 약 2배 정도의 빠르게 실행되는 것을 볼 수 있음
import timeit

def for_loop():
num_list = []
for i in range(1000):
num_list.append(i)

def list_comprehension():
num_list = [ i for i in range(1000) ]

if __name__ == "__main__":
time1 = timeit.Timer("for_loop()", "from __main__ import for_loop")
print("for loop time = ", time1.timeit(number=1000), "milliseconds")

time2 = timeit.Timer("list_comprehension()", "from __main__ import list_comprehension")
print("list_comprehension time = ", time2.timeit(number=1000), "milliseconds")

성능 측정을 위해 함수들을 1,000번 실행한 시간을 측정하는 코드인 timeit을 활용

# 1
cities = ["Tokyo","Shanghai", "Jakarta", "Seoul", "Guangzhou", "Beijing", "Karachi", "Shenzhen", "Delhi"]

def not_start_with_s(cities):
    cities_list = [city for city in cities if not city.startswith("S")]
    return cities_list
print(not_start_with_s(cities)) # ['Tokyo', 'Jakarta', 'Guangzhou', 'Beijing', 'Karachi', 'Delhi']

# 2

population_of_city = [('Tokyo', 36923000), ('Shanhai', 34000000), ('Jakarta', 30000000), ('Seoul', 25514000), ('Guangzhou', 25000000), ('Beijing', 24900000), ('Karachi', 24300000), ('Shenzen', 23300000), ('Delhi', 21753486)]

def cities_pop_dict(list):
    """returns information about city and population in Dictionary form"""
    pop_dict = {city:pop for (city, pop) in list}
    return pop_dict
print(cities_pop_dict(population_of_city)) # {'Tokyo': 36923000, 'Shanhai': 34000000, 'Jakarta': 30000000, 'Seoul': 25514000, 'Guangzhou': 25000000, 'Beijing': 24900000, 'Karachi': 24300000, 'Shenzen': 23300000, 'Delhi': 21753486}

Iterators

  • Iterators: 값을 순차적으로 꺼내올 수 있는 객체
    일반적으로 반복가능한 객체에 대해 iter() 메서드를 적용하여 이터레이터로 만듦
L = [1, 2, 3]
for x in L:
    print(x ** 2, end = ' ') # 1 4 9

반복 가능한 객체인지 확인하는 방법: dir로 호출하여 __iter__ 함수가 있는지 확인

dir: 어떤 객체를 인자로 넣으면, 해당 객체가 어떤 변수와 메서드를 가지고 있는지 나열해주는 내장함수

L=['__add__', '__class__', '__contains__', '__delattr__', '__delitem__',
 '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
 '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__',
 '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__',
 '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__',
 '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__',
 '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend',
 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

위와 같이 반복가능한 객체임(즉, 객체 내에 활용할 수 있는 메서드로 __iter__가 있음)을 확인했을 때,
object.__iter__() 혹은 iter(object)를 입력하면 다음과 같은 결과로 이터레이터를 반환함을 확인할 수 있음

L.__iter__()
# <list_iterator object at 0x7fe7b08f1c40>

이터레이터를 변수에 저장 후, 이터레이터에 대해 dir을 통해 내부의 객체를 확인해보면, 내부에 __next__메서드가 생기는 것을 볼 수 있다.
__next__ 메서드를 호출하면 for문의 동작과 유사하게 값을 하나씩 꺼내올 수 있다.

# 이터레이터 변수 저장
iterator_L = L.__iter__()
iterator_L = iter(L)
  • __next__: 이터레이터 객체에 대해 사용시 for루프 동작과 유사하게 값을 하나씩 꺼내옴
print(iterator_L.__next__()) # 1
print(iterator_L.__next__()) # 2
print(iterator_L.__next__()) # 3
print(iterator_L.__next__()) # 3
# Traceback (most recent call last):
#  File "<stdin>", line 1, in <module>
#StopIteration

next(object) 혹은 object.__next__()를 통해 값을 하나씩 출력 하며, 이터레이터의 인덱스를 벗어나 더이상 출력할 값이 없을 때는 StopIteration이 발생함

StopItration에 대한 예외처리를 통해 다음과 같은 while문 구현이 가능함

I = iter(L)
while True:
    try:
        X = next(I)
    except StopIteration:
        break
    print(X**2, end = " ")

Generators

  • 제너레이터: 일반적 함수와 같이 값을 반환하기는 하지만 산출(yield)한다.
    즉, 이터레이터를 생성해주는 함수라고 볼 수 있음
def generator_squares():
    for i in range(3):
        yield i ** 2
print("gen object=", end=""), print(generator_squares())

# gen object=<generator object generator_squares at 0x7fc146809900>

Generators 함수 형식에 대해 yield 대신 return을 쓰면?
위의 generators_squares() 함수에 대해 yield 대신 return을 사용하면, 일반 함수로써 첫 값으로 0을 반환한 뒤 함수가 종료된다.

def generator_squares():
    for i in range(3):
        return i ** 2 # 0

위에서 yield는 제너레이터 함수에서 값을 반환할 때 사용되며,
yield 호출후에 다시 next가 호출될 때까지 현재의 상태에 머물다가 next 함수가 호출되면 이전의 상태에 이어서 다음 연산을 수행

제너레이터를 dir로 내부의 변수와 메서드를 확인해보면, __iter____next__가 포함되어 있다.

print("dir gen=" end=""), print(dir(generator_squares()))

# dir gen =['__class__', '__del__', '__delattr__', '__dir__', '__doc__',
# '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
# '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__',
# '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__',
# '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__',
# 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

이터레이터 함수를 만들 때, __iter__ 함수를 사용하는 과정을 거친 것과 다르게,
제너레이터 함수는 yield를 활용하여 제너레이터 형식으로만 만들어주면 자동으로 제너레이터로써 동작한다.

  • __next__(): 이터레이터와 마찬가지로, 다음 차례의 값을 반환하며, 반환할 값이 더이상 없을 경우 StopIteration이 발생
gen = generator_squares()
print(gen.__next__()) # 0
print(gen.__next__()) # 1
print(gen.__next__()) # 4
print(gen.__next__()) 
# StopIteration                             Traceback (most recent call last)
# <ipython-input-52-9340d28f24b7> in <module>
# ----> 1 print(gen.__next__())
# StopIteration: 
  • __send__(): 제너레이터 함수 실행 중 __send__() 함수를 통해 값 전달이 가능함

아래는 send 함수의 인수를 통해 받은 값을 yield로 받아 received_value에 할당하고, 그 값의 2배 수를 리턴 받는 제너레이터이다.

def generator_send():
    received_value = 0

    while True:

        received_value = yield
        print("received_value = ",end=""), print(received_value)
        yield received_value * 2

gen = generator_send()
next(gen)
print(gen.send(2))
# received_value = 2
4
next(gen)
print(gen.send(3))
# received_value = 3
6

제너레이터에서는 위의 case 처럼 yield를 통해 제너레이터 실행 중 값을 전달할 수 있으며, 이를 응용하여 제너레이터 함수를 사용해서 main 실행 루프의 연산 결과에 따라 호출을 제어할 수 있음

  • 제너레이터 표현식(Generator Expression): Lazy Evaluation을 위해 사용하며, 괄호(())를 활용하여 생성

Lazy Evaluation(느긋한 계산법): 계산의 결과값이 필요할 때까지 계산을 늦추는 기법

L = [1, 2, 3]

def generate_square_from_list():
    result = (x*x for x in L)
    print(result)
    return result

def print_iter(iter):
    for element in iter:
        print(element)

print_iter(generate_square_from_list())
  • Assignment:
import time

def print_iter(iter):
    for element in iter:
        print(element)

def lazy_return(num):
    print("sleep 1s")
    time.sleep(1)
    return num

print("comprehension_list=")
comprehension_list = [ lazy_return(i) for i in L ]
print_iter(comprehension_list)

print("generator_exp=")
generator_exp = ( lazy_return(i) for i in L )
print_iter(generator_exp)
  1. comprehension_list

[lazy_return(i) for i in L]의 경우, L 리스트에 담긴 값 1, 2, 3을 차례로 불러 lazy_return()의 인자로 넣어서

(1) print("sleep 1s")를 출력하고
(2) 1초 간 쉬고
(3) 반환받은 결과값을 차례로 리스트에 넣는다.

따라서 comprehension_list에는 [1, 2, 3] 값이 들어가며, 최종적으로 다음 print_iter(comprehension_list)에서 for루프를 통해 comprehension_list의 값 1, 2, 3을 차례로 돌아 출력한다.

  1. generator_exp

(lazy_return(i) for i in L)은 제너레이터 표현식을 만들며 이를 generator_exp라는 변수명에 저장한다. 따라서 위 과정을 마친 후의 generator_exp변수는 제너레이터이며, print_iter(generator_exp)를 통해 제너레이터 객체를 순회하며

(1) print("sleep 1s")를 출력
(2) 1초간 쉬고
(3) L의 값을 순회하며 출력

과정을 반복한다.

Lazy Evaluation 개념 정리

  • 개념
  • 장점

lambda Expression

  • lambda: 인라인 함수를 정의할 때 사용하며,
    익명함수(Anonymous Functions) 또는 람다 표현식(Lambda Expression)으로 칭함

  • 기존 함수와의 차이점

    1. 이름의 유무
    2. 여러 문장의 블록으로 구성이 불가능하고, 간단한 표현식만으로 작성
    3. return문이 없어도 표현식의 결과가 리턴
# 함수
def name(arg1, arg2, ...):
    block of statement
    
# lambda 함수
lambda argument1, argument2, ... argumentN : expression using arguments

lambda 또한 중첩되거나 복잡한 구조를 가질 수 있지만, 일반적으로 한줄로 간단한 형식으로 활용

  • 예시
f = lambda x, y, z : x + y + z

print(f) # <function <lambda> at 0x7f7edf1698b0>
print(f(1, 2, 3)) # 6
  • 활용
    1. 인라인 콜백 함수 생성
    2. 함수 내에서 복잡한 처리를 할 수 없을 때

콜백함수

  • 특정 이벤트가 발생했을 때 호출되는 함수
  • 해당 콜백함수가 여러블록으로 구성된 실행문이 아니고 다른 컴포넌트에서 사용되지 않는다면 해당 컴포넌트만을 위한 람다 표현식이 적절함
Lambdas = [
    lambda x : x ** 2,
    lambda x : x ** 3,
    lambda x : x ** 4
]

for lambda_func in Lambdas:
    print( lambda_func(2) )
# 4
# 8
# 16
  • Assignment
  1. types 모듈을 활용해 LambdaType 확인하기
import types

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

print(f) # <function <lambda> at 0x7f7edf169af0>
print(type(f)) # <class 'function'>
print(type(f) == types.LambdaType) # True

types 모듈에 포함된 다른 타입들: <참조>

  1. 비밀번호 조건을 확인하는 함수를 lambda 표현식을 활용한 형식으로 수정하기
# 기본 함수
def check_password(password):
    if len(password) < 8:
        return 'SHORT_PASSWORD'

    if not any(c.isupper() for c in password):
        return 'NO_CAPITAL_LETTER_PASSWORD'

    return True
    
# lambda 함수 활용
lambdas = [
    lambda password : 'SHORT_PASSWORD' if len(password) < 8 else None,
    lambda password : 'NO_CAPITAL_LETTER_PASSWORD' if not any(c.isupper() for c in password) else None
]

def check_password_using_lambda(password):

    for f in lambdas:
        if f(password) is not None:
            return f(password)

    return True


print( check_password_using_lambda('123') )            # SHORT_PASSWORD
print( check_password_using_lambda('12356789f') )      # NO_CAPITAL_LETTER_PASSWORD
print( check_password_using_lambda('123456789fF') )    # True

lambda 표현식 안에서 if문 사용하기

lambda arguments: '값1' if '판별식1' else '값2' if '판별식2' else

all()any()

# all
all(True, True, True) # True
all(True, False, False) # False
all(False, False, False) # False
#any
any(True, False, False) # True
any(True, False, False) # True
any(False, False, False) # False

0개의 댓글