https://eyeballs.tistory.com/648

 

[IT] CS 면접 대비 Python 질문 모음

< First-Class 함수 > First-Class 함수 : 프로그래밍 언어가 함수(Function)를 first-class 시민으로 취급하는 것 함수가 다른 함수의 인자로 전달될 수 있고, 함수의 결과로 리턴될 수 있고, 변수에 함수를 할

eyeballs.tistory.com

 

 


python3 : print("hi eyeballs!")
python2 : print "hi eyeballs!"

 


python 은 동적 언어(dynamic language) 이기 때문에
변수를 생성할 때 타입을 직접 작성하지 않음
또한, 변수(데이터 값)는 객체이며
객체는 내부에 타입 정보, 실제 값, 객체 ID, 참조 횟수 등을 갖고 있음
그래서 type(123) 등으로 타입을 확인할 수 있는 것임


타입을 확인하기 위해 아래 메소드를 사용 가능

type(1)  # <class 'int' >
isinstance(1, int)  # True
isinstance("hi!", int)  # False

 


python은 강타입(strong type) 언어임
즉, 객체의 값 변경은 가능하지만, 객체의 타입은 변경할 수 없음

 


python 에서 변수는 객체를 가리키는 이름임
다른 정적 언어(static language) 들은 변수 자체에 타입이 있기 때문에,
변수에 값을 할당 할 때부터 타입을 지정해줘야 하지만
python 은 동적 언어이기 때문에, 변수 자체에 타입이 없고 
변수에 값을 할당 할 때 타입 지정이 필요 없음


a = 1
b = a
print(a)  # 1
id(a)  #9440320
print(b)  # 1
id(b)  #9440320

a = 2
print(a)  # 2
id(a)  # 9440355
print(b)  # 1
id(b)  # 9440320

여기서 id 값이 달라짐
왜냐면 a = 2 를 통해 a에 다른 불변 객체(2)를 바라보도록 할당했기 때문
b는 여전히 기존 불변 객체(1)를 바라보고 있음


a = [1,2,3]
b = a
print(a)  # [1,2,3]
print(b)  # [1,2,3]

a[0] = 99
print(a)  # [99,2,3]
print(b)  # [99,2,3]

반대로 여기선 b 가 a와 동일하게 업데이트 되었음
왜냐면 list 는 가변 값의 배열이기 때문

하지만 list 객체 자체는 불변임
만약 a를 새로 할당했다면, b 는 바뀌지 않았을 것

a = [1,2,3]
b = a
a = [2,3,4]
print(a)  # [2,3,4]
print(b)  # [1,2,3]


a = "!"
b = "!"
id(a)  #21926656
id(b)  # 21926656

이렇게 자주 사용된다 싶은 객체(여기선 "!" 를 담고 있는 객체)는 파이썬이 따로 저장해 둠


def func(p) : ...
func(1)
func([1,2,3])
func("eyeballs")

함수 파라미터에 들어가는 정보는 "변수의 참조값"임
def func(p) 에서 p 는 참조값을 넘겨받은 변수가 됨
이를 Call by Object Reference 라고 부름



 


0이나 empty 값, None 아닌 값은 True 로 간주함

bool(True)  #True
bool(1)  #True
bool(-1)  #True
bool(0)  #False
bool(0.0)  #False
bool("")  #False
bool(None)  #False
bool(Set())  #False
bool({})  #False
bool(())  #False
bool([])  #False

 


숫자를 표현할 때 세 자릿수를 underbar 를 이용하여 표현 가능
million = 1_000_000
print(million)  # 1000000

물론 꼭 위와 같이 쓰지 않아도, 숫자 사이에 어느 곳에나 넣을 수 있음
a = 1_2_3
print(a)  # 123

 


