변수의 범위, 람다, 내장 함수(
map,filter), 파일 입출력, 그리고 클래스의 첫걸음.
함수를 "값처럼" 다루기 시작한 날이다.
학습 네 번째 날이다. 오늘 배운 내용은 이렇다.
lambda)와 함수를 변수에 담기input()과 split()all, any, chr, dir, divmod, enumerate, eval, filter, map__init__, selfJS에서 배운 전역 변수와 지역 변수의 개념은 Python에서도 그대로 적용된다.
a = 1
def vartest(a):
a = a + 1
vartest(a)
print(a) # 1
vartest 안의 a는 그 함수 안에서만 쓰이는 지역 변수라서, 함수 밖의 a 값에는 영향을 주지 않는다. 그래서 출력은 2가 아니라 1이다.
def vartest(a):
a = a + 1
vartest(3)
print(a) # NameError: name 'a' is not defined
이런 코드도 문제가 된다. 함수 밖 어디에도 a라는 변수가 정의되어 있지 않아서 오류가 난다. 정상적으로 출력되게 고치면 이렇다.
def vartest(a):
a = a + 1
return a
a = vartest(3)
print(a) # 4
함수 안의 결과를 밖에서 쓰려면 return으로 돌려주고, 밖에서 변수에 받아야 한다. return이 없으면 함수를 호출해도 결과값이 없어서 a = vartest(3)의 a는 None이 된다.
# 함수명 = lambda 매개변수: 실행문
abc = lambda a, b: a * b
sub = lambda x, b: x - b
print(abc(3, 5)) # 15
print(sub(9, 4)) # 5
lambda는 이름 없이 한 줄로 만드는 간단한 함수다. 같은 함수를 def로 쓰면 이렇다.
def multi(x, y):
return x * y
multi2 = lambda x, y: x * y
print(multi2(5, 2)) # 10
def plus(x):
return x + 10
plus2 = lambda x: x + 10
print(plus2(11)) # 21
식 하나로 값을 돌려주는 단순한 함수는 lambda로 간결하게 쓸 수 있다. return을 따로 쓰지 않고, 콜론 뒤의 식이 곧 반환값이다. JS의 화살표 함수((x, y) => x * y)와 비슷한 역할이다.
필기에는 람다를 쓰는 이유로 "메모리 절약", "즉시 실행 함수라 정의하자마자 실행됨"이라고 적었는데, 정확하지 않다. 람다는 정의한다고 바로 실행되지 않고, 함수를 만들 뿐이다. 이유는 가독성과 간결함 정도로 보는 것이 맞고, 바로 실행하려면
(lambda x: x + 1)(3)처럼 괄호로 감싸서 호출해야 한다.
def multi(x, y):
return x * y
print(multi(4, 5)) # 20
a = multi # 함수 이름을 a에 할당 (a는 별칭이 된다)
print(a(4, 5)) # 20 (multi에 넣은 것과 같은 결과)
print(type(a)) # <class 'function'>
a = multi처럼 괄호 없이 함수 이름만 대입하면 a가 그 함수를 가리키게 된다. JS에서 head.onclick = on;처럼 함수를 값으로 다루던 것과 같다.
함수는 다른 함수의 매개변수로도 넘길 수 있다. (Day.8의 콜백, 고차 함수와 같은 개념이다.)
def final(x, y, func1):
print(x, y, func1(5, 6)) # 넘겨받은 함수에 5, 6을 넣어서 호출
final(3, 5, a) # 3 5 30
final의 세 번째 인자로 multi(a)를 넘겼고, 안에서 func1(5, 6)이 multi(5, 6)으로 실행되어 30이 나온다.
input()으로 받은 값은 기본적으로 모두 문자열이다.
name = input('이름은?')
score = input('학점은?')
age = input('나이는?')
print(name, score, age)
print(type(name), type(score), type(age)) # 전부 <class 'str'>
숫자로 쓰려면 변환한다.
num1 = int(input('첫번째 정수')) # 문자열 → 정수
num2 = int(input('두번째 정수'))
print(num1 + num2)
float1 = float(input('실수를 입력'))
print(float1, type(float1)) # 정수 3을 입력하면 3.0으로 출력된다
한 줄에 여러 값을 입력받아 나누고 싶을 때 split()을 쓴다.
a, b = input('숫자입력').split() # 입력: 3 4 → ['3', '4']
a, b = int(a), int(b) # 각각 정수로 변환
print(a + b, type(a + b)) # 7 <class 'int'>
split()은 문자열을 기준 문자로 잘라서 리스트로 만드는 함수다. 괄호 안에 아무것도 넣지 않으면 공백(띄어쓰기, 엔터)을 기준으로 자른다.
a = input('숫자입력').split() # 입력: 1 2 3 4 5 6 7 8
# a = ['1', '2', '3', '4', '5', '6', '7', '8']
공백이 아닌 다른 문자로 입력하면 어떻게 될까.
a = input('숫자입력').split() # 입력: 1,2,apple,home,66,go
print(a, type(a)) # ['1,2,apple,home,66,go'] <class 'list'>
공백이 없어서 자를 곳이 없으니 통째로 하나인 리스트가 된다. 쉼표로 자르려면 split(',')처럼 기준 문자를 써 줘야 한다.
print('1,2,apple'.split(',')) # ['1', '2', 'apple']
문자열의 split 함수는 split(sep, maxsplit) 두 파라미터를 가진다.
| 파라미터 | 기본값 | 의미 |
|---|---|---|
sep | None | 구분자. None이면 공백(띄어쓰기, 엔터)으로 자른다 |
maxsplit | -1 | 최대 몇 번 자를지. -1이면 제한 없이 끝까지 자른다 |
'a,b,c'.split(',', 1) # ['a', 'b,c'] (1번만 자름)
'a b c'.split(maxsplit=1) # ['a', 'b c']
# 'a b'.split(1) # 에러: 첫 번째 자리는 sep이라 문자열이어야 한다
sep은 생략하고 split(',')처럼 써도 되고, maxsplit만 쓰려면 이름을 붙여서(maxsplit=1) 써야 한다. (참고: blockdmask 블로그의 split 정리)
문제: 빈 딕셔너리를 만들고, 무한 루프로 이름과 TV를 좋아하는지 입력받아 딕셔너리에 추가한다. "친구한테도 물어볼까?"에 no가 입력되면 루프를 종료하고, 마지막에 items()로 전부 출력한다.
empty = dict()
while True:
name = input('이름을 말해봐')
like_tv = input('TV보는 것은 좋아해?')
empty[name] = like_tv # 이름을 키로, 대답을 값으로 저장
then = input('친구한테도 물어볼까?')
if then == 'no':
break
print(empty.items())
필기에서는 empty['name'], empty['like'] = name, like_tv로 썼는데, 이러면 키가 'name'과 'like' 두 개로 고정되어서 반복할 때마다 값이 덮어씌워진다. empty[name] = like_tv처럼 입력받은 이름을 키로 쓰면 사람마다 하나씩 차곡차곡 쌓인다.
1~5를 담은 리스트를 세 가지 방식으로 만들 수 있다.
# 리스트 컴프리헨션
nums = [i for i in range(1, 6)]
print(nums) # [1, 2, 3, 4, 5]
# list()로 형 변환
li2 = list(range(1, 6))
print(li2) # [1, 2, 3, 4, 5]
# 빈 리스트에 for문으로 추가
re = [] # re = list()
for i in li2:
re.append(i)
print(re) # [1, 2, 3, 4, 5]
append로 하나씩 쌓는 방식은 range(1, 6)을 바로 list()로 감싸면 되는 일이지만, append를 연습하기 위한 흐름이었다.
미리 만들어져 있는 내장 함수들도 배웠다.
| 함수 | 설명 |
|---|---|
abs(x) | 절댓값 |
all(모음) | 요소가 모두 참이면 True, 하나라도 거짓이면 False. 빈 값이면 True |
any(모음) | 요소 중 하나라도 참이면 True, 모두 거짓이면 False. 빈 값이면 False |
chr(i) | 유니코드 숫자 값을 받아 그에 해당하는 문자를 반환 |
dir(모음) | 객체가 가진 내장 함수(.append, .count 등)를 보여 줌 |
divmod(a, b) | a를 b로 나눈 몫과 나머지를 튜플로 반환 |
enumerate(모음) | 순서(인덱스)와 값을 함께 꺼냄 |
eval(문자열) | 문자열로 된 표현식을 실행한 결과를 반환 |
filter(함수, 모음) | 함수의 반환값이 참인 요소만 걸러냄 |
map(함수, 모음) | 모든 요소에 함수를 적용 |
print(all([1, 2, 0]), all([])) # False True
print(any([0, 0]), any([])) # False False
print(chr(97)) # a
print(divmod(7, 3)) # (2, 1)
for i, name in enumerate(['body', 'foo', 'bar']):
print(i, name)
# 0 body
# 1 foo
# 2 bar
for문과 함께 쓰면 현재 요소의 순서와 값을 한 번에 알 수 있다. 값뿐 아니라 인덱스도 필요할 때 편하다.
print(eval('1+2')) # 3
print(eval("'hi'+'a'")) # hia
print(eval('divmod(4,3)')) # (1, 1)
eval은 문자열 안의 식을 코드로 실행해서 결과를 돌려준다. 숫자 식뿐 아니라 문자열 연산이나 함수 호출도 된다. 바깥 따옴표를 벗겨낸 식을 계산하는 것이다. JS의 eval과 같은 역할이고, 사용자가 입력한 문자열을 그대로 넣으면 의도하지 않은 코드가 실행될 수 있으므로 조심해서 써야 한다.
def positive(x):
return x > 0
print(list(filter(positive, [1, 2, -3, -4, -5, 6]))) # [1, 2, 6]
# 람다로 바꾸면
result = list(filter(lambda x: x > 0, [1, 2, -3, -4, -5, 6]))
print(result) # [1, 2, 6]
filter(함수, 모음)은 첫 번째 인자로 함수, 두 번째로 반복 가능한 데이터를 받는다. 데이터의 요소를 하나씩 함수에 넣어서 반환값이 참인 것만 남긴다. 그래서 함수는 조건식이어야 한다. for와 if를 합친 기능이다. JS Day.9에서 배운 filter와 같은 역할이다.
def two_times(numberList):
result = []
for number in numberList:
result.append(number * 2)
return result
print(two_times([1, 2, 3, 4])) # [2, 4, 6, 8]
def two_timess(x):
return x * 2
print(list(map(two_timess, [1, 2, 3, 4]))) # [2, 4, 6, 8]
print(list(map(lambda x: x * 2, [1, 2, 3, 4]))) # [2, 4, 6, 8]
map(함수, 모음)은 요소를 하나씩 함수에 넣어서 그 결과로 바꾼다. filter가 조건(참/거짓)으로 거르는 것이라면, map은 연산을 적용해서 값을 바꾸는 것이다. for로 append하던 것을 한 줄로 줄여 준다.
def pp(x):
return x + 20
print(pp(4)) # 24
print(list(map(pp, [1, 2, 3]))) # [21, 22, 23]
print(list(map(lambda x: x + 20, [1, 2, 3]))) # [21, 22, 23]
re2 = list(map(lambda i: i, [5, 4, 3, 2, 1]))
print(re2) # [5, 4, 3, 2, 1]
list1 = [1, 2, 3, 4, 5]
re3 = list(map(lambda x: x + 10, list1))
print(re3) # [11, 12, 13, 14, 15]
같은 일을 세 가지로 쓸 수 있다.
list1 = [1, 2, 3, 4, 5]
def square(x):
return x ** 2
print(list(map(square, list1))) # [1, 4, 9, 16, 25]
print(list(map(lambda x: x ** 2, list1))) # 람다로 변경
print([i ** 2 for i in list1]) # 리스트 컴프리헨션으로 변경
# 3보다 작은 값만 (filter)
print(list(filter(lambda x: x < 3, range(1, 6)))) # [1, 2]
# 1~20 중 2의 배수만
print([x for x in range(2, 21, 2)]) # 증감값 이용
print([x for x in range(1, 21) if x % 2 == 0]) # if 조건 이용
print(list(filter(lambda x: x % 2 == 0, range(1, 21)))) # filter 이용
| 하는 일 | map / filter | 리스트 컴프리헨션 |
|---|---|---|
| 모든 요소를 변환 | list(map(lambda x: x*2, a)) | [x*2 for x in a] |
| 조건에 맞는 요소만 | list(filter(lambda x: x>0, a)) | [x for x in a if x>0] |
map과 filter는 문법 구조가 같아서 하나를 익히면 나머지도 쉽다. 컴프리헨션은 같은 일을 더 짧게 쓸 수 있다.
map과 filter는 결과를 리스트가 아니라 map 객체로 돌려준다. 그래서 list()로 감싸야 값이 보인다. JS의 map, filter가 곧바로 배열을 돌려주던 것과 다르다. 이 객체는 값이 필요해질 때 계산하는 방식이라, 아무도 값을 꺼내지 않으면 함수가 실행되지도 않는다.
list(map(lambda x: print(x + 100, end=' '), range(1, 11)))
# 101 102 103 ... 110 (list()로 감싸서 값을 꺼내야 실행됨)
list() 없이 map(...)만 쓰면 아무것도 출력되지 않는다. 다만 출력만 하려는 목적이라면 for문이 더 읽기 쉽다.
| 모드 | 의미 |
|---|---|
r | 읽기 (read) |
w | 쓰기 (write). 파일이 없으면 생성하고, 있으면 내용을 지우고 새로 쓴다 |
a | 추가 (append). 기존 내용 뒤에 이어 쓴다 |
f = open('파일명', '파일열기모드')로 파일을 연다.
C:\Python\test.py)./, ../, ../../)한글과 영어는 파일에 저장될 때 숫자로 바뀌어 저장되므로, 저장할 때의 규칙과 읽을 때의 규칙이 같아야 한다. 그래서 open을 할 때 encoding='UTF-8'을 꾸준히 써 주는 것이 좋다.
f = open('./resource/news.txt', 'r', encoding='UTF-8')
print(f.encoding) # 인코딩 확인
print(f.name) # 파일명 확인
print(f.mode) # 열기 모드 확인
result = f.read() # 파일 전체 읽기
print(result)
f.close() # 파일을 열었으면 반드시 닫기
파일 객체는 for문으로 줄 단위로 읽을 수도 있다.
f = open('./resource/news.txt', 'r', encoding='UTF-8')
for a in f:
print(a) # 한 줄씩
f.close()
각 줄 끝에는 줄바꿈(
\n)이 붙어 있어서,print(a.strip())나print(a, end='')로 없앨 수 있다.
with open('./resource/news.txt', 'r', encoding='UTF-8') as f:
c = f.read() # f의 내용 전체를 문자열로 반환
print(type(c)) # <class 'str'>
print(list(c)) # 글자 하나하나를 리스트로
with를 쓰면 블록이 끝날 때 파일이 자동으로 닫힌다. 그래서 f.close()를 따로 쓰지 않아도 된다.
# read(n): n글자만 읽기
with open('./resource/news.txt', 'r', encoding='UTF-8') as f:
c = f.read(30) # 30글자만 읽어서 c에 저장
print(c)
# readline(): 한 줄 읽기
with open('./resource/news.txt', 'r', encoding='UTF-8') as f:
line = f.readline() # 첫 줄
print(line)
line = f.readline() # 한 번 더 쓰면 다음 줄
print(line)
# readlines(): 모든 줄을 리스트로
with open('./resource/news.txt', 'r', encoding='UTF-8') as f:
lines = f.readlines()
print(lines)
for i in lines:
i = i.strip() # 양끝의 공백과 개행문자(\n)를 제거
print(i)
| 방법 | 읽는 범위 |
|---|---|
read() | 전체를 문자열 하나로 (read(n)은 n글자) |
readline() | 한 줄. 호출할 때마다 다음 줄 |
readlines() | 모든 줄을 리스트로 |
for a in f | 한 줄씩 |
readline()을 두 번 쓰면 첫 줄, 둘째 줄이 차례로 나오는 것은 파일에서 읽던 위치가 이어지기 때문이다.
# 'story.txt' 파일을 생성하여 쓴다
with open('./resource/story.txt', 'w', encoding='utf-8') as f:
f.write('hi 파이썬\n') # 문자열 쓰기(저장)
# 이어서 추가
with open('./resource/story.txt', 'a', encoding='utf-8') as f:
f.write('이어서 한 줄 더\n')
# 여러 줄: writelines
with open('./resource/list.txt', 'w', encoding='utf-8') as f:
li = ['React\n', 'DB\n', 'Python\n']
# f.write(li) # 에러: write는 문자열만 받는다
f.writelines(li) # 리스트는 writelines를 이용
write()는 문자열 하나만 쓸 수 있어서 리스트를 넣으면 에러가 난다. 리스트는 writelines()를 쓴다. writelines는 줄바꿈을 자동으로 넣어 주지 않으므로 각 문자열 끝에 \n을 직접 붙여야 한다. 또 'w'로 같은 파일을 다시 열면 이전 내용이 사라지므로, 이어 쓸 때는 'a'를 쓴다.
# Q3. 프로그램 오류 수정하기
input1 = int(input('첫 번째 숫자를 입력하세요'))
input2 = int(input('두 번째 숫자를 입력하세요'))
total = input1 + input2
print(f'두 수의 합은 {total}입니다')
input의 결과는 문자열이라서, int()로 감싸야 숫자로 더해진다. 감싸지 않으면 '3' + '4'가 '34'로 이어 붙여진다.
# Q4. 출력 결과가 다른 것은?
print('you', 'need', 'python') # you need python
# 쉼표로 구분한 값 사이에는 자동으로 공백이 들어간다
# Q5. 프로그램 오류 수정하기 2
f1 = open('text.txt', 'w')
f1.write('life is too short')
f1.close() # 닫아야 내용이 확실히 저장된다
f2 = open('text.txt', 'r')
print(f2.read())
쓰기를 마친 파일을 close()하지 않고 바로 읽으면 내용이 아직 저장되지 않았을 수 있다. 한글을 쓸 때는 encoding='utf-8'도 함께 써 준다.
# Q6. 사용자 입력 저장하기
user_input = input('저장할 내용을 입력하세요 :')
f = open('test.txt', 'a', encoding='utf-8')
f.write(user_input)
f.write('\n')
f.close()
# Q7. 파일의 문자열 바꾸기
with open('test2.txt', 'r', encoding='utf-8') as f:
body = f.read()
body = body.replace('java', 'python')
with open('test2.txt', 'w', encoding='utf-8') as f:
f.write(body)
파일 전체를 읽어서 replace로 바꾼 뒤, 'w'로 다시 열어서 덮어쓰는 흐름이다. replace는 원본 문자열을 바꾸지 않고 바뀐 새 문자열을 돌려주므로 body = body.replace(...)처럼 다시 대입해야 한다.
123, 'Hello', [1, 2, 3], Dog() 등class Profile:
def __init__(self):
pass
a, b = Profile(), Profile() # a와 b는 Profile의 인스턴스
print(type(a)) # <class '__main__.Profile'>
__init__과 selfclass Profile:
name = 'default' # 클래스 변수 (클래스 블록 안, 모든 인스턴스가 공유)
# 초기화 함수 (self: 객체 자기 자신)
# __init__은 객체를 만들 때 자동으로 호출되는 메소드
def __init__(self, name, age): # 인스턴스 변수: 각 인스턴스마다 따로 존재
self.name = name
self.age = age
my_name = Profile('Tom', 25) # 객체가 생성되면 __init__이 자동 호출된다
print(my_name.name, my_name.age, type(my_name), type(my_name.name), type(type(my_name.age)))
# Tom 25 <class '__main__.Profile'> <class 'str'> <class 'type'>
p2 = Profile('Amy', 30)
print(p2.name) # Amy
Profile('Tom', 25)처럼 클래스 이름 뒤에 괄호를 붙이면 인스턴스가 만들어지고, 그 순간 __init__이 자동으로 호출된다. self는 만들어지는 인스턴스 자신이며, self.name = name은 "이 인스턴스의 name에 값을 저장한다"는 뜻이다. 인스턴스를 만들 때는 self 자리에 값을 넣지 않고 그 다음 매개변수부터 넘긴다.
type(type(my_name.age))가 <class 'type'>으로 나오는 것은, age의 타입이 int이고 int 자체가 type 클래스의 객체이기 때문이다. 파이썬에서는 클래스도 객체다.
print(my_name == p2) # False (서로 다른 객체)
print(id(my_name), id(p2)) # 주소가 다르다
print('{} {} {} {}'.format(my_name.name, my_name.age, p2.name, p2.age))
# Tom 25 Amy 30
c = my_name
print(my_name == c, id(my_name), id(c)) # True, 주소가 같음 (같은 객체)
from copy import copy
c = copy(my_name)
print(my_name == c, id(my_name), id(c)) # False, 주소가 다름
필기의 주석에는
copy한 경우를 "값은 같지만 주소가 다름"이라고 적었는데, 이 코드의==결과는False다. 직접 만든 클래스는==를 따로 정의하지 않으면 같은 객체인지(주소)로 비교하기 때문이다. 값이 같은지로 비교하게 하려면 클래스에__eq__메소드를 정의해야 한다.
위 Profile에는 name이 두 군데 있다. 클래스 블록 안의 name = 'default'는 클래스 변수(모든 인스턴스가 공유), __init__ 안의 self.name은 인스턴스 변수(인스턴스마다 개별 존재)다.
print(Profile.name) # default (클래스 변수)
print(my_name.name) # Tom (인스턴스 변수가 우선)
print(p2.name) # Amy
인스턴스에 같은 이름의 변수가 있으면 인스턴스 변수가 먼저 쓰이고, 없을 때만 클래스 변수를 찾아간다.
class Test:
def func1(): # self 인자가 없는 함수
print('self 없는 함수')
def func2(self): # self 인자가 있는 함수 → 인스턴스 메소드
print(id(self))
print('인스턴스 메소드임')
a = Test()
# a.func1() # 에러
a.func2() # 호출하면 자동으로 self(인스턴스 자신)가 전달된다
Test.func1() # 클래스에서 직접 호출하므로 에러 없이 작동
# Test.func2() # 에러: self에 들어갈 인스턴스가 없다
Test.func2(a) # 클래스에서 직접 부를 때는 self 자리에 인스턴스를 직접 넘긴다
a.func1()이 에러인 이유: 인스턴스를 통해 호출하면 파이썬이 내부적으로 func1(a)처럼 인스턴스를 첫 인자로 자동 전달하는데, func1은 받을 매개변수가 없어서 takes 0 positional arguments but 1 was given 오류가 난다.Test.func2()가 에러인 이유: 클래스에서 직접 호출하면 자동 전달이 없으므로 self 자리가 비어 버린다. Test.func2(a)처럼 직접 넘겨 주면 된다.필기에서는
self가 없는func1을 "클래스 메소드"라고 불렀는데, 파이썬에서 클래스 메소드는@classmethod를 붙여 만드는 별도의 것(첫 인자cls)이다.self없이 클래스 안에 쓴 이 함수는 그냥 인스턴스로 부를 수 없는 함수일 뿐이라, 용어를 구분해 두는 것이 좋다.
return으로 돌려주고 받아야 한다.lambda는 이름 없는 한 줄 함수이고 정의한다고 바로 실행되지 않는다. 함수는 변수에 담거나 다른 함수의 인자로 넘길 수 있다.input()은 항상 문자열이고, split()은 문자열을 잘라서 리스트로 만든다(기본은 공백 기준).map은 변환, filter는 걸러내기이고, 결과는 list()로 감싸야 보인다.'r'(읽기), 'w'(덮어쓰기), 'a'(추가)로 열고, with를 쓰면 자동으로 닫힌다. 인코딩은 utf-8로 맞춘다.__init__은 생성 시 자동 호출되고, self는 인스턴스 자신이다.Tags: Python lambda map filter 파일입출력 클래스 self 변수스코프 개발자 학습기록