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 등)로 확인 가능





 

 

 

 

 

 

 

 

+ Recent posts