print(1/2)  # 0.5
print(1//2)  # 0. 소수점 이하 버려짐
print(1%2)  # 1. 나머지를 반환

 


print(chr(65))  # 'A'
print(ord('A'))  # 65

 


int 는 굉장히 큰 값도 들어감. 심지어 10의 100제곱(googol)도 들어감

print(int(10**100))  # 10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

 


a = b = c = 1
print(a)  # 1
print(b)  # 1
print(c)  # 1


print(1<2<3)  # True


a = 1
print(a == 1)  # True
print(a != 1)  # False
print((not a) == 1)  # False

 


문자열, 배열 등 내부 값을 갖는 객체에서 어떤 값이 포함되어있는지 확인하려면 in 을 사용

print('a' in "abcde")  # True
print("abc" in "abcde")  # True
print('a' in ('a','b','c'))  # True
print('a' in {'a':'a', 'b':'b'})  # True

 


:= 는 바다코끼리 연산자라고 불림...
이것은 코드 실행실행 결과 할당을 한 번에 처리할 수 있게 도와줌

이를테면 아래 두 코드는 동일한 결과를 출력함

diff = 2 - 1
if diff >= 0 :
    print("+")
else :
    print("-")

출력 결과 : +

if diff := 2-1 >= 0 :
    print("+")
else :
    print("-")

 


탈출 문자(Escape character) 를 무효화하려면 r 포맷팅을 사용

print(r"a\nb\tc\\d\e")  # a\nb\tc\\d\e


문자열을 연결할 때 + 를 사용 할 필요 없음

print("a" "b" "c")  # "abc"



print("aba".replace('a', 'x'))  # xbx
print("abcdefg"[::2])  # aceg. 2 개씩 건너뛰면서 슬라이싱
print("abcdefg"[::-1])  # gfedcba. -1 개씩 건너뛰며 슬라이싱. 결과적으로, 뒤에서부터 읽게 되어 문자열이 reverse 됨



문자열 검색에 사용되는 메소드는 두 가지
find() : 처음부터 문자열을 찾으면 오프셋(index)를 반환, 못 찾으면 -1 을 반환
index() : 처음부터 문자열을 찾으면 오프셋(index)를 반환, 못 찾으면 에러
rfind() : 끝에서부터 문자열을 찾으면 오프셋(index)를 반환, 못 찾으면 -1 을 반환
rindex() : 끝에서 부터 문자열을 찾으면 오프셋(index)를 반환, 못 찾으면 에러

"abcba".find("b")  # 1
"abcba".find("x")  # -1
"abcba".rfind("b")  # 3



문자열이 알파벳이나 숫자로 이루어져있는지 확인 가능

"123".isnumeric()  #True. 숫자로만 이루어져 있음
"123".isdigit()  #True. 숫자로만 이루어져 있음
"abc".isalpha()  # True. 문자로만 이루어져 있음
"abc123".isalnum()  # True. 숫자와 문자로만 이루어져 있음



"..a..".strip('.')  # 'a'
"..a..".lstrip('.')  # 'a..'
"..a..".rstrip('.')  # '..a'



format 사용 예제

"a{}{}d".format("b","c")  # 'abcd'
"a{1}{0}d".format("b","c")  # 'acbd'
"a{x}{y}d".format(x="b", y="c")  # 'abcd'



f-문자열 사용 예제

a = "a"
b = "b"
f"x{a}{b}y"  # "xaby"
f"x{a=}{b=}y"  # "xa='a'b='b'y"


 


break 문이 포함된 loop 문에서 break 가 실행되지 않으면 실행되는 무언가를 넣어야 할 때 else 를 사용함



while index < len([0,1,2,3]):
    index += 1
    if index > 5 : break
else : print("no break")

출력 결과 : no break



for n in (0,1,2) :
    if n < 0 : break
else : print("no break")

출력 결과 : no break



위와 같이 else 를 break checker 로 사용 가능


 


(1,2,3) ['a', 'b', 'c'] 를 각 index 별로 묶어서
[(1, 'a'), (2, 'b'), (3, 'c')] 로 만드는 기능은 zip 메소드를 통해 가능함

a = (1,2,3)
b = ['a', 'b', 'c']
z = zip(a, b)

type(z)  # <class 'zip' >

print(list(z))  # [(1, 'a'), (2, 'b'), (3, 'c')]

for x, y in zip(a,b) :
    print(x+", "+y)

출력 결과 : 
1, a
2, b
3, c


>>> a = (1,2,3)
>>> b = ['a','b','c']
>>> c = ([], (), {})
>>> z = zip(a,b,c)
>>> print(list(z))
[(1, 'a', []), (2, 'b', ()), (3, 'c', {})]



>>> a = (1,2,3)
>>> b = ['a','b','c']
>>> print( dict(zip(a,b)) )
{1: 'a', 2: 'b', 3: 'c'}



a = (1,2,3)  # a 는 3개
b = ['a']  # b 는 1개. a 보다 2개 적음
z = zip(a, b)

print(list(z))  # [(1, 'a')]. 가장 적은 개수를 갖는 b 에 따라 zip 결과가 정해짐


zip 메소드로 나온 결과를 어떤 방식으로든 한 번 사용하면
그 뒤로는 empty 값이 나와버림

z = zip(a,b)
print(list(z))  # [(1, 'a'), (2, 'b'), (3, 'c')]
print(list(z))  # [] 바로 윗 줄에서 사용했기 때문에 z 에 빈 값이 들어감


 


튜플 만드는 법

t = ()
t = "eyeballs",
t = ("eyeballs",)
t = 1, 2, 3
t = (1, 2, 3)
t = tuple([1,2,3])
t = (1, 2) + (3, 4)
t += t


튜플로 여러 변수에 값을 넣어줄 수 있음

a, b, c = (1,2,3)
a, b, c = 1, 2, 3
a, b = b, a



named tuple 이란 것이 있음
이름과 위치로 값에 접근 가능한 자료구조임
튜플의 서브클래스이며, collections 모듈을 통해 사용 가능

이름이 있는 필드를 가진 불변(immutable) 객체를 만들어 사용한다고 생각하면 됨
튜플처럼 동작하면서도 필드에 이름을 부여할 수 있어 가독성이 뛰어나고 코드 유지보수성이 좋아짐


from collections import namedtuple
>>> Person = namedtuple("Person", ["name", "age"])
>>> p1 = Person(name = "A", age = 30)  # 속성은 두 가지 뿐이지만, 불변의 dict 같은 객체를 만들 수 있음
>>> p2 = Person(name = "B", age = 60)
>>> p1[0], p1[1]
('A', 30)
>>> p2.name, p2.age
('B', 60)

>>> p3 = Person._make(['C', 90])  # _make 를 사용하여, 리스트를 namedtuple 에 바로 넣음
>>> p3[0], p3[1]
('C', 90)

>>> type(p3._asdict())  # _asdict 를 사용하여, namedtuple 을 dictionary 로 변경
<class 'dict'>
>>> print(p3._asdict())
{'name': 'C', 'age': 90}

>>> p4 = p3._replace(age=10)  # _replace 를 사용하여, 기존 namedtuple 속성을 수정한 새로운 namtedtuple 객체 생성
>>> print(p4)
Person(name='C', age=10)
>>> id(p3)
68810088
>>> id(p4)
68794984  # 기반이 된 p3 과는 다른, 새로운 객체 p4


이게 왜 tuple 이랑 비슷하게 동작한다는 것인지 모르겠음...
dirtionary 랑 더 비슷해 보임

dictionary 보다 namedtuple 이 더 효율적으로 동작한다고 함




리스트 만드는 법

l = []
l = [1,2,3]
l = list()
l = list('eyeballs')  # ['e','y','e','b','a','l','l','s']
l = list((1,2,3))
l = "a.b.c".split(".")
l += l



append 를 사용하면 메소드의 리스트  인자가 리스트의 마지막 항목에 들어감
extend 를 사용하면 메소드의 리스트 인자가 리스트에 병합됨

>>> a = [1,2,3]
>>> a.append([4,5])
>>> print(a)  # [1, 2, 3, [4, 5]]

>>> a = [1,2,3]
>>> a.extend([4,5])
>>> print(a)  # [1, 2, 3, 4, 5]



list 에서 항목 제거하기

a = [1,2,3]
del a[1]  # 숫자 2를 삭제
print(a)  # [1,3]

a = [1,2,3]
a.remove(2)  # 숫자 2를 삭제
print(a)  # [1,3]


a.del(1) 이런 문법은 아님...



list 에서 항목을 가져옴과 동시에 제거하기

a = [1,2,3,4,5]
n = a.pop()
print(n)  # 5
print(a)  # [1,2,3,4]

a = [1,2,3,4,5]
n = a.pop(0)
print(n)  # 1
print(a)  # [2,3,4,5]

a = [1,2,3,4,5]
n = a.pop(1)
print(n)  # 2
print(a)  # [1,3,4,5]
n = a.pop(1)
print(n)  # 4
print(a)  # [1,4,5]



sort() 는 list 자체 내부 정렬을 진행함
sorted() 는 list 의 정렬된 복사본을 반환

s = [2,5,4,1,3]
result = s.sort()
print(result)  # None
print(s)  # [1,2,3,4,5]

s = [2,5,4,1,3]
result = sorted(s)
print(result)  # [1,2,3,4,5]
print(s)  # [2,5,4,1,3]

내림차순 정렬하려면 인수에 reverse = True 추가

s = [2,5,4,1,3]
result = s.sort(reverse = True)
print(s)  # [5,4,3,2,1]



a = [1,2,3]
b = a

여기서 a 와 b 는 동일한 리스트 객체를 바라보고 있음
a 에서 리스트가 수정되면 b에서도 수정된 객체를 바라봄

a 의 복사본을 b 에 넣고 싶다면 아래와 같은 방법들을 사용

a = [1,2,3]
b = a.copy()
a[0]=-99
print(a)  # [-99, 2, 3]
print(b)  # [1, 2, 3]

a = [1,2,3]
b = a[:]
a[0]=-99
print(a)  # [-99, 2, 3]
print(b)  # [1, 2, 3]

a = [1,2,3]
b = list(a)
a[0]=-99
print(a)  # [-99, 2, 3]
print(b)  # [1, 2, 3]



copy 는 얕은 복사임

a = [1,2,[3,4,5]]
b = a.copy()
a[2][0]=-99
print(a)  # [1, 2, [-99, 4, 5]]
print(b)  # [1, 2, [-99, 4, 5]]
id(a[2])  # 30645800
id(b[2])  # 30645800

내부에 중첩된 list 까지 모두 제대로 복사하려면 deepcopy 를 사용하면 됨

import copy
a = [1, 2, [3, 4, 5]]
b = copy.deepcopy(a)
a[2][0] = -99
print(a)  # [1, 2, [-99, 4, 5]]
print(b)  # [1, 2, [3, 4, 5]]
id(a[2])  # 62359848
id(b[2])  # 30659592



리스트 컴프리헨션을 사용하여, 한 줄로 for 문을 구현할 수 있음

mylist = [i for i in a]
print(mylist)  # [1, 2, 3, 4, 5]

mylist = [i**2 for i in a]
print(mylist)  # [1, 4, 9, 16, 25]

mylist = [i for i in a if i%2==0]
print(mylist)  # [2, 4]



list 보다 tuple 을 사용하는 이유는
- tuple 이 공간을 더 적게 사용함
- tuple 은 한 번 생성되면 내부 값들이 변하지 않기 때문에, 값이 손상될 염려가 없음
- tuple 을 dictionary key 로 사용 가능 (list 는 안 되나보네..?)
- namedtuple  이 객체의 단순한 대안으로 사용 가능함


 


dictionary 만드는 법

d = {}
d = dict()
d = {"a" : 1, 2 : "b"}
d = dict( [ ['a',1], ['b',2], ['c',3] ] )
d = dict( ( ['a',1], ['b',2], ('c',3) ) )



dictionary 에서 값 확인 및 추출하는 법

d['a']  # 만약 'a' 키가 없으면 exception
d.get('a')  # 만약 'a' 키가 없으면 None 반환
d.get('a', 'nothing here')  # 만약 'a' 키가 없으면 'nothing here' 를 반환

'a' in d  # 'a' 가 d 의 key 값이라면 True
if key := 'a' in d:
    print("key : ", key)
    print("value : ", d['a'])

d.keys()  # 모든 키 얻기
d.values()  # 모든 값 얻기
d.items()  # 모든 키값 쌍 얻기



dictionary 합치기

d1 = {1:1}
d2 = {1:1, 2:2}
d3 = {2:2, 3:3}
d = {**d1, **d2}  #{1: 1, 2: 2}
d = {**d1, **d2, **d3}  # {1: 1, 2: 2, 3: 3}

d1 = {1:1}
d2 = {1:1, 2:2}
d1.update(d2)  # 결과값이 나오는 메소드가 아님, 자기 자신의 dictionary 에 추가하는 것
print(d1)  # {1: 1, 2: 2}

d1 = {1:1}
d2 = {1:'a'}
d1.update(d2)
print(d1)  # {1:'a'}.  key 가 동일한 아이템은 update 의 인자(d2) 값으로 업데이트 됨



dictionary 삭제

d = {1:1, 2:2}
del d[1]
print(d)  # {2:2}
del d[-99]  # exception 발생

d = {1:1, 2:2}
a = d.pop(1)
print(a)  # 1
print(d)  # {2:2}
a = d.pop(-99)  # exception 발생
a = d.pop(-99, 'Nothing here')  # key 가 없는 경우, default 값 반환
print(a)  # Nothing here
d.pop()  # exception 발생



list 와 마찬가지로, dictionary 로 얕은 복사, 깊은 복사가 있음

얕은 복사
a = {1:1}
b = a.copy()

깊은 복사
import copy
a = {1:1}
b = copy.deepcopy(a)



== 혹은 != 를 사용하여 비교 가능함

a = {1:1, 2:2}
b = {2:2, 1:1}

a==b  # True
a!=b  # False
not a==b  # False

a = {1:[1,2]}
b = {1:[2,3]}
a==b  # False



list 와 마찬가지로, 딕셔너리 컴프리헨션을 사용하여 for 문을 한 줄로 사용 가능함

>>> d = {k : k for k in (1,2,3)}
>>> d
{1: 1, 2: 2, 3: 3}


>>> word = "eyeballs"
>>> letter_counter = {key: word.count(key) for key in word}
>>> letter_counter
{'e': 2, 'y': 1, 'b': 1, 'a': 1, 'l': 2, 's': 1}


>>> word = "eyeballs"
>>> letter_counter = {key: word.count(key) for key in word if key in ('b','l','s')}
>>> letter_counter
{'b': 1, 'l': 2, 's': 1}



존재하지 않는 key 로 접근할 시 default 값을 반환하도록 할 수 있음

>>> d = {'a':1, 'b':2}
>>> print(d.get('c'))  # get 으로 접근하면 None 을 반환받음
None


>>> d = {'a':1, 'b':2}
>>> print(d.setdefault('a', 3))  # 'a' 는 존재하는 key 이므로 1 을 반환
1
>>> print(d.setdefault('b', 3))  # 'b' 는 존재하는 key 이므로 2 를 반환
2
>>> print(d.setdefault('c', 3))  # 'c' 는 존재하지 않는 key 이므로 default 값으로 넣은 두 번째 인수 3 을 반환 
3
>>> print(d)
{'a': 1, 'b': 2, 'c': 3}  # 더불어 'c':3 을 추가해줌



defaultdict 를 사용하여, 존재하지 않는 key 로 접근할 시 default 값을 반환하도록 할 수 있음
이 때 함수를 넣어줄 수 있음

>>> from collections import defaultdict
>>> dd_int = defaultdict(int)  # int 를 넣었음, int 의 default 값은 0으로 제공됨
>>> print(dd_int)
defaultdict(<class 'int'>, {})  # 처음에는 아무것도 없음

>>> dd_int['a'] = 1  # key 'a', value 1 을 넣음
>>> print(dd_int['a'])  # key 'a' 는 존재하기 때문에 1 을 반환
1
>>> print(dd_int['b'])  # key 'b' 는 존재하지 않기 때문에 int 의 default 값인  0을 반환
0
>>> print(dd_int)
defaultdict(<class 'int'>, {'a': 1, 'b': 0})  # 더불어 a, b 모두 dict 에 넣어줌


>>> print(defaultdict(str)['key'])  # str 의 default 값은 "" 으로 제공됨

>>> print(defaultdict(dict)['key'])  # dict 의 default 값은 {} 으로 제공됨 
{}
>>> print(defaultdict(list)['key'])  # list 의 default 값은 [] 으로 제공됨
[]

>>> def default_func(): return "default_value"  # default 값을 반환하는 함수를 넣어 default 값을 설정할 수 있음
>>> print(defaultdict(default_func)['key'])
default_value

>>> print(defaultdict(lambda: 'default_value')['key'])  # 간단하게 lambda 를 사용하여 default 값을 반환하는 함수 넣기 가능
default_value


 


set 생성하기

s = set()
s = {1,2,3,3}
s = set('aabbcc')  # {'a', 'b', 'c'}
s = set( [1,1,2,2,3,3] )  # {1,2,3}
s = set( {1:'a', 2:'b', 2:'c'} )  # {1,2}. 키 값만 사용됨



s = {1,2,3}
s.add(4)  # {1,2,3,4}
s.remove(1)  # {2,3,4}



a = {1,2,3}
b = {2,3,4}

>>> a & b  # {2, 3}. 교집합
>>> a - b  # {1}. 차집합
>>> b - a  # {4}. 차집합
>>> a | b  # {1, 2, 3, 4}. 합집합
>>> a.symmetric_difference(b)  # {1, 4}. exclusive

아래는 부분집합
a = {2,3}
b = {1,2,3,4}
>>> a.issubset(b)  # True
>>> a <= b  # True
>>> a < b  # True
>>> b.issubset(a)  # False
>>> b <= a  # False
>>> b < a  # False

a = {1, 2, 3}
b = {2}
b < a  # True. b가 a 와 같지 않으면서 b의 모든 요소가 a 안에 포함된 진부분집합
b <= a  # True. b의 모든 요소가 a 안에 포함된 부분 집합

a = {1, 2, 3}
b = {1, 2, 3}
b < a  # False. b가 a 와 같기 때문에 False. 진부분집합이 되지 못 함
b <= a  # True. b의 모든 요소가 a 안에 포함된 부분 집합



셋 컴프리헨션

>>> s = {a for a in (1,1,2,2)}
>>> s
{1, 2}
>>> s = {a%3 for a in (1,2,3,4,5,6)}
>>> s
{0, 1, 2}
>>> s = {a%3 for a in (1,2,3,4,5,6) if a % 3 != 0}
>>> s
{1, 2}



set 의 값을 불변(추가, 삭제, 업데이트 되지 않는 불변)으로 만들려면 frozenset 을 사용

>>> s = frozenset([1,1,2,2])
>>> s.add(3)
Traceback (most recent call last):
  File "<pyshell#136>", line 1, in <module>
    s.add(3)
AttributeError: 'frozenset' object has no attribute 'add'


 


None 과 False 구분 할 때는, is 를 사용함

a = None
if a is None : print("None")
else : print("False")

 


함수 호출시 인수 이름으로 값 직접 지정 가능

def func(a, b) :
    print(a, b)

print(func(b = "BB", a = "AA"))  # "AA, BB"


함수 인수의 기본값 설정

def func(a, b="BB"):
    print(a, b)

print(func("AA"))  # "AA, BB"
print(func(a = "AA"))  # "AA, BB"
print(func("AA", "XX"))  # "AA, XX"
print(func(a = "AA", b = "XX"))  # "AA, XX"



함수 인수의 기본값은, 함수 호출할 때 계산되는 게 아니라, 함수가 정의될 때 계산됨
즉, 함수가 정의될 때 인수의 기본값이 유지되는 것임
아래 예제로 이해해보자 

>>> def func(a, l = []):
    l.append(a)
    print(l)
>>> func(1)
[1]
>>> func(2)
[1, 2]  # [2] 가 나올 줄 알았지만, 바로 위 1 이 포함된 [1, 2] 가 나옴. 왜냐면 인수의 기본값이 유지되기 때문


함수에 리스트 같은 가변 인수가 들어가는 경우엔, 함수 파라미터에 리스트의 참조값이 복사되기 때문에
함수 내부에서 가변 작업한 것이 리스트에 그대로 적용됨

l = [1,2,3]
def func(l) :
    l.append(99)
print(l)  # [1,2,3,99]



함수에 넣을 인자 개수를 특정지을 수 없는 상황일 때 인수에 애프터리크 ( * ) 를 사용

>>> def func(*args):
    print(args)
>>> func(1)
(1,)
>>> func(1,2)
(1, 2)
>>> func(1,2,3,4,5)
(1, 2, 3, 4, 5)

아래와 같이, 함수 파라미터로 넣을 튜플에 애프터리스크를 사용하면
함수 내부에서 튜플로 인식하지 않고 각각의 값이 들어온 것으로 인식함(즉, 매개변수로 분해함)

>>> def func(*args):
    print(args)
>>> a = (1,2,3)
>>> func(a)
((1, 2, 3),)  # 튜플 하나가 들어온 것으로 인식
>>> func(*a)
(1, 2, 3)  # 1, 2, 3 이 들어온 것으로 인식



가변 인자(*args) 가 앞에 올 수도 있음
가변 인자 뒤에 오는 인자들이 키워드 기반 인자라면...

>>> def func(*args, c, d):
    print(args, c, d)


>>> func(1, c="CC", d="DD")
(1,) CC DD

>>> func(1,2,3,4,5, c="CC", d="DD")
(1, 2, 3, 4, 5) CC DD

>>> func(c="CC", d="DD", 1,2,3)  # 가변 인자 값부터 넣어야 함
SyntaxError: positional argument follows keyword argument



애프러리스크가 두 개 붙으면, 받은 keyword 값 쌍들을 함수 내부에서 dictionary 로 만들어 받음

>>> def func(**kwargs):
    print(kwargs)
>>> func()
{}
>>> func(a="AA", b="BB")
{'a': 'AA', 'b': 'BB'}
>>> func(c="CC")
{'c': 'CC'}  # 애프터 리스크 사용시 인수의 기본값 유지되지 않음


근데 정작 dictionary 를 넣으면 에러가 발생..
>>> func({1:1, 2:2})
Traceback (most recent call last):
  File "<pyshell#174>", line 1, in <module>
    func({1:1, 2:2})
TypeError: func() takes 0 positional arguments but 1 was given



단일 애프터리스크는, 함수의 위치 기반 인수키워드 전용 인수 사이에 넣어, 이 둘을 구분하는 역할을 함
예를 들어 def func(a, b, *, c="CC", d="DD") 처럼 위치 기반 인수(a, b) 와 키워드 전용 인수(c, d) 를 나누고 구분짓는 역할

애프터리스크 앞에 위치한 a, b 에는 (무조건) 위치 기반의 인수가 들어가야하고
애프터리스크 뒤에 위치한 c, d 에는 (무조건) 키워드 기반 인수가 들어가야 함

>>> def func(a, b, *, c="CC", d="DD") :
print(a,b,c,d)

>>> func(1,2)
1 2 CC DD

>>> func(1)  # 위치 기반으로 들어와야 할 b 의 인수가 들어오질 않아서 에러
Traceback (most recent call last):
  File "<pyshell#206>", line 1, in <module>
    func(1)
TypeError: func() missing 1 required positional argument: 'b'

>>> func(1,2)
1 2 CC DD

>>> func(1,2, c="XX")
1 2 XX DD

>>> func(c="XX", 1, 2)  # 위치 기반으로 들어와야 할 a 자리에 c 가 들어와서 에러
SyntaxError: positional argument follows keyword argument

>>> func(1,2,3,4)  # 키워드 기반으로 들어와야 할 c, d 자리에 키워드가 들어오질 않아서 에러
Traceback (most recent call last):
  File "<pyshell#210>", line 1, in <module>
    func(1,2,3,4)
TypeError: func() takes 2 positional arguments but 4 were given



단일 애프터리스크를 이용하여, 모든 인자를 키워드 기반 인자로 받도록 강제할 수 있음

>>> def func(*, a, b):
    print(a,b)

>>> func(a="AA", b="BB")  # a 와 b 에 키워드 기반 인자를 넣음
AA BB

>>> func(1)  # 키워드 기반 인자가 아닌 값은 들어갈 수 없음
Traceback (most recent call last):
  File "", line 1, in 
    func(1)
TypeError: func() takes 0 positional arguments but 1 was given

>>> func(1, a="AA", b="BB")  # 키워드 기반 인자가 아닌 값은 들어갈 수 없음
Traceback (most recent call last):
  File "<pyshell#224>", line 1, in <module>
    func(1, a="AA", b="BB")
TypeError: func() takes 0 positional arguments but 1 positional argument (and 2 keyword-only arguments) were given



함수 바디가 시작되기 전에 문자열을 넣어 함수에 대한 간단한 문서를 작성할 수 있음
문서는 help 를 사용하거나, 함수의 .__doc__ 을 호출하여 확이 가능
이를 독스트링이라고 부름

예를 들어
>>> def func():
    "this func print your name"
    print("eyeballs")

>>> help(func)
Help on function func in module __main__:
func()
    this func print your name

>>> func.__doc__
'this func print your name'


이런 방식을 통해, 사용법(무슨 인자가 얼마나 어떻게 들어가야 하는지)을 모르는 함수 사용시 도움을 받을 수 있음

>>> import copy
>>> help(copy.deepcopy)
Help on function deepcopy in module copy:

deepcopy(x, memo=None, _nil=[])
    Deep copy operation on arbitrary Python objects.
    
    See the module's __doc__ string for more info.

이 간단한 문서를 읽어보고, 나는
"아 인수로 x, memo, _nil 이 들어갈 수 있고 memo 는 default 값이 None 이구나" 라고 알 수 있음



추가로 어떤 객체를 받았을 때 그 객체가 사용 가능한 메소드를 보려면 dir 를 사용하면 됨

>>> dir(copy.deepcopy)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

>>> dir("eyeballs")
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']



함수 내에 함수를 선언하고 사용할 수 있음
이렇게 만든 내부 함수는 함수 바깥에서 사용 불가하기 때문에
임시로 만들어 사용하기 좋음

>>> def func():
     def inner_func():
          print("hi eyeballs")
     inner_func()


>>> func()
hi eyeballs
>>> inner_func()  # 여기서 내부 함수 호출이 불가능
Traceback (most recent call last):
  File "<pyshell#292>", line 1, in <module>
    inner_func()
NameError: name 'inner_func' is not defined



내부 함수를 이용하여 '클로저'를 만들 수 있음
chatGPT 에 의하면, 클로저란 다음과 같음

"클로저(Closure)는 함수 내부에서 또 다른 함수를 정의하고 반환할 때 만들어지는 함수 객체입니다. 반환된 내부 함수는 자신이 선언된 환경(외부 함수의 변수 등)을 기억하여, 외부 함수의 실행 컨텍스트가 종료된 후에도 이 정보를 사용할 수 있습니다."

즉, 생성될 때 넣어준 정보를 계속 지니고 있는 함수가 클로저임


>>> def func(mention):
    def inner_func():
        return f"what you said was this : {mention}"
    return inner_func

>>> a = func("hi eyeballs!")
>>> b = func("bye eyeballs!")

>>> a()  # a 는 클로저(함수)이기 때문에 실행이 가능하며, 실행시 클로저를 정의할 때 넣어줬던 정보를 기억함
'what you said was this : hi eyeballs!'
>>> b()  # b 도 클로저(함수)이기 때문에 실행이 가능하며, 실행시 클로저를 정의할 때 넣어줬던 정보를 기억함
'what you said was this : bye eyeballs!'



클로저를 사용하면, 함수 객체 내부의 상태를 계속 유지할 수 있음.
내부 함수에서 내부 상태를 업데이트하는 기능을 넣어주면, 상태를 계속 업데이트 할 수 있음


>>> def func():
    c = 0
    def add(a=1):
        nonlocal c
        c += a
        return c
    return add

>>> closure = func()
>>> closure()
1
>>> closure()
2
>>> closure(98)
100


여기서 중요한 점, 외부 함수에 정의된 변수에 내부 함수가 접근할 수 없음

>>> def outer():
    c = 0
    def inner():
        c+=1  # 내부함수(inner)에서 외부함수에 정의된 변수 c 에 접근 시도
        print(c)
    inner()

>>> outer()
Traceback (most recent call last):
  File "<pyshell#375>", line 1, in <module>
    outer()
  File "<pyshell#374>", line 6, in outer
    inner()
  File "<pyshell#374>", line 4, in inner
    c+=1
UnboundLocalError: local variable 'c' referenced before assignment



외부 함수에 정의된 변수에 내부 함수가 수정하려면 nonlocal 을 사용

>>> def outer():
    c = 0
    def inner():
        nonlocal c
        c+=1
        print(c)
    inner()

>>> outer()
1


근데 nonlocal 이 "함수 바깥의 변수"에 수정하는 것을 도와주는 명령어는 아님

>>> c = 0
>>> def func():
    nonlocal c  # 가장 바깥쪽 c 에 접근하면 문법 에러가 발생함...

SyntaxError: no binding for nonlocal 'c' found


이렇게 가장 바깥쪽 c 변수가 위치한 곳을 "global scope" 라고 함
global scope 에 있는 변수는 nonlocal 을 이용하여 접근할 수 없음
nonlocal 은, 단지 nearest enclosing scope (outer scope) 에 정의된 변수에만 접근 및 수정 할 수 있게 도와줌

global scope 에 있는 변수에 접근 및 수정하려면 global 을 사용해야 함

>>> c = 0
>>> def func():
    global c
    c+=1
    print(c)

>>> func()
1
>>> print(c)
1
>>> func()
2
>>> print(c)
2


추가로, global scope(namespace) 에 있는 변수들을 보려면 global() 을 실행하여 확인 가능
local scope(namespace) 에 있는 변수들을 locals() 을 실행하여 확인 가능



클로저를 사용하면, 내부 데이터를 은닉(캡슐화)할 수 있음
아래 예제에서 625 라는 숫자는, 클로저를 호출하는 바깥에서는 볼 수 없는 미지의 은닉된 숫자임

>>> def func():
    def inner_func(a):
        if a == 625 : print("correct!")
        else : print("wrong")
    return inner_func

>>> closure = func()
>>> closure(1)
wrong
>>> closure(50)
wrong
>>> closure(100)
wrong
>>> closure(625)
correct!



클로저는 정보를 은닉하여 계속 유지하기 때문에, 메모리에 계속 남아있게 됨
따라서 사용하지 않는 클로저는 삭제하는 것이 좋음

del closure



람다 lambda 함수는 단일 문장으로 표현되는 익명 함수임
따로 def 를 이용하여 정의내리지 않고, 그 때 그 때 필요한 때 사용하고 버림

일반적인 함수

def func(a, b):
    print(a, b)

동일한 역할을 하는 람다 함수

lambda a, b : print(a, b)


아래처럼 간단하게 사용 가능

a = lambda a, b : print(a,b)
a(1,2)  # 1 2


lambda 함수는, 콜론 ( : ) 뒤의 명령어가 실행되거나 반환됨

>>> a = lambda a, b : a+b
>>> a(1,2)
3

>>> x, y = 1, 2
>>> swap = lambda a, b: (b, a)
>>> x, y = swap(x, y)
>>> print(x, y)
2 1



아래와 같은 함수를 인자로 받는 함수에서 

>>> def func(mylist, myfunc):
    for i in mylist:
        print(myfunc(i))


def 로 정의된 함수를 넣어도 되지만

>>> def myfunc(i):
    return i.capitalize()
>>> func(["hi", "eyeballs"], myfunc)
Hi
Eyeballs


lambda 함수를 바로 넣을 수 있음

>>> func(["hi", "eyeballs"],lambda i: i.capitalize())
Hi
Eyeballs



아무것도 실행하지 않는 함수를 만들기 위해 pass 를 사용

def doNothing():
    pass


 


python3 에서 모든 객체는 기본적으로 "강한 참조"로 생성됨

강한 참조는 참조 카운트를 증가시키고, garbage collector 에 의해 수거되지 않음
약한 참조는 참조 카운트를 증가시키지 않고, garbage collector 에 의해 수거됨
약한 참조는 일부러 만들어야 함
강한 참조는 메모리의 객체 자체를 직접 참조하지만, 약한 참조는 객체를 간접적으로 참조함

강한 참조와 약한 참조 예제

>>> import weakref
>>> def func(): pass  # 강한 참조를 갖는 객체 생성
>>> weak_ref = weakref.ref(func)  # 약한 참조를 갖는 객체 생성
>>> type(weak_ref())  # 약한 참조 객체 실행해보면 반환값이 function 인 것을 확인
<class 'function'>
>>> del func  # 강한 참조 객체가 삭제되면, 약한 참조도 따라서 삭제됨
>>> type(weak_ref())  # 약한 참조 객체 실행해보면 반환값이 None 으로 변한 것을 확인
<class 'NoneType'>


- 강한 참조 (Strong Reference)
  - 참조 카운트 : 증가
  - 객체 생존 : 강한 참조가 존재하면 삭제되지 않음
  - 활용례 : 일반적인 객체

- 약한 참조 (Weak Reference)
  - 참조 카운트 : 증가하지 않음
  - 객체 생존 : 강한 참조가 없으면 Garbage Collection 가능 (수거되어 사라짐)
  - 활용례 : cache 를 만들거나, 메모리 관리가 중요한 앱을 만들 때 사용됨


이거 마치 리눅스의 하드 링크와 소프트(심볼릭) 링크의 관계 같다는 느낌이 듦...
강한 참조를 갖는 객체를 바라보는 약한 참조 객체는 soft link 같아서
강한 참조 객체가 사라지면(원본 file/dir 가 사라지면) 약한 참조 객체는 참조 할 객체가 사라지게 됨(soft link 가 갈 길을 잃음)


 


nested list 가 중첩된 리스트를 flatten 하게 만들기 위해 generator 를 아래와 같이 사용 가능

>>> lol = [1,[2,[3,4],5,[6],7]]
>>> def flatten(l):
    for item in l:
        if isinstance(item, list):
            for subitem in flatten(item):
                  yield subitem
        else:
            yield item
 
>>> list( flatten(lol) )
[1, 2, 3, 4, 5, 6, 7]


 


try-except 예제


try: 
    1/0
except: 
    print("divided by zero")



try:
    [1,2,3][4]
except IndexError as ie:
    print("index error. message : ", ie)  # 여기서 ie 는 시스템에서 작성해주는 error 메세지
except Exception as e:
    print("exception. message : ", e)



def divide(by):
    try : result = 1/by
    except ZeroDivisionError as e :
        print("divided by zero", e)
    else : print(result)  # else 는 예외가 발생하지 않았을 때 실행됨
    finally : print("done")



try:
    raise Exception("exception by developer on purpose")  # 일부러 Exception 을 발생. error message 입력 할 수 있음
except Exception as e:
    print("exception message : ", e)
    riase  # raise 를 다시 사용하여, 똑같은 exception 을 다시 발생시킴

exception message :  exception by developer on purpose
Traceback (most recent call last):
  File "<pyshell#619>", line 2, in <module>
    raise Exception("exception by developer on purpose")
Exception: exception by developer on purpose



try:
    raise RuntimeError("exception by developer on purpose")  # 원하는 Error 를 발생시킬 수 있음
except RuntimeError as re:
    print("exception message :", re)

exception message : exception by developer on purpose



assert 를 이용하여 예외를 발생시킬 수 있음
asset 는 나와선 안 되는 조건을 검사할 때 넣는 명령어임
지정된 조건식이 False 일 때 AssertionError 를 발생시킴

>>> def func(i):
    assert i % 3 == 0, '3의 배수가 아님'
    print(i,'는 3의 배수임')


>>> func(3)
3 는 3의 배수임
>>> func(1)
Traceback (most recent call last):
  File "<pyshell#638>", line 1, in <module>
    func(1)
  File "<pyshell#636>", line 2, in func
    assert i % 3 == 0, '3의 배수가 아님'
AssertionError: 3의 배수가 아님



예외를 직접 만들 수 있음

>>> class NotThreeMultipleError(Exception):
    def __init__(self):
        super().__init__('3의 배수가 아닙니다.')

>>> try
    raise NotThreeMultipleError
except Exception as e:
    print("error message", e)


error message 3의 배수가 아닙니다.



예외 메세지를 raise 에 붙일 수 있음

>>> class NotThreeMultipleError(Exception):
pass

>>> try:
    raise NotThreeMultipleError("3의 배수가 아님")
except NotThreeMultipleError as e:
    print("message :", e)

message : 3의 배수가 아님


 


객체(Object) : 파이썬의 모든 데이터. 인스턴스를 포함. 객체는 데이터(변수, 속성)와 코드(메소드)를 포함하는 자료구조
인스턴스(Instance) : 특정 클래스에서 생성된 객체 (특정 클래스의 '사례')

모든 인스턴스는 객체이지만, 모든 객체가 인스턴스는 아님
즉, 인스턴스는 객체의 부분집합



클래스에 dictionary 마냥 속성 추가 가능

class MyClass(): pass

my_class = MyClass()
my_class.name = "eyeballs"
print(my_class.name)  # eyeballs



class 생성시 속성 초기화 실행

class MyClass():
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def printing(self)
        print("name : ", self.name, "age : ", self.age)

my_class = MyClass("eyeballs", 625)
my_class.printing()


여기서 self 는 클래스의 인스턴스(instance) 자신을 참조하는 변수임
__init__ 메소드를 포함한 모든 인스턴스 메서드호출될 때 첫 번째 인수로 자동으로 인스턴스 자신이 전달
따라서, self를 사용하여 클래스 내부에서 해당 인스턴스의 속성과 메서드에 접근할 수 있음

위에 MyClass 인스턴스에서 printing() 이 실행되면,
printing() 의 첫번째 인수(self) 자리에 my_class 인스턴스가 전달됨
그래서 인스턴스(my_class) 의 name 과 age 를 사용할 수 있게 됨


참고로 __init__ 은 생성자가 아니라 단지 초기화 메소드임
왜냐면, __init__ 호출 전에 이미 객체가 만들어지기 때문




class Parent(): pass
class Child(Parent): pass

issubclass(Child, Parent)  # True



super() 를 사용하여 부모의 메소드를 이용하면, 부모 클래스 레벨에서 작업이 이루어짐

>>> class Parent():
    def __init__(self, name):
        self.parent_name = name

>>> class Child(Parent):
    def __init__(self, name, age):
        super().__init__(name)
        self.child_age = age

>>> child = Child("eyeballs", 625)
>>> dir(child)
['__class__', '__delattr__', '__dict__', .......  'child_age', 'parent_name']



클래스가 갖고 있지 않은 메소드 혹은 속성을 참조하면
python 은 모든 부모 클래스를 다 조사함

만약 다중 상속을 받은 경우라면, 상속받은 순서대로 조사함

>>> class High():
    def name(self):
        return "High"

>>> class Middle1(High):
    def name(self):
        return "Middle1"

>>> class Middle2(High):
    def name(self):
        return "Middle2"

>>> class Low1(Middle1, Middle2): pass
>>> Low1().name()
'Middle1'  # Middle1 이 먼저 상속되었기 때문에 Middle1의 name() 이 호출됨

>>> class Low2(Middle2, Middle1): pass
>>> Low2().name()
'Middle2'  # Middle2 가 먼저 상속되었기 때문에 Middle2의 name() 이 호출됨



python 에는 class 의 속성에 접근하지 못하게 막는 private 접근지시어 등은 없음
대신, 속성 이름을 다른 이름으로 가려서 접근을 우회하여 막는 방법이 있음


class MyClass():
    def __init__(self, name):
        self.private_name = name
    def getter(self):
        return self.private_name
    def setter(self, name):
        self.private_name = name
    public_name = property(getter, setter)

my_class =MyClass("eyeballs")
print(my_class.public_name)   # MyClass 를 사용하는 시점에서 private_name 대신 public_name 을 사용함
my_class.public_name = "w"
print(my_class.public_name)

propert 에 getter 와 setter 를 주고 public_name 을 설정함으로써
private_name 에 접근하지 못하도록 함
(물론 "private_name" 이라는 키워드를 (dir 등으로) 알고 있는 개발자라면 접근 가능함.....)



class 내의 @property 는 '계산된 값'에 접근하도록 돕기도 함

class MyClass():
    def __init__(self, name):
        self.name = name
    @property
    def name_length(self):
        return len(self.name)

my_class =MyClass("eyeballs")
print(my_class.name_length)  # name_length 는 메소드지만, 마치 변수에 접근하는 것 마냥 접근함
8


참고로 property 로 설정된 속성은 read-only 가 됨. 아래처럼 수정하려면, setter 를 설정해야 함

>>> my_class.name_length=1
Traceback (most recent call last):
  File "py.py", line 11, in <module>
    my_class.name_length = 1
AttributeError: can't set attribute



property 를 통해 속성 이름을 다른 것으로 바꾸는 방법 외에
dunder (double underbar) 를 통해 이름을 다르게 바꿀 수 있음

>>> class MyClass():
    def __init__(self, name):
        self.__name = name  # 이름 앞에 dunder 를 붙이면, 인스턴스에서 __name 으로 접근이 불가능


>>> my_class = MyClass("eyeballs")
>>> dir( my_class )
['_MyClass__name', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

__name 대신 _MyClass__name 이 새로 생김
__name 에 접근하고 싶으면 getter, setter를 사용하던가, 아니면 (숨겨진 이름의) _MyClass__name 을 사용

이렇게 변수나 메소드의 이름을 컴파일 단계에서 일정한 규칙을 통해 바꾸는 것을 맹글링이라고 함



class method 는 인스턴스가 아닌 클래스 자기 자신 전체에 영향을 미치는 메소드임
(인스턴스가 아니라) 클래스 속성을 변경하거나 새로운 객체를 생성하는 용도로 사용됨

@classmethod 데코레이터를 추가하여 class method 생성 가능


class MyClass():
    count = 0
    def __init__(self):
        MyClass.count += 1
    @classmethod
    def get_count(cls):  # self 대신 cls 를 사용. 즉, class 자기 자신을 가리키며 참조함
        return cls.count

a = MyClass()
b = MyClass()
print(MyClass.get_count())  #2. 클래스를 통해 호출
print(b.get_count())  #2. 인스턴스를 통해 호출


self 는 클래스의 인스턴스 자신을 가리키는 변수이며
cls 는 클래스 자기 자신을 가리키는 변수임



class method 를 통해 인스턴스를 찍어내는 팩토리 패턴 구현 가능

class MyClass():
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def get_instance(cls, name, birth_year):
        return cls(name, 2025-birth_year)  # cls 를 이용하여 인스턴스를 생성한 후 반환

a = MyClass.get_instance("eyeballs", 1991)
b = MyClass.get_instance("w", 1965)

print(a.name, a.age)
print(b.name, b.age)



static method 는 클래스에서 곧바로 호출 가능한 메소드. 편의(유틸리티)를 위해 존재함
@staticmethod 데커레이터를 사용하여 정의


>>> class MyClass():
    @staticmethod
    def read_me():
    print("please read this first")


>>> MyClass.read_me()
please read this first




타입을 미리 정하는게 아니라 실행이 되었을 때 해당 Method들을 확인하여 타입을 정하는 것을 '덕타이핑'이라고 함

def func(obj):
    print(obj.name())


class A:
    def name(self):
        return self.__class__.__name__

class B:
    def name(self):
        return self.__class__.__name__

func(A())  # 'A' 출력.
func(B())  # 'B' 출력.


func 메소드의 obj 에 뭐가 들어오던간에
name() 이라는 메소드가 있다면 실행하게 됨. 그것도 런타임에.




dataclass 를 통해, 클래스에 데이터(속성)를 직관적으로 지정하여 설정할 수 있음
아래 두 가지 class 는 동일하게 name 속성을 갖는 class 임


class MyClass():
    def __init__(self, name):
        self.name = name

from dataclasses import dataclass
@dataclass
class MyDataClass():
    name : str

print(MyClass("eyeballs").name)  # eyeballs
print(MyDataClass("eyeballs").name)  # eyeballs


>>> from dataclasses import dataclass
>>> 
@dataclass
class MyDataClass():
    name: str
    age: int = 30

>>> MyDataClass("A", 20)
MyDataClass(name='A', age=20)

>>> MyDataClass(20, "A")   # 놀랍게도, 문법적으로 허용 됨. 왜냐면 파이썬은 동적 언어라서, 런타임에 실제 타입을 검사하거나 변환하지 않음. 위에 name: str 에서 str 은 단지 개발자한테 '이렇게 넣으셈' 하는 힌트일뿐이고, 문법적인 강제는 없음
MyDataClass(name=20, age='A')

>>> MyDataClass(name="A", age=20)
MyDataClass(name='A', age=20)

>>> MyDataClass(age=20, name="A")
MyDataClass(name='A', age=20)

>>> MyDataClass("A")  # age 생략, default 로 설정한 30이 대신 사용됨
MyDataClass(name='A', age=30)




타입을 강제하고 싶다면, __post_init__ 에서 체커를 추가함
__post_init__ 은 dataclass 의 __init__ 메서드가 호출된 후 자동으로 실행되는 메서드(__init__ 이 __post_init__ 을 호출함)


from dataclasses import dataclass

@dataclass
class MyDataClass:
    name: str

    def __post_init__(self):
        if not isinstance(self.name, str):
            raise TypeError(f"Expected str, got {type(self.name).__name__}")

MyDataClass(name="A")
MyDataClass(name=20)  # exception



데이터 클래스는 매직메소드(duner 가 들어가는 메소드들)를 자동으로 만들어주기도 함
예를 들어, __init__, __repr__, __eq__ 같은 메소드들을 자동으로 만들어 줌
위에서 __init__ 없이 데이터 클래스를 정의할 수 있었던 것도 다 이런 이유에서였음


>>> class A:
    def __init__(self, name):
        self.name = name


>>> @dataclass
class B:
    name : str


>>> print(A("eyeballs"))
<__main__.A object at 0x01562D00>  # __repr__ 가 존재하지 않아, A 클래스의 메모리값을 대신 반환

>>> print(B("eyeballs"))  # dataclass 가 __repr__ 를 대신 생성해주어, B 의 name 을 포함하여 반환
B(name='eyeballs')



 


mymodule.py 과 mycode.py 가 한 dir에 존재하는 경우 다음과 같이 바로 import 가능

< mymodule.py >
from random import choice
mylist = [1,2,3,4,5]
def pick():
    return choice(mylist)

< mycode.py >
import mymodule
print(mymodule.pick())

혹은

< mycode.py >
from mymodule import pick
print(pick())

혹은

< mycode.py >
from mymodule import pick as p
print(p())



"mypackage" 라는 dir 안에 mymodule.py 과 mymodule2.py 를 넣어둠
mycode.py 는 mypackage dir 와 동일한 위치에 존재함

< mypackage/mymodule.py >
from random import choice
mylist = [1,2,3,4,5]
def pick():
    return choice(mylist)

< mypackage/mymodule2.py >
from random import choice
mylist = [6,7,8,9,10]
def pick():
    return choice(mylist)


mypackage dir 안에 있는 mymodule.py, mymodule2.py 를 아래와 같이 from, import 로 나눠 불러올 수 있음

< mycode.py >
from mypackage import mymodule, mymodule2
print(mymodule.pick())
print(mymodule2.pick())

혹은 

from mypackage.mymodule import pick
from mypackage import mymodule2
print(pick())
print(mymodule2.pick())



위와 같이 하위 dir 에서 module 을 불러올 때 from, import 를 사용함
그럼 from random 은 어디서 불러올까?
이 모듈 파일은, 현재 작업중인 dir 이내에 없기 때문에, python 이 다른 위치에서 해당 module 을 불러옴
그 "다른 위치"라는 곳은 아래와 같이 확인 가능

>>> import sys
>>> for p in sys.path:
print("\"",p,"\"")

" "
"C:\Users\EYE\Python\"
"C:\Users\EYE\Python\Python38-32\Lib\idlelib"
"C:\Users\EYE\Python\Python38-32\python38.zip"
"C:\Users\EYE\Python\Python38-32\DLLs"
"C:\Users\EYE\Python\Python38-32\lib"
"C:\Users\EYE\Python\Python38-32"
"C:\Users\EYE\Python\Python38-32\lib\site-packages"

(현재 윈도우에서 작업중이라 위와 같은 경로들이 나타남)

가장 먼저 path 에 나타나는 것이 빈 문자열(" ") 임
이 말은, python 실행시 현재 dir 를 기준으로 먼저 module 을 찾는다는 의미임

임의의 path 를 추가하려면 아래와 같이 실행

import sys
sys.path.insert(0, "C:\Users\EYE\my\python\module\path")  # 0순위로 찾게 됨



module 경로를 상대적으로 넣어줄 수 있음
예를 들어 mycode.py 와 동일한 dir 위치에 있는 mymodule.py 을 불러오려면

< mycode.py >
from . import mymodule
...


mycode.py 보다 상위 dir 위치에 있는 mymodule.py 을 불러오려면

< mycode.py >
from .. import mymodule
...


mycode.py 보다 상위 dir 위치의 mypackage 에 있는 mymodule.py 을 불러오려면

< mycode.py >
from ..mypackage import mymodule
...



놀랍게도, import 한 module 의 값을 직접 업데이트 할 수 있음
module 을 import 한 프로그램에 module 의 사본이 생성된다고 이해하면 됨
다시 import 해도 동일한 업데이트가 반영되며,
나중에 다른 프로그램에서 동일한 module 을 import 하면 그에 맞춰 새로운 사본이 생성되기 때문에
pi=3 으로 업데이트 한 내용이 다른 프로그램에 영향을 미치지 않음

>>> import math
>>> math.pi
3.141592653589793
>>> math.pi = 3
>>> math.pi
3
>>> import math
>>> math.pi
3


 


Deque 는 스택과 큐의 기능을 모두 갖고 있음
즉, 양쪽으로 pop 이 가능함

>>> from collections import deque
>>> dq = deque([1,2,3,4,5])
>>> print(dq.pop())  # 가장 마지막에서 pop
5
>>> print(dq)
deque([1, 2, 3, 4])
>>> print(dq.popleft())  # 가장 처음에서 pop
1
>>> print(dq)
deque([2, 3, 4])



여러 시퀀스들을 차례대로 순회하기 위해 itertools.chain 을 사용

>>> import itertools
>>> for item in itertools.chain([1], [2,3], (4,5,6)):   # 3개의 다양한 시퀀스를 넣음
    print(item)

1
2
3
4
5
6



하나의 시퀀스를 순회하며 누적 계산을 하기 위해 itertools.accumulate 를 사용

>>> import itertools
>>> for item in itertools.accumulate([1,2,3,4]):
print(item)

1
3
6
10

기본적으로 누적 합계를 계산함
합계가 아닌 다른 누적 계산을 진행하려면, def 를 추가로 넣어주면 됨

>>> def mul(a,b):
    return a*b
>>> for item in itertools.accumulate([1,2,3,4], mul):
print(item)

1
2
6
24



from pprint import pprint

pprint 는 일반 print 보다 훨씬 가독성 좋게 출력해줌
dictionary 를 출력하면 정렬도 해 줌....


 


python 은 pip 를 통해 PyPI(Python package index. https://pypi.org) 로부터 패키지를 다운받아 설치할 수 있음
"패키지를 설치"한다는 것의 의미는, 해당 패키지의 코드와 관련 종속성(dependencies)을 Python 환경에 다운로드하고
적절한 위치에 배치하여 사용할 수 있도록 등록하는 과정을 의미
여기서 말하는 '적절한 위치'란, site-packages(Python의 라이브러리 디렉터리)를 의미함.
이 site-packages 에 패키지 파일을 복사함
패키지 설치 이후에 python 스크립트 내에서 import 를 통해 패키지 및 모듈 사용이 가능하게 됨

패키지 파일 위치는 아래와 같이 확인 가능

import requests
print(requests.__file__)  # 패키지의 실제 경로 출력

일반적으로 pip 로 설치한 패키지는 모든 python 프로젝트에서 사용 가능함 (global installation)
어느 특정 python 프로젝트 에서만 사용 가능하도록 패키지를 설치하려면 
venv 또는 conda 같은 가상 환경을 사용해야 함


pip list  # 설치된 패키지 목록 확인
pip show requests  # 특정 패키지 정보 확인 

pip 로 패키지 설치하기
pip install flask
pip install flask==0.9.0
pip -r installations.txt  # 해당 txt 파일에는 패키지 이름들이 개행 간격으로 적혀있고, txt 파일 내 모든 패키지가 설치됨

pip install --upgrade  # 설치된 모든 패키지를 최신 패키지로 업그레이드
pip uninstall requests  # 패키지 삭제



Python의 가상환경은 독립적인 실행 환경을 만들어서 
각 프로젝트마다 별도의 패키지 및 Python 버전을 관리할 수 있도록 도와주는 기능

global package 와 다른 버전을 사용해야 할 때 사용되며,
venv, virtualenv, conda 등이 있음



< venv (Python 내장 가상환경) >
Python 3.3 이상에서 기본 제공되는 가상환경 도구

가상 환경 생성 명령어 : python -m venv my_env
my_env 라는 dir 가 생성되고, 이 dir 내에 가상환경 관련 파일이 저장됨
해당 프로젝트에서 설치한 패키지들이 이 my_env dir 에 설치됨

가상환경 활성 명령어 : source my_env/bin/activate

가상환경 비활성 명령어 : deactivate

가상환경 제거 명령어 : rm -rf my_env
(그냥 dir 를 지우는 거네)



< virtualenv (확장 기능이 있는 가상환경) >
venv보다 더 다양한 기능 제공함
Python 2.x 및 3.x 모두 지원하며, 
하나의 가상환경에서 여러 Python 버전 사용 가능.

virtualenv 설치 명령어 : pip install virtualenv

가상환경 생성 명령어 : virtualenv my_env
특정 python 버전으로 가상환경 생성하는 명령어 : virtualenv -p /usr/bin/python3.8 my_env

가상환경 활성 명령어 : source my_env/bin/activate

가상환경 비활성 명령어 : deactivate



< conda (데이터 과학 및 패키지 관리 특화) >
데이터 과학, 머신러닝, 딥러닝 등에 최적화된 가상환경을 제공함
pip와 달리 Python 패키지뿐만 아니라 비(非)Python 패키지도 설치 가능 (예: numpy, tensorflow, R).
virtualenv 와 동일하게, 여러 Python 버전 사용 가능.

conda 를 통해 가상환경을 설정하려면, Anaconda 를 설치해야 함
설치 후 anaconda 가 제공하는 명령어를 통해 가상환경을 구축할 수 있음

특정 python 버전으로 가상환경 생성하는 명령어 : conda create --name my_env python=3.8

가상환경 활성 명령어 : conda activate my_env

가상환경 비활성 명령어 : conda deactivate

가상환경 제거 명령어 : conda remove --name my_env --all



주피터 노트북 설치 명령어 : pip install jupyter
주피터 노트북 실행 명령어 : jupyter notebook

주피터lab 설치 명령어 : pip install jupyterlab
주피터lab 실행 명령어 : jupyter lab

 


python unittest 사용 예제
첫 글자를 대문자로 바꾸는 함수를 테스트 할 예정


import unittest
def func(text):
    if text is None : return text
    try:
        if i := int(text) : return text
    except: pass
    return text.capitalize()

class Test(unittest.TestCase):

    def setUp(self): pass  # 테스트가 진행되기 전 실행되는 메소드
    def tearDown(self): pass  # 테스트가 마무리 된 후 실행되는 메소드

    def test1(self):
        text = "eyeballs"
        result = func(text)
        self.assertEqual(result, "Eyeballs")

    def test2(self):
        text = "hi eyeballs"
        result = func(text)
        self.assertEqual(result, "Hi eyeballs")

    def test3(self):
        text = 123
        result = func(text)
        self.assertEqual(result, 123)

    def test4(self):
        text = None
        result = func(text)
        self.assertEqual(result, None)

    def test5(self):
        text = True
        result = func(text)
        self.assertEqual(result, True)

if __name__ == '__main__' :
    unittest.main()


테스트 성공

C:\Users\EYE\Desktop\python>python mycode.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK



테스트 실패

C:\Users\EYE\Desktop\python>python mycode.py
.F..F
======================================================================
FAIL: test2 (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mycode.py", line 24, in test2
    self.assertEqual(result, "Hi Eyeballs")
AssertionError: 'Hi eyeballs' != 'Hi Eyeballs'
- Hi eyeballs
?    ^
+ Hi Eyeballs
?    ^


======================================================================
FAIL: test5 (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "mycode.py", line 39, in test5
    self.assertEqual(result, None)
AssertionError: True != None

----------------------------------------------------------------------
Ran 5 tests in 0.003s

FAILED (failures=2)


 


로깅 모듈 사용하여 로깅할 때 필요한 개념들

메세지 : 로그 메세지
레벨 : 로깅의 심각한 정도 debug, info, warning, error, critical
로거 logger : 모듈과 연결되는 하나 이상의 객체
핸들러 handler : 터미널, 파일, DB 등으로 메세지를 전달하는 역할
포매터 formatter : 결과를 생성
필터 filter : 입력 기반으로 판단

import logging
logging.debug("debug")
logging.info("info")
logging.warning("warning")
logging.error("error")
logging.critical("critical")

출력 결과 
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical


기본적으로 debug, info 는 결과를 출력하지 않고 warning, error, critical 만 출력함
basicConfig 를 통해 기본 level 을 debug 로 지정하면,
이후부터 debug 부터 critical 까지 전부 출력

logging.basicConfig(level=logging.DEBUG)

출력 결과
DEBUG:root:debug
INFO:root:info
WARNING:root:warning
ERROR:root:error
CRITICAL:root:critical



로거를 설정해두면, 어떤 위치에서 어떤 로거에 의해 출력되었는지 따로 구분할 수 있음

import logging
logging.basicConfig(level=logging.DEBUG)

A_logger = logging.getLogger('A')
B_logger = logging.getLogger('B')

A_logger.warning("warning")
B_logger.critical("critical")

출력 결과
WARNING:A:warning
CRITICAL:B:critical



basicConfig 에 filename 을 추가하면, 로그를 stdout 이 아니라 file 로 저장할 수 있음

import logging
logging.basicConfig(level=logging.DEBUG, filename='warning.log')
logging.warning("warning")

프로그램을 실행한 위치에 warning.log 가 생성됨



basicConfig 에 format 을 추가하면, 로그의 포맷을 변경할 수 있음

import logging
fmt = '%(asctime)s %(levelname)s %(lineno)s %(message)s'
logging.basicConfig(level=logging.DEBUG, format=fmt)
logging.warning("warning")

출력 결과
2025-01-30 21:24:58,157 WARNING 4 warning



 


Python의 Global Interpreter Lock (GIL)
멀티스레드 환경에서 한 번에 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 하는 메커니즘

Python 은 분명 멀티스레드를 지원하지만,
GIL 에 의해 동시에 두 개 이상의 스레드가 Python 코드 실행을 병렬로 수행할 수 없음

GIL 은 여러 스레드가 하나의 자원을 수정하면 발생하는 race condition 을 방지하기 위해 존재하며,
여러 스레드가 동시에 실행될 시 GC 실행이 불안정해지는 것을 방지하기 위함 (메모리 관리 안정성을 보장)
(이 GC 는 순환 참조 문제를 해결하기 위해 좀 더 복잡한 기법을 사용하는 GC라고 함...)


< CPU bound 작업 실행시 >

GIL은 멀티코어 CPU의 성능을 제대로 활용하지 못하게 만듦
실제로 멀티스레딩을 사용해도 실제 성능 향상이 거의 없음.
GIL이 있기 때문에 여러 개의 CPU 코어를 제대로 활용할 수 없음

따라서 멀티프로세싱(multiprocessing) 대신 사용하거나,
GIL 을 지원하지 않는 Numpy, Pandas, 혹은 PyPy 를 사용



< IO bound 작업 실행시 >

GIL은 입출력(I/O) 중심 작업에서는 큰 영향을 주지 않음
Python은 GIL을 사용하지만, I/O 작업을 수행하는 동안에는 GIL을 자동으로 해제함.
따라서 멀티스레딩이 성능 향상에 도움이 될 수 있음.




python 을 통해 새로운 process 를 실행하여 shell 명령어를 실행할 수 있음.
(shell 명령어를 실행한다는 의미는, process 를 하나 실행한다는 의미가 됨)

import subprocess
result = subprocess.getoutput('ls -laht | grep 2025 | wc -l')  # shell 명령어를 수행 후 stdout/stderr 결과를 반환
result = subprocess.getstatusoutput('ls -laht')  # shell 명령어 수행 후 stdout/stderr 결과와 상태 코드를 튜플로 반환
result = subprocess.call('date')  # shell 명령어 수행 후 결과의 상태 코드(0(성공) 혹은 0 외의 값)만 반환
result = subprocess.call(['date', '-u'])  # shell 명령어를 수행 후 stdout/stderr 결과를 반환
result = subprocess.call('date -u', shell=True)  # shell 명령어를 수행 후 stdout/stderr 결과를 반환

os 를 통해서도 shell 명령어 실행 가능

import os
result = os.system("ls -laht")   # shell 명령어 수행 후 결과의 상태 코드(0(성공) 혹은 0 외의 값)만 반환



python multiprocessing 예제 

import multiprocessing
import os

def target_func(name):  # multiprocessing 으로 처리 될 함수
    print(f"process id : {os.getpid()} name : {name}")

def run_multi_process():
    target_func("main process")

    p1 = multiprocessing.Process(target=target_func, args=('first multiprocess',))  # args 는 튜플로 넣어줘야 함
    p1.start()
    print(f"p1) process id : {p1.pid} name : {p1.name}")

    p2 = multiprocessing.Process(target=target_func, args=('second multiprocess',))
    p2.start()
    print(f"p2) process id : {p2.pid} name : {p2.name}")

    p3 = multiprocessing.Process(target=target_func, args=('third multiprocess',))
    p3.start()
    print(f"p3) process id : {p3.pid} name : {p3.name}")

if __name__=='__main__':
    run_multi_process()



결과
process id : 16664 name : main process

p1) process id : 10564 name : Process-1
p2) process id : 11740 name : Process-2
p3) process id : 9108 name : Process-3

process id : 10564 name : first multiprocess
process id : 11740 name : second multiprocess
process id : 9108 name : third multiprocess


위에 p1.pid 와 (target_func 내의) os.getpid() 가 동일한 것을 확인할 수 있음
즉, target_func 내에서 실행되는 명령어들은 모두 sub process 에서 동작함

multiprocessing.process 를 종료하려면 아래와 같이 terminate 실행

p1.terminate()
p2.terminate()
p3.terminate()



python multi threading 예제


import threading
import os

def target_func(name):
    print(f"process id : {os.getpid()} name : {name}")

def run_multi_thread():
    target_func("main process")
    t1 = threading.Thread(target=target_func, args=('first thread',))
    t1.start()
    print(f"t1) thread ident : {t1.ident} native_id : {t1.native_id} name : {t1.name} ")

    t2 = threading.Thread(target=target_func, args=('second thread',))
    t2.start()
    print(f"t2) thread ident : {t2.ident} native_id : {t2.native_id} name : {t2.name}")

    t3 = threading.Thread(target=target_func, args=('third thread',))
    t3.start()
    print(f"t3) thread ident : {t3.ident} native_id : {t3.native_id} name : {t3.name}")

if __name__=='__main__':
    run_multi_thread()



결과


process id : 8900 name : main process

process id : 8900 name : first thread
t1) thread ident : 19272 native_id : 19272 name : Thread-1

process id : 8900 name : second thread
t2) thread ident : 13024 native_id : 13024 name : Thread-2

