Now Loading ...
-
-
-
-
Empty 자료형의 type annotation에 관하여
Intro
Mypy와 친하게(?) 지내다보면 문득 어떤 type annotation를 줘야할지 모호한 경우가 왕왕 발생합니다. (안 발생한다면 mypy랑 베프인 분들 :thumbsup:) 그 중 재밌었던 부분은 빈 리스트 []는 어떤 typing을 주어야할지에 대한 고민입니다. 보통 비어있다고 생각하면 typing 모듈의 Optional을 생각하게 되는데, 사실 약간 불편한 느낌이 있습니다. 예를 들어, Optional[str]은 Union[str, None]과 동일하며 그 의미는 “None을 허용한다”이므로, list[Optional[str]]는 리스트의 element로 str이 오거나 None이 올 수 있다는 말이 됩니다. 즉, [None]도 허용한다는 의미가 포함되게 됩니다.
그렇다면 [None]은 제외하고 순수하게 empty list []만 허용하고 싶을 때는 어떻게 해야 할까요?
결론만 이야기하면, list[str]으로 충분합니다. 굳이 Optional을 사용해 [None]의 경우까지 허용시킬 필요가 없습니다.
실제로 이것이 맞는지 PEP와 Mypy로 함께 확인해봅시다 :smiley:
Empty list의 타입
기본적인 list 타입은 위와 같이 선언할 수 있습니다. list[str]는 str 타입의 element로 구성된 리스트를 허용한다는 의미죠. Mypy로 체킹해봐도 dogs: list[str] = ["Welsh Corgi", "Golden Retriever", "Bulldog"]가 문제없이 허용됩니다.
[]도 허용하고 싶을 때는 어떻게 해야할까요?
list[Optional[str]]은 ["Welsh Corgi", "Golden Retriever", "Bulldog"], [None], [] 3가지 경우를 허용합니다. 보통 우리는 [None]에 대한 허용을 필요로 하지 않죠.
따라서, 통상적인 의미의 빈 리스트를 허용 type annotation은 단순히 list[str]을 사용하면 됩니다. list[str]은 ["Welsh Corgi", "Golden Retriever", "Bulldog"], [] 2가지 경우를 허용합니다.
list[int], list[float], list[bool] 역시 동일하게 []를 허용합니다.
Data type에 따른 분류
기본 자료형
기본 자료형의 경우는 Optional을 사용해주는 것이 본래 의도와 맞을 것입니다.
Optional[int] example: 2, None
Optional[float] example: 3.14, None
Optional[bool] example: True, None
컬렉션 자료형
컬렉션 자료형의 경우, Optional 없이 본래의 타입을 사용하는 것이 의도에 맞을 것입니다.
list[str] example: [Welsh Corgi, 'Poodle'], []
dict[int, str] example: {1: "Barking", 2: "Running"}, {}
set[int] example: {1, 2}, set()
다만 튜플은 길이가 고정되는 자료형이기 때문에, 빈 튜플을 표현하거나 튜플의 길이를 가변적으로 표현하고 싶다면 다른 방법을 사용해야합니다.
tuple[int] example: (4,)
tuple[()] example: () (=empty tuple)
Union[tuple[()], tuple[int]] example: (), (4,)
tuple[int, ...] example: (), (4,), (3, 4, 5) (=Arbitrary-length homogeneous tuple)
PEP 484 & Mypy docs
빈 자료형을 어떤 타입으로 표현해야 하는지만을 따로 설명한 챕터는 없습니다. 다만, 이에 대해 신빙성있게 명시된 부분들은 PEP 484 – Type Hints와 Mypy docs - Type inference and type annotations에서 직간접적으로 찾아볼 수 있습니다.
PEP 484의 type comments 설명을 보면, empty list를 어떤 타입으로 명시할 수 있는지가 간접적으로 드러나 있습니다.
PEP 484의 The typing Module 챕터에서는 empty tuple은 tuple[()], arbitrary-length homogeneous tuple은 tuple[int, ...]를 사용하라고 명확히 설명해주었네요.
Mypy docs에서도 collection 자료형의 타입에 관하여 명시된 부분이 있습니다. 이에 따르면, empty list는 list[int], empty dict는 dict[str, int], empty set은 set[int] 등으로 표현 가능합니다.
Outro
Empty 자료형에 대해 온전히 설명하는 PEP가 있다면 좀 더 좋았을텐데라는 생각이 들지만, 한편으로는 여러 reference에서 이에 대한 증거들을 찾아가는 과정도 꽤 흥미로웠습니다.
Empty 자료형은 type annotation을 조금 헷갈리게 할 수 있습니다. 하지만 충분히 직관적으로 타입을 표현할 수 있으니, 이를 염두해서 type annotation을 사용하면 좋을 것 같습니다 :)
P.S. Tuple의 type annotation은 직관적인가…? 자료형에 특성에 따른 예외이니까 kindly하게 받아들여야겠다…!
Reference
PEP 484 - Type hinting #type-comments
PEP 484 - Type hinting #the-typing-module
Mypy docs - Explicit types for collections
-
SQLAlchemy 기본
SQLAlchemy
동기 지원 모듈: sqlalchemy
create_engine (데이터베이스 엔진)
Session (세션)
sessionmaker (세션 팩토리)
ORM Setting 기본 단계
DB engine 생성 및 접속
세션 정의 및 생성
테이블 초기 생성
Session을 만드는 2가지 방법
Session 객체를 직접 생성
사용 코드
def get_db():
db = Session(bind=engine)
try:
yield db
finally:
db.close()
FastAPI의 Depends(get_db)를 통해 의존성 주입하면 편리
Session 팩토리
사용 코드
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
db = SessionLocal()
db.close() - 사용 후에는 직접 끊어줘야 함
sessionmaker 옵션
autocommit
세션 작업 후 자동으로 커밋되도록 활성화
False로 두고 명시적으로 커밋하는게 좋음
autoflush
트랜잭션 안에서 바로바로 데이터 반영 시킬지 여부
예를 들어, DB에 100개의 데이터가 있는데 현재 트랜잭션 내에서 insert 쿼리 후 count 쿼리를 날리면, autoflush가 true일 때 101개 결과를 반환
과거 방식이기도 하고, False가 바람직
테이블 초기 생성
Base.metadata.create_all(bind=engine)
조회 Syntax
모든 컬럼 조회
db.query("TableObjectName")
= SELECT * FROM TableName
e.g. db.query(User)
특정 컬럼 조회
db.query("TableObjectName.columnname")
= SELECT columnname FROM TableName
e.g. db.query(User.email)
WHERE절
filter
e.g. filter(User.nickname == 'veluga')
filter_by
e.g. filter_by(nickname="john")
AND & OR
AND
e.g. filter("조건").filter("조건")
e.g. filter("조건", "조건")
OR
or_을 임포트해 사용
from sqlalchemy import or_
e.g. filter(or_(User.username == "veluga", User.id == 1))
정렬
오름차순 정렬
order_by(User.id)
내림차순 정렬
from sqlalchemy import desc
order_by(desc(User.id))
조회 실행 (쿼리 실행)
단건 조회
first()
결과가 여러 개면 그 중 첫 번째 리턴
없을 경우 None 반환
one()
결과가 여러 개거나 없을 경우 에러
scalar()
결과가 여러 개일 경우 에러
없을 경우 None 반환
복수 리스트 조회
all()
scalars()
조회 결과의 개수 반환
count()
그룹화 및 집계 함수 사용 패턴
func에서 원하는 집계함수 사용 (count, sum, max, min…)
from sqlalchemy import func
db.query(func.count(User.id).label('total')).group_by(User.id).all()
삭제 Syntax
db.delete("조회한 모델 객체")
db.commit()
Reference
2.0 style query 결과 가져오기 총 정리 (한 개 또는 여러 개)
SQLAlchemy 1.x 와 2.0의 Query 스타일 비교
-
Python zoneinfo - UTC 시간대를 더욱 쉽게 적용합시다!
이전에 python에서 UTC 시간대를 적용할 때는 pytz 라이브러리가 주로 사용되었습니다.
특히 aware 타입과 naive 타입을 비교하기 어렵기 때문에, pytz를 사용해 datetime 객체를 aware 타입으로 바꾸고 비교하는 것은 매우 유용했습니다. (aware 타입은 timezone 정보가 포함된 datetime이고 naive 타입은 timezone 정보가 포함되지 않은 datetime입니다.)
그러나 pytz는 2018년 서울과 평양시간을 UTC+9 시간이 아닌 UTC+08:30으로 표현하는 버그, 실수를 유발할 수 있는 사용 방법 등 이슈도 공존했습니다. 이를 보완하기 위해, Python은 3.9 버전부터 표준 라이브러리로 zoneinfo 모듈을 제공합니다. 덕분에, 따로 pytz를 인스톨하지 않고도 datetime에 쉽게 원하는 시간대를 적용할 수 있습니다.
Python official document - zoneinfo
https://docs.python.org/ko/3/library/zoneinfo.html
ZoneInfo 클래스
ZoneInfo(key: str)
ZoneInfo는 key를 생성자의 인자로 받는 클래스입니다. 예를 들어, “America/New_York”, “Europe/London”를 key로 던지면, 해당 시간대 정보를 가지는 인스턴스를 생성합니다. 시간대 적용은 이 인스턴스를 활용합니다.
현재 시간에 ZoneInfo 적용하기
from zoneinfo import ZoneInfo
from datetime import datetime
dt = datetime.now(ZoneInfo('UTC'))
# datetime.now(tz=ZoneInfo('UTC'))와 동일
print(dt)
# 2022-01-09 11:05:40.133971+00:00
zoneinfo는 기존 datetime 객체에 그대로 적용할 수 있습니다. UTC 시간대를 적용한 현재 시간을 알고 싶다면, datetime.now(ZoneInfo('UTC'))을 사용합니다.(datetime.now(tz=ZoneInfo('UTC'))와 동일합니다.) 반환된 dt는 aware 타입 객체가 될 것입니다.
dt = datetime.now(ZoneInfo('Asia/Seoul'))
print(dt)
# 2022-01-09 20:05:40.133971+09:00
서울의 시간대로 현재 시간을 알고 싶다면, ZoneInfo의 인자로 ‘Asia/Seoul’ key를 적용합니다.
임의의 datetime에 ZoneInfo 적용하기
from zoneinfo import ZoneInfo
from datetime import datetime, timedelta
dt = datetime(2020, 10, 31, 12, tzinfo=ZoneInfo("America/Los_Angeles"))
print(dt)
# 2020-10-31 12:00:00-07:00
원하는 시간에 ZoneInfo를 적용하고 싶다면, datetime의 tzinfo에 ZoneInfo 정보를 줍시다.
dt_add = dt + timedelta(days=1)
print(dt_add)
# 2020-11-01 12:00:00-08:00
datetime끼리의 연산 역시 summer time을 고려해 알아서 계산됩니다.
Windows와 tzdata
zoneinfo는 Python의 표준 라이브러리에 포함되기 때문에, 따로 인스톨없이 사용할 수 있습니다.
다만, 윈도우의 경우 zoneinfo 모듈을 사용할 때 다음과 같은 에러가 발생할 수 있습니다.
ModuleNotFoundError: No module named ‘tzdata’
zoneinfo는 기본적으로 시스템의 시간대 데이터를 사용합니다. 하지만, 윈도우는 시간대를 다루는 시스템이 다른 OS와 조금 달라서, zoneinfo와 호환되지 않는다고 합니다. (PEP 615)
However, not all systems ship a publicly accessible time zone database — notably Windows uses a different system for managing time zones — and so if available zoneinfo falls back to an installable first-party package, tzdata, available on PyPI. [d] If no system zoneinfo files are found but tzdata is installed, the primary ZoneInfo constructor will use tzdata as the time zone source. - Sources for time zone data (PEP 615)
이 때는, CPython 핵심 개발자가 유지 보수하는 first-party 패키지인 tzdata를 인스톨합시다. (pip install tzdata)
zoneinfo는 참고할 수 있는 시간대 데이터가 없을 시 자동으로 tzdata를 시간대 데이터로 사용하므로, 인스톨 시 문제가 해결됩니다.
개발 시 최대한 신뢰할 수 있는 라이브러리를 사용하고 이외의 라이브러리에 대한 의존성을 줄일 필요가 있습니다. 고마웠던 pytz지만, 가능하다면 표준 라이브러리에 포함된 zoneinfo 사용을 지향해봐야겠습니다.
Reference
Python 3.10 document - zoneinfo
PEP 615 - Support for the IANA Time Zone Database in the Standard Library
PYTHON 3.9에 등장한 상큼한 8가지 FEATURES
평양 및 서울의 timezone관련 pytz 이슈
-
Fast API tutorial - Validation
각각의 Parameters는 인자로 받을 데이터에 대해 여러가지 조건을 걸어 validations(유효성 검사)를 수행할 수 있습니다. 만일 incorrect한 데이터가 감지될 경우 validation에 의해 error가 응답됩니다.
Parameter의 종류를 선언하는 함수
앞에서 살펴봤듯이 parameter는 path parameter, query parameter, request body parameter 등 여러가지 형태의 종류가 존재합니다. 이외에도 cookie parameter, header parameter등 더 다양한 형태가 존재하는데, 이러한 parameter를 조금 더 명시적으로 선언할 수 있게 도와주는 함수가 각각 존재합니다.
from typing import Optional
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_items(item_id: Path(...), q: Query(None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
만일 parameter의 default 값으로 Path(...)를 설정해주면, 해당 parameter는 required한 path parameter가 됩니다. 혹은 Default 값으로 Query(None)를 사용한다면 해당 parameter는 not required한 query parameter가 됩니다. 이러한 함수들은 함수의 첫 번째 parameter로 default 값을 받습니다.
Path([default 값])
Query([default 값])
etc…
이렇게 각각의 parameters는 자신의 이름을 딴 함수를 갖고 있습니다. fastapi에서 import해오는 Path, Query 등이 그 예입니다. 사실 각각의 함수들은 해당 이름의 클래스에서 인스턴스를 만들어 return하는 기능을 하므로, default parameter로 설정하는 것은 해당 이름의 객체가 됩니다. Parameter에 대한 validation은 이러한 함수들을 사용해 적용합니다.
이러한 클래스들이 비슷한 느낌을 띄는 이유가 있습니다. 해당 클래스들은 모두 Param 클래스의 subclass들입니다. 그래서 이들은 validation과 metadata의 추가를 모두 똑같은 방식으로 적용할 수 있습니다.
Path(), Query(), Body() 함수로 required parameter 만들기
앞에서 Query 함수의 첫번째 parameter로 None을 사용해 optional parameter를 만들었는데, 만일 Query 함수를 사용해 required parameter를 만들고 싶다면 Query의 첫 번째 argument로 ... (Ellipsis)를 사용하면 됩니다. 이는 나중에 사용할 Path, Body 함수와 더불어 같은 맥락의 함수들에 똑같이 적용됩니다.
Query(…)
Path(…)
Body(…)
etc…
String Validations
Additional Information
Fast API는 type hinting과 default parameter를 통해 이에 대한 추가 정보를 인식하고 활용합니다.
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
async def read_items(q: Optional[str] = None):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
위 코드는 async def read_items(q: Optional[str] = None): 부분에 query parameter q에 대한 타입을 명시했습니다. q는 str타입이 단서가 되어 query parameter로 인식됩니다. 또한, = None을 통해 not required한 optional parameter로 인지됩니다.
Additional validation
Parameter에 인자로 받을 데이터에 대한 validation을 걸어줄 수 있습니다. 일례로, query parameter q에 대해 인자로 들어올 str 데이터의 최대 길이가 50이 넘지 않게끔 검사를 수행하는 validation을 만들겠습니다.
from typing import Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Optional[str] = Query(None, max_length=50)):
results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
if q:
results.update({"q": q})
return results
우선 fastapi에서 Query 함수를 import합니다.
from fastapi import FastAPI, Query
그리고 Query 함수를 다음과 같은 형태로 default parameter 자리에 사용합니다.
Query([default 값], [조건식])
q: Optional[str] = Query(None, max_length=50)는 default 값으로 None을 유지한 상태에서 q의 최대 길이를 50으로 지정합니다. 그리고 실제로 전달된 데이터의 길이가 50을 넘어가면, error를 응답합니다.
또한, Query 함수는 다음과 같이 parameter를 더 추가해 여러 개의 validation을 지정할 수 있으며, 정규표현식을 validation으로 지정할 수도 있습니다.
Query(None, min_length=3, max_length=50)
Query(None, min_length=3, max_length=50, regex="^fixedquery$")
이러한 validation 정보들은 Interactive Documentation에도 업데이트됩니다.
Parameter에 Multiple values 받기
from typing import List, Optional
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(q: Optional[List[str]] = Query(None)):
query_items = {"q": q}
return query_items
Parameter를 특정 parameter를 만드는 함수를 사용해 선언한다면, multiple values를 받는 parameter로 만들 수 있습니다. 만일 query parameter를 Query 함수를 사용해 만든다면, multiple values를 받는 query parameter를 만드는 식입니다. 이 경우 query parameter는 반드시 Query 함수와 함께 정의되어야 하는데, 그렇지 않으면 Fast API가 해당 parameter를 request body로 간주할 수 있기 때문입니다. (Singular type이 아닌 type으로 parameter를 선언할 때 나타나는 현상입니다!)
q: Optional[List[str]] = Query(None)
위와 같이 List 타입으로 q를 선언하면, http://localhost:8000/items/?q=foo&q=bar 요청과 같이 URL에 여러 개의 query 값이 전달되어도 리스트로 한 번에 받아 처리할 수 있습니다. Fast API는 자동으로 multiple query를 인식해 리스트에 담아줍니다. 위 URL 요청에 대한 response은 다음과 같습니다.
{
"q": [
"foo",
"bar"
]
}
만일 리스트로 받을 내부 요소들의 타입까지 체크하고 싶진 않다면, 다음과 같이 list로만 타입을 선언하면 됩니다.
q: list = Query([])
Parameter에 metadata 넣기
Parameter의 종류를 선언하는 함수에 인자를 설정해주면, 함수를 적용한 parameter에 metadata를 추가할 수 있습니다. 예를 들어, Query 함수의 parameter를 사용하면 다음과 같이 query parameter에 또 다른 metadata들을 추가할 수 있습니다.
async def read_items(
q: Optional[str] = Query(
None,
title="Query string",
description="Query string for the items to search in the database that have a good match",
min_length=3,
)
):
여기선 title과 description parameter를 추가했는데, 이렇게 추가된 query parameter 정보들은 Interactive Document에도 반영됩니다.
Parameter에 Alias 설정하기
REST하게 URL을 만들고 싶다면, _보다 -를 사용하는 것이 좋습니다. 언더스코어 _는 밑줄이 그어지면 가독성이 떨어지기 때문입니다. 그러나 parameter 이름을 item-query처럼 사용하는 것은 Python 문법에 어긋납니다. 따라서, 이러한 경우에는 parameter에 alias를 item-query로 설정해줍니다. 아래는 query parameter의 예입니다.
q: Optional[str] = Query(None, alias="item-query")
Parameter Deprecating하기
Deprecated는 특정 기능이 아직까지 사용되고는 있지만, 중요도가 떨어져 조만간 사라지게 될 상태를 말합니다. 만일 특정 parameter를 언젠가 제거할 계획이지만 이를 계속 사용하는 클라이언트 개발자들을 위해 한 동안 남겨두려는 상황이라면, 해당 parameter를 deprecating하여 Interactive API Documentation에 해당 parameter가 deprecated 상태임을 명확히 알려줄 수 있습니다. (Documentation은 클라이언트 개발자들과의 소통 창구 역할을 합니다!)
예를 들어, 다음과 같이 Query 함수의 parameter로 deprecated=True를 설정해줍니다.
q: Optional[str] = Query(None, deprecated=True)
Deprecated 상태에서는 parameter의 이용이 여전히 가능하지만, Interactive Documentation에는 해당 parameter의 deprecated 상태가 명확히 반영됩니다.
Numeric Validations
앞에선 String과 관련된 validation을 많이 살펴봤지만, Numeric 형태의 데이터를 다룰 때도 물론 validation을 수행하거나 metadata를 추가해줄 수 있습니다. 이 경우는 Numeric value를 자주 사용하는 path parameter를 주로 사용해서 살펴보겠습니다.
Path 함수는 다음과 같이 import해 사용합니다.
from fastapi import Path
Path Parameter에 metadata 넣기
from typing import Optional
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_items(
item_id: int = Path(..., title="The ID of the item to get"),
q: Optional[str] = Query(None, alias="item-query"),
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
Path 함수에 metadata를 넣을 때도, Query와 똑같은 방식으로 사용합니다. item_id path parameter에 title 정보를 넣고 싶다면 다음과 같이 Path 함수에 parameter로 삽입하여 적용합니다.
item_id: int = Path(..., title="The ID of the item to get")
이 때, path parameter는 path의 일부분이기 때문에, 인자가 반드시 존재해야 하는 parameter입니다. 따라서, 첫 번째 파라미터로 ...을 사용해 Fast API에게 required parameter임을 알려줍니다. 사실 ...이외의 None이나 다른 default 값을 사용하더라도 문제 없이 실행되지만 큰 의미는 없으며, 해당 path parameter는 여전히 required parameter로 기능합니다.
Number validations
Parameter에 대하여 몇몇 숫자에 대한 validation을 추가할 수 있습니다.
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/items/{item_id}")
async def read_items(
*,
item_id: int = Path(..., title="The ID of the item to get", gt=0, le=1000),
q: str,
):
results = {"item_id": item_id}
if q:
results.update({"q": q})
return results
위 코드의 Path 함수에 들어간 gt, le 같은 validation parameter들의 의미는 다음과 같습니다.
gt: greater than
ge: greater than or equal
lt: less than
le: less than or equal
Reference
Fast API 공식 문서 튜토리얼
-
Fast API tutorial - Params
Fast API 튜토리얼 - Parameters of Path, Query, Request body
Path Parameters
Path Parameters의 정의와 형태
Path parameter는 path 내에 들어있는 variable의 value를 전달받은 parameter를 말합니다.
@app.get("/items/{item_id}")
def read_item(item_id):
return {"item_id": item_id}
위의 코드에서, item_id는 path parameter에 해당합니다. HTTP 요청이 들어오면 해당 URL에서 {item_id}에 해당하는 value를 획득하고, 이 value는 read_item함수의 item_id에 인자로 전달됩니다.
위의 코드를 main.py에 추가해 저장한 후, http://127.0.0.1:8000/items/foo에 들어가면 response로 {"item_id":"foo"}이 확인됩니다.
Data conversion and validation
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"item_id": item_id}
또한, path operation function에서 인자로 사용한 path parameter에 타입 힌트를 줄 수 있습니다. (다른 parameter도 마찬가지로 적용됩니다.) 그리고 이렇게 자료형을 annotate한 parameter는 들어온 인자 값을 annotated된 자료형대로 형 변환해서 parameter에 담습니다. 만일 http://127.0.0.1:8000/items/3으로 요청이 들어온 경우, 원래는 path parameter를 str 타입으로 받아 item_id 값이 ‘3’이 되지만 위 코드에서는 타입 힌트를 보고 int로 형 변환된 3이 담깁니다. 즉, Fast API는 타입 힌트를 통해 자동으로 parsing을 통한 data conversion을 제공합니다.
만일 path parameter에 annotated된 타입과 다른 타입의 값이 요청된다면, 해당 HTTP 요청은 에러를 일으킵니다. 이는 Fast API가 데이터 유효성 검사까지 수행함을 보여줍니다. 실제로 http://127.0.0.1:8000/items/foo에 들어가면 응답에 오류가 발생합니다. Annotated된 int 타입으로 형 변환이 이뤄질 수 없는 foo가 값으로 들어왔기 때문입니다. http://127.0.0.1:8000/items/4.2의 경우도 마찬가지입니다.
타입 힌트로 annotated된 변수는 Interactive API documentation에도 적용됩니다.
http://127.0.0.1:8000/docs에 들어가면 path parameter item_id가 integer로 선언되어 있음을 확인할 수 있습니다.
Fast API에서 이러한 data conversion 및 validation이 가능한 이유는 내부적으로 Pydantic 라이브러리의 도움 덕분입니다.
Pydantic이란?
파이썬 타입 힌트를 사용해 데이터 유효성 검사를 해주는 라이브러리입니다. 만일 어노테이션된 타입과 다른 데이터를 만나면 에러를 띄웁니다. Fast API에서는 Pydantic을 활용하여 간편하게 데이터 유효성 검사를 수행합니다.
Path Operation 정의 순서의 중요성
어떤 path operation들은 정의하는 순서에 따라 예상치 못한 처리를 일으킬 수 있습니다. 예를 들어, 고정된 path를 가진 path operation과 path parameter를 가진 path operation이 모두 정의된 경우를 살펴봅시다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/me")
async def read_user_me():
return {"user_id": "the current user"}
@app.get("/users/{user_id}")
async def read_user(user_id: str):
return {"user_id": user_id}
/users/me 코드는 /users/{user_id}보다 앞에 쓰여져야 합니다. 만일 순서가 바뀌면, Fast API는 me를 user_id의 value로 오해하여 본래 의도와 다르게 read_user 함수를 호출할 것입니다.
Path Parameter의 값으로 Path를 받는 경우
때로는 path parameter의 값으로 home/dogs/wealsh와 같은 path가 올 수 있습니다. 만일 path operation의 path가 기존처럼 /files/{file_path}이라면, file_path는 /files/home/dogs/wealsh 요청이 들어왔을 때 이를 온전히 인식하지 못하고 {"detail":"Not Found"}를 응답합니다. 하지만, Starlette에서 제공하는 Path convertor를 사용하면 path parameter의 인자가 path 형태로 들어와도 이를 온전히 인식하게 됩니다. Fast API는 Starlette을 기반으로 만들어졌기 때문에, 특별한 import 없이 다음과 같이 써주면 path convertor가 동작합니다.
/files/{file_path:path}
이를 활용하면 다음과 같이 path operation에 http://127.0.0.1:8000/files/home/dogs/wealsh 형태로 요청을 보내도 온전히 동작합니다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
return {"file_path": file_path}
위의 요청의 경우 files/home/dogs/wealsh 값이 file_path에 담겨 응답됩니다. 만일 /files/home/dogs/wealsh 형태로 앞에 /를 추가하여 file_path에 담고 싶다면 http://127.0.0.1:8000/files//home/dogs/wealsh 형태로 요청을 보내면 됩니다.
Query Parameters
Query Parameters의 정의와 형태
Path operation function에 path parameter가 아닌 다른 parameter를 선언했다면, 해당 parameter들은 자동으로 query parameter로 인식됩니다. Query parameter는 request로 들어오는 query의 값이 담기는 parameter입니다.
Query는 URL의 ?뒤에 오는 key-value pair를 의미하며 각각의 query는 &로 구분됩니다. 다음은 request에 담긴 query의 예시입니다.
http://127.0.0.1:8000/items/?skip=0&limit=10
또한, 다음과 같은 path operation은 이러한 request에 대해 query parameter를 받습니다.
from fastapi import FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]
이 경우, query parameter는 skip과 limit이고 각각 0과 10을 인자로 받습니다.
원래대로라면 URL로부터 들어온 str타입의 ‘0’과 ‘10’으로 값을 받았겠지만, skip과 limit의 타입을 int로 선언했기 때문에 형 변환하여 값을 받습니다. 즉, query parameter에도 path parameter에서 적용되던 다음과 같은 프로세스들이 그대로 적용됩니다.
Editor Support (Auto completion, Error check, etc…)
Data conversion
Data validation
Automatic Documentation
Default value & Optional Parameters
from fastapi import FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
return fake_items_db[skip : skip + limit]
Query parameter는 default parameter를 설정할 수 있습니다. 이 경우 skip과 limit의 default 값은 각각 0과 10입니다.
from typing import Optional
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Optional[str] = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}
또한, query parameter에는 typing 모듈을 활용해서 Optional 타입을 선언할 수 있습니다. q: Optional[str] = None은 query parameter q가 str 타입의 value를 인자로 받거나 혹은 인자가 없을 때는 None을 default value로 가진다는 의미입니다. 즉, Fast API는 q를 required하지 않은 parameter로 인식합니다.
이 때, Fast API는 = None부분을 인식해 query parameter q의 required 여부를 구분합니다. 또한, : Optional[str] 부분에서 Fast API는 str 부분만 인식해 data conversion 및 data validation에 사용합니다. 그리고 나머지 Optional 부분은 Fast API가 아닌 Editor의 Auto completion과 Error check를 support하기 위해 사용됩니다.
Required parameter란?
Parameter가 Required하다는 것은 특정 parameter가 필수적으로 인자를 받아야만 함을 말합니다. 보통 특정 parameter에 default값을 설정해두면 not required, default 값을 설정하지 않으면 required 상태로 인식됩니다. 만일 not required한 parameter를 굳이 특정 값이 있지 않아도 되는 Optional parameter로 만들고 싶다면, default 값으로 None을 설정하면 됩니다.
Request Body
Request Body의 정의와 형태
Request body는 클라이언트에서 API로 보내는 data를 의미합니다. 반면에, API가 클라이언트에게 보내는 data는 response body라고 합니다. Response body는 API가 항상 보내야 하는 반면, request body는 클라이언트가 필수적으로 보낼 필요는 없습니다.
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
return item
Request body는 Pydantic model을 통해 선언합니다. pydantic 라이브러리에서 BaseModel을 import하고, BaseModel을 상속하는 클래스를 생성해 Pydantic 모델을 만듭니다. Model의 attribute들은 query parameter와 같은 방식으로 required 여부를 정할 수 있습니다. 위 경우, name, price는 required한 attribute이고 description, tax는 not required하면서 optional한 attribute입니다.
따라서, 위 모델은 다음과 같은 JSON 객체(혹은 Python dict 객체)를 선언한 것과 같습니다.
{
"name": "Foo",
"description": "An optional description",
"price": 45.2,
"tax": 3.5
}
description과 tax는 optional하기 때문에 다음과 같은 JSON 객체도 request body로 유효하게 전달 받을 수 있습니다.
{
"name": "Foo",
"description": "An optional description",
"price": 45.2,
"tax": 3.5
}
그리고 path operation fucntion의 parameter에 원하는 pydantic model을 타입 선언 해주면, 해당 파라미터는 request body를 전달받는 parameter로 인식됩니다. 위 코드에서는 async def create_item(item: Item):에서 Item pydantic model을 타입으로 선언해 item을 request body parameter로 만들었습니다.
이렇게 선언된 request body parameter는 다음과 같은 특징을 가집니다.
Request body로 들어온 data를 JSON 형식으로 읽어들입니다.
필요할 경우 들어온 data를 선언된 타입에 일치하도록 data conversion합니다.
선언된 타입으로 Data validation을 수행합니다. (Incorrect data에는 error를 띄웁니다!)
Editor support를 지원합니다.
해당 model에 대한 JSON schema를 생성해, Automatic Documentation에 적용합니다.
Request Body로 전달받은 Model 사용법
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
@app.post("/items/")
async def create_item(item: Item):
item_dict = item.dict()
if item.tax:
price_with_tax = item.price + item.tax
item_dict.update({"price_with_tax": price_with_tax})
return item_dict
Request body를 전달받은 item은 클래스의 attribute를 사용하는 것과 똑같은 방식으로 자유롭게 사용할 수 있습니다. 예를 들어, item.tax처럼 tax 속성에 접근해 value를 사용할 수 있습니다. 또한, pydantic model의 .dict() 메서드를 사용해 item.dict()로 해당 model의 데이터를 python dict 형태로 사용할 수도 있습니다.
위 코드는 tax 속성에 인자가 들어왔다면, price_with_tax = item.price + item.tax로 새로운 value를 만들고 item에서 추출한 item_dict에 item_dict.update({"price_with_tax": price_with_tax})로 새로운 key-value를 추가하여 item_dict를 return합니다.
Path + Query + Request Body Parameters
Path, query, request body parameter는 모두 동시에 사용할 수 있습니다. Fast API는 각각의 parameters를 자동으로 구분해냅니다
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
description: Optional[str] = None
price: float
tax: Optional[float] = None
app = FastAPI()
@app.put("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: Optional[str] = None):
result = {"item_id": item_id, **item.dict()}
if q:
result.update({"q": q})
return result
위 경우 item_id는 path parameter, item은 request body parameter, q는 query parameter로 자동 인식됩니다. 기본적으로 parameter 자동 인식은 다음과 같은 기준으로 진행됩니다.
Path 안에도 선언되어 있는 parameter는 path parameter로 인식합니다. (혹은 Path(...)가 선언되어 있는 parameter)
int, float, str, bool 등의 singular type으로 선언된 parameter는 query parameter로 인식합니다. (혹은 Query(...)가 선언되어 있는 parameter)
Pydantic model로 type이 선언된 parameter는 request body parameter로 인식합니다. (혹은 Body(...)가 선언되어 있는 parameter)
Path, Query, Request body Parameters의 순서 문제
Query parameter를 default 값이 없는 required parameter로 만들고, path parameter는 default 값으로 Path 인스턴스를 넣어 not required한 parameter로 만드는 다음과 같은 상황을 가정해보겠습니다.
async def read_items(
item_id: int = Path(..., title="The ID of the item to get"), q: str
):
이 때, Python 문법으로 인해 default 값이 있는 parameter는 default 값이 없는 parameter의 앞에 위치하지 못합니다. 따라서, 위 코드는 오류를 일으킵니다.
하지만, 다음과 같이 순서를 정리하면 오류를 피할 수 있습니다.
async def read_items(
q: str, item_id: int = Path(..., title="The ID of the item to get")
):
Fast API는 parameter의 이름, 타입, default parameter 등의 단서를 통해 parameter의 종류를 인식하므로, 순서에 대한 문제는 Python 문법에서만 고려하면 됩니다.
만일 다음과 같은 약간의 트릭을 사용한다면, default 값 여부에 상관 없이 자유로운 parameter 배열이 가능합니다.
async def read_items(
*, item_id: int = Path(..., title="The ID of the item to get"), q: str
):
*를 함수의 첫 번째 parameter로 사용하면 위와 같이 default 값이 없는 parameter가 뒷 순서로 와도 상관 없습니다. *는 Python 함수의 special parameter 중 하나로, * 뒤에 위치한 parameter들은 모두 키워드 인자만 받도록 강제합니다. Special parameter에 대해 더 자세히 알고 싶다면, Python 공식 튜토리얼 문서의 Special parameters 부분을 읽어 보시길 바랍니다.
Reference
Fast API 공식 문서 튜토리얼
-
비동기 프로그래밍을 돕는 asyncio 라이브러리
asyncio(Asynchronous I/O)
파이썬은 인터프리터 언어의 특성과 함께 속도가 느린 언어로 알려져있습니다. 그렇기에 비동기로 처리해 속도를 높이는 방법은 파이썬의 단점을 극복하는 하나의 해답이 됩니다. 다만, 파이썬에는 멀티스레드에서 발생하는 복잡한 문제들을 막기 위한 GIL(Global Interpreter Lock)이 존재하고, 이로 인해 항상 한 번에 하나의 스레드만 작업을 수행할 수 있어 진정한 의미의 멀티스레딩은 실현되기 어렵습니다. GIL은 보다 복잡한 문제를 막기 위한 심플하고 효과적인 방법이지만, 파이썬의 한계점이자 파이썬이 태생적으로는 비동기 프로그래밍에 적합하지 않은 언어임을 보여주죠.
한계가 뚜렷함에도 파일 읽기 및 쓰기, Http 통신 대기와 같은 Blocking I/O 상황에서는 비동기 프로그래밍이 여전히 파이썬에서 위력을 발휘합니다. 이러한 상황의 비동기 프로그래밍을 좀 더 간편하게 하기 위해 나온 모듈이 asyncio입니다. 그리고 asyncio로 인한 변화 덕분에, 파이썬에서도 점점 비동기 프로그래밍 사용이 용이해지고 있습니다.
asyncio는 비동기 프로그래밍을 위한 모듈로, async/await 구문을 사용해 CPU 작업과 I/O 작업을 병렬로 처리할 수 있도록 도와줍니다. asyncio 모듈은 파이썬 3.4에 새로이 추가되었고, 3.5 부터 async def와 await 구문이 지원되었습니다. 그래서 파이썬 3.4 미만에서는 비동기 프로그래밍을 @asyncio.coroutine 데코레이터와 yield from을 사용해 구현해야 합니다. 3.3의 경우 pip install asyncio로 모듈을 설치하고 데코레이터와 yield from을 사용하면 됩니다.
동기(synchronous) 처리
특정 작업이 끝나면 다음 작업을 처리하는 순차처리 방식입니다. (프로그램의 코드가 순차적으로 처리되는 방식의 프로그래밍을 말합니다.) 아래 코드처럼 main 함수의 코드들이 작성된 순서대로 처리되는 경우 동기적으로 처리되었다고 말합니다. 특정 작업을 멈출 때도 비동기 프로그래밍에서 사용하는 asyncio.sleep과 대비되게 time 모듈을 사용합니다.
import time
def main():
print('time')
foo('text')
print('finished')
def foo(text):
print(text)
time.sleep(2)
main()
# 실행 결과
# time
# text
# finished
비동기(asynchronous) 처리
여러 작업을 처리하도록 예약한 뒤 작업이 끝나면 결과를 받는 방식입니다. (프로그램의 코드가 여러 프로세스 여러 스레드로 나뉘어 처리되는 방식을 말합니다.) 코드 아래에서 이어 설명하겠습니다.
비동기 함수, 네이티브 코루틴
코루틴은 필요에 따라 일시정지할 수 있는 함수를 말합니다. 코루틴은 다양한 언어에 존재하고 여러 형태로 구현될 수 있습니다. 특히, 파이썬에서는 제너레이터에 기반한 코루틴과 구분하기 위해, async def로 만든 코루틴을 네이티브 코루틴이라고 부릅니다. 이러한 코루틴 함수를 비동기 함수라고도 부르며, 네이티브 코루틴은 앞서 이야기한 것처럼 파이썬 3.5에서부터 등장합니다.
import asyncio
async def main(): # async def로 네이티브 코루틴을 만듦
print('Hello, world!')
asyncio.run(main()) # main 코루틴 함수를 실행
# 실행 결과
# Hello, world!
간단한 네이티브 코루틴을 구현했습니다. 먼저 async def로 네이티브 코루틴을 만듭니다. 그리고 async def 함수 범위 바깥에서 코루틴 함수를 실행하기 위해, asyncio.run(코루틴 객체)을 사용합니다. 네이티브 코루틴 함수를 호출하면 코루틴 객체를 생성하므로, 이를 asyncio.run()에 넣어주면 됩니다. 이렇게 하면 비동기 함수의 실행이 완료되고 생각했던 출력 결과를 얻습니다. 하지만 아직 코드에는 비동기적인 느낌이 없습니다.
await으로 네이티브 코루틴 실행하기
이번엔 조금 더 비동기적인 느낌을 내어 프로그램을 짜보겠습니다. 이를 위해, await이 필요합니다.
await은 네이티브 코루틴 함수 내에서만 사용할 수 있으며, 두 가지 기능을 수행합니다. 첫 번째 기능은 코루틴 함수를 실행(execute)하는 것입니다. 원래 async def 구문에서는 await이 코루틴 함수를 실행하는 키워드입니다. 하지만 앞서 말했듯 await은 async def 함수 내부에서만 사용이 가능하기 때문에, 코루틴 함수 밖에서 코루틴 함수를 실행할 때는 앞에서 봤던 asyncio.run()을 사용합니다. 두 번째 기능은 await 키워드 의미 그대로 await에 지정된 코루틴 함수가 종료될 때까지 기다리는 것입니다. 실제로 await 뒤에 코루틴 객체, 퓨처 객체, 태스크 객체를 지정할 수 있으며, 해당 객체가 끝날 때까지 기다린 뒤 결과를 반환합니다. (3가지 객체는 코루틴과 관련된 객체들이며 보통 어웨이터블(awaitable) 객체로 불립니다. 여기서는 코루틴 함수를 호출하면 리턴되는 코루틴 객체와 이후 만들 태스크 객체만 다루겠습니다.) 용법은 다음과 같고 변수에 할당하지 않아도 되지만, 할당한다면 해당 코루틴 함수가 return하는 값이 담깁니다.
변수 = await 코루틴객체
변수 = await 퓨처객체
변수 = await 태스크객체
await을 사용해 네이티브 코루틴을 실행해보겠습니다.
import asyncio
async def main():
print('time')
await foo('test')
print('finished')
async def foo(text):
await asyncio.sleep(1)
print(text)
asyncio.run(main())
# 출력 결과
# time
# test
# finished
이 경우 ‘time’이 출력되고 1초 후 ‘ test’와 ‘finished’가 출력되면서 네이티브 코루틴이 잘 실행됨을 확인할 수 있습니다.
다만, 실제 비동기 프로그래밍이라면 프로그램의 코드가 여러 스레드로 동시에 작업을 수행하기 때문에, foo 함수에서 1초를 기다리는 동작은 수행하더라도, 실제로 1초도 되기전에 ‘time’, ‘test’, ‘finished’가 모두 출력되며 프로그램이 마무리될 것입니다. (foo 함수에서 1초 기다리는 코드 asyncio.sleep(1)이 완료되기 전에 main 함수가 먼저 종료되기 때문에 foo 함수 종료 이전에 프로그램 자체가 먼저 종료될 것입니다!) 그래서 실제 비동기 실행을 위해서는 task 객체를 사용해야 합니다.
Task 객체를 생성해 비동기 실행하기
Task를 사용하면 실제로 비동기 실행을 할 수 있습니다.
import asyncio
async def main():
print('time')
asyncio.create_task(foo('test'))
print('finished')
async def foo(text):
asyncio.create_task(asyncio.sleep(1))
print(text)
asyncio.run(main())
# 출력 결과
# time
# finished
# test
await은 코루틴을 실행하는 역할과 해당 코루틴 함수가 종료될 때까지 기다리는 역할을 수행합니다. 그러나 await만으로는 여러 스레드에서 동시에 프로그램이 실행되는 비동기적인 실행이 되지 않습니다. 이러한 비동기적 실행을 위해서 task 객체를 사용합니다. asyncio.create_task(코루틴 객체)를 사용하면 해당 코루틴 객체에 대한 task 객체를 생성함과 동시에 해당 코루틴 함수를 비동기적으로 실행합니다. 즉, await을 사용하지 않아도 실제 비동기적으로 코루틴 함수를 실행합니다. 따라서, 위 코드는 앞서 이야기한 것처럼 ‘time’을 출력한 후 1초를 기다리지 않고 ‘finished’와 ‘test’가 출력됩니다.
‘finished’가 ‘test’보다 먼저 출력된 이유는 context switch에 의한 것으로 예상됩니다. 정확한 알고리즘은 알 수 없지만, 스레드가 서로 교차하다가 ‘finished’ 출력 스레드가 먼저 완료되고 그 다음 ‘test’ 출력 스레드가 완료되며 함수가 종료되었을 것입니다.
또한, asyncio.create_task(asyncio.sleep(1))에 해당하는 스레드는 아직 수행 중이겠지만, 1초가 지나기 이전에 main 함수가 종료되면서 프로그램은 종료됩니다. 만일 asyncio.create_task(asyncio.sleep(1)) 코드의 수행을 온전히 완료하고 프로그램을 종료하고 싶다면, 다음과 같이 원하는 위치에서 await으로 함수 종료를 기다리면 됩니다.
import asyncio
async def main():
print('time')
await asyncio.create_task(foo('test'))
print('finished')
async def foo(text):
await asyncio.create_task(asyncio.sleep(1))
print(text)
asyncio.run(main())
# 출력 결과
# time
# test
# finished
이렇게 되면, 프로그램은 비동기적으로 실행했지만 원하는 위치에서 임의로 함수의 종료를 기다린 후 다음 동작을 실행하게끔 할 수 있습니다. 위 경우 비동기적으로 함수를 실행했음에도 ‘time’ 출력 후 1초 기다린 다음 ‘test’와 ‘finished’가 출력됨을 확인할 수 있습니다.
조금 더 심화된 비동기 예제 살펴보기
앞의 내용들을 적용하여 조금 더 심화된 비동기 코드를 살펴보겠습니다.
import asyncio
async def main():
print('time')
asyncio.create_task(foo('test'))
await asyncio.sleep(0.5)
print('finished')
await asyncio.sleep(1)
async def foo(text):
await asyncio.sleep(1)
print(text)
await asyncio.sleep(1)
print(text)
asyncio.run(main())
# 출력 결과
# time
# finished
# test
위 코드는 task 객체를 생성해 비동기적으로 foo 함수를 실행합니다. 코드 수행이 여러 스레드로 나뉘어 동시에 이뤄지므로, 위 코드의 경우 main 함수와 foo 함수가 동시에 병렬적으로 실행되는 상황입니다. 시간 단위로 살펴보면 다음과 같습니다.
약 0초: ‘time’이 출력되면서 main과 foo 함수가 분기됩니다.
약 0.5초: main 함수의 await asyncio.sleep(0.5) 코루틴이 종료되고 ‘finished’가 출력됩니다.
약 1초: foo 함수의 await asyncio.sleep(1) 코루틴이 종료되고 ‘test’가 출력됩니다.
약 1.5초: main 함수의 await asyncio.sleep(1) 코루틴이 종료되면서 main 함수 종료와 함께 프로그램이 종료됩니다.
따라서 foo 함수의 마지막 ‘test’는 출력되지 않습니다.
Reference
Python3.8 asyncio, async/await 기초 - 코루틴과 태스크
asyncio 사용하기
Python 3, asyncio와 놀아보기
-
Fast API tutorial - Installation
Fast API 공식 문서의 튜토리얼을 살펴보고 정리합니다. 본 글은 윈도우 환경을 기준으로 작성되었습니다.
Fast API 설치하기
앞에서는 간단히 Fast API와 uvicorn만 설치하여 진행했지만, 이번엔 튜토리얼을 편하게 진행하기 위해 Fast API와 이에 따른 의존 관계가 있는 모듈들을 한꺼번에 설치하겠습니다.
가상환경을 사용한다면 활성화시켜주시고, [all] 옵션을 사용해 Fast API와 관련 모듈들을 한번에 설치합니다.
> pip install fastapi[all]
이 때 uvicorn 서버도 함께 설치되기 때문에, 따로 uvicorn을 설치할 필요없이 프로젝트 디렉토리에 main.py 파일만 만들고 바로 서버를 구동할 수 있습니다.
가장 simplest한 형태의 Fast API 코드를 main.py에 작성해 실행해봅시다. main.py 파일을 프로젝트 폴더에 생성하고 다음 코드를 입력해 저장합니다.
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"message": "Hello World"}
코드 분석
from fastapi import FastAPI: fastapi 모듈에서 파이썬 클래스 FastAPI를 import합니다.
app = FastAPI(): app 변수에 FastAPI 인스턴스를 만들어 담습니다. 해당 변수에 이름에 따라 바로 이어 나올 uvicorn 명령어를 다르게 사용합니다. uvicorn main:[변수이름] --reload처럼 말이죠!
@app.get("/"): path operation decorator를 만듭니다. path란 //를 제외하고 url에서 첫 번째로 만나는 /로부터 시작되는 url의 뒷 부분을 말하며, operation은 HTTP method를 말합니다. 이 코드의 경우, decorator가 장식하는 함수가 ‘GET operation을 사용해 / path로 가라는 요청’을 처리하는 역할을 한다고 FastAPI에게 알려줍니다.
def root():: path operation function을 정의합니다. 이 코드의 경우, FastAPI는 GET operation으로 URL /에 대한 요청이 들어오면 이 함수를 호출합니다.
return {"message": "Hello World"}: content를 리턴합니다. 리턴할 수 있는 객체는 dict, list, int, str, Pydantic model 등 다양합니다.
그리고 Uvicorn 서버를 구동합니다.
> uvicorn main:app --reload
uvicorn main:app --reload의 의미
uvicorn: uvicorn 서버를 실행합니다
main: main.py 파일(모듈)을 의미합니다.
app: main.py 내에서 생성한 FastAPI 클래스의 객체를 의미합니다.
--reload: 코드를 수정한 후 자동으로 서버를 재시작해주는 옵션입니다. 현재 개발 중일 때 사용합니다.
이제 브라우저로 로컬머신에서 작동 중인 앱을 확인해봅시다. http://127.0.0.1:8000 주소에 들어가면, JSON 형태의 응답으로 다음과 같이 새로이 마주하는 Fast API 세상과 인사를 나눌 수 있습니다!
Uvicorn이란?
uvloop와 httptools를 사용하는 초고속 ASGI(Asynchronous Server Gateway Interface) web server입니다. 최근까지 파이썬은 asyncio 프레임 워크를 위한 저수준 서버 / 애플리케이션 인터페이스가 없었는데, uvicorn의 등장으로 Fast API같은 프레임워크의 비동기 처리 성능이 크게 향상됐습니다.
Starlette이란?
Uvicorn 위에서 실행되는 비동기적으로 실행할 수 있는 web application server입니다. FastAPI는 Starlette 위에서 동작하고, Starlette 클래스를 상속받았기 때문에, Starlette의 모든 기능을 사용할 수 있습니다.
Reference
Fast API 공식 문서 튜토리얼
Uvicorn이란?
비동기 Micro API server로 좋은 FastAPI
-
파이썬 데코레이터 (Decorator)
파이썬의 함수는 일급 시민이자 일급 객체
일급 객체(First-class object)란 다음과 같은 몇 가지 조건을 갖춤으로 인해서, 해당 객체를 사용할 때 다른 요소들과 아무런 차별이 없는 객체를 의미합니다. 다음은 Robin Popplestone이 정의한 일급 객체의 일반적인 조건입니다.
모든 일급 객체는 함수의 실질적인 매개변수가 될 수 있다.
모든 일급 객체는 함수의 반환값이 될 수 있다.
모든 일급 객체는 할당의 대상이 될 수 있다. (변수 대입)
모든 일급 객체는 비교 연산(==, equal)을 적용할 수 있다.
일급 객체는 자바스크립트에서 파생된 개념이지만 지금은 대다수 프로그래밍 언어에 적용되는 개념입니다. 파이썬에서는 모든 것이 객체이자 일급객체여서, 함수 역시 위 조건을 만족하는 일급 객체에 해당합니다.
데코레이터란?
데코레이터란 기존 함수를 수정하지 않은 상태에서 새로운 기능을 추가할 때 사용하는 장식자입니다. 함수 위에 @를 붙인 것들이 모두 데코레이터에 해당됩니다.
def basic_latte(func):
def wrapper():
print('Milk')
func()
print('Espresso')
return wrapper
def vanilla():
print('Vanilla Syrup')
def caramel():
print('Caramel Syrup')
vanilla_latte = basic_latte(vanilla)
vanilla_latte()
print()
caramel_latte = basic_latte(caramel)
caramel_latte()
# 출력 결과
# Milk
# Vanilla Syrup
# Espresso
#
# Milk
# Caramel Syrup
# Espresso
데코레이터의 이해를 위해 다양한 시럽을 베이스로 라떼를 제조해보는 예제로 데코레이터의 기본 구조를 살펴보겠습니다. 위의 basic_latte 함수는 우유와 에스프레소를 추가해주는 함수입니다. 특이한 점은 함수를 인자로 받고 내부에서 새로 정의한 함수 wrapper를 리턴하는 부분인데, 이렇게 하면 기존 함수 func을 매개변수로 사용해 추가 기능을 자유롭게 덧입힐 수 있습니다. 위와 같은 경우 vanilla 함수, caramel 함수에 각각 에스프레소와 우유를 덧입혀 출력한 것이죠! 파이썬의 closure의 개념을 알고 있다면, 이 예제 역시 closure의 일종으로 이해할 수 있습니다.
이 같은 구현이 가능한 이유는 파이썬의 함수가 일급 객체이기 때문입니다. 함수를 인자로 받고 리턴하고 변수에 할당하는 것이 가능함으로 인해 앞으로 강력하게 사용될 데코레이터가 탄생할 수 있었던 것이죠.
def basic_latte(func):
def wrapper():
print('Milk')
func()
print('Espresso')
return wrapper
@basic_latte
def vanilla():
print('Vanilla Syrup')
@basic_latte
def caramel():
print('Caramel Syrup')
vanilla()
print()
caramel()
# 출력 결과
# Milk
# Vanilla Syrup
# Espresso
#
# Milk
# Caramel Syrup
# Espresso
데코레이터를 사용하면 위에서 살펴본 라떼 제조를 간단히 실행할 수 있습니다. 단순히 원하는 함수 위에 @추가기능함수이름을 달아주면, 굳이 basic_latte(vanilla)를 하지 않고 vanilla()만 실행해도 원하는 결과를 확인할 수 있습니다.
만일 여러개의 데코레이터를 지정하고 싶다면 다음과 같이 호출하면 됩니다.
def espresso(func):
def wrapper():
func()
print('Espresso')
return wrapper
def milk(func):
def wrapper():
func()
print('Milk')
return wrapper
@espresso
@milk
def vanilla():
print('Vanilla Syrup')
vanilla()
# 출력 결과
# Vanilla Syrup
# Milk
# Espresso
에스프레소와 우유를 각각 덧입혀 바닐라 라떼 제조에 성공했습니다! @를 쓰지 않았을 때의 코드 동작은 espresso(milk(vanilla))() 와 동일합니다.
데코레이터에서 매개변수와 반환값을 처리하기
이번에는 매개변수와 반환값을 처리하는 데코레이터를 만들어 보겠습니다.
def make_latte(func):
def wrapper(espresso, milk):
latte = func(espresso, milk)
print(f'{func.__name__}(espresso={espresso}ml, milk={milk}ml) -> latte={latte}ml')
return latte
return wrapper
@make_latte
def mix(espresso, milk):
return espresso + milk
print(mix(60, 200))
# 출력 결과
# mix(espresso=60ml, milk=200ml) -> latte=260ml
# 260
데코레이터가 매개변수를 처리할 수 있게끔 만드려면, 안쪽 wrapper 함수를 mix와 똑같은 형태로 매개변수를 받을 수 있게 만들어줘야 합니다. (결국 wrapper 함수가 인자를 받아 실행될 것이기 때문이죠!) 그리고 wrapper 함수 안에서 추가하고 싶은 기능을 만들어 줍니다. 여기서는 mix 함수를 실행한 리턴값을 변수로 저장하고 라떼 레시피와 제조 과정을 출력했습니다. 마지막으로 mix 함수는 에스프레소와 우유의 용량을 합친 수를 리턴해야 하므로, wrapper 함수에서 mix 함수의 반환값을 리턴해주도록 합니다. 만일 이를 잊어버리면, mix 함수를 호출해도 리턴값이 나오지 않으므로 유의해야 합니다.
이로써 매개변수와 반환값을 잘 처리하는 라떼 제조 데코레이터 구현에 성공했습니다. 만일 가변 인수 함수에 기능을 추가하고 싶은 상황이라면 데코레이터 안쪽 wrapper 함수에 *arg, **kwarg를 사용해주면 됩니다. 이렇게 만든 가변 인수 데코레이터는 고정 인수를 사용하는 일반적인 함수에도 사용할 수 있습니다.
매개 변수가 있는 데코레이터 만들기
데코레이터의 또 하나 강력한 점은 인자를 받아 동적으로 적용되는 추가 기능을 덧입힐 수 있다는 것입니다.
def make_variation(syrup_name): # 데코레이터의 인자를 추가하는 부분
def make_latte(func): # 실제 데코레이터 부분
def wrapper(espresso, milk):
latte = func(espresso, milk)
print(f'{func.__name__}(espresso={espresso}ml, milk={milk}ml) with {syrup_name} syrup')
print(f'-> {syrup_name}_latte={latte}ml')
return latte
return wrapper
return make_latte # 실제 데코레이터 함수 반환
@make_variation('green_tea')
def mix(espresso, milk):
return espresso + milk
print(mix(60, 200))
# 출력 결과
# mix(espresso=60ml, milk=200ml) with green_tea syrup
# -> green_tea_latte=260ml
# 260
보통 기본 베이직 커피에 무언가를 더 가미해 다양한 맛을 낸 커피를 베리에이션(variation)이라고 하는데, 여기선 데코레이터의 인자로 시럽의 이름을 받아 기본 라떼의 베리에이션을 만들어 보겠습니다.
코드를 보면 기존에 만들었던 데코레이터와 큰 차이 없이, 단순히 데코레이터의 인자를 받을 함수를 하나 더 덧입혀 삼중으로 처리하고 wrapper 함수의 출력문을 조금 바꿨습니다. 그리고 mix 함수 위에는 새로 덧입힌 함수를 데코레이터로 사용하고 인자로 녹차시럽(green_tea)을 받았습니다. 이렇게 하면, 녹차시럽을 가미한 베리에이션으로 녹차 라떼가 완성됩니다. 데코레이터의 인자를 바닐라 시럽이나 카라멜 시럽으로 바꾸면 동적으로 다른 베리에이션을 만드는 것도 가능합니다.
여러 개의 데코레이터를 지정하다가 원래 함수의 이름이 나오지 않을 때
여러 베리에이션을 만들면 다음과 같이 원래 함수의 이름이 나오지 않을 수 있습니다.
# 실제 동작: make_variation('green_tea')(make_variation('vanilla')(mix))(60, 200)
@make_variation('green_tea')
@make_variation('vanilla')
def mix(espresso, milk):
return espresso + milk
print(mix(60, 200))
# 결과 출력
# mix(espresso=60ml, milk=200ml) with vanilla syrup
# -> vanilla_latte=260ml
# wrapper(espresso=60ml, milk=200ml) with green_tea syrup
# -> green_tea_latte=260ml
# 260
참고로 위 함수의 실제 동작은 make_variation('green_tea')(make_variation('vanilla')(mix))(60, 200)으로 실행됩니다. 이 때 원하지 않는 출력 결과로 wrapper 함수의 이름이 나타났는데, 이를 개선하려면 wrapper 함수 위에 functools 모듈의 wraps 데코레이터를 사용해야 합니다.
import functools
def make_variation(syrup_name):
def make_latte(func):
@functools.wraps(func) # @functools.wraps에 func을 인자로 넣은 뒤 wrapper 함수 위에 지정
def wrapper(espresso, milk):
latte = func(espresso, milk)
print(f'{func.__name__}(espresso={espresso}ml, milk={milk}ml) with {syrup_name} syrup')
print(f'-> {syrup_name}_latte={latte}ml')
return latte
return wrapper
return make_latte
@make_variation('green_tea')
@make_variation('vanilla')
def mix(espresso, milk):
return espresso + milk
print(mix(60, 200))
# 결과 출력
# mix(espresso=60ml, milk=200ml) with vanilla syrup
# -> vanilla_latte=260ml
# mix(espresso=60ml, milk=200ml) with green_tea syrup
# -> green_tea_latte=260ml
# 260
@functools.wraps 데코레이터를 사용하면 출력 결과가 원하는대로 나오는 것을 확인할 수 있습니다. @functools.wraps 데코레이터는 원래 함수의 정보를 유지시켜 디버깅을 용이하게 합니다. 따라서 데코레이터를 만들 때 함께 사용하는 것이 유용합니다.
클래스로 데코레이터 만들기
기존에 함수로 만들던 데코레이터는 클래스로도 만들 수 있습니다. 다만, 클래스로 데코레이터를 만들 때는 인스턴스를 함수처럼 호출하게 도와주는 __call__ 매직 메서드를 사용해야 합니다.
class basic_latte:
def __init__(self, func):
self.func = func
def __call__(self):
print('Milk')
self.func()
print('Espresso')
@basic_latte
def vanilla():
print('Vanilla Syrup')
vanilla() # basic_latte(vanilla)() 형태로 동작해 인스턴스가 생성되고, ()로 인해 __call__ 메서드가 호출됨
# 출력 결과
# Milk
# Vanilla Syrup
# Espresso
이렇게 코드를 짜면 기존의 함수로 만든 데코레이터와 동일한 결과를 얻을 수 있습니다. 데코레이터로 인해 basic_latte(vanilla)가 먼저 동작해 basic_latte 클래스의 인스턴스가 생성되고 해당 인스턴스에 ()가 붙어 __call__ 메서드가 수행되어 추가로 구현한 기능이 동작하게 됩니다.
클래스로 만든 데코레이터로 매개변수와 반환값도 처리할 수 있습니다.
class make_latte:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
latte = self.func(*args, **kwargs)
print('{}(espresso={}ml, milk={}ml) -> latte={}ml'.format(self.func.__name__, *args, latte))
return latte
@make_latte
def mix(espresso, milk):
return espresso + milk
print(mix(60, 200))
# 출력 결과
# mix(espresso=60ml, milk=200ml) -> latte=260ml
# 260
__call__ 메서드에 mix 함수가 받을 인자를 똑같이 받도록 만들고 mix 함수의 리턴 값을 __call__메서드에서 반환해주면, 기존의 함수 데코레이터와 동일한 결과를 얻는 데코레이터를 클래스로 만들 수 있습니다.
매개 변수가 있는 데코레이터도 클래스로 구현해보겠습니다.
class make_variation:
def __init__(self, syrup_name):
self.syrup_name = syrup_name
def __call__(self, func):
def wrapper(*args, **kwargs):
latte = func(*args, **kwargs)
print('{}(espresso={}ml, milk={}ml) with {} syrup'.format(func.__name__
, *args, self.syrup_name))
print(f'-> {self.syrup_name}_latte={latte}ml')
return latte
return wrapper
@make_variation('green_tea')
def mix(espresso, milk):
return espresso + milk
print(mix(60, 200))
# 출력 결과
# mix(espresso=60ml, milk=200ml) with green_tea syrup
# -> green_tea_latte=260ml
# 260
__init__ 메서드에서 데코레이터의 인자를 초깃값으로 받으면서, 인스턴스 속성으로 저장합니다. 그리고 __call__ 메서드에서 함수를 인자로 받도록 하고, 메서드 내부에 wrapper 함수를 새로 만들어 호출할 함수와 똑같은 형태로 매개변수를 받을 수 있도록 만들어 줍니다. 추가할 기능 역시 wrapper 함수에 구현하고 __call__ 메서드가 wrapper 함수를 리턴하도록 합니다. 그리고 mix 함수의 반환 값을 wrapper 함수가 리턴하도록 만들면 인자를 받는 데코레이터 구현이 완료됩니다. 똑같이 녹차 라떼가 제조됨을 확인할 수 있죠!
데코레이터의 의의
이로써 파이썬에서 데코레이터를 만드는 다양한 형태와 방법을 살펴봤습니다. 클로저 개념에서 발전되어 등장한 데코레이터는 기존 함수를 변형하지 않고 새로운 기능을 추가하는 목적으로 사용하지만, 디버깅에서도 훌륭한 수단이 됩니다. 함수의 성능 측정이나 함수 실행 전 데이터 확인 같은 다양한 목적으로도 사용되므로, 데코레이터에 익숙해지는 것은 효과적인 프로그래밍에 큰 도움이 될 것입니다.
Reference
python의 함수 decorators 가이드
파이썬 코딩 도장 - 데코레이터
1급 객체(first-class object)란?
-
-
-
-
파이썬 클래스 개념 조각 모음
클래스(Class)를 사용하는 이유
관련 있는 데이터를 묶기 위해 배열이, 데이터 묶음 요소마다 의미를 부여하기 위해 딕셔너리가, 의미를 확장해 다양한 정보와 동작들을 한데 묶어 표현하기 위해 클래스가 탄생했다.
보안상의 이슈를 다루기 위해 코드들을 바깥과 분리하여 감싸는 encapsulation 기능이 필요했다.
__main__을 사용하는 이유
C, C++ 같은 언어의 main 함수 영향을 받았다.
프로그램의 중심이 되는 코드들을 한 곳에 정리하기 위한 관리상 요인이 작용했다. (덕분에 프로그래밍의 시작점 파악이 용이)
속성(Attribute)의 종류
인스턴스 속성
인스턴스를 통해 접근할 수 있는 속성 (클래스 바깥에서는 인스턴스.속성, 클래스 내부에서는 self.속성으로 접근)
__init__ 메서드 안에 정의한 속성
인스턴스 별로 독립되어 있는 속성이며, 각 인스턴스가 값을 따로 저장해야 할 때 사용
인스턴스를 생성한 후에도 자유롭게 속성을 추가할 수 있음
인스턴스.속성 = something (방법 1)
클래스 내 메서드에 속성을 정의하고, 인스턴스 생성 후 호출 (방법 2)
__slots__ 메서드로 특정 속성만 추가를 허용하도록 지정 가능
__slots__ = ['속성이름1, '속성이름2'] (속성 이름은 문자열로 지정)
>>> class Person:
... __slots__ = ['name', 'age'] # name, age만 허용(다른 속성은 생성 제한)
...
>>> maria = Person()
>>> maria.name = '마리아' # 허용된 속성
>>> maria.age = 20 # 허용된 속성
>>> maria.address = '서울시 서초구 반포동' # 허용되지 않은 속성은 추가할 때 에러가 발생함
Traceback (most recent call last):
File "<pyshell#32>", line 1, in <module>
maria.address = '서울시 서초구 반포동'
AttributeError: 'Person' object has no attribute 'address'
클래스 속성
클래스에 바로 만든 속성
클래스 내부, 클래스 바깥 모두에서 접근 가능하다. (언더스코어 2개를 사용해 비공개 속성으로도 만들 수 있음)
모든 인스턴스가 공유하는 속성이며, 인스턴스 전체가 사용해야 하는 값을 저장할 때 사용
class Person:
bag = []
def put_bag(self, stuff):
Person.bag.append(stuff) # self.bag.append(stuff)라고 써도 되지만, 클래스 이름을 쓰는 것이 명확
james = Person()
james.put_bag('책')
maria = Person()
maria.put_bag('열쇠')
print(james.bag) # ['책', '열쇠']
print(maria.bag) # ['책', '열쇠']
속성과 메서드 이름을 찾는 순서
파이썬에서 속성, 메서드 이름을 찾을 때, 인스턴스, 클래스 순으로 찾는다. 위 예에서도 마치 인스턴스 속성을 사용한 것 같지만, 인스턴스 속성이 없으면 클래스 속성을 찾게 되므로 실제로 클래스 속성을 리턴한 것이다.
인스턴스나 클래스에서 __dict__ 속성을 출력해보면 현재 인스턴스와 클래스의 속성을 딕셔너리로 확인할 수 있다.
>>> james.__dict__
{}
>>> Person.__dict__
mappingproxy({'__module__': '__main__', 'bag': ['책', '열쇠'], 'put_bag': <function Person.put_bag at 0x028A32B8>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None})
메서드(Method)의 종류
인스턴스 메서드
인스턴스를 통해 접근할 수 있는 메서드
대부분의 일반적인 메서드에 해당되며 첫 번째 파라미터로 self를 지정하는 메서드 (self는 instance 그 자체를 받음)
정적 메서드
인스턴스를 통하지 않고 클래스에서 바로 호출 가능
메서드 위에 @staticmethod를 붙이고 파라미터로 self를 지정하지 않는 메서드
self를 받지 않기 때문에 인스턴스 속성에 접근할 수 없음
그래서 보통 인스턴스 속성, 인스턴스 메서드가 필요없는 메서드, 인스턴스의 상태를 변화시키지 않는 순수함수를 만들 때 사용
class Calc:
@staticmethod
def add(a, b):
print(a + b)
@staticmethod
def mul(a, b):
print(a * b)
Calc.add(10, 20) # 클래스에서 바로 메서드 호출 / 30
Calc.mul(10, 20) # 클래스에서 바로 메서드 호출 / 200
클래스 메서드
인스턴스를 통하지 않고 클래스에서 바로 호출 가능
메서드 위에 @classmethod를 붙이고 첫번째 파라미터로 cls를 지정하는 메서드 (cls는 class 그 자체를 받음)
cls를 받기 때문에 클래스 속성, 클래스 메서드에 접근할 수 있음
메서드 안에서 클래스 속성, 클래스 메서드에 접근하거나 메서드 안에서 현재 클래스의 인스턴스를 만들 때 사용
class Person:
count = 0 # 클래스 속성
def __init__(self):
Person.count += 1 # 인스턴스가 만들어질 때
# 클래스 속성 count에 1을 더함
@classmethod
def print_count(cls):
print('{0}명 생성되었습니다.'.format(cls.count)) # cls로 클래스 속성에 접근
james = Person()
maria = Person()
Person.print_count() # 2명 생성되었습니다.
비공개 속성과 비공개 메서드
비공개 속성 (Private Attribute)
클래스 바깥에서는 접근할 수 없고 클래스 안에서만 사용할 수 있는 속성
클래스 바깥에 드러내고 싶지 않은 값에 사용한다.
__속성으로 사용
class Person:
def __init__(self, name, age, address, wallet):
self.name = name
self.age = age
self.address = address
self.__wallet = wallet # 변수 앞에 __를 붙여서 비공개 속성으로 만듦
maria = Person('마리아', 20, '서울시 서초구 반포동', 10000)
maria.__wallet -= 10000 # 클래스 바깥에서 비공개 속성에 접근하면 에러가 발생함
클래스 내 메서드에서는 접근 가능
class Person:
def __init__(self, name, age, address, wallet):
self.name = name
self.age = age
self.address = address
self.__wallet = wallet # 변수 앞에 __를 붙여서 비공개 속성으로 만듦
def pay(self, amount):
self.__wallet -= amount # 비공개 속성은 클래스 안의 메서드에서만 접근할 수 있음
print('이제 {0}원 남았네요.'.format(self.__wallet))
maria = Person('마리아', 20, '서울시 서초구 반포동', 10000)
maria.pay(3000)
비공개 메서드 (Private Method)
클래스 바깥에서는 접근할 수 없고 클래스 안에서만 사용할 수 있는 메서드
클래스 바깥에 드러내고 싶지 않고 보통 내부에서만 호출되어야 할 때 사용한다.
__메서드로 사용
class Person:
def __greeting(self):
print('Hello')
def hello(self):
self.__greeting() # 클래스 안에서는 비공개 메서드를 호출할 수 있음
james = Person()
james.__greeting() # 에러: 클래스 바깥에서는 비공개 메서드를 호출할 수 없음
파이썬 접근 제어
다른 언어와 달리 파이썬은 접근제어자 키워드가 따로 존재하지 않기 때문에 네이밍(naming)을 통해 접근 제어를 수행한다.
다만, 파이썬에서는 네이밍을 사용해 접근을 제어해도 완벽하게 차단할 수는 없다.
public, protected, private은 상황별로 다음과 같은 양상을 보인다.
public
언더스코어(_)없이 시작하는 속성, 메서드
어디서나 접근 가능
protected
언더스코어 1개로 시작하는 속성, 메서드
어디서나 접근 가능하지만, 암묵적 규칙에 의해 해당 클래스 내부와 파생 클래스에서만 접근해야 함 (파이썬은 protect 기능이 X)
private
언더스코어 2개로 시작하는 속성, 메서드
해당 클래스 내부에서만 접근 가능
주요 Dunder Method (=Magic method)
__repr__
해당 class의 string representation을 설정
객체를 출력하면 미리 설정된 사용자가 이해할 수 있는 문자열을 반환
self 파라미터 하나만 받고, 반드시 문자열을 리턴해야 한다.
class Employee():
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
argus = Employee("Argus Filch")
print(argus)
# prints "Argus Filch"
__add__
+ 기호에 대응하는 메서드
더하는 메서드로서 self 파라미터와 여기에 더할 인자 하나를 받는다.
class Color:
def __init__(self, red, green, blue):
self.red = red
self.green = green
self.blue = blue
def __repr__(self):
return "Color with RGB = ({red}, {green}, {blue})".format(red=self.red, green=self.green, blue=self.blue)
def __add__(self, other):
"""
Adds two RGB colors together
Maximum value is 255
"""
new_red = min(self.red + other.red, 255)
new_green = min(self.green + other.green, 255)
new_blue = min(self.blue + other.blue, 255)
return Color(new_red, new_green, new_blue)
red = Color(255, 0, 0)
green = Color(0, 255, 0)
blue = Color(0, 0, 255)
# Color with RGB: (255, 0, 255)
magenta = red + blue
# Color with RGB: (0, 255, 255)
cyan = green + blue
# Color with RGB: (255, 255, 0)
yellow = red + green
# Color with RGB: (255, 255, 255)
white = red + green + blue
__len__
len() 함수를 호출했을 때의 결과 값을 임의로 설정해 리턴할 수 있는 메서드
__iter__
iterator 객체를 반환해 반복가능한 객체로 만들어 주는 메서드
__contains__
멤버 연산자 in을 사용할 수 있게 해주는 메서드
클래스 관련 메서드
특정 클래스의 인스턴스인지 확인하기
isinstance(인스턴스, 클래스)
True, False 반환
해당 객체가 특정 속성을 가지고 있는지 여부 확인하기
hasattr(객체, '속성')
True, False 반환
hasattr(attributeless, "fake_attribute")
# returns False
해당 객체에서 특정 속성의 값을 가져오기
getattr(객체, '속성', default)
속성이 있으면 속성의 값 반환, 없으면 디폴트 값 반환
getattr(attributeless, "other_fake_attribute", 800)
# returns 800, the default value
특정 클래스 A가 클래스 B의 subclass인지 확인하기
issubclass(클래스 A, 클래스 B)
True, False 반환
Reference
파이썬 코딩 도장
Codecademy - learning python 3
private, proteted, public 의 차이
인스턴스 메소드의 종류와 용법 (Instance methods): Public, Protected, Private 접근제어자 (Access Modifiers)
-
순간 놓치기 쉬운 파이썬 개념들 정리
2진수, 8진수, 16진수로 정수 표현하기
>>> 0b110 # 2진수
6
>>> 0o10 # 8진수
8
>>> 0xF # 16진수
15
보다 정교한 계산으로 부동소수점 오류를 피하는 자료형 Decimal
from decimal import Decimal
cost_of_gum = Deciaml('0.10')
cost_of_gumdrop = Decimal('0.35')
cost_of_transaction = cost_of_gum + cost_of_gumdrop
print(cost_of_transaction)
# Returns 0.45 instead of 0.44999999999999996
빈 변수 만들기
>>> x = None # 다른 언어의 null 값
>>> print(x)
None
del 키워드가 사용되는 경우
변수 삭제, 리스트 요소 삭제, 딕셔너리 요소 삭제
언더스코어 변수( _ )
파이썬 셸에서 코드를 실행했을 때 결과는 _(밑줄 문자) 변수에 저장됩니다. 따라서 _를 사용하면 직전에 실행된 결과를 다시 가져올 수 있습니다.
단락 평가(short-circuit evalution)
단락 평가란 첫 번째 값만으로 결과가 확실할 때 두 번째 값은 확인(평가)하지 않는 방법을 말합니다. 논리 연산에서 단락 평가는 중요합니다. 예를 들어, Fasle and True는 and 앞이 False이기 때문에 뒷 부분을 고려할 필요없이 결과가 False가 됩니다. 실제 연산에서도 False and True는 단락 평가를 진행해 앞 부분만 확인하여 결과를 리턴합니다. 따라서, 복잡한 논리 연산일수록, 전체 결과를 빠르게 판단할 수 있는 식이 있다면 최대한 앞으로 빼서 효율적으로 연산이 동작하게끔 작성해야 합니다.
또한, 파이썬에서 논리 연산자는 마지막으로 단락 평가를 실시한 값을 그대로 반환하는 점을 유의해야 합니다. 논리 연산자는 무조건 불을 반환하지 않습니다.
True and 'Welsh Corgi' # 'Welsh Corgi' 리턴
'Welsh Corgi' and True # True 리턴
'Welsh Corgi' and False # False 리턴
False and 'Welsh Corgi' # False 리턴
0 and 'Welsh Corgi' # 0 리턴
자료형(객체) 구분
1. 시퀀스 자료형
리스트, 튜플, range, 문자열처럼 값이 연속적으로 이어진 자료형을 시퀀스 자료형(sequence types)라고 부릅니다.
수행 가능 연산:
in 연산자
+, * 연산 (range 제외)
len() 함수
인덱싱([] ∝ getitem 메서드) & 슬라이싱(∝ 슬라이스 객체 생성 후 [] 또는 getitem 메서드에 삽입)
인덱스로 값 할당 및 del 삭제 (list만 가능, 다만 범위를 벗어나면 안됨)
슬라이싱으로 값 할당 및 del 삭제 (list만 가능)
슬라이싱으로 값 할당 및 del 삭제
슬라이싱으로 범위를 지정해 값 할당 및 삭제를 진행할 때, 새 리스트를 생성하지 않고 기존 리스트를 변경합니다.
범위와 요소의 개수가 정확히 일치하지 않아도 됩니다.
예 1)
a = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
a[2:5] = ['a'] # 인덱스 2부터 4까지에 값 1개를 할당하여 요소의 개수가 줄어듦
a
[0, 10, 'a', 50, 60, 70, 80, 90]
예 2)
a = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
a[2:5] = ['a', 'b', 'c', 'd', 'e'] # 인덱스 2부터 4까지 값 5개를 할당하여 요소의 개수가 늘어남
a
[0, 10, 'a', 'b', 'c', 'd', 'e', 50, 60, 70, 80, 90]
예 3)
a = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
a[2:5] = ['a', 'b', 'c'] # 인덱스 2부터 4까지 값 할당
a
[0, 10, 'a', 'b', 'c', 50, 60, 70, 80, 90]
범위에 인덱스 증가폭을 설정해서 값을 할당할 수도 있습니다. (다만, 이 때는 범위에 해당하는 요소 개수와 할당할 요소의 개수가 일치해야 합니다.)
예)
a = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
a[2:8:2] = ['a', 'b', 'c'] # 인덱스 2부터 2씩 증가시키면서 인덱스 7까지 값 할당
a
[0, 10, 'a', 30, 'b', 50, 'c', 70, 80, 90]
del을 사용해 일반적으로 값을 삭제할 수 있지만, 인덱스 증가폭을 사용해서 값을 삭제할 수도 있습니다.
예)
a = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
del a[2:8:2] # 인덱스 2부터 2씩 증가시키면서 인덱스 6까지 삭제
a
[0, 10, 30, 50, 70, 80, 90]
2. 반복 가능한(iterable) 객체
문자열, 리스트, 딕셔너리, 세트 같이, 요소가 여러 개 들어있고, 한 번에 하나씩 꺼낼 수 있는 객체입니다. 반복 가능한 객체는 iter 메서드를 포함하고 있으며, 이 메서드를 호출해 이터레이터(iterator)를 생성할 수 있습니다.
3. 변경 가능한(Mutable) 객체
객체의 값이나 요소가 변경 가능한지 아닌지에 따라 나뉘는 기준이다. Mutable한 객체 list, dict, set을 외워두는게 기억하기 편리하다.
참고하기 좋은 표
얕은 복사와 깊은 복사
얕은 복사 (=주소값 복사)
파이썬은 모든 변수에 주소값이 담기므로, 단순히 변수에 할당하는 방식으로는 새로운 객체를 복사하는 것이 아니라 동일한 객체를 가리키게 된다.
a = [0, 0, 0, 0, 0]
b = a
a is b
True
깊은 복사 (1차원)
파이썬 내장함수 copy()
슬라이싱을 통한 복사 ex) a[:]
깊은 복사 (다차원)
copy 모듈의 deepcopy() 메서드 사용
튜플을 사용하는 이유
리스트가 언제든 요소를 추가할 수 있게 하기 위해 실제 데이터보다 큰 메모리를 사용하는데 반해, 튜플은 요소 변경이 없어 고정된 메모리를 사용합니다. 또한, 튜플의 구조는 간단해서 리스트보다 빠른 성능을 보여줍니다. 따라서, 요소 변경이 없는 상황에서는 튜플을 사용하는 것이 메모리를 아끼고 성능을 높이는 방법입니다.
defaultdict를 사용해 기본값이 빈 리스트인 딕셔너리 생성하기
from collections import defaultdict
a = defaultdict(list)
a['x']
a['y']
print(a)
defaultdict(<class 'list'>, {'x': [], 'y': []})
딕셔너리에서 요소를 삭제하는 방법
딕셔너리에서 요소를 삭제하는 방법은 제한적이기 때문에 보통 다음과 같은 방법을 사용한다.
1. 키를 통해 삭제하기
del 예약어, pop(‘키’, 기본값) 메서드 사용합니다. popitem() 메서드를 사용하면, 파이썬 3.6 이상에서는 딕셔너리의 가장 마지막에 있는 키, 값 쌍을 삭제하여 튜플로 반환하고, 3.6 미만에서는 임의의 키, 값 쌍을 삭제하여 튜플로 반환합니다.
2. 값을 통해 삭제하기 (특정 값을 제외하여 새로 딕셔너리를 생성)
딕셔너리 표현식을 사용합니다.
x = {'a': 10, 'b': 20, 'c': 30, 'd': 40}
x = {key: value for key, value in x.items() if value != 20}
x
{'a': 10, 'c': 30, 'd': 40}
파일 객체는 이터레이터입니다!
open을 통해서 가져오는 파일 객체는 이터레이기 때문에, for문에서 반복하거나 언패킹할 수 있습니다.
file = open('welsh.txt', 'r')
a, b, c = file
a, b, c
('안녕하세요.\n', '멍멍!\n', '저는 웰시 코기입니다.\n')
random 모듈에서 자주 사용되는 메서드들
import random
# 0이상 1미만 범위의 난수 생성
random.random() # return: 0 <= x < 1에 해당하는 x 값
# 지정한 범위에 해당하는 정수 하나를 랜덤하게 가져오기
random.randint(1, 16) # return: 1 <= x <= 16에 해당하는 int 타입 x 값
# 지정한 범위에 해당하는 실수 하나를 랜덤하게 가져오기
random.uniform(1, 20) # return: 1.0 <= x < 20.0에 해당하는 float 타입 x 값
# range(start, stop, step) 함수로 만들어지는 정수들 중 랜덤하게 하나를 가져오기
random.randrange(1, 9, 2) # return: 1, 3, 5, 7 중 하나의 값
seq = ['a', 'b', 'c', 'd']
# 시퀀스 객체 내 요소 순서를 무작위로 변경하기
random.shuffle(seq) # seq: ['c', 'b', 'a', 'd']
# 시퀀스 객체에서 요소 하나를 랜덤하게 가져오기
random.choice(seq) # return: 'c', 'b', 'a', 'd' 중 하나의 값
# 시퀀스 객체에서 요소 여러 개를 랜덤하게 가져오기
random.sample(seq, 2) # return: seq 리스트에서 2개의 요소를 뽑아 리스트로 만들어 리턴
datetime 모듈 사용법
from datetime import datetime
# datetime 객체 생성하기
# datetime(년, 월, 일, 시간, 분, 초)
birthday = datetime(1994, 6, 27) # datetime.datetime(1994, 6, 27, 0, 0)
birthday = datetime(1994, 6, 27, 6, 30, 27) # datetime.datetime(1994, 6, 27, 6, 30, 27)
# year, month, day, hour, minute, second 속성에 접근 가능
birthday.year # 1994
birthday.month # 6
# weekday() 메서드를 사용하면 요일을 0(월) ~ 6(일) 인덱스로 반환
birthday.weekday() # 0
# 현재 시간으로 datatime 객체 생성하기
datetime.now() # datetime.datetime(2021, 5, 7, 23, 46, 7, 925228)
# datetime 객체로 두 날짜 사이의 시간 차이 구하기
datetime(2021, 1, 2) - datetime(2020, 1, 1) # datetime.timedelta(days=367)
datetime.now() - datetime(2021, 1, 1) # datetime.timedelta(days=126, seconds=86052, microseconds=468421)
# 문자열로 된 시간을 datetime 객체로 파싱하기
parsed_date = datetime.strptime('Jan 15, 2019', '%b %d, %Y')
parsed_date.month # 1
parsed_date.day # 15
parsed_date.minute # 0
# datetime 객체를 문자열로 만들기
date_string = datetime.strftime(datetime.now(), '%b %d, %Y')
date_string # 'May 08, 2021'
strftime() and strptime() Format Codes 는 다음 링크에서 확인
https://docs.python.org/3/library/datetime.html
함수에서 파라미터의 초깃값을 빈 리스트로 만들고 싶은 경우
함수 파라미터의 초깃값으로는 Immutable한 객체만 사용해야 한다.
만일 Mutable한 객체라면, 여러번 함수를 호출해도 처음에 초깃값으로 생성한 객체를 조작하게 된다.
따라서, 리스트를 인자로 받을 파라미터의 초깃값을 None으로 설정하고 함수 내부에서 if 조건문으로 체크하는 것이 바람직하다.
def add_author(authors_books, current_books=None):
if current_books is None:
current_books = []
current_books.extend(authors_books)
return current_books
Reference
파이썬 코딩 도장
파이썬 del - 제타위키
파이썬 기초
Python, 파이썬 - Call by assignment, mutable, immutable, 파이썬 복사(Python Copy)
-
Django 간단한 블로그 만들기
1. 가상환경 설정하기 (Window)
가상환경이란 자신이 원하는 환경을 구축하기 위해 필요한 모듈만 담아 놓는 바구니를 말한다. 프로젝트 기초 전부를 분리해 사용할 수 있기 때문에 유용하다.
Virtualenv를 통한 설정
가상환경 생성하기
먼저, 명령 프롬프트에서 가상환경을 생성할 폴더를 만들고 해당 폴더로 이동한다. 홈 디렉토리(C:\Users\Name)에 생성하면 적당한 선택이다.
mkdir djangogirls
cd djangogirls
그리고 가상 환경을 생성한다. 가상환경을 이름을 설정할 수 있는데 여기서는 myvenv로 생성하기로 한다.
python -m venv myvenv
가상환경 사용하기
myvenv\Scripts\activate
만일 실행이 안될 경우, cmd를 관리자 권한으로 실행한다.
2. 장고 설치하기
pip을 최신 버전으로 업데이트하기
python3 -m pip install --upgrade pip
장고 설치하기
pip install django~=2.0.0
3. 장고 프로젝트 시작하기
생성할 장고 프로젝트의 구조
djangogirls
├───manage.py
└───mysite
settings.py
urls.py
wsgi.py
__init__.py
manage.py: 사이트 관리를 도와주는 파일
settings.py: 웹사이트 설정이 있는 파일
urls.py: urlresolver가 사용하는 패턴 목록을 포함하는 파일
장고 프로젝트 시작하기 명령 (mysite는 프로젝트 이름이므로 변경가능)
현재 디렉토리에서 장고 프로젝트 생성
(myvenv) C:\Users\Name\djangogirls> django-admin.py startproject mysite .
settings.py 설정 변경
정확한 현재 시간 설정 (선택)
TIME_ZONE = 'Asia/Seoul'
정적파일 경로 추가
파일 끝에 STATIC_URL 바로 밑에 STATIC_ROOT 추가
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
호스트 이름 일치시키기
DEBUG가 True이고 ALLOWED_HOSTS가 비어 있으면, 호스트는 [‘localhost’, ‘127.0.0.1’, ‘[::1]’] 에 유효
PythonAnywhere에 앱을 배포한다면 다음과 같이 수정
ALLOWED_HOSTS = ['127.0.0.1', '.pythonanywhere.com']
데이터베이스 설정하기
settings.py 파일 안에 sqlite 데이터베이스가 기본적으로 설치되어 있음 (기본 장고 데이터베이스 어댑터)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
데이터 베이스 생성 명령
(myvenv) ~/djangogirls$ python manage.py migrate
웹 서버 시작하기
(myvenv) ~/djangogirls$ python manage.py runserver
4. 장고 앱 만들기
장고 앱 만들기
프로젝트 내부에 장고 애플리케이션 생성 (blog는 앱 이름이므로 변경 가능)
(myvenv) ~/djangogirls$ python manage.py startapp blog
settings.py 속 INSTALLED_APPS에 새로 생성한 앱 등록 (앱 이름을 끝에 추가)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
]
앱 생성 후 프로젝트 구조
djangogirls
├── mysite
| __init__.py
| settings.py
| urls.py
| wsgi.py
├── manage.py
└── blog
├── migrations
| __init__.py
├── __init__.py
├── admin.py
├── models.py
├── tests.py
└── views.py
5. 모델 만들기
블로그 글 모델 객체 만들기
blog/models.py에 Model 객체를 선언해 모델 생성 (파일 내 내용 전부 삭제 후 아래 코드 추가)
from django.conf import settings
from django.db import models
from django.utils import timezone
class Post(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
text = models.TextField()
created_date = models.DateTimeField(
default=timezone.now)
published_date = models.DateTimeField(
blank=True, null=True)
def publish(self):
self.published_date = timezone.now()
self.save()
def __str__(self):
return self.title
데이터베이스에 모델 추가하기
모델에 생긴 변화를 알리기 위해 migration 파일 생성
python manage.py makemigrations blog
데이터베이스에 반영하기
python manage.py migrate blog
6. 장고 관리자
관리자 페이지 언어 변경 (선택)
setting.py의 LANGUAGE_CODE = ‘ko’로 바꿀 것
blog/admin.py 파일 내용 수정
생성한 모델 import 및 모델 등록
```python
from django.contrib import admin
from .models import Post
admin.site.register(Post)
```
관리자 계정 생성
서버를 실행하는 중에 관리자 계정을 생성해야만 한다.
코드 실행 후 유저 네임, 이메일 주소 및 암호 입력
(myvenv) ~/djangogirls$ python manage.py createsuperuser
Username: admin
Email address: admin@admin.com
Password:
Password (again):
Superuser created successfully.
7. 서버 배포하기
PythonAnywhere로 배포
.gitignore 파일 설정 (github에 코드 push 전)
특정 파일들을 .gitignore 파일에 등록하면, git이 해당 파일들의 변화는 무시하고 추적하지 않게끔 할 수 있다.
여기서 db.sqlite3는 로컬 데이터베이스이고 이는 테스트 공간으로만 사용하는 것이 좋으므로, github 저장소에 저장하지 않는다.
에디터를 사용해 아래와 같은 내용으로 .gitignore 파일을 프로젝트 폴더(djangogirls)에 만들자.
*.pyc
*~
__pycache__
myvenv
db.sqlite3
/static
.DS_Store
PythonAnywhere 서버에 Github에서 코드 가져오기
PythonAnywhere 콘솔에 다음 코드 입력 (my-first-blog는 github 저장소 이름)
git clone https://github.com/<your-github-username>/my-first-blog.git
PythonAnywhere에서 가상환경 생성 및 활성화하기
$ cd my-first-blog
$ virtualenv --python=python3.6 myvenv
Running virtualenv with interpreter /usr/bin/python3.6
[...]
Installing setuptools, pip...done.
$ source myvenv/bin/activate
(myvenv) $ pip install django~=2.0
Collecting django
[...]
Successfully installed django-2.0.6
PythonAnywhere에서 데이터베이스 초기화 및 관리자 계정 생성하기
(mvenv) $ python manage.py migrate
Operations to perform:
[...]
Applying sessions.0001_initial... OK
(mvenv) $ python manage.py createsuperuser
web app으로 블로그 배포하기
PythonAnywhere 대시보드에서 Web을 클릭하고 Add a new web app을 선택
도메인 이름 확정 후, 수동설정(munual configuration)을 클릭하고 Python 3.6을 선택한 다음, 다음(Next)을 클릭
가상환경 설정하기
PythonAnywhere 설정 화면의 가상환경(Virtualenv) 섹션에서 가상환경 경로를 입력해주세요(Enter the path to a virtualenv)라고 쓰여 있는 빨간색 글자를 클릭하고 /home/<your-username>/my-first-blog/myvenv/ 라고 입력
이동 경로를 저장하려면, 파란색 박스에 체크 표시하고 클릭
WSGI 파일 설정하기
PythonAnywhere에게 웹 애플리케이션의 위치와 Django 설정 파일명을 알려주는 역할
“WSGI 설정 파일(WSGI configuration file)” 링크(페이지 상단에 있는 “코드(Code)” 섹션 내 /var/www/<your-username>_pythonanywhere_com_wsgi.py 부분)를 클릭
모든 내용을 삭제하고 아래 코드 추가
import os
import sys
path = '/home/<your-PythonAnywhere-username>/my-first-blog' # PythonAnywhere 계정으로 바꾸세요.
if path not in sys.path:
sys.path.append(path)
os.environ['DJANGO_SETTINGS_MODULE'] = 'mysite.settings'
from django.core.wsgi import get_wsgi_application
from django.contrib.staticfiles.handlers import StaticFilesHandler
application = StaticFilesHandler(get_wsgi_application())
저장(Save)을 누르고 웹(Web) 탭 누르기
큰 녹색 다시 불러오기(Reload) 버튼을 누르면, 모든 배포 작업 완료
8. URL 설정 및 뷰(View) 만들기
장고는 URLconf (URL configuration)를 사용
URLconf는 장고에서 URL과 일치하는 뷰를 찾기 위한 패턴들의 집합이다.
mysite/urls.py에서 url 설정
mysite/urls.py의 초기 코드
"""mysite URL Configuration
[...]
"""
from django.contrib import admin
from django.urls import path
urlpatterns = [
path('admin/', admin.site.urls),
]
blog 앱에서 mysite/urls.py로 url들 가져오기
from django.contrib import admin
from django.urls import path, include # include 추가
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('blog.urls')), # blog.urls를 가져오는 코드 추가
]
blog/urls.py 파일 생성 및 코드 추가
from django.urls import path
from . import views
urlpatterns = [
path('', views.post_list, name='post_list'), # post_list 뷰를 루트 url에 할당
]
뷰 만들기
뷰는 애플리케이션의 로직을 넣는 곳으로, 모델에서 필요한 정보를 받아와 템플릿에 전달하는 역할을 한다.
blog/views.py 안에 뷰 만들기
초기 코드
from django.shortcuts import render
# Create your views here.
뷰 만들기
from django.shortcuts import render
# Create your views here.
def post_list(request):
'''
요청(request)을 넘겨받아 render메서드를 호출한다.
이 함수는 render 메서드를 호출하여 받은(return) blog/post_list.html 템플릿을 보여준다.
'''
return render(request, 'blog/post_list.html', {})
9. 템플릿 만들기
템플릿이란 서로 다른 정보를 일정한 형태로 표시하기 위해 재사용 가능한 파일을 말한다. (장고의 템플릿 양식은 HTML)
blog/templates/blog 디렉토리를 만들고, 디렉토리 내부에 html 파일 생성
하위 디렉토리를 만드는 것은 폴더가 구조적으로 복잡해졌을 때 찾기 쉽게 하기 위한 관습적 방법이다!
post_list.html 파일 생성 및 원하는 html 코드 추가
예시
```html
Django Girls blog
Django Girls Blog
published: 14.06.2014, 12:14
My first post
Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
published: 14.06.2014, 12:14
My second post
Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Donec id elit non mi porta gravida at eget metus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut f.
```
10. 모델로부터 템플릿에 동적으로 데이터 가져오기
뷰에서 모델 연결하기
blog/views.py 파일 수정
from django.shortcuts import render
from django.utils import timezone # 쿼리셋 동작을 위해 추가
from .models import Post # Post 모델을 사용하기 위해 import
def post_list(request):
# 쿼리셋 추가
posts = Post.objects.filter(published_date__lte=timezone.now()).order_by('published_date')
return render(request, 'blog/post_list.html', {'posts': posts}) # 'posts' 매개변수 추가
템플릿에서 템플릿 태그를 사용해 보여주기
blog/templates/blog/post_list.html 에서 템플릿 태그 사용
<div>
<h1><a href="/">Django Girls Blog</a></h1>
</div>
# 장고 템플릿에서의 루프 테크닉
{% for post in posts %}
<div>
<p>published: {{ post.published_date }}</p>
<h1><a href="">{{ post.title }}</a></h1>
<p>{{ post.text|linebreaksbr }}</p>
</div>
{% endfor %}
11. 간략하게 CSS 다루기
부트스트랩 설치하기
인터넷에 있는 파일을 연결하므로써 진행
html 파일 내 <head>에 아래 링크 추가
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css">
정적 파일 (static files)
css 파일과 이미지 파일이 해당
앱에 static 폴더를 추가하고 폴더 안에 정적 파일 저장 (장고는 static 폴더를 자동을 찾을 수 있음!)
blog 앱 안에 static 폴더 생성
djangogirls
├── blog
│ ├── migrations
│ ├── static
│ └── templates
└── mysite
static 폴더 내부에 css 폴더를 만들고, css 파일을 생성해 저장
blog/static/css/blog.css 파일 생성
djangogirls
└─── blog
└─── static
└─── css
└─── blog.css
blog.css 파일에 다음과 같은 예시 코드 추가
```css
.page-header {
background-color: #ff9400;
margin-top: 0;
padding: 20px 20px 20px 40px;
}
.page-header h1, .page-header h1 a, .page-header h1 a:visited, .page-header h1 a:active {
color: #ffffff;
font-size: 36pt;
text-decoration: none;
}
.content {
margin-left: 40px;
}
h1, h2, h3, h4 {
font-family: ‘Lobster’, cursive;
}
.date {
color: #828282;
}
.save {
float: right;
}
.post-form textarea, .post-form input {
width: 100%;
}
.top-menu, .top-menu:hover, .top-menu:visited {
color: #ffffff;
float: right;
font-size: 26pt;
margin-right: 20px;
}
.post {
margin-bottom: 70px;
}
.post h1 a, .post h1 a:visited {
color: #000000;
}
```
html 파일에서 정적 파일 로딩 및 css 파일 링크 추가
class를 추가하고 정적 파일을 로딩하여 css파일과 링크한 html 파일 예시
```django
{% raw %}
{% load static %} // 정적 파일 로딩
Django Girls blog
// css 파일 링크 추가
Django Girls Blog
{% for post in posts %}
published: {{ post.published_date }}
{{ post.title }}
{{ post.text|linebreaksbr }}
{% endfor %}
{% endraw %}
```
12. 장고 템플릿 확장하기
템플릿 확장은 웹사이트 안의 서로 다른 페이지에서 HTML의 일부를 동일하게 재사용 할 수 있게 하는 것을 말한다.
기본 템플릿 생성하기
blog/templates/blog/ 에 base.html 파일 생성
block 템플릿 태그를 적절히 삽입한 뼈대 html 코드 추가
post_list.html 파일의 전체 코드 중 <body> 태그 내용만 바꿔 다음과 같이 base.html에 코드 추가
```django
{% raw %}
{% load static %}
Django Girls blog
Django Girls Blog
{% block content %}
{% endblock %}
{% endraw %}
```
기본 템플릿과 확장 템플릿 연결하기
확장할 html 파일에는 블록에 대한 템플릿의 일부만 남김
block 템플릿 태그 추가
확장 태그를 파일 맨 앞에 추가
blog/templates/blog/post_list.html을 다음 코드로 변경
{% raw %}
{% extends 'blog/base.html' %}
{% block content %}
{% for post in posts %}
<div class="post">
<div class="date">
{{ post.published_date }}
</div>
<h1><a href="">{{ post.title }}</a></h1>
<p>{{ post.text|linebreaksbr }}</p>
</div>
{% endfor %}
{% endblock %}
{% endraw %}
Reference
가상환경
장고걸스 튜토리얼
Touch background to close