Python后端:Django框架

Django学习笔记

参考书籍:《Django 3 By Example Build powerful and reliable Python web applications from scratch》
Django 基础教程 by Leif Azzopardi David Maxwell (z-lib.org)(中译版)

为什么要学Django?

Django是一个强大的Python Web开发的后端框架,优势在于简单,简洁,适合任何人上手,随着你深入学习Django,你会对此深有体会


下面,我们开始学习,记得坚持

第一个Django项目:startproject

使用Django我们需要先下载并配置python

参考:
https://zhuanlan.zhihu.com/p/344887837
https://blog.csdn.net/qq_44214671/article/details/113469811

下载好后,建好项目的文件夹,在该文件夹终端输入:
(注意Python版本和Django版本要兼容)

1
pip install django

并通过

1
2
3
4
5
6
(base) (Django_environment) PS D:\DjangoProject\Django_environment\Scripts> python
Python 3.11.8 | packaged by Anaconda, Inc. | (main, Feb 26 2024, 21:34:05) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import django
>>> django.get_version()
'4.2.7'

来检查django是否安装好,可以看到我的环境是我的环境是python3.11+django4.2.7
选择安装内容:虚拟环境
虚拟环境可以帮助我们进行版本管理与维护,不需要的可以跳过这段
通过

1
python3.11 venv -m Django_environment(虚拟环境文件夹名称,其他也可以)

创建虚拟环境后,我们可以通过激活该环境来使用其中的 Python 版本和包。在不同的操作系统上,激活虚拟环境的命令也略有不同。在 Windows 上,可以运行以下命令:

1
Django_environment(刚才创建的文件夹)\Scripts\activate

这里也可以使用virtualenv,具体参考https://www.simononsoftware.com/virtualenv-tutorial/

这样我们就进入了虚拟环境

创建项目

在项目所在文件夹,输入

1
django-admin startproject tango_with_django_project

建立了第一个文件,django-admin startproject是创建Django项目的命令,后面的是项目名称
创建完后,项目目录中有个名为 manage.py 的文件,在开发过程中时常用到。它提供了一系列维护 Django 项目的命令,例如通过它可以运行内置的 Django 开发服务器,可以测试应用,还可以运行多个数据库命令。几乎每个 Django 命令都要调用这个脚本。
在另一个文件夹(即项目代码文件夹)下,有这么几个文件:

  • init.py:一个空 Python 脚本,存在的目的是告诉 Python 解释器,这个目录是一个Python包
  • settings.py:存放 Django 项目的所有配置
  • urls.py:存放项目的 URL 模式
  • wsgi.py:用于运行开发服务器和把项目部署到生产环境的一个 Python 脚本。
    执行指令,来看一下本地的Django轻量级服务器是什么样的
    1
    python manage.py runserver

    这里你可能会遇到报错,但没关系,跟着提示的指令,运行它提供的命令,从而应用迁移(跟数据库有关,这里我们暂且不关注)

python manage.py migration
点开返回的地址+端口
看到小火箭了吗?
你也可以通过一些指令使同一局域网或者远程的同伴一起来观看

startapp

运行命令

1
python manage.py startapp rango

startapp 目录在项目的根目录(也就是manage.py所在的目录)中创建一个新目录,这个app名为 rango,其中包含一些 Python 脚本

  • init.py:与前面那个的作用完全一样
  • admin.py:注册模型,让 Django 为你创建管理界面
  • apps.py:当前应用的配置
  • models.py:存放应用的数据模型,即数据的实体及其之间的关系
  • tests.py:存放测试应用代码的函数
  • views.py:存放处理请求并返回响应的函数
  • migrations 目录:存放与模型有关的数据库信息
    views.py 和 models.py 是任何应用中都有的两个文件,是 Django 所采用的设计模式”模型-视图-模板“的主要部分。

为了让Django项目知道这个app的存在,这里在startproject命令创造的那个子文件夹的setting.py中修改

1
2
3
4
5
6
7
8
9
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rango',#添加内容,告诉框架我们新添加了这个应用
]

当然,Django是个非常人性化的框架,有时候也会自动追加到INSTALLED_APPS中,这时候就不必手动了

编写视图

视图有模型和模板,这里我们先尝试用视图返回一些简单文本

1
2
3
4
5
6
#edit in rango/views.py
def index(request):
return HttpResponse("Rango says hey there partner! <br/> <a href='/rango/about'>About</a>")

def about(request):
return HttpResponse("Rango says here is the about page. <br/> <a href='/rango/'>Index</a>")