process id : 8900 name : third thread
t3) thread ident : 364 native_id : 364 name : Thread-3


process id  는 모두 동일한 것을 확인
ident 는 python 내부에서 관리하는 thread 의 고유 id 이며, os 레벨에서 관리할 수 없음
native_id 는 OS 가 관리하는 실제 thread 의 고유 id 이며, os 의 프로세스 관리도구(ps 등)로 확인 가능





 

 

 

 

 

 

 

 

 

Scala 언어 문법책 공부 후 핵심만 정리함

 

 

Scala 는 JVM 언어임. 자바 런타임을 사용하여 실행됨

 

 

 

Literal : 숫자 5, 문자 'A', 문자열 "eyeballs" 처럼 소스 코드에 바로 등장하는 데이터

값(value) : 불변의 타입을 갖는 저장 단위. 정의될 때 데이터가 할당되며 재할당 불가능

변수(variable) : 가변의 타입을 갖는 저장 단위. 정의될 때 데이터가 할당되며 재할당 가능

타입(type) : 데이터의 종류, 정의, 분류. Scala 의 모든 데이터는 특정 타입에 대응하며, 모든 Scala 타입은 그 데이터를 처리하는 메소드를 갖는 클래스로 정의됨

 

불변의 값(value) 를 사용하면, 다른 어떤 코드에서 접근하더라도 같은 값을 유지하는 안정성을 갖출 수 있어

