functional programming, lazy technique, 상속

jwKim·2023년 7월 3일
0

1. functional programming

1.1. callable과의 융합

callable의 개념에서 봤듯 괄호는 붙일 수 있는 대상이 따로 정해져있다. 그래서 괄호를 붙일 수 없는 대상에게 괄호를 붙이면 not callable error가 발생한다.

class A:
  pass

a = A()
a()

그런데 class 내부에 __call__을 선언하면 괄호를 위와 같은 상황에서 괄호를 붙일 수 있다.

class A :
    def __call__(self):
        print('call')
>>> b = A()
>>> b()
call

그렇다는 것은 class에도 closure 기법을 적용할 수 있다는 뜻이다.

class A :
    def __init__(self, x): # -> 첫 번째 괄호
        self.x = x
    def __call__(self, y): # -> 두 번째 괄호
        return self.x * y

A(3)(1), A(3)(2), A(3)(3), A(3)(4) # => 3을 곱하는 함수처럼 사용 가능하다

따라서 class도 하나의 동작을 하는 함수처럼 사용이 가능해졌다. 이 개념 때문에 functional programming 개념과 객체지향의 경계가 무너지게 되었다. 따라서 함수와 class 모두 functional programming을 지원할 수 있다는 것을 기억하고, 상황에 따라 함수로도, class로도 모두 구현할 수 있어야한다.

1.2. functional programming을 구현하는 주요 함수들

간단히 리뷰하자면 데이터 처리와 메모리, 실행 속도, 코드 길이 부분에서 모두 좋은 성능을 발휘하는 functional programming은 python을 사용하기 위해서는 반드시 알아야 하는 방법이다. python에는 funcional programming을 가능하게 하는 built in으로 구현되어있어서 알아야 할 필요가 있다.

map, filter, reduce가 그것인데, 세 함수는 모두 iterator 개념이 적용되어있고 모두 함수를 인자로 받는 higher order이다.(=함수를 인자로 받는 함수들) 이 함수들은 functional programming을 잘 나타내는 주요 함수들이니 자세히 알아보자.

1.2.1. map

map은 인자로 받은 iterator 에서 값을 하나씩 뽑아서 인자로 받은 함수에 넣은 결과를 반환한다. 아래 예를 보자.

>>> m = map(lambda x : x+1, [1, 2, 3, 4])
>>> next(m)
2 
>>> next(m)
3
>>> next(m)
4
>>> next(m)
5

next()는 iterator와 generator가 갖는 중요한 특징으로 호출 시 하나의 데이터를 반환하고 다음 데이터를 반환할 준비를 한다. 모든 데이터를 반환하면 StopIter error가 발생한다.

map을 사용하는 또 따른 예이다.

# 예를 들어 b열에 10씩 더해야한다면??
import pandas as pd

data = {
    'a' : [1, 2 , 3],
    'b' : [5, 6, 7]
}

df = pd.DataFrame(data)

list(map(lambda x : x+10, df['b']))

이번에는 값을 모두 확인하기 위해 next()를 사용하지 않고 list로 바꾸었다. 이렇게 iterator를 받아 함수를 적용하는 함수가 map이다.

그런데 map에대한 설명을 보면 *iterables 라고 나와있다. 그렇다는 것은 iterator를 두 개 이상 받을 수 있다는 뜻 아닌가?

# iterable을 여러개 쓰면 같은 인덱스에 있는 것끼리 뽑아서 함수에 적용한다!
>>> list(map(lambda x,y : x + y,[1, 2, 3, 4], [10, 20, 30, 40]))
[11, 22, 33, 44]

맞다! iterator를 여러 개 넣어주면, 인덱스를 맞춰서 데이터를 뽑아 함수에 적용한다.

1.2.2. filter

filter도 인자로 받은 iterator에서 원하는 값만 걸러내는 함수이다.

>>> list(filter(lambda x : x > 3, [1,2,3,4,5,6,7]))
[4, 5, 6, 7]