这里index,about两个函数分别返回两个HttpResponse对象,里面包含一个指向某一页面的链接
那么,如何实现文本返回呢?或者说如何触发他们?(类似于Qt的信号槽和JS的Listener)

1
2
3
4
5
6
7
8
9
10
#edit in project/urls.py
from django.urls import include, path
from django.contrib import admin
from rango import views
urlpatterns = [
#将以rango/ 为开头的url交给rango应用处理
path('rango/', include('rango.urls')),
path("admin/", admin.site.urls),
]

在rango

1
2
3
4
5
6
7
8
9
#edit in rango/urls.py
from django.urls import path
from rango import views

urlpatterns = [
#只要匹配到"rango/ "空字符串,就会调用views.index()视图函数
path('', views.index, name='index'),
path('about/', views.about, name='about'),
]

这两段代码实现了这一功能,包括:

  • 利用url统一资源定位符进行视图管理,在第一段代码中,我们用urlpatterns中的path约束了admin开头的,跳转到admin.site.urls;而rango开头的统一由rango.urls管理(即是include(‘rango.urls’)**)
  • 触发views.py视图中的函数,第二段代码中以rango/为结尾的调用views.index函数,以rango/about结尾的调用views.about函数

理解Django基本的运行流程

上面我们提到了,Django设计模式是”模型-视图-模板”,具体来说,它们三兄弟的天赋点各不相同:

  • 模型:定义数据的单元,位于models.py
  • 视图:实现前后端互动响应,同时生成并传递上下文字典,位于views.py
  • 模板:即前端代码,在Django前期学习中一般都是html代码,位于/project_name/template/app_name目录中(这里project_name和app_name都是指具体的项目和应用名)

这三兄弟联动,其实就可以抽象成后端-前后端交互-前端的框架,当然实际上后端还要和数据库交互,不过在Django中这不是我们想关注的细节

实例讲解

定义模型以及数据

说了这么多,我们用具体的例子来讲解一下,理解之后你会发现,其实Django前期的很多东西都是这个思路,其他诸如相对url,正则表达式,模板标签其实需要的时候problem-driven learning就可以
好,接下来我们定义两个模型,用于我们元数据的定义
@rango/models.py

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
from django.db import models
from django.template.defaultfilters import slugify
# Create your models here.
#分类模型
class Category(models.Model):
name=models.CharField(max_length=128,unique=True)
views=models.IntegerField(default=0)
likes=models.IntegerField(default=0)
slug = models.SlugField(default='',blank=True)
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super(Category, self).save(*args, **kwargs)
class Meta:
verbose_name_plural='Categories'

def __str__(self):
return self.name
#ForeignKey建立一对多关系 字段需要一个 on_delete 参数,
# 这是 Django 2.0 以后的要求。on_delete
# 参数用于指定当关联的 Category 对象被删除时,应该如何处理关联的 Page 对象
class Page(models.Model):
category=models.ForeignKey(Category,models.CASCADE)
title=models.CharField(max_length=128)
url=models.URLField()
views=models.IntegerField(default=0)

def __str__(self):
return self.title

这里我们可以先把一对多,super,Meta之类可能看起来一头雾水的东西放在一边,关注数据本身,其中Category有name,views,likes,slug几种属性,Page有title,url,views几种属性,同时我们通过ForeignKey实现了一个Category对应多个Pages(但这里还没指定谁对应谁,后面会实现)

导入数据

在项目根目录,创立一个名为populate_rango.py的文件

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tango_with_django_project.settings')

import django
django.setup()
from rango.models import Category, Page

def populate():
# 首先创建一些字典,列出想添加到各分类的网页
# 然后创建一个嵌套字典,设置各分类
# 这么做看起来不易理解,但是便于迭代,方便为模型添加数据
python_pages = [
{"title": "Official Python Tutorial",
"url":"http://docs.python.org/2/tutorial/"},
{"title":"How to Think like a Computer Scientist",
"url":"http://www.greenteapress.com/thinkpython/"},
{"title":"Learn Python in 10 Minutes",
"url":"http://www.korokithakis.net/tutorials/python/"} ]
django_pages = [
{"title":"Official Django Tutorial",
"url":"https://docs.djangoproject.com/en/1.9/intro/tutorial01/"},
{"title":"Django Rocks",
"url":"http://www.djangorocks.com/"},
{"title":"How to Tango with Django",
"url":"http://www.tangowithdjango.com/"} ]
other_pages = [
{"title":"Bottle",
"url":"http://bottlepy.org/docs/dev/"},
{"title":"Flask",
"url":"http://flask.pocoo.org"} ]


