如何想使用库那样自由的使用Django

文章首发于【Python七号】欢迎订阅。

原文链接

先问你个问题,框架和库有什么区别?

简单的说,框架控制你,库则由你控制,框架让你做填空题,库让你做问答题。

初学 Django,你觉得它是框架,用的久了,你也可以像三方库一样使用。

Django 之于 Python,犹如 Spring 之于 Java。Django 是 Python 的 web 开发框架,既然是框架,就是一套完整的解决方案,使用框架的时候,需要把你的代码放到框架合适的地方,框架会在合适的时机调用你的代码,框架控制一切,我们只需要按照规则写代码。

如果只是用 Django 进行 Web 开发,直接填空就好了。但是如果只想使用 Django 的部分功能,比如 Django 的 ORM、发送邮件、模版渲染,就像使用三方库那样,直接导入 Django 相关的包来使用呢?

为什么我会提出这个问题?

一是因为 Django 的 ORM 足够简单和好用,二是我懒得学习其他 ORM 框架,原理大同小异,我先入手的 Django,就想一直用 Django。说多点,我倾向通用的技术,也就是一招武功走天下。Django 的 ORM 有多好用,这里举个例子,User 对象对应数据库的一张表,操作 User,就是操作数据库,完全不用写 sql:

# 获取数据
from .models import User
User.objects.all()
User.objects.count()
User.objects.filter(name='somenzz').count()
# 匹配,对应SQL:select * from User where name = 'somenzz'
User.objects.filter(name='somenzz')
# 不匹配,对应SQL:select * from User where name != 'somenzz'
User.objects.exclude(name='somenzz')
# 获取单条数据
user_123 = User.objects.get(id=123)
# 修改数据
user_123.age += 1
user_123.save()

以上这些代码通常会在 Django 给你生成好的视图文件,比如 views.py 里出现,如果单独写一个文件,如 orm_demo.py,把上述代码贴过来,然后执行 python orm_demo.py 就会报错,下面带你顺藤摸瓜来解决这个问题。

Django 开发环境的启动往往是 python manage.py + 命令 来实现,比如常见的 python manage.py runserver, python manage.py shell。关键就在于 manage.py 文件,让我们来看一下 manage.py 的内容:

版本 Django==3.1

#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys


def main():
    """Run administrative tasks."""
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)

if __name__ == '__main__':
    main()

可以看到,使用 Django 的第一步需要指定 Django 的配置文件,这是必须的,不然 Django 怎么知道如何连接数据库呢,因此需要在我们的代码中加入

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_project.settings')

如果你配置文件不在 django_project 同级的目录,请使用 sys.path.append 来添加,确保 Django 的配置文件 setting.py 可以被导入。

Django 官网也提到,不使用配置文件也是可以的,可以在代码中使用 settings.configure 来使用配置 Django,比如:

from django.conf import settings
settings.configure(DEBUG=True)

接下来我们跳转至函数 execute_from_command_line(sys.argv) 看看 Django 做了什么:

def execute_from_command_line(argv=None):
    """Run a ManagementUtility."""
    utility = ManagementUtility(argv)
    utility.execute()

继续看 execute 函数的主要代码:

if settings.configured:
    # Start the auto-reloading dev server even if the code is broken.
    # The hardcoded condition is a code smell but we can't rely on a
    # flag on the command class because we haven't located it yet.
    if subcommand == 'runserver' and '--noreload' not in self.argv:
        try:
            autoreload.check_errors(django.setup)()
        except Exception:
            # The exception will be raised later in the child process
            # started by the autoreloader. Pretend it didn't happen by
            # loading an empty list of applications.
            apps.all_models = defaultdict(dict)
            apps.app_configs = {}
            apps.apps_ready = apps.models_ready = apps.ready = True

            # Remove options not compatible with the built-in runserver
            # (e.g. options for the contrib.staticfiles' runserver).
            # Changes here require manually testing as described in
            # #27522.
            _parser = self.fetch_command('runserver').create_parser('django', 'runserver')
            _options, _args = _parser.parse_known_args(self.argv[2:])
            for _arg in _args:
                self.argv.remove(_arg)

    # In all other cases, django.setup() is required to succeed.
    else:
        django.setup()

self.autocomplete()

会发现有个django.setup() 会在 settings.configured 为真的情况下调用,至此已经真相大白。如果要想独立使用 Django,有两点是需要做的,一是配置 Django,二是调用执行 django.setup() 。setup 的作用就是加载设置并填充 Django 的应用程序注册表。setup 函数的代码如下:

def setup(set_prefix=True):
    """
    Configure the settings (this happens as a side effect of accessing the
    first setting), configure logging and populate the app registry.
    Set the thread-local urlresolvers script prefix if `set_prefix` is True.
    """
    from django.apps import apps
    from django.conf import settings
    from django.urls import set_script_prefix
    from django.utils.log import configure_logging

    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)
    if set_prefix:
        set_script_prefix(
            '/' if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME
        )
    apps.populate(settings.INSTALLED_APPS)

因此,即使 Django 不作为 Web 开发的框架,也可以作为实用工具库来使用,例如,编写一个 Python 脚本来加载一些 Django 模板并进行渲染,或者使用 ORM 来获取某些数据。一种优雅的独立使用 Django 的方式如下:

import django
from django.conf import settings
settings.configure(
    INSTALLED_APPS=["django_app.apps.DjangoAppConfig"],
    DATABASES={
        "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db.sqlite3",}
    },
)
if __name__ == "__main__":
    django.setup()
    from django_app.models import CrawlerMonitor, DownloadedDocs
    crawler_monitor = CrawlerMonitor.objects.get(id=1)
    print(crawler_monitor.web_site)

请注意 django.setup() 仅当您的代码真正独立时才需要调用,因此,避免将可重用的应用程序逻辑放在独立的脚本中,如果实在无法避免的话,你可以这样做:

if __name__ == '__main__':
    import django
    django.setup()

附上其他 Django ORM 操作:

# 小于等于,<=,对应SQL:select * from User where id <= 724
User.objects.filter(id__lte=724)
# 同时大于和小于, 1 < id < 10,对应SQL:select * from User where id > 1 and id < 10
User.objects.filter(id__gt=1, id__lt=10)
# 包含,in,对应SQL:select * from User where id in (11,22,33)
User.objects.filter(id__in=[11, 22, 33])
# 为空:isnull=True,对应SQL:select * from User where pub_date is null
User.objects.filter(pub_date__isnull=True)

# 不匹配,大小写敏感,对应SQL:select * from User where name not like '%sre%',SQL中大小写不敏感
User.objects.exclude(name__contains="sre")

# 不匹配,大小写不敏感,对应SQL:select * from User where name not like '%sre%',SQL中大小写不敏感
User.objects.exclude(name__icontains="sre")

# 范围,between and,对应SQL:select * from User where id between 3 and 8
User.objects.filter(id__range=[3, 8])

# 以什么开头,大小写敏感,对应SQL:select * from User where name like 'sh%'
User.objects.filter(name__startswith='sre')

# 以什么开头,大小写不敏感,对应SQL:select * from User where name like 'sh%'
User.objects.filter(name__istartswith='sre')

# 排序,order by,正序,对应SQL:select * from User where name = 'somenzz' order by id
User.objects.filter(name='somenzz').order_by('id')

# 多级排序,order by,先按name进行正序排列,如果name一致则再按照id倒叙排列
User.objects.filter(name='somenzz').order_by('name','-id')

# 排序,order by,倒序,对应SQL:select * from User where name = 'somenzz' order by id desc
User.objects.filter(name='somenzz').order_by('-id')

# limit,对应SQL:select * from User limit 3;
User.objects.all()[:3]

# offset,取出结果的第10-20条数据(不包含10,包含20),也没有对应SQL,参考上边的SQL写法
User.objects.all()[10:20]

# 分组,group by,对应SQL:select username,count(1) from User group by username;
from django.db.models import Count
User.objects.values_list('username').annotate(Count('id'))

# 去重distinct,对应SQL:select distinct(username) from User
User.objects.values('username').distinct().count()

# filter多列、查询多列,对应SQL:select username,fullname from accounts_user
User.objects.values_list('username', 'fullname')

# filter单列、查询单列,正常values_list给出的结果是个列表,里边里边的每条数据对应一个元组,当只查询一列时,可以使用flat标签去掉元组,将每条数据的结果以字符串的形式存储在列表中,从而避免解析元组的麻烦
User.objects.values_list('username', flat=True)

# int字段取最大值、最小值、综合、平均数
from django.db.models import Sum,Count,Max,Min,Avg
User.objects.aggregate(Count('id'))
User.objects.aggregate(Sum('age'))