filter는 predicate(=true나 false를 반환하는 함수)를 인자로 받는다. iterator에서 predicate의 결과가 True인 것만 걸러서 반환한다.

1.2.2.1. predicate 함수 - all(), any()

predicate가 나온 김에 추가로 설명하자면, all()any() 함수를 알아보자. all()any()도 iterator를 인자로 받는다. all()은 입력받은 iterator가 모두 True일 때 True를 반환하고 하나라도 False가 있으면 False를 반환하다. any()는 하나라도 True가 있을 떄 True를 반환하고, 모두 False일 때 False를 반환한다.

>>> all([1, 2, 3])
True

>>> all(['2', None])
False

>>> any([1, 2, '', None])
True

>>> any(['', None, 0])
False

1.2.3. reduce

reduce를 알기 위해서는 accumulator를 알아야한다.

1.2.3.1. accumulator

from itertools import accumulate

accumulate도 iterator와 함수를 인자로 받는다. 입력받은 함수에 맞는 누적값을 반환하는 함수이다.

>>> c = accumulate([1, 2, 3], lambda x, y : x+y)
>>> next(c)
1

>>> next(c)
3

>>> next(c)
6

iterator에서 하나를 뽑아 전 계산 결과에 동일한 연산을 한다.

1.2.3.2. reduce 사용법

reduce는 accumulator와 비슷한데, 마지막 결과만 반환한다는 점이 특징이다. accumulator의 예시와 동일하게 해보자.

>>> from functools import reduce

# reduce는 accumulate의 결과 중 마지막 결과만 반환한다!!!!
>>> reduce(lambda x, y : x + y, [1, 2, 3])
6

reduce를 사용해 팩토리얼을 만들어보자.

>>> reduce(lambda x, y : x * y, [1, 2, 3, 4, 5])
120

lambda 부분은 조금 더 간결히 사용해보자.

>>> from operator import mul
>>> reduce(mul, [1, 2, 3, 4, 5])
120

팩토리얼을 직접 구현하려면 코드를 꽤 많이 쳐야하는데 operator와 reduce를 사용하니 단 한줄에 구현되었다. 굉장히 python스럽고 functional programming스러운 방식이다!

2. lazy technique

2.1. next

2.1.1. iterator, generator

Python에는 iterable이라는 중요한 개념이 있다. iterable은 괄호가 붙어 객체가 되면 iterator 객체가 될 수 있는 것들이다. iterable은 내부적으로 __iter__ 가 실행되면 iterator 객체가 된다. iterator와 비슷한 특징을 갖는 객체로 generator가 있다.

iterator와 generator의 가장 큰 특징은 next 함수를 사용할 수 있다는 점이다. (map, filter, reduce 예제에서도 확인 했으므로 자세한 설명은 생략)

iterator는 iterable이 정의되어 객차가 되면 만들어진다고 했다. 그에 반해 generator를 만들 수 있는 방법은 두 가지가 있다.

  • 튜플에 comprehension을 적용하면 generator 객체를 만들 수 있다.
  • 함수에 yield를 사용하여 generator 객체를 만들 수 있다.

특별히 yield를 사용하는 방법에는 from이 사용될 수 있다.

# 방법 1 - from 사용 x
def make_generator():
  yield 1
  yield 2
  yield 3
  
# 방법 2 - from 사용 o
def make_generator():
  yield from[1, 2, 3]

2.2. iterator와 관련된 기능들

2.2.1. cycle

iterator는 next로 인자를 다 뽑아내면 StopIteration 에러가 발생한다. 그런데 모든 인자를 뽑아내고도 에러가 발생하지 않게 하는 방법이 바로 cycle이다. cycle은 마짐가 인자까지 반복이 끝나면 첫 번째 인자로 돌아가게하는 특징을 갖는다.