cats = {
"Python": {"pages": python_pages
,"views":128,"likes":64},
"Django": {"pages": django_pages
,"views":64,"likes":32},
"Other Frameworks": {"pages": other_pages
,"views":32,"likes":6}
}

for cat, cat_data in cats.items():
c = add_cat(cat,cat_data["views"],cat_data["likes"])
for p in cat_data["pages"]:
add_page(c, p["title"], p["url"])
#打印添加的分类
for c in Category.objects.all():
for p in Page.objects.filter(category=c):
print("- {0} - {1}".format(str(c), str(p)))

def add_page(cat, title, url, views=0):
p = Page.objects.get_or_create(category=cat, title=title)[0]
p.url=url
p.views=views
p.save() #p.save():这行代码将 Page 对象保存到数据库。
return p

def add_cat(name,views,likes):
c = Category.objects.get_or_create(name=name,views=views,likes=likes)[0]
c.save()
return c

# 从这开始执行
if __name__ == '__main__':
print("Starting Rango population script...")
populate()

利用Model.object.get_or_create()函数,通过循环遍历add_cat,add_page创建,通过save函数导入数据库,在之后我们通过调用模型来调用其背后数据库中的一个个Category/Page了

前端

html如果没学的可以在网上随便找个教程,学习模式与markdown一样,大概花个一两个小时大概了解一下,边学边敲点例子就可以了,这里我们创建一个主页

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
32
<!DOCTYPE html>
<html>

<head>
<title>Rango</title>
</head>

<body>
<h1>Rango says...</h1>

<div>
hey there partner!
<br />
</div>