코드를 읽고 디버깅하는 일이 더 쉬워짐

동시 또는 멀티 스레드 코드에서 값을 사용하여 에러 발생 가능성을 낮출 수 있음

 

 

 

 

이름 설명 크기 최솟값 최댓값
Byte 부호 있는 정수 1byte -128 127
Short 부호 있는 정수 2byte -32768 32767
Int 부호 있는 정수 4byte -2^31 2^31 - 1
Long 부호 있는 정수 8byte -2^63 2^63 - 1
Float 부호 있는 부동 소수 4byte n/a n/a
Double 부호 있는 부동 소수 8byte n/a n/a

 

 

 

 

 

 

Literal Type 설명
5 Int 접두사/접미사 없는 정수 리터럴은 기본이 Int
0x0f Int 접두사 0x : 16진수
5l Long 접미사 l : Long 타입
5.0 Double 접두사/접미사 없는 소수 리터럴은 기본이 Double
5f Float 접미사 f : Float 타입
5d Double 접미사 d : Double 타입

 

 

 

 

정규 표현식

 

matches : 정규 표현식이 전체 문자열과 맞으면 true

"abc" matches ".*" : true

 

replaceAll : 일치하는 문자열을 모두 치환

"abc" replaceAll ("b|c","a") : "aaa"

 