>>> from intertools import cycle
>>> a = cycle([1, 2, 3])
>>> next(a)
1
>>> next(a)
2
>>> next(a)
3
>>> next(a)
1

2.2.2. permutations

iterator를 사용하는 함수 중에는 순열을 반환하는 permutations가 있다.

>>> from itertools import permutations
>>> p = permutations([1, 2, 3, 4], 3)
>>> next(p)
(1, 2, 3)
>>> next(p)
(1, 2, 4)
>>> next(p)
(1, 3, 2)
>>> next(p)
(1, 3, 4)
>>> next(p)
(1, 4, 2)

2.2.3. combinations

iterator를 사용하는 함수 중에는 콤비네이션을 반환하는 함수도 있다.

>>> from itertools import combinations
>>> c = combinations([1, 2, 3], 2)
>>> next(c)
(1, 2)
>>> next(c)
(1, 3)
>>> next(c)
(2, 3)
>>> next(c)
StopIteration Error

3. 상속

3.1. 상속 개요

객체지향 언어에서는 함수와 클래스를 재활용의 목적으로 만들어 사용한다. 그 중 클래스는 다른 클래스의 특성을 그대로 가져와 기본 틀로 사용하는 방법이 있는데, 상속이 그것이다. Python에서 모든 클래스는 object클래스를 상속한다. 상속은 가져오는 클래스 전부를 사용할 수도 있고, 일부만 사용할 수도 있다.

흔히 클래스 간의 상속으르 '물려받음'으로 이해하기 쉬운데, 사실 물려받는 개념보다는 값을 지정하는 순서와 관련이 있다. 자식 클래스에서 처리할 수 없는 값이나 메소드를 부모 클래스에 있는 값이나 메소드로 지정하는 것이기 때문이다. 아래 코드를 보자.

class A:
  x = 1
  y= 10

class B(A): # 클래스 A 상속
  x = 2 # 오버라이딩
>>> B.x
2
>>> id(A.y)
1956488571472
>>> id(B.y)
1956488571472

클래스 B에 x가 있어서 B.x를 호출했을 때 1이 아닌 2가 나왔다. 그런데 y는 B에 없으므로 부모 클래스에서 값을 가져오게 된다. 부모 클래스의 값을 가져온다는 것을 확인하기 위해 메모리 id를 확인해보면 두 메모리 주소가 같은 것을 확인할 수 있다. 즉, B에서는 y에 대한 값을 가져올 수 없으므로 부모 클래스인 A에서 값을 가져온 것이다.

3.2. 다중 상속

객체지향 언어에서는 상속에 대하여 단일 상속, 다중 상속이 있는데, python에서는 다중 상속만 지원한다. 위에 class A와 class B만 보더라도 다중 상속 되었다.(class B는 A와 object 모두 상속한 것이기 때문이다.)

다중 상속에서 가장 중요한 것은 상속 순서이다. 아래 코드는 TypeError가 발생하는 코드이다.

# MRO 에러 발생 
class A:
    pass

class B(A):
    pass

class C(A,B):
    pass

에러 발생 이유는 C를 선언할 때 생긴다. class 선언 순서는 아래와 같다. 클래스 A가 중복되어 호출되기 때문에 호출 순서가 엉키게 되기 때문에 에러가 발생한다.

C → A → B → A

위의 상속 체계를 올바르게 고쳐보자

# 이렇게 순서를 바꾸면 된다.
class A:
    pass

class B(A):
    pass

class C(B, A):
    pass

이 순서가 올바른 이유는, python에서 내부적으로 메소드 호출 순서를 지정하기 때문이다.(정확히 말하자면 Meta class에서 이를 지정한다.) 메소드 호출 순서를 확인하기 위해서는 mro() 함수를 사용하면 된다.

>>> C.mro()
[__main__.C, __main__.B, __main__.A, object]

mro는 Meta class에서 정의된 메소드이다. (meta class에 대해서는 6일차에서 배우자)

0개의 댓글

관련 채용 정보