django不像java框架那样有诸多配置文件,django更依赖于约定 ,比如,

  • models.py,存放数据库相关model
  • tests.py,存放单元测试代码

基于这种简单约定,创建一个django项目后几乎马上就可以从命令行启动。但将所有model存放于一个models.py文件中并不是一个特别好的选择,随着项目代码的不断增加,models.py很容易就会膨胀。相较而言,更喜欢java的一个class一个文件的做法,项目结构目录清晰,方便阅读查找。

拆分models.py

定义model,增加app label

django支持将models存放于单个文件中,在django app中创建models目录,在models中创建不同文件来存放model,比如,

  • app/models/post.py
  • app/models/category.py

在model定义的时候需要增加指明其从属的app,

class Category(models.Model):
  name = models.CharField(max_length=100, db_index=True)
  created_time = models.DateTimeField(auto_now_add=True, db_index=True)

  def __unicode__(self):
      return self.name

  class Meta:
      app_label = 'blog'

在定义完上述model之后,django还不能找到这个model,还需要进一步操作。

修改models/init.py

__init__.py的作用是告诉python当前目录是一个python package。django默认从models里面读取定义的各model,因此在__init__.py中我们需要import这些model,这样django就能依照其约定读取到正确的代码。

手动import

在__init__.py中可以手动import定义的各model,

from blog.models.post import Post
from blog.models.category import Category

自动import

手动import繁琐且容易遗忘,所以我们得寻找自动import model的方法。借助python的importlib,我们可以做到这一点。在models/init.py中添加如下代码,

class_list = fetch_all_classes(settings.HOME_DIR, os.path.join(settings.HOME_DIR, 'blog/models/'))

current_global = globals()
for clazz in class_list:
    current_global[clazz.__name__] = clazz

上述代码片段中的fetch_all_classes函数用于寻找指定目录下的所有python class。其代码也并不长,

import importlib
import inspect
import re
import os

def list_py_files(source_folder):
    paths = []
    for base, folders, files in os.walk(source_folder):
        for py_file in files:
            if not py_file.endswith('.py'):
                continue
            path = os.path.join(base, py_file)
            paths.append(path)
    return paths

def build_relative_path(root_path, absolute_path_list):
    return [path[len(root_path):] for path in absolute_path_list]

def build_import_name(path):
    if path.startswith('/'):
        path = path[1:-3]
    return re.sub('/', '.', path)

def get_module_classes(module_instance):
    class_list = filter(lambda instance: inspect.isclass(instance), module_instance.__dict__.values())
    return {clazz.__name__: clazz for clazz in class_list}

def fetch_all_classes(root_path, source_folder):
    absolute_path_list = list_py_files(source_folder)
    relative_path_list = build_relative_path(root_path, absolute_path_list)
    clazz_dict = {}
    for path in relative_path_list:
        module_name = build_import_name(path)
        instance = importlib.import_module(module_name)
        clazz_dict.update(get_module_classes(instance))
    return clazz_dict.values()

完成上述两步之后,在models目录中就可以在任意文件中定义model了。

拆分tests.py

接着来分拆单元测试代码,思路与拆分models.py一样。只要让django在tests中找到所有的testcase就可以了。在tests/__init__.py中添加如下代码,

import os

from solilo.commons.importer import fetch_all_classes
import settings

class_list = fetch_all_classes(settings.HOME_DIR, os.path.join(settings.HOME_DIR, 'blog/tests/'))

current_global = globals()
for clazz in class_list:
    current_global[clazz.__name__] = clazz

分拆之后整个项目又有点接近java项目文件的繁杂,但至少个人看来结构清晰了一些,一定程度上提升了项目的可维护性。