replaceFirst : 첫번째로 일치하는 문자열을 치환

"abc" replaceFirst ("b|c","a") : "aac"

 

 

 

 

bool 비교 연산자인 &&,|| 은 게을러서 첫 번째 인수로 충분하다면 두 번째 인수는 평가하지 않음

&, | 은 결괏값 반환하기 전에 늘 두 인수를 모두 검사함

 

 

 

 

데이터 타입 연산

예제 설명
val aa = 5
myVal.asInstanceOf[Long]
원하는 타입으로 전환
val aa = 5
myVal.getClass
해당 값의 타입(=Class) 반환
val aa = 5
myVal.isInstanceOf[Int]
해당 값이 넣어준 타입에 해당하는지 확인
val aa = 5
myVal.hashCode
해당 값의 해시코드 반환
val aa = 5
myVal.toDouble
형변환
val aa = 5
myVal.toString
해당 값을 string 으로 변환

 

 

 

 

 

 

Tuple 은 선언 방법이 두 가지이며, 인덱스는 1부터 시작함

0부터 시작하지 않는 문제아. 아주 자기 멋대로야..

 

val myTuple = (1, "2", '3', "four", 5)

myTuple._1 // 1

myTuple._2 // "2"

 

val myTuple = 1 -> "2"

myTuple: (Int, String) = (1, "2")

 

 

 

 

 

 

표현식을 아래처럼 활용 가능

 

val result = { val x = 6*25; x*100 }

result: Int = 15000

 

여기서 x는 표현식 내에서만 사용 가능하며

표현식 바깥에서는 접근 불가

 

 

 

 

 

if ( true ) println("a")

결과 : a

 

if (1 > 2) println("a") else println("b")

결과 : b

 

if 문과 대체하여 사용 가능한 match 표현식은 아래처럼 사용 가능

 

val max = (1>2) match {

  case true => 1

  case false => 2

}

결과 : 2

 

val message = 500 match {

  case 200 => "ok"

  case 400 => { println("400"); "error" }

  case 500 => { println("500"); "error" }

}

결과 : 500 이 출력되고 "error" 가 message 에 들어감

 

val kind = "WED" match {

  case "MON" | "TUE" | "WED" | "THU" | "FRI" => "weekday"

  case "SAT" | "SUN" => "weekend"

}

결과 : weekday

 

val ifstatement = -1 match {

  case x if x > 0 => "plus"

  case x if x < 0 => "minus"

  case x => "zero"

}

결과 : minus

 

val other = 300 match {

  case 200 => "ok"

  case other => "error"

}

결과 : error

 

val wilecard = 300 match {

  case 200 => "ok"

  case _ => "error"

}

결과 : error

 

타입으로도 매칭이 가능

val x:Int = 123

x match {

  case x: String => "String"

  case x: Int => "Int"

}

결과 : Int

 

 

 

 

 

 

for 루프는 일정 범위의 데이터를 반복하며, 반복할 때마다 표현식을 실행함

yield 를 추가하면 반환값들을 컬렉션으로 돌려줌

(각 원소마다 특정 표현식을 적용하고 반환하는 것이 마치 map 과 비슷함)

 

for ( i <- 1 to 5 ) { print(i+" ") }

결과 : 1 2 3 4 5

 

for ( i <- 1 until 5 ) { print(i+" ") }

결과 : 1 2 3 4

 

val result = for ( i <- 1 to 5 ) yield { i }
결과 : results: scala.collection.immutable.IndexedSeq[Int] = Vector(1,2,3,4,5)

 

for ( i <- result ) { print(i+" ") }
결과 : 1 2 3 4 5

 

for ( x <- 1 to 2 ; y <- 1 to 3 } { print(s"($x,$y)") }

결과 : (1,1)(1,2),(1,3)(2,1)(2,2)(2,3)

 

 

아래와 같이 for문 안에 조건식을 넣을 수 있음

 

for ( i <- 1 to 10 if i%2!=0) print(i+" ")

결과 : 1 3 5 7 9

 

아래와 같이 여러개의 조건절을 넣을 수 있으며, 조건들은 서로 and 로 엮임

 

for {

  c <- "a,bc,,d,,ef".split(",")

  if c != null

  if c.size > 1

} print(c+" ")

결과 : bc ef

 

아래와 같이 for 루프 안에서만 쓰이는 임시 변수를 지정하여 사용 가능

아래 예제의 x 는 for 루프가 반복 할 때마다 매번 정의되고 할당됨

 

for ( i <- 1 to 5 ; x = i * i ) print(x+" ")

결과 : 1 4 9 16 25

 

while 루프도 사용 가능함

 

var x = 5

while( x > 0 ) x-=1

결과 : x 는 0이 됨

 

 

 

 

 

 

 

순수 함수는 아래의 조건들을 만족하는 함수임

- 하나 이상의 입력 매개변수를 가짐

- 입력 매개변수만을 이용하여 계산 수행

- 값을 반환함

- 동일 입력에 대해 항상 같은 값을 반환

- 함수 외부의 어떤 데이터도 사용하거나 영향을 주지 않음

- 함수 외부 데이터에 영향을 받지 않음

 

순수 함수는 상태 정보를 유지하지 않으며, 외부 데이터에 관계없이 독립적임

본질적으로 순수함수는 변경될 수 없어서 안정적임

(마치 수학에서 사용하는 함수와 동일한 성질을 갖음)

Scala 프로그래밍을 하면서 순수 함수 비율을 많이 늘리는 게 좋다고 함

 

 

 

함수는 def 를 사용하여 선언하며, 선언문 다음에는 표현식이 옴

 

선언 : def hi = "hi"

사용 : hi

 

선언 : def hi = {"hi"}

사용 : hi

 

선언 : def hi:String = {val result = "eyeballs", result}

사용 : hi

결과 : eyeballs


선언 : def hi() = "hi"

사용 : hi 혹은 hi()

 

선언 : def hi(name:String) = "hi "+name

사용 : hi("eyeballs")

결과 : hi eyeballs

 

선언 : def hi(name:String):String = {

  if (name != null) return "hi "+name

  else return "it's null"

}

사용 : hi("eyeballs"), hi(null)

결과 : "hi eyeballs", "it's null"

 

선언 : def hi(name:String):String = {

  if (name != null) return "hi "+name

  "it's null"

}

사용 : hi("eyeballs"), hi(null)

결과 : "hi eyeballs", "it's null"

 

선언 : def hi(name:String, ch:Char) = "hi "+name+ch