<div>
<h2>Category</h2>
{% if categories %}
<ul>
{% for category in categories %}
<li>
<a href="{% url 'show_category' category.slug %}">{{ category.name }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<strong>There are no categories present.</strong>
{% endif %}
</div>

</body>
</html>

这里我们前端想呈现出各个Category的名字,把鼠标放在上面还可以跳转至某个地方(也就是该category的域名)
仔细想想,我们有哪些需要解决?

  1. 这里{}中间包着的% %是什么?
  2. 我们如何获取到category的数据?我们又是从哪里获取到代码中categories的信息的?
  3. 我们如何跳转到”url ‘show_category’ category.slug”这个地方,这里具体是指什么,category.slug是作为变量吗?用来干嘛的?
    通过回答这几个问题,我们就可以大概理解前端-前后端交互-后端的流程了

第一个问题

实际上,在Django中,Django模板系统的语言嵌入到了html中,感兴趣的可以阅读
https://docs.djangoproject.com/zh-hans/5.0/ref/templates/language/

第二个问题

既然是前端-前后端交互-后端,必然要层层递进获取数据,这里我们通过urls.py和views.py来实现前后端交互
@urls.py

1
2
3
4
5
6
7
8
9
10
11
12
from django.urls import path
from django.urls import re_path #用于正则表达式
from rango import views
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
#只要匹配到"rango/ "空字符串,就会调用views.index()视图函数
path('', views.index, name='index'),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

@views.py

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from django.shortcuts import render
from django.http import HttpResponse
from rango.models import Category
from rango.models import Page

def index(request):
# 查询数据库,获取目前存储的所有分类
# 按点赞次数倒序排列分类
# 获取前 5 个分类(如果分类数少于 5 个,那就获取全部)
# 把分类列表放入 context_dict 字典
# 稍后传给模板引擎
category_list = Category.objects.order_by('-likes')[:5]
pages_list = Page.objects.order_by('-views')[:5]
context_dict = {'categories': category_list,
'most_liked_category':category_list.first(),
'most_viewd_pages':pages_list}
# 渲染响应,发给客户端
return render(request, 'rango/index.html', context_dict)

def about(request):
#打印请求方法,GET/POST
print(request.method)
#打印用户名
print(request.user)
return render(request, "rango/about.html",{})

def show_category(request, category_name_slug):
# 创建上下文字典,稍后传给模板渲染引擎
context_dict = {}
try:
# 能通过传入的分类别名找到对应的分类吗?
# 如果找不到,.get() 方法抛出 DoesNotExist 异常
# 因此 .get() 方法返回一个模型实例或抛出异常
category = Category.objects.get(slug=category_name_slug)
# 检索关联的所有网页
# 注意,filter() 返回一个网页对象列表或空列表
pages = Page.objects.filter(category=category)
# 把得到的列表赋值给模板上下文中名为 pages 的键
context_dict['pages'] = pages
# 也把从数据库中获取的 category 对象添加到上下文字典中
# 我们将在模板中通过这个变量确认分类是否存在
context_dict['category'] = category
except Category.DoesNotExist:
# 没找到指定的分类时执行这里
# 什么也不做
# 模板会显示消息,指明分类不存在
context_dict['category'] = None
context_dict['pages'] = None
# 渲染响应,返回给客户端
return render(request, 'rango/category.html', context_dict)

@templates
我们需要在项目目录中建一个template文件夹并配置好它
settings.py 文件的顶部有个名为 BASE_DIR 的变量,它的值是 settings.py 文件所在目录的路径。这
里用到了 Python 的特殊属性 __file__,它的值是所在文件的绝对路径。调用 os.path.dirname()
的作用是获取 settings.py 文件所在目录的绝对路径,再调用一次 os.path.dirname() 又去掉一层,
因此 BASE_DIR 最终的值是 /tango_with_django_project/。如果你还不太理解这个过
程,可以把下面几行代码放到 settings.py 文件中:
print(file)
print(os.path.dirname(file))
print(os.path.dirname(os.path.dirname(file)))
有了 BASE_DIR 之后,我们便可以轻易引用 Django 项目中的文件和目录。我们可以定义一个名为
TEMPLATE_DIR 的变量,指向 templates 目录的位置。这里还要使用 os.path.join() 函数拼接多个
路径片段。TEMPLATE_DIR 变量的定义如下:
TEMPLATE_DIR = os.path.join(BASE_DIR, ‘templates’)
我们使用 os.path.join() 函数把 BASE_DIR 变量和 ‘templates’ 字符串拼接起来,得到
/tango_with_django_project/templates/。如此一来,我们便可以使用 TEMPLATE_DIR
变量替代前面在 TEMPLATES 中硬编码的路径。把 DIRS 键值对改成下面这样:
‘DIRS’: [TEMPLATE_DIR, ]
@templates/index.html

所以,我们根据个人爱好创立并编写index.html和about.html以及category.html
当html中存在变量时,通过__views__.py中的对应函数提供上下文字典,而上下文字典从数据库中的模型中调取,这样我们便实现了前后端的数据传递

第三个问题

在 Django 的模板中,% url ‘show_category’ category.slug % 是一个 URL 模板标签,它会生成一个 URL。这个 URL 是通过查找名为 ‘show_category’ 的 URL 模式并将 ‘category.slug’ 作为参数来生成的。

show_category 是在你的 urls.py 文件中定义的 URL 名称,它对应一个视图函数,这个视图函数负责处理请求并返回一个响应。当用户点击这个链接时,浏览器会向这个 URL 发送一个 GET 请求,然后 Django 会调用与这个 URL 关联的视图函数。

category.slug 是一个变量,它是 Category 对象的一个属性。在创建models时,初始函数会根据models的name来生成它的slug并保存。在这个上下文中,category 是一个 Category 对象,slug 是这个对象的一个字段,它通常包含一个用于 URL 的简短标签。在这个例子中,category.slug 的值会被插入到生成的 URL 中。

所以, url ‘show_category’ category.slug 、
这个模板标签会生成一个链接到特定分类页面的 URL,当用户点击这个链接时,他们会被带到这个分类的页面。
具体定义如下
@rango/models.py/class category

1
2
3
4
5
6
7
8
9
10
11
12
13
class Category(models.Model):
name=models.CharField(max_length=128,unique=True)
views=models.IntegerField(default=0)
likes=models.IntegerField(default=0)
slug = models.SlugField(default='',blank=True)
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super(Category, self).save(*args, **kwargs)
class Meta:
verbose_name_plural='Categories'

def __str__(self):
return self.name

@rango.urls.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.urls import path
from django.urls import re_path #用于正则表达式
from rango import views
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
#只要匹配到"rango/ "空字符串,就会调用views.index()视图函数
path('', views.index, name='index'),
path('about/', views.about, name='about'),
re_path(r'^category/(?P<category_name_slug>[\w\-]+)/$',
views.show_category, name='show_category'),
#(?P<category_name_slug>[\w\-]+)/$ 是一个正则表达式,
# 它会匹配 URL 中的一部分,并将其作为 category_name_slug 参数传递给 show_category 视图函数。
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

其中引入了正则表达式的概念,感兴趣的同学可以去了解一下