Django Coding Style

Django 공식 문서의 “Django Coding Style” 문서를 번역한 글

Django에서 권장하는 코딩 스타일 가이드를 정리해보았다. (부끄러운 얘기지만 1년넘게 Django를 사용하면서 이 문서 전체를 읽어본 적은 없었다.)

코딩 스타일을 지키는 것이 중요한 가장 큰 이유다른 사람(혹은 미래의 나)과의 협업에 용이하기 때문이다. 공통 Rule이 있기 때문에 불필요한 논쟁을 줄여주고, 코드가 명확한 의미를 표현하도록 권장하므로 다른 사람의 코드를 읽고, 유지 보수하기도 쉽다.

정리하면서 “왜 이렇게 정했을까?”를 질문하고 고민해보고 찾아보니 대부분 이유가 있었다. 이 글을 읽는 분들도 각 항목에 대해 이유를 생각하면서 읽어보면 더 재미있을 것이다.

목차

1. Python Style
2. Imports
3. Template
4. View
5. Model
6. django.conf.settings 사용
7. 기타

1. Python Style

1-1. PEP 8

  • 웬만하면 PEP8을 따름
  • Flake8 Plugin 사용 (setup.cfg)
  • 한 줄에 허용되는 글자 수는 예외를 둠
    • 코드 : 119자 (GitHub Code Review의 너비)
    • 주석, Docstrings : 79자

1-2. Indentation (들여쓰기)

  • EditorConfig를 지원하는 Editor를 사용 (.editorconfig에 들여쓰기 규칙을 명세)
  • 공백 수
    • Python : 공백 4개
    • HTML : 공백 2개
  • 코드 라인이 길어질 경우, 수직 정렬 대신 공백 4개로 들여쓰기
    • 장점 : 첫 번째 줄의 길이가 변경되어도 아래의 문자열을 다시 정렬할 필요가 없음
      1
      2
      3
      4
      5
      6
      7
      8
      9
      # NO
      raise AttributeError('Here is a multine error message ' # 이 줄의 길이가 변경되면 아래 줄을 다시 정렬해야 함
      'shortened for clarity.')

      # YES
      raise AttributeError(
      'Here is a multine error message '
      'shortened for clarity.'
      )

1-3. Quote (따옴표)

  • 단따옴표(') 사용
  • 문자열이 단따옴표를 포함하는 경우에만 쌍따옴표() 사용

1-4. Comment (주석)

  • 주석에는 “우리가(We)” 라는 말은 쓰지 않음 (영미권)

    1
    2
    3
    4
    5
    # NO
    # We loop over

    # YES
    # Loop over

1-5. Naming (이름짓기)

  • Variable, Function, Method의 이름은 underscore로 작성 (camelCase X)

    1
    2
    3
    4
    5
    6
    7
    # NO
    def getUniqueVoters():
    pass

    # YES
    def get_unique_voters():
    pass
  • Class 혹은 Class를 반환하는 Factory Function의 이름은 첫 글자를 대문자로 함 (InitialCaps)

    1
    2
    3
    4
    5
    6
    7
    # NO
    class blog_writer:
    pass

    # YES
    class BlogWriter:
    pass

1-6. DocString

1-7. Test

  • assertRaises() 보다 assertRaisesMessage()를 사용. (예외 메시지를 확인)
    • Regular Expression Matching이 필요한 경우에는 assertRaisesRegex() 사용
  • Docstring에는 명확하게 예상되는 행동을 적음
    • 예) 사용자 추가 후 비밀번호 일치 확인
    • “이러이러한 Test”, “이러이러한 것을 보증하는” 등의 서문 금지
    • 명확한 표현이 어려울 때는 Issue Number로 대체
      1
      2
      3
      4
      5
      def test_new_user_same_pw():
      """
      사용자 추가 후 비밀번호 일치 확인 (#SYS-123).
      """
      ...

2. Imports

2-1. 자동 Sorting

  • isort와 같은 plugin을 사용
  • 주석으로 skip 가능
    1
    import module  # isort:skip

2-2. 작성 순서

  • Import Group 작성 순서
    1. Standard libraries
    2. Third-party libraries
    3. Django components : Absolute imports
    4. Local Django components : (Explicit) Relative imports
    5. try/excepts
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    # future
    from __future__ import unicode_literals

    # 1. Standard libraries
    import json
    from itertools import chain

    # 2. Third-party libraries
    import bcrypt

    # 3. Django components
    from django.http import Http404
    from django.http.response import (
    Http404, HttpResponse, HttpResponseNotAllowed, StreamingHttpResponse,
    cookie,
    )

    # 4. Local Django components
    from .models import LogEntry

    # 5. try/excepts
    try:
    import yaml
    except ImportError:
    yaml = None

    CONSTANT = 'foo'


    class Example(object):
    # ...
  • Object Import 전에 Module을 Import

    1
    2
    import module 
    from module import object
  • 각 행은 Alphabet 순으로, 대문자가 먼저 오도록 정렬