사용 : hi("eyeballs", '!')

결과 : hi eyeballs!

 

선언 : def hi(name:String, ch:Char) = "hi "+name+ch

사용 : hi(name = "eyeballs", ch = '!') , hi(ch = '!', name = "eyeballs")

결과 : hi eyeballs!

 

선언 : def hi(name:String, ch:Char = '!') = "hi "+name+ch

사용 : hi("eyeballs"), hi(name = "eyeballs")

결과 : hi eyeballs!

 

선언 : def hi(name:String)(ch:Char) = "hi "+name+ch

사용 : hi("eyeballs")('!')

결과 : hi eyeballs!

 

아래처럼 인수 개수가 가변적일 때 * 를 사용하여 처리 가능

 

선언 : def sum(items: Int*): Int = {

  var total = 0

  for ( i <- items) total += i

  total

}

사용 : sum(0), sum(1,2), sum(1,2,3,4,5)

결과 : 0, 3, 15

 

아래처럼 사용시 표현식을 사용할 수 있음

아래 에제에서 myname 은 호출할 때만 잠깐 사용되는 값

 

선언 : def hi(name:String) = "hi "+name

사용 : hi {val myname="eyeballs"; hi(myname)}

결과 : hi eyeballs

 

프로시저란 반환값을 갖지 않는 함수이며, 이 때 함수의 반환값은 Unit 이 됨

예를 들어 아래 두 함수는 동일한 함수임

def log(d:Double) = println("value : "+d)

def log(d:Double):Unit = println("value : "+d)

 

아래처럼 함수 안에 함수 중첩 가능

Scala 는 오버로딩이 가능하기 때문에, 아래 예제의 파라미터 3개짜리 max와 파라미터 2개짜리 max 는 서로 다른 함수임

파라미터 2개짜리 max는 파라미터 3개짜리 max 내에서만 사용 가능 (max 함수 바깥에서 사용 불가)

 

선언 : def max(a:Int, b:Int, c:Int) = {

  def max(x:Int, y:Int) = if (x>y) x else y

  max(a, max(b, c))

}

사용 : max(1,2,3)

결과 : 3

 

Generic 처럼 함수 내에서 다루는 타입 자체를 사용시 직접 넣어줄 수 있음

이것을 타입 매개변수라고 부르며 [ ] 를 사용하여 선언함

마지막 예제처럼 타입 매개변수를 넣어주지 않아도 추론이 가능한 경우는 에러가 나지 않음

 

선언 : def identity[TYPE] (x:TYPE):TYPE = x

사용 : identity[Int](1), identity[String]("eyeballs"), identity[Double](0.1), identity("eyeballs")

결과 : 1, eyeballs, 0.1, eyeballs

 

 

 

 

 

 

클래스 내 메소드를 호출할 때 dot 을 사용할 수 있지만, white scape 를 사용하는 것도 가능

아래 세 가지는 모두 같은 의미를 갖음

"eyeballs".endsWith("s")

"eyeballs" endsWith("s")

"eyeballs" endsWith "s"

 

 

 

 

 

Scala 에서 함수는 일급 객체임

일급 객체란, 일반적인 데이터 타입처럼 언어의 모든 부분에 사용 가능한 객체를 말 함

일급 함수는 값, 변수 등의 컨테이너에 저장될 수 있고

다른 함수의 매개변수로 사용되거나

다른 함수의 반환값으로 사용될 수 있음

 

다른 함수를 매개변수로 받아들이거나

다른 함수를 반환값으로 반환하는 함수를 고차함수 라고 함

 

함수는 일급객체이기 때문에, 아래처럼 함수를 값에 넣을 수 있음

 

def hi = "hi"

val copyHi = hi

사용 : copyHi

 

def hi() = "hi"

val copyHi = hi

사용 : copyHi()


def hi(name:String) = "hi "+name

val copyHi = hi

사용 : copyHi("eyeballs")


def hi(name:String, ch:Char) = "hi "+name+ch

val copyHi = hi _

사용 : copyHi("eyeballs", '!')

 

def hi(name:String)(ch:Char) = "hi "+name+ch

val copyHi = hi _

사용 : copyHi("eyeballs")('!')

 

함수는 일급객체이기 때문에, 아래처럼 함수의 파라미터에 함수를 넣을 수 있음

 

def reverser(s: String) = s.reverse

def hi(name: String, f:String => String) = {

  if(name!=null) f(name)

  else name

}

사용 : hi("abcde" reverser), hi(null, reverser)

결과 : edcba, null

 

아래처럼 함수 리터럴(익명함수) 를 사용하여 바로 값에 할당 가능

 

선언 : val hi = (s: String) => s.reverse

사용 : hi("abc")

 

선언 : def hi(name: String, f:String => String) = { if(name!=null) f(name) else name }

사용 : hi("abcde", s => s.reverse) , hi("abcde", (s:String) => s.reverse)

 

선언 : val logging = () => "logging..."

사용 : println(logging())

 

하지만 아래처럼 함수 리터럴을 선언에 사용하는 것은 안 됨

def hi(name:String, s=>s.reverse) = {....} //안 됨 

def hi(name:String, (s:String)=>s.reverse) = {....} //안 됨

 

 

 

 

자리표시자 구문은 함수 리터럴의 축약형임

지정된 매개변수를 와일드카드( _ )로 대체함

함수의 명식적 타입이 리터럴 외부에 지정되어 있고, 매개변수가 한 번 이상 사용되지 않는 경우에만 자리표시자 사용 가능

 

예를 들어 다음과 같이 사용 가능

val double: Int => Int = _*2

Int => Int 를 통해 함수에 입력값이 int 인 것을 알려주었고,

함수 내부에서 _ 가 한 번만 사용되었음

 

비슷한 예로 아래와 같이 사용 가능

선언 : val rev: String => String = _.reverse

사용 : rev("eyeballs")

 

선언 : def hi(name: String, f:String => String) = { if(name!=null) f(name) else name }

일반 사용 : hi("abcde", s => s.reverse) , hi("abcde", (s:String) => s.reverse)

자리표시자 사용 : hi("abcde", _.reverse)

 

선언 : def combination(x:Int, y:Int, f:(Int,Int)=>Int) = f(x,y)

일반 사용 : combination(2,3, (x,y) => x*y)

자리표시자 사용 : combination(2,3, _ * _)

 

선언 : def combination[A,B](x:A, y:A, f:(A,A)=>B) = f(x,y)

일반 사용 : combination[Int,Double](2,3, (x,y) => x/y*1.0)

자리표시자 사용 : combination[Int,Double](2,3, _ / _ * 1.0)

 

 

 

함수를 val 에 넣을 때 파라미터를 고정시킨 후에 넣을 수 있음

이것을 부분 적용 함수라고 함

예를 들어, 아래 함수에 들어가는 x, y  두 파라미터 중 하나는 고정하고 싶다면,

자리표시자를 사용한 부분에만 파라미터를 받을 수 있게 만들면 됨

 

def factorOf(x:Int, y:Int) = y%x == 0

val multipleOf3 = factorOf(3, _:Int)

사용 : factorOf(3, 10), multipleOf3(10)

 

아래처럼 부분 적용 함수를 만들어서 val 에 넣을 수 있음

 

val f = factorOf _

val f = factorOf(_, _)

val f = factorOf(3, _)

val f = factorOF(3, _:Int)

 

def factorOf(x:Int)(y:Int) = y%x == 0

val multipleOf3 = factorOf(3) _

사용 : multipleOf3(4), multipleOf3 {val a=2; a*2}

 

 

 

 

 

 

함수에 "이름에 의한 매개변수(call by name)"를 사용하면

리터럴 값이 와도 되고, 함수가 와도 됨

예를 들어 f 라는 함수의 매개변수로 이름에 의한 매개변수를 넣음

def doubles(x: => Int) = { print(s"got ${x} from doubles"); x*2 }

 

이 doubles 함수에 넣을 파라미터로 2, 5 등의 리터럴 값이 들어갈 수 있음

doubles(2) //결과 got 2 from doubles, 4

doubles(5) //결과 got 5 from doubles, 10

 

또한 함수 자체가 들어갈 수 있음

def f(a:Int) = {println(s"got ${a} from f"); a}

doubles(f(4)) //결과 got 4 from f, got 4 from doubles, for 4 from f, 8

 

이렇게 들어간 f 는 doubles 내부에서 사용 될 때마다 호출됨

위 출력 결과에도 from f 가 두 번 떴음, 왜냐하면 doubles 내부에 x 가 두 번 사용되기 때문

 

 

 

 

case 를 추가하여 함수에 파라미터를 특정지어 처리할 수 있음

 

선언 :

val caseFunc : Int=>String = {

  case 1 => "one"

  case -1 => "minus one"

  case _ => "the others"

}

사용 : caseFunc(1), caseFunc(-1), caseFunc(3)

 

 

 

 

 

 

 

 

 

Scala 의 class 는 Java 와 마찬가지로 new 로 생성할 수 있음

 

선언 : class User

생성 : val user = new User

확인 : user.isInstanceOf[User]  //true

user.isInstanceOf[AnyRef]  //true

user.isInstanceOf[Any]  //true

 

선언 : 

class User {

  val name : String = "eyeballs"

  def greet : String = s"hello ${name}"

  override def toString = s"user name is ${name}"

}

생성 : val user = new User

사용 : user.greet, new User().greet

 

아래처럼 생성자를 넣을 수 있으며, 이를 클래스 매개변수라고 함

클래스 매개변수 n 은 메소드(greet, toString 등) 내부에서 사용 불가능함

클래스 매개변수n 은 단지 필드를 초기화하거나 메소드에 전달되는 용도로만 사용됨

 

class User(n: String) {

  val name : String = n

  def greet : String = s"hello ${name}"

  override def toString = s"user name is ${name}"

}

 

클래스 매개변수 n 앞에 val, var 를 붙이면

n 은 클래스의 필드가 되기 때문에, 내부 메소드에서도 사용 가능

 

class User(val n: String) {

  def greet : String = s"hello ${n}"

  override def toString = s"user name is ${n}"

}

 

class A {

  override

}

class B extends A

class C extends B {

  override def toString = "C : "+getClass.getName

}

val a:A = new B

bal b:B = new A //에러남. 자식은 부모를 받아줄 수 없음

 

선언 : 

class Car( val make: String, var reserved: Boolean ) {

  def reserve(r: Boolean): Unit = {reserved = r}

}

생성 : val t = new Car("eyeballs company", false), val t = new Car(make = "eyeballs company", reserved = false)

사용 : t.reserve(true)

 

선언 :

class Car( val make: String = "eyeballs company", var reserved: Boolean = true, val year: Int = 2024) {

  def reserve(r: Boolean): Unit = {reserved = r}

}

생성 : val t = new Car(), val t = new Car(year=2025)

 

선언 :

class myClass[A](element: A) {

  val a:A = element

  print(a.isInstanceOf[A])

}

생성 : 아래 모두 true 를 출력

val myClass = new myClass(1)

val myClass = new myClass[Int](1)
val myClass = new myClass("eyeballs")

val myClass = new myClass[String]("eyeballs")

 

 

추상 클래스는 abstract 를 사용하여 선언 가능하며

자기 자신은 인스턴스를 생성하지 않고 오로지 다른 클래스에 의해 상속되어지기만 하는 클래스임

 

선언 : 

abstract class Car {

  val year: Int

  val color: String

}

사용 : 

class myCar extends Car {

  val year = 2024

  val color = "Red"

}

class myCar(val year:Int = 2024) extends Car {

  val color = "Red"

}

 

추상 클래스를 상속하지 않고도, 생성함과 동시에 내용을 구현하는 것으로 사용 가능함

 

선언 : abstract class Car (val year:Int) { def show }

생성 : val myCar = new Car(2024) {

  def show { println(s"this car is ${year}" years old) }

  }

사용 : myCar.show

혹은

new Car(2024) { def show { println(s"this car is ${year}" years old) } }.show

 

class 내 오버로딩도 가능함

class MyClass {

  def print(a:String) = println(a)

  def print(a:Int) = println(a)

  def print(a:String, b:Int) = println(a+" "+b)

}

 

apply 라는 메소드를 구현하면, 사용할 때 메소드 이름을 사용하지 않고 생성한 클래스 이름 그대로 사용 가능

 

선언 : 

class Multiple(factor: Int){

  def apply (input: Int): Int = input * factor

}

생성 : val triple = new Multiple(3)

사용 : triple(7), triple.apply(7)   //둘 다 결과 21

 

필드에 lazy 를 사용하면, 그 필드가 인스턴스 될 때만 생성(구현)되도록 할 수 있음

즉, lazy 필드에 처음 접근 할 때 초기화(initial) 됨

 

선언 :

class Lazy {

  val x = { println("now initial"); 1 }

  lazy val y = { println("lazy initial"); 2 }

}

생성 :

val l = new Lazy()  // 여기서 "now initial" 이 출력됨

println(l.y)   // 여기서 lazy val y 가 사용되었으므로, "lazy initial" 이 출력됨

 

 

 

 

 

 

 

기본적으로 Scala 는 프라이버시 제어를 추가하지 않음

우리가 작성한 모든 클래스는 누구나 인스턴스를 생성할 수 있고

클래스 내부 필드와 메소드에 접근 가능

 

하지만 원한다면 프라이버시 제어를 추가할 수 있음

바로 필드 메소드 앞에 protected 혹은 private 을 추가하는 것임

 

protected 가 붙은 필드와 메소드는 동일 클래스 혹은 그 클래스의 자식 클래스에서만 접근 가능하게 됨

 

선언 :

class User { protected val password = "12345" }

class CheckUser extends User { def isValid = ! password.isEmpty }

사용 : 

new User().password   // 접근 불가 에러

new CheckUser().isValid   // 결과 true. 자식 클래스인 CheckUser 에서는 User 의 password 에 접근 가능

 

private 이 붙은 필드와 메소드는 이를 정의한 클래스에서만 접근 가능하게 됨

 

선언 :

class User { private val password = "12345" }

사용 : 

new User().password   // 접근 불가 에러

class CheckUser extends User { def isValid = ! password.isEmpty }   // 자식 클래스에서 접근 불가하여 선언 에러가 발생

 

 

패키지 단위로 접근을 제어할 수 있도록 할 수 있음

 

선언 : 

package com.eyeballs {

  private[eyeballs] class Config {   // com.eyeballs 패키지 내부에서만 접근 가능

    val url = "eyeballs.tistory.com"

  }

  class Test { println(new Config().url) }

}

 

사용 : 

new com.eyeballs.Test   // 결과 : eyeballs.tistory.com

new com.eyeballs.Config   // com.eyeballs 가 아닌 외부 패키지에서 Config 에 접근 불가하기 때문에 에러 발생

 

 

final 을 이용하여, 어떤 클래스의 자식 클래스를 만들지 못하도록 하거나

자식이 부모의 필드, 메소드를 재정의 할 수 없도록 할 수 있음

 

final class A

class B extends A   // A 의 자식을 만들 수 없어 에러 발생

 

class A { final val a = "a" }

class B extends A { val a = "b" }   // 부모 클래스의 필드인 A.a 를 재정의 할 수 없어 에러 발생

 

 

 

 

 

 

 

 

class 와 비슷하지만, 용도가 다른 object, case class 에 대해 설명함

 

object 는 하나 이상의 인스턴스를 가질 수 없는 형태의 class

singleton 이 적용된 class 라고 보면 됨

