간단하게 파이썬 기초 문법을 정리해보자
변수 (Variable)은 데이터 값의 식별자이다.
# 나의 고양이 이름 문자열 "bezzy"에 cat을 할당
cat = "bezzy"
print(cat)
> bezzy
id()
메서드를 통해 해당 변수의 위치를 알 수 있다. (C 언어에서의 변수의 메모리상에서의 위치라고 할 수 있다.)
>>> id(cat)
140186100161536
>>> cat2 = "bezzy"
>>> id(cat2)
140186100161536
>>> cat == cat2
True
파이썬에서는 보통 snake case 스타일로 변수 네이밍을 한다.
즉 띄어쓰기 있는 부분에 언더스코어를 넣으면 된다.
myCat(X)
my_cat(O)
파이썬은 동적 타입 시스템을 가진 언어다. (Dynamically Typed Language) 말인즉슨, 변수가 하나의 타입에 고정되지 않고 여러가지 타입을 가질 수 있다는 뜻.
>>> my_cat = "bezzy"
>>> type(my_cat)
<class 'str'>
>>>
>>> my_cat = 12332
>>> type(my_cat)
<class 'int'>
변수 자체가 타입을 가지고 있는 다른 언어들(C, C++, Static Typed Languages)과 달리 파이썬의 변수는 타입이 지정되어있지 않다.
변수가 런타임동안 여러 타입의 값을 가질 수 있다는 것이 파이썬의 타입 시스템이 약하다는 것은 아니다. 파이썬의 타입 시스템이 동적, 다이나믹 하다는 것은 변수가 특정 타입에 종속되어 불변으로 고정되지 않는다는 것이지, 특정 타입을 가지고 있는 값들의 타입이 런타임동안 마음대로 변할 수 있다는 것은 아니다.
파이썬은 Strongly Typed Language이다.
예를들어, 정수형1
과 문자열“1”
의 더하기 연산은 TypeError를 일으킨다. 이를 피하기 위해서는 필수적으로 명시적 형 변환(Explicit Type Conversion)이 필요하다.
파이썬은 당연하지만 기초적인 연산을 모두 제공한다.
+
: 더하기 연산-
: 빼기 연산/
: 나누기 연산 (파이썬 2에서는 이 연산자가 정수 나누기 연산이었다. PEP238에서 실수 나눗셈으로 변경됨.)^2*
: 곱하기 연산//
: 정수 나누기 연산 (floor division), 나눈 값을 내림해서 가져온다. (파이썬 3에서 추가됨.)%
: 모듈러 연산 (Modulo Operator), 나눗셈의 나머지만 가져온다.**
: 거듭제곱 연산 위의 기본 연산들에
=
을 붙이면 변수에 바로 연산된 값을 할당 가능하다.
>>> a = 10
>>> a += 2
>>> a
12
>>> a %= 5
>>> a
2
논리 연산은 연산의 대상의 참 / 거짓 값을 평가하여 해당 논리 연산(and / or / not)에 맞는 연산을 수행한다. 이때 파이썬의 Lazy Evaluation을 고려하면 연산의 결과를 예측 가능하다.
# 첫 요소가 거짓인 경우
## 파이썬은 첫 번째 요소의 참 거짓 유무(거짓)를 판단한 후 거짓이므로 and operator의 성질에 따라 8을 볼 필요도 없이 결과값인 첫 번째 요소(거짓)를 반환한다.
>>> 0 and 8
0
>>> False and 8
False
>>> print(None and 8)
None
# 두 요소 모두 참인 경우
## 두 번째 요소까지 보고 참이므로 두 번째 요소(참)를 반환한다.
>>> 1 and 3
3
# 마지막 요소가 거짓인 경우
## 두 번째 요소까지 보고 거짓이므로 두 번째 요소(거짓)을 반환한다.
>>> 3 and 0
0
>>> 3 and False
False
>>> print(3 and None)
None
# 두 요소 모두 거짓인 경우
## 두 번째 까지는 볼 것도 없이 첫 번째 거짓인 요소를 반환한다.
>>> False and 0
False
>>> 0 and False
0
# 첫 요소만 거짓인 경우
## 첫 요소가 거짓이어도 두 번째 요소까지 검사한 후 참인 두 번째 요소를 반환한다.
>>> 0 or "TRUE"
'TRUE'
# 두 요소 모두 참인 경우 또는 첫 요소만 참인 경우
## 첫 요소만 봐도 or 연산의 결과를 알 수 있으니 참인 첫 요소를 반환한다.
>>> 33 or False
33
>>> 33 or 44
33
# 두 요소 모두 거짓인 경우
## 두 번째 요소까지 검사하고 거짓이므로 거짓인 두 번째 요소를 반환한다.
>>> False or 0
0
>>> 0 or False
False
포함 연산은 해당 값이 iterable
한 두 번째 인수에 포함되는지의 여부를 반환한다. not in
연산자는 반대의 논리를 수행한다. 포함 연산의 결과는 bool
타입이다.
비교연산은 두 인자가 같은 값을 가지는지의 여부를 반환한다. 비교 연산의 결과는 bool
타입이다.
<
>
>=
<=
==
!=
객체 비교연산
객체 비교연산은 두 인자가 같은 객체를 가리키는지의 여부를 반환한다.
is
is not
그런데 문자열의 경우 list
와는 다른 결과가 나온다. 즉 문자열은 같은 문자열을 할당할 때 변수는 실제로 같은 객체를 가리키게 된다. 이 부분을 완전히 이해하기 위해서는 인터프리터가 가지고 있는 interning table
에 대해 이해하는게 필요하다. ^3 ^5 파이썬은 불변 타입(immutable types)에 대해서는 interning table
을 사용한다고 한다. (e.g. 정수, False, True와 같은 싱글턴 타입) 그래서 이런 intern을 사용하는 값에 대해서는 같은 값을 할당하면 같은 객체를 가리키게 되는 경우가 있다. (하지만 구현에 따라 반드시 그런 것은 아니라고 한다..)
문자열의 경우는 같은 모듈에서 같은 문자열이 쓰인 경우 최적화를 위해서 컴파일러가 값을 folding
하기 때문에 이런 경우가 생긴다고 한다.
하지만 프로그래머 입장에서 객체의 동일성을 비교할 일은 많지 않으니 아래의 두 경우가 아니라면 그냥 보통의 경우 값을 비교하는 ==
를 쓰면 될 것 같다.
1. Singleton 값의 가독성 좋은 비교 x is None
2. 두 가변 변수가 서로 같은 객체를 가리켜서 한 변수의 변화가 다른 변수에 영향 끼치는지의 여부를 알고 싶을 때.
# is 연산이 True가 나오기 위해서는 해당 객체의 주소인 id 메서드의 값이 같아야 한다.
>>> a = [1,2]
>>> b = [1,2]
>>> a is b
False
>>> id(a)
140186100056904
>>> id(b)
140186100165704
# 그런데..
>>> z = 'abc'
>>> w = 'abc'
>>> z is w
True
>>> id(z)
140186098086160
>>> id(w)
140186098086160
# 작은 정수의 경우
>>> a = 12
>>> b = 12
>>> a is b
True
# 큰 정수의 경우
>>> a = 123123498314987
>>> b = 123123498314987
>>> a is b
False
is
와==
의 차이?
is
는 비교하는 대상이 같은 객체일 경우에 True를 반환
==
는 비교하는 대상의 값이 같은 경우에 True를 반환
즉 is가 더 좁은 비교이다.
>>> a = [1,2,3]
>>> b = a
>>> c = [1,2,3]
>>> a is b
True
>>> a is c
False
>>> a == b
True
>>> a == c
True
대상의 바이너리 값을 비트 별로 논리 연산한다.
&
: 비트 별 AND|
: 비트 별 OR^
: 비트 별 XOR~
: 비트 별 NOT<<
: left shift>>
: right shift참고: 위 비트 연산도 =을 붙여 바로 할당 가능.
>>> 9 & 10
8
>>> print(bin(9)[2:])
1001
>>> print(bin(10)[2:])
1010
>>> print(bin(9 & 10)[2:])
1000
>>> 0b1000
8
# 2칸 right shift
>>> a = 10
>>> a >>= 2
>>> a
2
>>> bin(10)
'0b1010'
>>> bin(2)
'0b10'
# 2칸 left shift
>>> a = 10
>>> a <<= 2
>>> a
40
>>> bin(40)
'0b101000'
# !! indentation 중요..!
# 조건은 논리 조건으로 참일때만 해당 블록이 실행된다.
'''
if (조건):
내용
elif (조건2):
내용
else:
내용
'''
# in-line으로 쓰려면?
'''
if (조건): 내용
'''
# 3항 연산
'''
(내용1) if (조건1) else (내용2)
(내용1) if (조건1) else (내용2) if (조건2) else (내용3)
'''
for
loop는 특정 반복 회수가 정해진 상태에서의 반복문이다.
'''
for <변수> in <iterable object>:
<statements>
'''
>>> a = ['com','jun','dev']
>>>
>>> for x in a:
print(x)
com
jun
dev
Iterable
한 객체는 반복 가능한 객체를 의미한다. iter()
함수를 통해 iterator
로 변환될 수 있다.__iter__
메서드를 정의하고 iterator
클래스를 반환해주면 해당 클래스가 iterable하게 만들 수 있다.Iterator
는 Iterable한 객체로부터 연속적으로 값을 산출해내는 객체를 의미한다.Iterator
객체의 next
메서드를 통해 다음 값을 가져올 수 있다.Iterable
객체의 마지막 요소를 뱉은 후에는 StopIteration
예외가 발생한다.>>> a
['com', 'jun', 'dev']
>>> itr = iter(a)
>>> itr
<list_iterator object at 0x7f7f9eb3df60>
>>> itr.__next__()
'com'
>>> next(itr)
'jun'
>>> next(itr)
'dev'
>>> next(itr)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
while
loop는 특정 조건이 만족될 때 까지 수행되는 반복문이다.
# 표현식이 참인 동안 인덴테이션 블록 안을 반복 실행한다.
'''
while <표현식>:
<statements>
'''
>>> a = ['foo', 'bar', 'baz']
>>> while a:
... print(a.pop(-1))
...
baz
bar
foo
# 한 줄로도 표현 가능하다.
>>> a = ['foo', 'bar', 'baz']
>>> while a:
... print(a.pop(-1))
...
baz
bar
foo
함수를 프로그래밍하는 것은 해당 함수의 기능을 추상화하고 재사용할 수 있다는 점에서 매우 중요하다. 잘 짜여진 함수를 통해 프로그램의 기능들을 모듈화하고 기능 구현에 있어서 작은 문제들의 해결을 통해 큰 목적을 해결할 수 있다.
네임스페이스
파이썬에서 함수가 만들어지면 자신만의 네임스페이스 또한 생성된다. 이를 통해 함수의 내부와 외부를 구분하여 변수의 이름을 정하거나 사용할 수 있다.
'''
def <함수 이름>([<함수의 파라미터(들)>]):
<함수 바디>
'''
함수의 인자는 함수가 실행될 때 외부에서 받아오는 입력을 의미한다. 파이썬 함수에 입력을 제공하는 방법에는 여러가지가 있다.
함수에 인자를 제공하는 가장 간단한 방법이다. 함수의 정의 부분에 입력 파라미터를 콤마로 구분해 제공하면 함수를 실행하는 부분에서 순서에 맞게 해당 파라미터에 입력값이 들어가게 된다.
정의된 파라미터 수 보다 적은 수나 더 많은 수의 파라미터가 들어가면 에러가 발생한다.
인자에 디폴트 값을 줄 수도 있지만 이 때에는 디폴트 인자가 다른 인자보다 선행할 수 없다. 이유는 당연하다. 디폴트 값을 가진 인자 여러개가 디폴트 값을 가지지 않은 위치 인자 중간에 끼어있다면, 함수에 전달되는 인자들이 디폴트 값을 재설정 하기 위한 것인지 본래 위치 인자에 넣고자 하는 것인지 알아 챌 수 없기 때문이다. 아래 함수가 정의됐다고 가정하고 func_with_default(1,2,3)
을 호출하면 b와 c중 무엇이 2로 디폴트 값이 변경되었는지 알 길이 없다.
>>> def func(a, b, c):
return print(f'{a}, {b}, {c}')
>>> func(1,3,5)
1, 3, 5
>>> def func_with_default(a, b="default", c="default2", d):
return print(f'{a}, {b}, {c}, {d}')
File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
주의! 가변 디폴트 변수
함수 인자의 디폴트 값은 함수가 정의되었을 때 (즉,def
키워드로 시작하는 함수 정의문이 실행되었을 때) 한 번만 선언된다. 즉 매번 함수 실행시마다 선언되지 않는다.
그래서 함수 인자에 디폴트 값으로 가변 객체(예를들어, 빈 리스트)와 같은 것을 주고 해당 값에 대한 작업을 수행하면 함수가 실행될때마다 파라미터가 초기화되지 않고 변화가 누적된다. 이것을 간과하면 프로그램에서 예기치 않은 오류가 발생할 수 있으니 주의가 필요하다.
이를 해결하기 위해서는 디폴트 값을None
으로 먼저 주고, 디폴트 값이 변하지 않았을 시 함수 내부에서 디폴트 값을 초기화 해주는 방법이 있다.
def my_strange_func(x=[]):
x.append("My strange function is executed!")
print(x)
>>> my_strange_func()
['My strange function is executed!']
>>> my_strange_func()
['My strange function is executed!', 'My strange function is executed!']
>>> my_strange_func()
['My strange function is executed!', 'My strange function is executed!', 'My strange function is executed!']
def not_strange_anymore(x=None):
if x is None:
x = []
x.append("Executed!")
print(x)
>>> not_strange_anymore()
['Executed!']
>>> not_strange_anymore()
['Executed!']
>>> not_strange_anymore()
['Executed!']
파이썬의 함수로는 변수가 어떻게 전달될까?
다른 프로그래밍 언어에서는 함수에 인자가 전달되는 방식에 크게 두 가지 방식이 있다. 첫 번째는 값에 의한 전달(call by value), 두 번째는 참조에 의한 전달(call by reference)이다. 값에 의한 전달에서 함수에 전달된 인자 값은 함수 내부에서 어떤 변화를 겪어도 함수를 콜 한 환경에서의 변수에는 영향을 끼치지 못한다. 그러나 참조에 의한 전달의 경우에는 함수 내부에서의 조작이 함수의 콜 환경에 있는 변수가 참조하는 값에 영향을 준다. 파이썬의 경우 어떠할까?
결론부터 말하자면 파이썬은 두 경우 모두에 속하지 않는다. 파이썬의 인자 전달 방식을 할당에 의한 전달 (passed by assignment)라고 한다. 파이썬에서 모든 값은 객체이고 모든 변수는 해당 객체를 가리키고 있을 뿐이다. 예를들어 3이라는 불변 객체를 a 라는 변수가 가리키고 있는 상황을 우리는 a = 3 이라는 파이썬 코드로 표현하는 것이다. 이제 a += 1 이라는 방식으로 a의 값을 변화시켰다고 해보자. 직관적으로는 a 라는 변수가 가리키던 값 3에 1이 더해져서 3이 4로 변화했다고 생각할 수 있으나, 사실은 a가 4라는 다른 새로운 객체를 가리키게 된 것이다. (아래 코드 참고)
>>> id(3)
4339656592
>>> id(4)
4339656624
>>> a = 3
>>> id(a)
4339656592
>>> a += 1
>>> a
4
>>> id(a)
4339656624
def change_to_3(number):
print(id(number))
number = 3
print(id(number))
>>> a = 4
>>> print(id(a))
4339656624
>>> change_to_3(a)
4339656624
4339656592
>>> print(a)
4
함수에 이러한 불변 객체를 가리키고 있는 a와 같은 변수가 인자로 전달되었을 때도 마찬가지다. 위의 change_to_3이라는 함수에 a가 인자로 입력되었을 때 일어나는 일은 함수의 number라는 파라미터가 a가 가리키는 4라는 객체를 함께 가리키게 된다. (4라는 객체를 a와 number라는 변수 둘이서 가리키고 있는 상황) 이후 number가 3으로 재할당 되는 상황은 그저 number가 가리키는 대상이 변할 뿐 원래 a가 가리키는 4에는 아무 영향을 끼치지 못하는 것이다. 이러한 상황은 가변 객체가 전달될 때는 조금 달라진다.
a = [1,2,3,4]
def touch_inside_list(l):
print("Something Changed!")
l[2] = 100
>>> print(a)
[1, 2, 3, 4]
>>> print("id of a:")
id of a:
>>> print(id(a))
140700944638280
>>> print("id of a[2]:")
id of a[2]:
>>> print(id(a[2]))
4339656592
>>> touch_inside_list(a)
Something Changed!
>>> print(a)
[1, 2, 100, 4]
>>> print("id of a:")
id of a:
>>> print(id(a))
140700944638280
>>> print("id of a[2]:")
id of a[2]:
>>> print(id(a[2]))
4339659696
위 상황에서 함수
touch_inside_list
가 전달받은 것은 함수의 콜 스택에 있는 변수 a가 가리키는 가변 객체 [1,2,3,4]로의 참조이다. 함수의 내부 변수l
또한 해당 객체를 바라보게 되는데 이때 l을 다른 객체로 재할당 한 것이 아닌, 가변 객체 내부의 요소l[2]
가 참조하는 객체를 3에서 100으로 바꾸어 준다.
결과적으로 콜스택의 a가 바라보는 가변객체 [1,2,3,4]의 위치는 함수 실행 전이나 후나 변하지 않았으나, 해당 가변객체 내부에서 참조하는 내용은 변할 수 있다. 이러한 파이썬의 함수 인자 전달 방식을 Pass By Assignment라고 한다.
만일 우리가 구현하고자 하는 함수의 인자 수가 미리 주어지지 않는다면 가변 길이 인자를 사용할 수 있다. 이를 위해 곱셈 연산에 쓰이는 별표 (Asterisk, *)를 컨테이너 타입의 데이터를 풀어내는데(unpack) 사용할 수 있다.
# Container Unpacking
>>> t = ('I', 'AM', 'Tuple')
>>> print(*t)
I AM Tuple # Set과 list도 마찬가지로 동작..
def use_variable_args(*args):
print(args)
>>> use_variable_args(*t)
('I', 'AM', 'Tuple')
def func_param_with_var_args(name, *args, age):
print("name=",end=""), print(name)
print("args=",end=""), print(args)
print("age=",end=""), print(age)
...
"""
위 함수에서 두 번째 가변 길이 인자가 몇 개인지 알 수 없기 때문에 사실상 age에는 인자가 할당 될 수 없다. 아래 두 번째 함수처럼 가변 길이 인자를 위치 인자의 뒤로 보내줘서 위치 인자에 먼저 할당 될 수 있게 해줘야 한다.
"""
>>> func_param_with_var_args("정우성", "01012341234", "seoul", 20)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: func_param_with_var_args() missing 1 required keyword-only argument: 'age
def func_param_with_var_args2(name, age, *args):
print("name=",end=""), print(name)
print("args=",end=""), print(args)
print("age=",end=""), print(age)
>>> func_param_with_var_args2("정우성", 20, "01012341234", "seoul")
name=정우성
args=('01012341234', 'seoul')
age=20
위치 인자 뿐 아니라 키워드 인자 또한 가변 길이로 전달 가능하다. 이 경우에는 파이썬의 dictionary를 언패킹해서 전달하면 된다.
d = {'1': 'I', '2': 'AM', '3': 'DICT'}
def use_kargs(**kwargs):
for k, i in kargs.items():
print(k,i)
use_kwargs(**d)
1 I
2 AM
3 DICT
"""
가변 키워드 인자의 경우에도 마지막에 와야 한다. 아니면 문법 에러가 난다.
"""
def func_param_with_kwargs(name, age, **kwargs, address=0):
print("name=",end=""), print(name)
print("age=",end=""), print(age)
print("kwargs=",end=""), print(kwargs)
print("address=",end=""), print(address)
>>>
def func_param_with_kwargs(name, age, **kwargs, address=0):
^
SyntaxError: invalid syntax
def func_param_with_kwargs2(name, age, address=0, **kwargs):
print("name=",end=""), print(name)
print("age=",end=""), print(age)
print("kwargs=",end=""), print(kwargs)
print("address=",end=""), print(address)
func_param_with_kwargs2("정우성", "20", mobile="01012341234", address="seoul")
name=정우성
age=20
kwargs={'mobile': '01012341234'}
address=seoul
가변 길이 인자와 가변 키워드 인자를 동시에 전달할 수도 있다. 이 경우에는 가변 길이 인자가 키워드 인자보다 앞서 와야 한다.
>>> def f(a, b, *args, **kwargs):
... print(F'a = {a}')
... print(F'b = {b}')
... print(F'args = {args}')
... print(F'kwargs = {kwargs}')
...
>>> f(1, 2, 'foo', 'bar', 'baz', 'qux', x=100, y=200, z=300)
a = 1
b = 2
args = ('foo', 'bar', 'baz', 'qux')
kwargs = {'x': 100, 'y': 200, 'z': 300}
"""
가변 길이 인자랑 키워드 인자랑 같이 쓸 때에도 위치를 잘 판단해야 한다.
"""
# 정의 안됨.
def mixed_params(name="아이유", *args, age, **kwargs, address):
print("name=",end=""), print(name)
print("args=",end=""), print(args)
print("age=",end=""), print(age)
print("kwargs=",end=""), print(kwargs)
print("address=",end=""), print(address)
def mixed_params2(age, address, *args, name="아이유", **kwargs):
print("name=",end=""), print(name)
print("args=",end=""), print(args)
print("age=",end=""), print(age)
print("kwargs=",end=""), print(kwargs)
print("address=",end=""), print(address)
>>> mixed_params2(20, "seoul", "01012341234", "male", name="정우성" ,mobile="01012341234")
name=정우성
args=('01012341234', 'male')
age=20
kwargs={'mobile': '01012341234'}
address=seoul