2-3. Indentation / Blank Line (들여쓰기 / 줄 공백)

Indentation

  • 줄이 길어지면 Line Breaking 후 들여쓰기
  • 마지막 Import뒤에도 Comma는 포함
  • 마지막 닫는 괄호가 있는 Line에는 괄호만 존재하도록
    1
    2
    3
    4
    from django.http.response import (
    Http404, HttpResponse, HttpResponseNotAllowed, StreamingHttpResponse, # Line Breaking 후 공백 4개로 들여쓰기
    cookie, # 마지막 Import 뒤에도 Comma 포함
    ) # 닫는 괄호가 있는 줄에는 괄호만 존재

Blank Line

  • 1줄 공백 : Import Group
  • 2줄 공백 : 마지막 Import와 첫 Function/Class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import requests
    import json
    # --------- 1줄 공백
    from django.http.response import (
    Http404, HttpResponse, HttpResponseNotAllowed, StreamingHttpResponse,
    cookie,
    )
    # --------- 2줄 공백
    # --------- 2줄 공백
    class Example:
    pass

3. Template

3-1. Spaces

  • 중괄호와 Tag Content 사이에는 1개 Space 만 둠
    1
    2
    3
    4
    5
    6
    <!-- NO -->
    {{var}}
    {{ var }}

    <!-- YES -->
    {{ var }}

4. View

4-1. Request Parameter

  • View Function의 첫 번째 Parameter의 이름은 request를 권장
    1
    2
    3
    4
    5
    6
    7
    # NO
    def my_view(req, foo):
    pass

    # YES
    def my_view(request, foo):
    pass

5. Model

5-1. Naming (이름 짓기)

  • Field 이름은 underscore 로 지음
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # NO
    class Person(models.Model):
    FirstName = models.CharField(max_length=20)
    Last_Name = models.CharField(max_length=40)

    # YES
    class Person(models.Model):
    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=40)

5-2. 순서

  • Model의 Inner Class와 Standard Method의 배치 순서
    1. All database fields
    2. Custom manager attributes
    3. class Meta
    4. def __str__()
    5. def save()
    6. def get_absolute_url()
    7. Any custom methods
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # "class Meta" 배치 예시
    # NO
    class Person(models.Model):
    class Meta:
    verbose_name_plural = 'people'

    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=40)

    # YES
    class Person(models.Model):
    first_name = models.CharField(max_length=20)
    last_name = models.CharField(max_length=40)

    class Meta:
    verbose_name_plural = 'people'

5-3. Choices

  • Model의 Field에 choices가 있는 경우, Model Class의 Attribute로 대문자 이름의(all-uppercase) tuple의 tuple로 정의
    1
    2
    3
    4
    5
    6
    7
    class MyModel(models.Model):
    DIRECTION_UP = 'U'
    DIRECTION_DOWN = 'D'
    DIRECTION_CHOICES = (
    (DIRECTION_UP, 'Up'),
    (DIRECTION_DOWN, 'Down'),
    )

6. django.conf.settings 사용

  • django.conf.settings에 저장된 settings를 모듈의 Top Level(모듈이 import될 때 실행됨)에서 사용하지 않음. (이유는 아래의 설명을 참고.)

    • 아래의 코드는 수동으로 settings 설정하는 코드임. (DJANGO_SETTINGS_MODULE에 의존하지 않는)

      1
      2
      3
      from django.conf import settings

      settings.configure({}, SOME_SETTING='foo')
    • settings.configure Line 이전에 settings에 접근한다면 위 코드는 작동하지 않음 (settings는 LazyObject)

    • 따라서 아래와 같은 모듈이 있다면, 이 모듈이 import될 때 settings가 구성되어 버림. 대신에 django.utils.functional.LazyObject, django.utils.functional.lazy(), lambda를 사용
      1
      2
      3
      4
      from django.conf import settings
      from django.urls import get_callable

      default_foo_view = get_callable(settings.FOO_VIEW)

7. 기타

  • 국제화를 위해 모든 String을 (Translatable String으로) 마킹 (참고)
  • 코드 변경 시, 더 이상 사용되지 않는 Import문 제거
    • flake8 사용. (남아 있어야 하는 경우 Import문의 끝에 #NOQA 표시)
  • 마지막에 붙는 공백 제거
    • 시각적으로 방해되거나 코드 병합 시 충돌이 발생할 수 있음
    • IDE에서 자동으로 제거하도록 설정할 것
  • Contribute 할 때 코드에 본인의 이름을 넣지 말 것. AUTHORS 파일에 추가할 것