object 는 new 키워드로 인스턴스를 생성하지 않음

대신 이름으로 직접 해당 객체에 접근함

object 에 최초로 접근할 때 (JVM 내에서) 자동으로 인스턴스화 됨

인스턴스화는 자동으로 생성되므로, 초기화를 위한 매개변수는 갖지 않음 (대신 apply 메소드에 넣을 매개변수는 갖을 수 있음)

 

object 는 다른 class 를 상속받을 수 있음

하지만 다른 class 가 object 를 상속받을 수 없음

왜냐면 object 의 필드, 메소드는 전역에서 접근 가능하므로, 자식 클래스를 만들 이유가 없기 때문

 

선언 : object Hi { println("call Hi"); def hi = "hi" }

사용 : println(Hi.hi)

결과 : call Hi, hi

 

Hi.hi 를 여러번 사용시, 최초 생성된 인스턴스가 재사용됨

singleton 성격을 갖기 때문에, 순수 함수를 구현하거나 DB 를 사용하는 I/O 함수, sparkSession 을 설정하는 용도 등으로 사용

 

class 와 이름이 같은 object 를 동반 객체 라고 부름

동반 객체에서는 class 의 private, protected 필드 및 메소드에 접근 가능함

 

선언 : 

class Multiplier(val x: Int) { def product(y:Int) = x*y }

object Multiplier { def apply(x: Int) = new Multiplier(x) }

 

사용 : 

val tripler = Multiplier(3)   // object 사용

val result = tripler.product(10)   // 결과 30

 

 

 

 

 

 

 

 

case class 는 자동으로 생성된 메소드 몇 가지를 갖은 상태로 (인스턴스가) 생성되는 클래스

case class 는 동반 객체도 자동으로 생성하며, 이 동반 객체도 자신만의 메소드를 자동으로 생성해둠

case class 는 주로 데이터를 저장하고 전송하는 역할로 사용되며

계층적인 클래스 구조를 위해 사용되지 않는 편

왜냐하면 자동으로 만든다는 그 메소드들은 상속받은 필드들은 고려하지 않기 때문

 

자동으로 만들어진다는 메소드들은 다음과 같음

 

이름 위치 설명
apply object (동반 객체) case class 를 인스턴스화하는 팩토리 메소드
copy class 요청받은 변경사항이 반영된 인스턴스의 사본을 반환. 매개변수는 현재 필드값으로 설정된 기본값을 갖는 클래스의 필드들
equals class 다른 인스턴스의 모든 필드가 이 인스턴스의 모든 필드와 일치하면 true 반환. 연산자 == 로도 호출 가능
hashCode class 인스턴스의 필드들의 해시 코드를 반환. 해시 기반의 컬렉션에 유용..
toString class 클래스명과 필드들을 모아 String 으로 반환
unapply object (동반 객체) 인스턴스를 그 인스턴스의 필드들의 튜플로 추출하여 패턴 매칭에 케이스 클래스 인스턴스를 사용할 수 있도록 함

 

선언 : case class Character (name: String, age: Int)

사용 : 

val a = Character ("AA", 2)

val b = a.copy(name="BB")

a == b   // false

 

 

 

 

 

 

 

 

리스트(List). 한 번 생성되면 내부 값을 바꿀 수 없음

내부에서 Linked List 로 구현되어 있음

 

val myList = List()

val myList = List(1,2,3)

val myList = List("a", "b", 1, 2)

myList(0)   // "a"

myList(1)   // "b"

myList(-1)   // error

myList(10)   // error

myList.size  // 4

myList.isEmpty  // false

myList == Nil   // false. 여기서 Nil 은 빈 값을 갖는 리스트인 List() 의 싱글톤 인스턴스

Nil == List()   // true

myList.head   // "a"

val tailList = myList.tail   // "b", 1, 2

for ( l <- myList ) { print(l+" ") }   // a b 1 2

 

foreach 는 함수를 취하고, 그 함수를 리스트의 모든 항목으로 호출함

myList.foreach( l => print(l+" ") )   // a b 1 2

 

map 은 단일 리스트 요소를 다른 값이나 타입으로 전환하는 함수를 취함

val newList = myList.map(l => "["+l+"]")   // [a], [b], [1], [2]

 

reduce 는 리스트 요소들을 앞에서부터 차례대로 두 개씩 선택한 후, 단일 항목으로 결합하는 함수를 취함

val combination = myList.reduce((a,b) => a+" "+b)   // "a b 1 2"

 

리스트를 생성하는 또 다른 방법은 :: 를 사용하는 것

val myList = 1 :: 2 :: 3 :: Nil

val newList = 0 :: myList   //0,1,2,3

 

혹은 두 리스트를 ::: 로 붙이는 것

val twoList = List(1,2) ::: List(3,4)   // 1,2,3,4

 

리스트에 ++ 를 사용하여 다른 컬렉션(이를테면 Set)을 붙일 수 있음

val twoCollections = List(1,2) ++ Set(3,3,3)  // List(1,2,3)

 

:+, +: 를 사용하여 간단하게 List 에 요소를 늘릴 수 있음

List(1,2,3) :+ 4   // List(1,2,3,4). Linked List 의 마지막까지 도달해야하기 때문에 성능 이슈 발생 가능

1 +: List(2,3,4)   // List(1,2,3,4)

 

== 를 사용하여 컬렉션(리스트, 집합.. 등) 간 비교 가능. 두 컬렉션의 타입과 내용이 같으면 true

List(1,2) == List(1,3)   // false

 

drop 으로 List 에서 처음의 n 개 요소를 제외함

val droppedList = List(1,2,3,4) drop 2   //List(3,4)

 

dropRight 로 List 에서 마지막의 n 개 요소를 제외함. Linked List 의 마지막 요소까지 순회해야하므로, 성능 이슈 발생 가능

val droppedList = List(1,2,3,4) dropRight 2   //List(1,2)

 

List 에서 distinct 로 중복 제거

List(1,2,1,2).distinct   // 1, 2

 

List 에 filter 추가하여 true 인 것만 남길 수 있음

val filteredList = List(1,2,3,4,5) filter (_>2)   // 3,4,5

 

nested List 가 포함된 경우, flatten 을 이용하여 내부 요소들을 모두 포함하는 단일 리스트를 만들 수 있음

List(List(1,2), List(3)).flatten   //1,2,3

 

근데 List 가 아닌 리터럴 값이 포함되어 있으면 에러가 발생함

List(List(1,2), List(3), 4).flatten   // error

 

partition 을 사용하여 조건의 참에 해당하는 리스트와 거짓에 해당하는 리스트 두 개를 만듦 (결과는 튜플이 됨)

val part = List(1,2,3,4,5) partition (_<3)

part._1 은 List(1,2)  // true 인 값들

part._2 는 List(3,4,5)   // false 인 값들

 

splitAt 을 사용하여 인덱스 기준으로 List 를 좌우로 쪼갬. 결과는 튜플이 됨

val split = List(1,2,3,4) splitAt 2

split._1 은  List(1,2)

split._2 는 List(3,4)

 

reverse 를 사용하여 List 요소의 순서를 뒤집음

List(1,2,3).reverse   // List(3,2,1)

 

slice 를 사용하여 List 요소의 단편만 가져옴. <=_<

List(1,2,3,4,5) slice (0,0)   // List()

List(1,2,3,4,5) slice (0,1)   // List(1)

List(1,2,3,4,5) slice (0,2)   // List(1,2)

List(1,2,3,4,5) slice (1,2)   // List(2)

List(1,2,3,4,5) slice (2,1)   // List()

 

take 를 사용하여 List 의 처음 n 개 요소만 추출함

List(1,2,3) take 2   // List(1,2)

 

takeRight 를 사용하여 List 의 마지막 n 개 요소만 추출함. Linked List 마지막 요소까지 순회해야 하므로 성능 이슈 발생 가능

List(1,2,3) takeRight 2   // List(2,3)

 

sorted 를 사용하여 List 요소를 정렬함. 사전 순 혹은 오름차순

List(3,2,1).sorted   // List(1,2,3)

List('c','b','a').sorted   // List('a','b','c')

 

sortBy 를 사용하여 원하는 기준으로 List 요소를 정렬함

List("abc","de","f") sortBy (_.size)   // List("f", "de", "abc")

 

zip 을 사용하면, 두 List 를 각 인덱스에 해당하는 요소들끼리 묶인 튜플의 리스트로 만들 수 있음

val z = List(1,2) zip List('a','b')

z 는 List( (1,a), (2,b) )

 

collect 와 case 를 사용하면, List 안의 요소들을 case 에 매칭된 것만 남기고, case 의 내용대로 변환함

List("a", "b", "c") collect {

  case "a" => "A"

}

결과 : List("A")

 

List("a", "b", "c") collect {

  case "a" => "A"

  case "b" => "B"

  case _ => "nothing"

}

결과 : List("A", "B", "nothing")

 

map 을 사용하면, List 안의 모든 요소들에 특정 함수를 적용한 결과값으로 치환함

List("a", "b", "c").map(_.toUpperCase)

결과 : List("A", "B", "C")

 

flatMap 을 사용하면, map 처럼 List 안의 각 요소들에 특정 함수를 적용한 결괏값으로 치환하지만, 

map 과 다르게 모든 결과를 평탄화하여 하나의 List 로 만들어줌

 

List("a,b,c","d,e,f").flatMap(_.split(","))

결과 : List("a","b","c","d","e","f")

 

List("a,b,c","d,e,f").map(_.split(","))

결과 : List(Array("a","b","c"), Array("d","e","f"))

 

List(1,2,3).max  // 3 최댓값

List(1,2,3).min  // 1 최솟값

List(1,2,3).product  // 6 모두 곱하기

List(1,2,3).sum  // 6 모두 더하기

 

contains 를 사용하여 List 내 요소를 포함하고 있는지 확인 가능

List(1,2,3) contains 2   // true

 

exists 를 사용하여 List 내 최소 하나의 요소가 조건자에 성립하는지 확인 가능

List(1,2,3).exists(_<2)   // true

List(1,2,3).exists(_<1)   // false

 

forall 을 사용하여 List 내 모든 요소가 조건자에 성립하는지 확인 가능

List(1,2,3).exists(_<2)   // false

List(1,2,3).exists(_<=3)   // true

 

startsWith 를 사용하여 List 의 처음 요소들이 특정 값을 갖는 List 로 시작하는지 확인 가능

List(1,2,3) startsWith List(1)   // true

List(1,2,3) startsWith List(1,2)   // true

List(1,2,3)startsWith List(1,3)   // false

 

endsWith 를 사용하여 List 의 마지막 요소들이 특정 값을 갖는 List 로 끝나는지 확인 가능

List(1,2,3) endsWith List(3)   // true

List(1,2,3) endsWith List(2,3)   // true

List(1,2,3) endsWith List(1,3)   // false

 

아래서부터 reduce 처럼 List의 값을 하나로 축소하는 함수에 대해 설명함

 

foldLeft 를 사용하여, List 를 주어진 시작값과 함께 왼쪽에서부터 축소

List(1,2,3).foldLeft(0)(_-_)   // -6

이유 :

0 - 1 = -1

-1 - 2 = -3

-3 - 3 = -6

List(1,2,3).foldLeft(1)(_-_)   // -5

List(1,2,3).foldLeft(2)(_-_)   // -4

 

foldRight 를 사용하여, List 를 주어진 시작값과 함께 오른쪽에서부터 축소

List(1,2,3).foldRight(0)(_-_)   // 2

이유 : 

3 - 0 = 3

2 - 3 = -1

1 - -1 = 2

 

List(1,2).foldRight(0)(_-_)   // -1

이유 : 

2 - 0 = 2

1 - 2 = -1

 

reduceLeft 를 사용하여 List 를 첫번째 요소값과 함께 왼쪽에서부터 축소

List(1,2,3).reduceLeft(_-_)   // -4

이유 : 

1 - 2 = -1

-1 - 3 = -4

 

reduceRight 를 사용하여 List 를 마지막 요소값과 함께 오른쪽에서부터 축소

List(1,2,3).reduceRight(_-_)   // 2

이유 : 

2 - 3 = -1

1 - -1 = 2

 

scanLeft 를 사용하여 List 를 주어진 시작값과 함께 왼쪽에서부터 처리한 각 누곗값의 List 를 반환

List(1,2,3).scanLeft(0)(_-_)   // List(0, -1, -3, -6)

이유 : 

처음 주어진 값 = 0 

0 - 1 = -1

-1 - 2 = -3

-3 - 3 = -6

 

scanRight 를 사용하여 List 를 주어진 시작값과 함께 오른쪽에서부터 처리한 각 누곗값의 List 를 반환

List(1,2,3).scanRight(0)(_-_)   // List(2, -1, 3, 0)

이유 : 

처음 주어진 값 = 0

3 - 0 = 3

2 - 3 = -1

1 - -1 = 2

 

reduceLeft, reduceRight 처럼 방향성이 있는 것과

그냥 reduce 처럼 방향성이 없는 것에 차이가 존재함

이를테면, 아래와 같은 연산을 진행할 때

방향이 존재하는 foldLeft, foldRight 는 실행 가능하고 

방향이 존재하지 않는 fold 는 실행이 불가능함

List(1,2,3).foldLeft(false) {(a,b) => if(a) a else b==2}   // true

List(1,2,3).foldLeft(false) {(a,b) => if(a) a else b==4}   // false

List(1,2,3).foldRight(false) {(a,b) => if(b) b else a==2}   // true

List(1,2,3).foldRight(false) {(a,b) => if(b) b else a==4}   // false

List(1,2,3).fold(false) {(a,b) => if(a) a else b==2}   // error

 

 

 

 

 

 

 

 

 

 

집합(Set). 한 번 생성되면 내부 값을 바꿀 수 없음 

 

val mySet = Set()

val mySet = Set(1,2,3)

val mySet = Set("a","b",1,1,2,2)   //"a", "b", 1, 2

 

 

 

 

 

 

 

 

 

 

 

Map (Java 의 HashMap, Python 의 dictionary). 이 역시 생성된 후 내부 값 변경이 불가능

 

val myMap = Map()

val myMap = Map(1->"a", 2->"b")

myMap(1)   // "a"

myMap(2)   // "b"

myMap.contains(1)   // true

myMap.contains(3)   // false

for ( pairs <- myMap ) {

  val key = pairs._1

  val value = pairs._2

  println(key+" "+value)

}

결과 : 1 "a", 2 "b"

 

 

 

 

 

collection 간 전환은 아래와 같이 가능함

 

mkString 를 사용하여 collection 을 구분자로 구분된 String 으로 전환

List(1,2,3).mkString("-")   //"1-2-3"

Set(1,2,3).mkString("-")   //"1-2-3"

 

toBuffer 를 사용하여 collection 을 가변의 List 로 전환

List(1,2,3).toBuffer   // Buffer(1,2,3)

Map(1->1, 2->2, 3->3).toBuffer   // Buffer((1,1), (2,2), (3,3))

 

toList 를 사용하여 collection 을 불변의 List 로 전환

Set(1,2,3).toList   // List(1,2,3)

 

toMap 을 사용하여 튜플이 담긴 collection 을 Map 으로 전환

Set((1,1), (2,2), (3,3)).toMap   // Map(1->1, 2->2, 3->3)

 

toSet 을 사용하여 collection 을 Set 으로 전환

List(1,1,2,2).toSet   // Set(1,2)

 

toString 을 사용하여, collection 의 타입과 내용을 String 으로 전환

List(1,2,3).toString   // "List(1,2,3)"

Set(1,2,3).toString   // "Set(1,2,3)"

 

 

JVM 을 사용하는 Scala 와 Java 간 collection 은 기본적으로 서로 호환되지 않지만

asJava, asScala 를 통해 호환되도록 만들 수 있음

 

import collection.JavaConverters._

val scalaList = List(1,2,3)

scalaList: List[Int] = List(1,2,3)

val javaList = scalaList.asJava

javaList: java.util.List[Int] = [1, 2, 3]

val scalaListAgain = javaList.asScala

scalaListAgain: scala.collection.mutable.Buffer[Int] = Buffer(1,2,3)

 

 

 

 

 

 

collection 을 match 표현식에 사용하는 방법 예제

 

val myList = List(1,2,3)

 

myList(0) match {

  case 1 => "A"

  case _ => "B"

}

결과 : "A"

 

myList(0) match {

  case x if x > 0 => "A"

  case _ => "B"

}

결과 : "A"

 

myList match {

  case x if x contains (2) => "A"

  case _ => "B"

}

결과 : "A"

 

myList match {

  case List(1,

 

 

 

 

 

 

 

call by name, call by ref 차이는?

 

 

still remember I said I'was strong and wan't tired after swimming.

but, turns out, It was really wrong.

after finishing my work and dinner at 7:30, I don't have any energy to do something.

Well, I thought 1 hour between dinner and swimming class was enough to study English or Computer Science.

Now I just lay down on my sofa and watch youtube. thats all.

since Thursday, I didn't want to go to the swimming class....

9PM class ruins my schedule and sucks my energy a lot.

but while taking the class It's very fun to swim.

Thank God It's Friday. really.

during the weekend I don't need to take the class and make time to study English.

Now I'm at a cafe to review this weekend and rearrange my life's dirention.

 

 

 

this is the result chatgpt reviewed.

 

I still remember saying I was strong and wasn't tired after swimming.

It turns out I was really wrong.

After finishing my work and dinner at 7:30PM, I don't have any energy to do anything.

I thought one hour between dinner and my swimming class would be enough to study English or Computer Science.

But now, I just lay down on my sofa and watch youtube. That's all.

Since Thursday, I haven't wanted to go to the swimming class.

The 9PM class ruins my schedule and drains my energy.

But, while taking the class, It's very fun to swim.

Thank God It's Friday. Really.

During the weekend, I don't need to take the class and can make time to study English.

Now, I'm at a cafe to review this weekend and rearrange my life's direction.

 

'English' 카테고리의 다른 글

Study English 24.06.29-07.02  (0) 2024.07.02
Study English 24.06.28  (0) 2024.06.29
Study English 24.06.27  (0) 2024.06.27
Study English 24.06.26  (0) 2024.06.26
Study English 24.06.25  (0) 2024.06.25

 

< Athena >

Athena 는 s3 버킷에 저장된 데이터 대상으로 쿼리를 날릴 수 있는 서비스

Athena 는 SQL 언어를 사용하는 Presto 엔진에 빌드됨

S3 데이터를 RDB 등에 넣지 않고도, s3 데이터 그대로 쿼리 실행이 가능

csv, json, orc, avro, parquet 등 다양한 형식으로 저장된 s3 데이터를 지원

Athena 는 Amazon QuickSight 라는 도구와 함께 사용하는 일이 많음

QuickSight 를 통해, 보고서와 대시보드를 생성함...!

Athena 를 통해, AWS 서비스에서 발생하는 모든 로그를 쿼리하고 분석 가능

 

Athena 성능을 향상하려면, 읽는 데이터 크기를 줄이면 됨.

열 기반 포맷인 Parquet, Orc 를 사용하면 Athena 성능이 향상됨

Parquet, Orc 를 사용하려면 Glue 를 사용해야 함

Glue 는 ETL 을 통해 csv 와 parquet 간 데이터 변환하는데 매우 유용함

성능 향상의 또 다른 방법은, 테이블을 파티션으로 나눠 저장하는 것.

예를들어

s3://mybucket/date=20240625/...

s3://mybucket/date=20240626/...

s3://mybucket/date=20240627/...

s3://mybucket/date=20240628/...

s3://mybucket/date=20240625/...

성능 향상의 다른 방법은, 큰 파일을 사용하여 오버헤드를 최소화 하는 것

크기는 작지만 수가 많은 파일들을 읽는 것보다

크기는 128mb 이상이지만 수는 적은 큰 파일들을 읽을 때 성능이 더 좋음

왜냐면 파일이 클수록 스캔과 검색이 쉽기 때문.

(HDFS 의 block size 같은 느낌인데?)

 

Athena 는 s3 뿐 아니라 어떤 곳의 데이터도 쿼리가 가능함

RDB, NOSQL, on premises 등 어떤 곳이든 가능

이게 어떻게 가능하냐? Data Source Connector 를 사용하면 됨

 

 

쿼리 결과는 s3 버킷에 저장 가능하다고 함

 

 

 

 

 

 

< EMR >

 

Elastic MapReduce 의 약자.

하둡 클러스터를 생성해 줌

EMR 은 EC2 인스턴스의 모음인 클러스터를 구성함

그리고 인스턴스들은 아래처럼 각자 역할을 부여받고 동작함

- Master Node : 클러스터를 관리하고 다른 모든 노드의 상태를 조정함. 오랫동안 실행되는 인스턴스

- Core Node : 태스크를 실행하고, 데이터를 저장함. 이것도 오랫동안 실행되는 인스턴스. cluster 에 처음부터 존재하며, cluster 가 확장될 때는 늘릴 수 있지만 줄이진 못 함.

- Task Node : 태스크를 실행함. Spot instance 사용이 가능. optional 인 노드임. Core Node 와 다른 점은, 일시적으로 데이터 처리 작업을 도와주는 노드라는 것임. (core node 가 할 수 있는 기능인)데이터를 저장하는 작업도 task node는 할 수 없음.  단지 작업 처리를 위한 cpu, ram 등의 리소스를 추가하고 분산 작업해주는 역할을 함. 필요에 따라 늘리고 줄일 수 있음

 

 

 

 

 

< Glue >

 

위에서 언급했듯, Glue 는 ETL 서비스를 관리하는 fully serverless 서비스임

분석을 위한 데이터 변환과 준비에 사용할 수 있음

 

사용례를 살펴보자.

S3 버킷이나 RDS 에 있는 데이터를 DW 인 Redshift 에 로드하는 경우

Glue 를 사용해 추출한 다음, 일부 데이터를 필터링하거나

열을 추가하는 등 원하는 대로 데이터를 변형할 수 있으며

그 최종 결과를 Redshift 에 저장(로드)할 수 있음

 

다른 사용례를 살펴보자.

Glue 는 S3 에 올라간 csv 형태의 데이터를 parquet 으로 변형하고

그 결과를 S3 에 저장할 수 있음

이후 Athena 를 통해 parquet 파일을 쿼리할 수 있음

(물론 Athena 가 csv 도 쿼리 가능하지만,

parquet 처럼 column 기반 데이터 포맷 대상으로 실행하는 쿼리 속도에서 차이가 발생함)

 

Glue Data Catalog 에 대해 알아보자.

Glue Data Catalog 는 datasets 의 카탈로그임.

S3, RDS, DynamoDB, JDBC 등으로부터 데이터를 크롤링 한 다음

Glue Data Catalog 에 데이터에 대한 정보(메타데이터)를 저장함

예를 들면 데이터베이스의 테이블, 열, 데이터 포맷 등등. 모든 메타데이터를 Glue Data Catalog 에 기록

이러한 메타데이터들은, Glue 의 ETL 작업에 활용됨

또한, Athena, EMR 이 데이터와 스키마를 검색할 때 백그라운드에서 Glue Data Catalog 의 데이터를 활용함

예를 들어 Athena 가 어떤 테이블의 파티션 컬럼이 무엇인지 확인할 때

Glue Data Catalog 에서 파티션 컬럼 정보를 갖고옴

(EMR 에서 데이터와 스키마 검색할 때 Data Catalog 를 사용한다고...?)

 

Glue 에 대해 알아야 하는 것들을 살펴보자

- Glue Job Bookmarks 는 새 ETL 작업을 실행할 때 이전 데이터의 재처리를 방지해 줌

- Glue Elastic Views 는 여러 데이터 스토어의 데이터를 (SQL 을 사용하여) 결합하고 복제함

가령 RDS 와 Aurora DB, S3 세 데이터 스토어들 내 테이블들을 SQL 을 사용하여 결합한 View 를 생성 가능함

Glue 가 원본 데이터의 변경 사항을 모니터링 한다고 함

(바로 위에서 설명했듯이) 여러 데이터 스토어에 분산된 가상 테이블(view) 생성 가능함

- Glue DataBrew 는 사전 빌드된 변환을 사용하여 데이터를 정리하고 정규화 함

- Glue Studio 는 ETL 작업들을 생성하고, 실행하고 모니터링하는 GUI 를 제공함

- Glue Streaming ETL 은 ETL 작업을 (배치 작업이 아니라) 스트리밍 작업으로 실행 가능하도록 하며,

실행시 Spark Structured Streaming 위에 빌드되어 작업이 진행됨

 

 

 

 

< Lake Formation >

Data Lake 란, 데이터 분석을 위해 모든 데이터를 한 곳에 저장한 중앙 집중식 저장소임

그리고 Lake Formation 은 Data Lake 생성을 수월하게 해주는 완전 관리형 서비스임

Lake Formation 을 사용하면, 보통 수개월씩 걸리는 작업을 며칠만에 완료할 수 있다고 함

 

Lake Formation 은 Data Lake 에서의 데이터 검색(discover), 정제(cleanse), 변환(transform), 삽입(ingest) 등의 작업을 도와줌

그리고 데이터 수집, 정제나 카탈로깅, 복제 및 복잡한 수작업을 자동화하고

기계 학습 변환 기능으로 중복 제거 수행을 진행할 수 있음

 

Data Lake 에서는 정형 데이터와 비정형 데이터 소스를 결합할 수 있으며, 블루 프린트를 제공함

여기서 말하는 블루프린트는 데이터를 Data Lake 로 migrate 하는 것을 도와주며

S3, RDS, NoSQL 등에서 지원된다고 함

 

Lake Formation 을 사용하는 이유는, 모든 데이터를 한 곳에서 처리할 수 있기 떄문이며

더불어 애플리케이션에서 행, 열 수준의 세분화된 액세스 제어를 할 수 있기 때문임

Lake Formation 에 연결된 애플리케이션에서는 세분화된 액세스 제어가 가능함

 

Lake Formation 은 Glue 위에 빌드되어 실행된다고 함

하지만 Glue 와 직접 상호작업 하지는 않는다고 함

 

예를 들어보자.

Data Lake 로 S3 storage 를 사용하는 상황임

Data Lake 에 넣는 원본 데이터는 RDS, S3, Aurora, NoSQL 등에서 오게 됨

Lake Formation 은 블루프린트를 통해 (바로 위) 원본 데이터들을 Data Lake(s3)로 ingest(주입)함

Lake Formation 에는 Source Crawlers 와 ETL  및 데이터 준비 도구, 데이터 카탈로깅 도구가 포함되어 있어서

크롤러를 통해 원본 데이터 스토리지에서 원본 데이터를 가져와 ETL 을 통해 Data Lake 에 데이터를 주입할 수 있음

Lake Formation 에는 Access Control 기능도 포함되어 있어서

Lake Formation 을 활용하는 Athena, RedShift, EMR, Spark 등의 서비스가 Data Lake 데이터에 접근하는 것을 컨트롤 할 수 있음

 

 

 

Lake Formation 이 사용되는 주요 기능은 바로 access control 기능임

Athena 등에 접근한 사용자들이 Athena 를 통해 Data Lake 의 모든 데이터를 보는 상황을 방지하기 위해

Lake Formation 에서 Athena 에 접근한 사용자별로 어떤 데이터에 읽기 권한을 줄 지, 쓰기 권한을 줄지 설정할 수 있음

 

Lake Formation 이 바라보는 Data Lake 데이터는 S3 에 저장되고

이렇게 저장된 데이터에 접근할 때 필요한 모든 액세스 제어, 행, 열 수준 보안은 Lake Formation 내에서 관리됨

Lake Formation 한 곳에서 보안 관리를 할 수 있다는 것이 큰 장점임

 

따라서 Lake Formation 에 연결하는 사용하는 모든 서비스는 읽기 권한이 있는 데이터만 볼 수 있음

 

 

 

 

 

 

 

 

 

I was invited by my Indian friend to introduce his mother who has stayed for 2 weeks in Kor.

I wanted to show my best to my friend's mother so I bought some Korean tea and 죽 without any meats because she is a vegetarian.

I went to his house at noon and met her mother.

She already prepared Indian food for guests.

the other guests arrived and we had lunch together talking about many nice topics.

Evenyone was from India except for me, Thankfully they used English only for me to understand them.

we also ordered mango 빙수 and enjoyed together.

It was her last day. Hoped she came back home safe.

 

I started to take a swimming lesson for beginners.

The class started at 9pm but I arrived 30 mins earlier just in case.

I made a register card, and entered.

I didn't know how to use 시설 at all, so I just followed how people did.
people visited the locker room first, and took off their cloths.

They took a shower, and then went to the swimming pool.

I couldn't wear my glasses, I saw nothing.

even I didn't know where was my class lane.

Thankfully I found by chance.

We, the beginners, started to walk and sink doing umm-pa umm-pa in the water at first day

50 mins were gone very fast. it meant I enjoyed a lot!

water was cold, so I took a shower with warm water.

I heared people get hungry a lot after swimming but I didn't feel it.

maybe I didn't use my energy a lot... Or I'm very strong lol

Let's see how much I will be different at the end of the class!

 

 

 

 

 

 

ChatGPT revied my diary.

 

I was invited by my Indian friend to meet his mother, who has been staying in Korea for two weeks.

I wanted to make a good impression, so I bought some Korean tea and vegetarian porridge since shw is a vegitarian.

I went to his house at noon and met her. 

She had already prepared Indian food for the guests.

The other guests arrived, and we had lunch together discussing many interesting topics.

Everyone was from India except for me, and thankfully, they spoke in English so I could understand.

We also ordered mang 빙수 and enjoyed it together.

It was her last day, and I hoped she would return home safely.

 

I also started taking a beginner's swimming lesson,

The class began at 9pm but I arrived 30 mins early just in case.

I registered and went in.

Not knowing how to use the facilities, I simply followed what others did.

People visited the locker room first, took off their clothes, showered, and then went to the swimming pool.

I couldn't wear my glasses, so I saw nothing and didn't even know where my class lane was.

Thankfully, I found it by chance.

As beginners, we started by walking and sinking, doing "umm-pa umm-pa" in the water on the first day.

The 50 mins passed very quickly which meant I enjoyed it a lot!

The water was cold, so I took a warm shower afterward.

I had heard that people get very hungry after swimming, but I didn't feel it.

Maybe I didn't use a lot of energy, or I'm just very strong lol

Let's see how much I improve by the end of the class.

 

 

 

 

'English' 카테고리의 다른 글

Study English 24.07.03-05  (0) 2024.07.06
Study English 24.06.28  (0) 2024.06.29
Study English 24.06.27  (0) 2024.06.27
Study English 24.06.26  (0) 2024.06.26
Study English 24.06.25  (0) 2024.06.25

+ Recent posts