├── .DS_Store ├── .gitignore ├── README.md ├── aggregation ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── db.sqlite3 ├── demo ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── admin.cpython-38.pyc │ ├── models.cpython-38.pyc │ ├── urls.cpython-38.pyc │ └── views.cpython-38.pyc ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_age.py │ ├── 0003_files.py │ ├── 0004_auto_20200511_1314.py │ ├── 0005_auto_20200511_1317.py │ ├── 0006_delete_file.py │ ├── 0007_image.py │ ├── 0008_postqs.py │ ├── 0009_book.py │ ├── __init__.py │ └── __pycache__ │ │ ├── 0001_initial.cpython-38.pyc │ │ └── __init__.cpython-38.pyc ├── models.py ├── tests.py ├── urls.py └── views.py ├── djangoKnowledgeBase ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-38.pyc │ ├── settings.cpython-38.pyc │ ├── urls.cpython-38.pyc │ └── wsgi.cpython-38.pyc ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── env.json ├── manage.py ├── md ├── 001-前言.md ├── 010-reverse()路由解析.md ├── 020-redirect()视图重定向.md ├── 030-get_object_or_404()获取资源.md ├── 040-path()路径映射.md ├── 050-include()路径调度.md ├── 060-敏感信息保存与读取.md ├── 070-用UUID作为模型主键.md ├── 080-模型save()方法更新部分字段.md ├── 090-F函数更新数据.md ├── 100-ForeignKey关联不同模型.md ├── 110-@property与模型方法.md ├── 120-模型的抽象基类和多表继承.md ├── 130-User模型的扩展.md ├── 140-管理静态文件.md ├── 150-批量上传图片.md ├── 160-QuerySet进行查询.md ├── 170-Manager()自定义模型方法.md ├── 180-Migrations数据迁移.md ├── 190-类视图as_view()解析.md ├── 200-transaction事务.md ├── 210-Aggregation聚合.md ├── 220-Session会话.md ├── 230-Middleware中间件.md ├── 240-信号的运用.md ├── 250-Python装饰器入门:从理解到应用.md ├── 260-Python闭包概念入门.md ├── 270-Python生成器.md └── 280-Python黑魔法:元类与元编程.md ├── middleware ├── __init__.py ├── admin.py ├── apps.py ├── middlewares.py ├── migrations │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── mig ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_remove_pen_purchase_date.py │ ├── 0003_pen_purchase_date.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── mySignal ├── __init__.py ├── admin.py ├── apps.py ├── handlers.py ├── migrations │ └── __init__.py ├── models.py ├── signals.py ├── tests.py ├── urls.py └── views.py ├── requirements.txt ├── static ├── test.css └── test.js ├── templates ├── base.html ├── header.html ├── home.html ├── midware_demo.html ├── path.html ├── post_detail.html ├── uploads_images.html └── visits_count.html └── transanction_demo ├── __init__.py ├── admin.py ├── apps.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | 5 | *.log 6 | local_settings.py 7 | db.sqlite3-journal 8 | 9 | .env 10 | .venv 11 | env/ 12 | venv/ 13 | ENV/ 14 | env.bak/ 15 | venv.bak/ 16 | 17 | .idea/ 18 | collectstatic/ 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/badge/python-3.8-orange.svg)](https://www.python.org) 2 | [![](https://img.shields.io/badge/django-3.0.5-green.svg)](https://docs.djangoproject.com) 3 | [![](https://img.shields.io/badge/license-CC_BY_NC_4.0-000000.svg)](https://creativecommons.org/licenses/by-nc/4.0/) 4 | 5 | # Django知识库 6 | 7 | 这是关于 Django 框架的零散但有用知识点的 Handbook。 8 | 9 | 如果你在寻找一个完整的 Django 入门,请看我的**Django搭建个人博客**教程: 10 | 11 | - [GitHub](https://github.com/stacklens/django_blog_tutorial/tree/master/md) 12 | - [个人博客](https://www.dusaiphoto.com/article/detail/2/) 13 | 14 | > 注:两个版本是完全相同的。 15 | 16 | 然后,你可以快速读一遍此 Django知识库,以便大脑中对部分重要的工具有大致印象;也可以在需要的时候再来查阅。请按实际情况食用。 17 | 18 | 如果你觉得这个专题对你有帮助,请给个小小的 Star,让更多的人能看到它~(比心👏) 19 | 20 | ## 内容导航 21 | 22 | 👉👉👉[GitHub 传送门](md/)👈👈👈 23 | 24 | 👉👉👉[博客 传送门](https://www.dusaiphoto.com/article/detail/78/)👈👈👈 25 | 26 | 想寻找学伴互相交流学习的,可以加 **Django交流QQ群**:107143175,或者**微信公众号**。一个人学习难免走弯路,有热心人帮忙就不再寂寞了。 27 | 28 | ![](https://www.dusaiphoto.com/static/img/QR.jpg) 29 | 30 | ## 许可协议 31 | 32 | 《Django 知识库》(包括且不限于文章、代码等内容)遵守 **署名-非商业性使用 4.0 国际 (CC BY-NC 4.0) 协议**。协议内容如下。 33 | 34 | **您可以自由地:** 35 | 36 | - **共享** — 在任何媒介以任何形式复制、发行本作品。 37 | - **演绎** — 修改、转换或以本作品为基础进行创作。 38 | 39 | 只要你遵守许可协议条款,许可人就无法收回你的这些权利。 40 | 41 | **惟须遵守下列条件:** 42 | 43 | - **署名** — 您必须给出**适当的署名**,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示许可人为您或您的使用背书。 44 | - **非商业性使用** — 您不得将本作品用于**商业目的**。 45 | 46 | - **没有附加限制** — 您不得适用法律术语或者技术措施从而限制其他人做许可协议允许的事情。 47 | 48 | > 适当的署名:您必须提供创作者和署名者的姓名或名称、版权标识、许可协议标识、免责标识和作品链接。 49 | > 50 | > 商业目的:主要目的为获得商业优势或金钱回报。 -------------------------------------------------------------------------------- /aggregation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/aggregation/__init__.py -------------------------------------------------------------------------------- /aggregation/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Author, Publisher, Book, Store 3 | 4 | # Register your models here. 5 | admin.site.register(Author) 6 | admin.site.register(Publisher) 7 | admin.site.register(Book) 8 | admin.site.register(Store) -------------------------------------------------------------------------------- /aggregation/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AggregationConfig(AppConfig): 5 | name = 'aggregation' 6 | -------------------------------------------------------------------------------- /aggregation/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-12 06:08 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Author', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=100)), 20 | ('age', models.IntegerField()), 21 | ], 22 | ), 23 | migrations.CreateModel( 24 | name='Book', 25 | fields=[ 26 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 27 | ('name', models.CharField(max_length=300)), 28 | ('pages', models.IntegerField()), 29 | ('price', models.DecimalField(decimal_places=2, max_digits=10)), 30 | ('rating', models.FloatField()), 31 | ('pubdate', models.DateField()), 32 | ('authors', models.ManyToManyField(to='aggregation.Author')), 33 | ], 34 | ), 35 | migrations.CreateModel( 36 | name='Publisher', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('name', models.CharField(max_length=300)), 40 | ], 41 | ), 42 | migrations.CreateModel( 43 | name='Store', 44 | fields=[ 45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 46 | ('name', models.CharField(max_length=300)), 47 | ('books', models.ManyToManyField(to='aggregation.Book')), 48 | ], 49 | ), 50 | migrations.AddField( 51 | model_name='book', 52 | name='publisher', 53 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='aggregation.Publisher'), 54 | ), 55 | ] 56 | -------------------------------------------------------------------------------- /aggregation/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/aggregation/migrations/__init__.py -------------------------------------------------------------------------------- /aggregation/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.http import JsonResponse 3 | 4 | class Author(models.Model): 5 | name = models.CharField(max_length=100) 6 | age = models.IntegerField() 7 | 8 | def __str__(self): 9 | return self.name 10 | 11 | 12 | class Publisher(models.Model): 13 | name = models.CharField(max_length=300) 14 | 15 | def __str__(self): 16 | return self.name 17 | 18 | 19 | class Book(models.Model): 20 | name = models.CharField(max_length=300) 21 | pages = models.IntegerField() 22 | price = models.DecimalField(max_digits=10, decimal_places=2) 23 | rating = models.FloatField() 24 | authors = models.ManyToManyField(Author) 25 | publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) 26 | pubdate = models.DateField() 27 | 28 | def __str__(self): 29 | return self.name 30 | 31 | 32 | class Store(models.Model): 33 | name = models.CharField(max_length=300) 34 | books = models.ManyToManyField(Book) 35 | 36 | def __str__(self): 37 | return self.name 38 | -------------------------------------------------------------------------------- /aggregation/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /aggregation/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/db.sqlite3 -------------------------------------------------------------------------------- /demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/__init__.py -------------------------------------------------------------------------------- /demo/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /demo/__pycache__/admin.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/__pycache__/admin.cpython-38.pyc -------------------------------------------------------------------------------- /demo/__pycache__/models.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/__pycache__/models.cpython-38.pyc -------------------------------------------------------------------------------- /demo/__pycache__/urls.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/__pycache__/urls.cpython-38.pyc -------------------------------------------------------------------------------- /demo/__pycache__/views.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/__pycache__/views.cpython-38.pyc -------------------------------------------------------------------------------- /demo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Post, UUIDModel, Owner, Group, Person, Human, Baby, MyCar, MyUser, Age, Image, PostQS, Book 3 | 4 | from django.contrib.auth.admin import UserAdmin 5 | 6 | ADDITIONAL_FIELDS = ((None, {'fields': ('phone_number',)}),) 7 | 8 | 9 | class MyUserAdmin(UserAdmin): 10 | fieldsets = UserAdmin.fieldsets + ADDITIONAL_FIELDS 11 | add_fieldsets = UserAdmin.fieldsets + ADDITIONAL_FIELDS 12 | 13 | class ImageAdmin(admin.ModelAdmin): 14 | list_display = ('image', 'admin_image') 15 | 16 | 17 | admin.site.register(MyUser, MyUserAdmin) 18 | admin.site.register(Post) 19 | admin.site.register(UUIDModel) 20 | admin.site.register(Owner) 21 | admin.site.register(Group) 22 | admin.site.register(Person) 23 | admin.site.register(Human) 24 | admin.site.register(Baby) 25 | admin.site.register(MyCar) 26 | admin.site.register(Age) 27 | admin.site.register(Image, ImageAdmin) 28 | admin.site.register(PostQS) 29 | admin.site.register(Book) 30 | -------------------------------------------------------------------------------- /demo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DemoConfig(AppConfig): 5 | name = 'demo' 6 | -------------------------------------------------------------------------------- /demo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-06 07:22 2 | 3 | from django.conf import settings 4 | import django.contrib.auth.models 5 | import django.contrib.auth.validators 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | import django.utils.timezone 9 | import uuid 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | initial = True 15 | 16 | dependencies = [ 17 | ('auth', '0011_update_proxy_permissions'), 18 | ] 19 | 20 | operations = [ 21 | migrations.CreateModel( 22 | name='MyUser', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('password', models.CharField(max_length=128, verbose_name='password')), 26 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 27 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 28 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 29 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 30 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 31 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 32 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 33 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 34 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 35 | ('phone_number', models.CharField(max_length=20)), 36 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 37 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 38 | ], 39 | options={ 40 | 'verbose_name': 'user', 41 | 'verbose_name_plural': 'users', 42 | 'abstract': False, 43 | }, 44 | managers=[ 45 | ('objects', django.contrib.auth.models.UserManager()), 46 | ], 47 | ), 48 | migrations.CreateModel( 49 | name='Bird', 50 | fields=[ 51 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('name', models.CharField(max_length=100)), 53 | ('flying_height', models.IntegerField(default=2000)), 54 | ('age', models.TextField(default='5 years old')), 55 | ], 56 | options={ 57 | 'abstract': False, 58 | }, 59 | ), 60 | migrations.CreateModel( 61 | name='Car', 62 | fields=[ 63 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 64 | ('name', models.CharField(max_length=30)), 65 | ], 66 | ), 67 | migrations.CreateModel( 68 | name='Group', 69 | fields=[ 70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 71 | ('username', models.CharField(max_length=100)), 72 | ], 73 | ), 74 | migrations.CreateModel( 75 | name='Human', 76 | fields=[ 77 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 78 | ('name', models.CharField(max_length=100)), 79 | ], 80 | ), 81 | migrations.CreateModel( 82 | name='Owner', 83 | fields=[ 84 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 85 | ('group', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to='demo.Group')), 86 | ('person', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owner', to=settings.AUTH_USER_MODEL)), 87 | ], 88 | ), 89 | migrations.CreateModel( 90 | name='Person', 91 | fields=[ 92 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 93 | ('first_name', models.CharField(max_length=50)), 94 | ('last_name', models.CharField(max_length=50)), 95 | ], 96 | ), 97 | migrations.CreateModel( 98 | name='UUIDModel', 99 | fields=[ 100 | ('id', models.UUIDField(default=uuid.uuid1, editable=False, primary_key=True, serialize=False)), 101 | ('content', models.TextField(default='uuid demo content')), 102 | ], 103 | ), 104 | migrations.CreateModel( 105 | name='Baby', 106 | fields=[ 107 | ('age', models.IntegerField(default=0)), 108 | ('human_ptr', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='demo.Human')), 109 | ], 110 | bases=('demo.human',), 111 | ), 112 | migrations.CreateModel( 113 | name='Post', 114 | fields=[ 115 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 116 | ('title', models.CharField(max_length=100)), 117 | ('body', models.TextField(blank=True)), 118 | ('views', models.IntegerField(default=0)), 119 | ('created', models.DateTimeField(default=django.utils.timezone.now)), 120 | ('updated', models.DateTimeField(auto_now=True)), 121 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='demo.Owner')), 122 | ], 123 | options={ 124 | 'ordering': ('-created',), 125 | }, 126 | ), 127 | migrations.CreateModel( 128 | name='MyCar', 129 | fields=[ 130 | ], 131 | options={ 132 | 'ordering': ['name'], 133 | 'proxy': True, 134 | 'indexes': [], 135 | 'constraints': [], 136 | }, 137 | bases=('demo.car',), 138 | ), 139 | ] 140 | -------------------------------------------------------------------------------- /demo/migrations/0002_age.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-11 03:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Age', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('year', models.IntegerField(default=6)), 18 | ('month', models.IntegerField(default=10)), 19 | ], 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /demo/migrations/0003_files.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-11 05:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo', '0002_age'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Files', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('file', models.FileField(upload_to='')), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /demo/migrations/0004_auto_20200511_1314.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-11 05:14 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo', '0003_files'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameModel( 14 | old_name='Files', 15 | new_name='File', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /demo/migrations/0005_auto_20200511_1317.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-11 05:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo', '0004_auto_20200511_1314'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='file', 15 | name='file', 16 | field=models.FileField(upload_to='files/%Y%m%d'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /demo/migrations/0006_delete_file.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-11 05:32 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo', '0005_auto_20200511_1317'), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name='File', 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /demo/migrations/0007_image.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-11 05:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo', '0006_delete_file'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Image', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('image', models.ImageField(upload_to='images/%Y%m%d')), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /demo/migrations/0008_postqs.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-13 05:13 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('demo', '0007_image'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='PostQS', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=100)), 21 | ('body', models.TextField(blank=True)), 22 | ('created', models.DateTimeField(default=django.utils.timezone.now)), 23 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts_QS', to=settings.AUTH_USER_MODEL)), 24 | ], 25 | options={ 26 | 'ordering': ('-created',), 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /demo/migrations/0009_book.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-15 06:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('demo', '0008_postqs'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Book', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('title', models.CharField(max_length=100)), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/migrations/__init__.py -------------------------------------------------------------------------------- /demo/migrations/__pycache__/0001_initial.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/migrations/__pycache__/0001_initial.cpython-38.pyc -------------------------------------------------------------------------------- /demo/migrations/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/demo/migrations/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /demo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | from django.urls import reverse 4 | from django.db.models import F 5 | from django.contrib.auth.models import User 6 | 7 | from djangoKnowledgeBase.settings import AUTH_USER_MODEL 8 | 9 | import uuid 10 | 11 | # from django.db.models.signals import post_save 12 | # from django.dispatch import receiver 13 | 14 | # MARK: - User 扩展 1 15 | # class Profile(models.Model): 16 | # user = models.OneToOneField(User, on_delete=models.CASCADE) 17 | # phone_number = models.CharField(max_length=20) 18 | # 19 | # 20 | # @receiver(post_save, sender=User) 21 | # def create_user_profile(sender, instance, created, **kwargs): 22 | # if created: 23 | # Profile.objects.create(user=instance) 24 | # 25 | # 26 | # @receiver(post_save, sender=User) 27 | # def save_user_profile(sender, instance, **kwargs): 28 | # instance.profile.save() 29 | 30 | 31 | from django.contrib.auth.models import AbstractUser 32 | 33 | 34 | # MARK: - User 扩展 2 35 | class MyUser(AbstractUser): 36 | phone_number = models.CharField(max_length=20) 37 | 38 | 39 | # 群组 40 | class Group(models.Model): 41 | username = models.CharField(max_length=100) 42 | 43 | 44 | # ForeignKey 桥接模型 45 | class Owner(models.Model): 46 | # 个人 47 | person = models.OneToOneField( 48 | AUTH_USER_MODEL, 49 | null=True, 50 | blank=True, 51 | on_delete=models.CASCADE, 52 | related_name='owner' 53 | ) 54 | # 群组 55 | group = models.OneToOneField( 56 | Group, 57 | null=True, 58 | blank=True, 59 | on_delete=models.CASCADE, 60 | related_name='owner' 61 | ) 62 | 63 | def get_owner(self): 64 | # 获取非空 Owner 对象 65 | if self.person is not None: 66 | return self.person 67 | elif self.group is not None: 68 | return self.group 69 | raise AssertionError("Neither is set") 70 | 71 | def __str__(self): 72 | if self.person is not None: 73 | return self.person.username 74 | elif self.group is not None: 75 | return self.group.username 76 | else: 77 | return 'No owner here..' 78 | 79 | 80 | class Post(models.Model): 81 | # MARK: - ForeignKey 对多个对象 82 | owner = models.ForeignKey(Owner, on_delete=models.CASCADE, related_name='posts') 83 | 84 | title = models.CharField(max_length=100) 85 | body = models.TextField(blank=True) 86 | 87 | views = models.IntegerField(default=0) 88 | 89 | created = models.DateTimeField(default=timezone.now) 90 | updated = models.DateTimeField(auto_now=True) 91 | 92 | class Meta: 93 | ordering = ('-created',) 94 | 95 | def get_absolute_url(self): 96 | return reverse('demo:detail', args=(self.id,)) 97 | 98 | def increase_view(self): 99 | # MARK: - F() 100 | # self.views += 1 101 | self.views = F('views') + 1 102 | self.save(update_fields=['views']) 103 | 104 | def __str__(self): 105 | return self.title 106 | 107 | 108 | # MARK: - UUID 109 | class UUIDModel(models.Model): 110 | id = models.UUIDField(primary_key=True, default=uuid.uuid1, editable=False) 111 | content = models.TextField(default='uuid demo content') 112 | 113 | def __str__(self): 114 | return self.id 115 | 116 | 117 | # MARK: - @property 118 | class Person(models.Model): 119 | first_name = models.CharField(max_length=50) 120 | last_name = models.CharField(max_length=50) 121 | 122 | def full_name_with_midname(self, midname): 123 | return f"{self.first_name} {midname} {self.last_name}" 124 | 125 | @property 126 | def full_name(self): 127 | return f"{self.first_name} {self.last_name}" 128 | 129 | 130 | # MARK: - Model 的继承 - 抽象基类 131 | class Animal(models.Model): 132 | name = models.CharField(max_length=100) 133 | age = models.IntegerField(default=5) 134 | finger_count = models.IntegerField(default=10) 135 | 136 | class Meta: 137 | abstract = True 138 | 139 | 140 | class Bird(Animal): 141 | flying_height = models.IntegerField(default=2000) 142 | age = models.TextField(default='5 years old') 143 | finger_count = None 144 | 145 | 146 | # MARK: - Model 的继承 - 多表继承 147 | class Human(models.Model): 148 | name = models.CharField(max_length=100) 149 | 150 | 151 | class Baby(Human): 152 | age = models.IntegerField(default=0) 153 | human_ptr = models.OneToOneField( 154 | Human, on_delete=models.CASCADE, 155 | parent_link=True, 156 | primary_key=True, 157 | ) 158 | 159 | 160 | # MARK: - Model 的继承 - 代理模式 161 | class Car(models.Model): 162 | name = models.CharField(max_length=30) 163 | 164 | 165 | class MyCar(Car): 166 | class Meta: 167 | ordering = ["name"] 168 | proxy = True 169 | 170 | def do_something(self): 171 | # ... 172 | pass 173 | 174 | 175 | # MARK: - F 函数 176 | class Age(models.Model): 177 | year = models.IntegerField(default=6) 178 | month = models.IntegerField(default=10) 179 | 180 | 181 | # MARK: - 批量上传文件 182 | class Image(models.Model): 183 | image = models.ImageField(upload_to='images/%Y%m%d') 184 | 185 | def admin_image(self): 186 | return '' % self.image 187 | 188 | admin_image.allow_tags = True 189 | 190 | 191 | # MARK: - Queryset 192 | class PostQS(models.Model): 193 | owner = models.ForeignKey(AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='posts_QS') 194 | 195 | title = models.CharField(max_length=100) 196 | body = models.TextField(blank=True) 197 | 198 | created = models.DateTimeField(default=timezone.now) 199 | 200 | class Meta: 201 | ordering = ('-created',) 202 | 203 | def __str__(self): 204 | return self.title 205 | 206 | 207 | # MARK: - create() by CustomManager 208 | class BookManager(models.Manager): 209 | def create_book(self, title): 210 | book = self.create(title=title) 211 | book.save() 212 | # do something with the book 213 | return book 214 | 215 | def get_queryset(self): 216 | return super().get_queryset().filter(title__contains='again') 217 | 218 | 219 | class Book(models.Model): 220 | title = models.CharField(max_length=100) 221 | 222 | objects = models.Manager() 223 | custom = BookManager() 224 | 225 | @classmethod 226 | def create(cls, title): 227 | book = cls(title=title) 228 | # do something with the book 229 | return book 230 | 231 | def save(self, *args, **kwargs): 232 | # do something here... 233 | 234 | super().save(*args, **kwargs) 235 | -------------------------------------------------------------------------------- /demo/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import ( 3 | ReverseView, 4 | HomePageWithContextView, 5 | RedirectView, 6 | PostDetailView, 7 | redirect_view, 8 | path_demo_view, 9 | uploads_files, 10 | session_visits_count, 11 | ) 12 | 13 | app_name = 'demo' 14 | 15 | urlpatterns = [ 16 | # MARK: - reverse() 17 | path('reverse/', 18 | ReverseView.as_view(), 19 | name='reverse'), 20 | 21 | path('reverse//', 22 | ReverseView.as_view(), 23 | name='reverse'), 24 | 25 | path('home-with-context/', 26 | HomePageWithContextView.as_view(), 27 | name='home_with_context'), 28 | 29 | path('home-with-context//', 30 | HomePageWithContextView.as_view(), 31 | name='home_with_context'), 32 | 33 | # MARK: - redirect() 34 | 35 | # byModel 36 | path('redirect//', RedirectView.as_view(), name='redirect'), 37 | # byView 38 | path('redirect-by-view//', redirect_view, name='redirect_view'), 39 | # 被跳转 url 40 | path('post-detail//', PostDetailView.as_view(), name='detail'), 41 | 42 | # MARK: - path() 43 | path('path///', path_demo_view, name='path'), 44 | 45 | # MRAK: - 批量上传文件 46 | path('uploads/', uploads_files, name='uploads'), 47 | 48 | # MARK: - Session 49 | path('visits-count/', session_visits_count, name='visits_count'), 50 | ] 51 | -------------------------------------------------------------------------------- /demo/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.generic import View 3 | from django.urls import reverse 4 | from django.shortcuts import redirect, get_object_or_404 5 | from django.http import HttpResponse 6 | 7 | from .models import Post, Person, Image 8 | 9 | 10 | # MARK: - reverse() 11 | 12 | # 首页view 13 | class HomePageView(View): 14 | def get(self, request): 15 | posts = Post.objects.all() 16 | 17 | name1, name2 = get_name() 18 | 19 | return render( 20 | request, 21 | 'home.html', 22 | context={'posts': posts, 'name1': name1, 'name2': name2} 23 | ) 24 | 25 | 26 | # 重定向view 27 | class ReverseView(View): 28 | def get(self, request, *args, **kwargs): 29 | id = kwargs.get('id') 30 | 31 | if id == None: 32 | # 不带参数 33 | url = reverse('demo:home_with_context') 34 | else: 35 | # 带有参数 36 | url = reverse('demo:home_with_context', args=(id,)) 37 | return redirect(url) 38 | 39 | 40 | # 被重定向view 41 | class HomePageWithContextView(View): 42 | def get(self, request, *args, **kwargs): 43 | id = kwargs.get('id') 44 | 45 | if id == 0: 46 | context = {'content': '我从 Url 模板语法解析回来,并带有参数 id - {} 哦'.format(id)} 47 | elif id == 1: 48 | context = {'content': '我从 Reverse() 回来,并带有参数 id - {} 哦'.format(id)} 49 | else: 50 | context = {'content': '我从 Reverse() 回来'} 51 | 52 | posts = Post.objects.all() 53 | 54 | name1, name2 = get_name() 55 | context.update({'posts': posts, 'name1': name1, 'name2': name2}) 56 | return render(request, 'home.html', context=context) 57 | 58 | 59 | # MARK: - redirect() && get_object_or_404() 60 | 61 | # 跳转view 62 | class RedirectView(View): 63 | def get(self, request, *args, **kwargs): 64 | id = kwargs.get('id') 65 | post = Post.objects.get(id=id) 66 | 67 | flag = id % 3 68 | if flag == 1: 69 | print('byModel') 70 | return redirect(post) 71 | elif flag == 2: 72 | print('byView') 73 | return redirect('demo:redirect_view', id=id) 74 | else: 75 | print('byURL') 76 | return redirect('/demo/post-detail/{}/'.format(post.id)) 77 | 78 | 79 | # model 跳转 80 | class PostDetailView(View): 81 | def get(self, request, *args, **kwargs): 82 | id = kwargs.get('id') 83 | 84 | # post = Post.objects.get(id=id) 85 | post = get_object_or_404(Post, id=id) 86 | 87 | (post, owner) = detail_setup(post) 88 | 89 | return render(request, 'post_detail.html', context={'post': post, 'owner': owner}) 90 | 91 | 92 | # view_name 跳转 93 | def redirect_view(request, id): 94 | # post = Post.objects.get(id=id) 95 | queryset = Post.objects.filter(title__startswith='S') 96 | post = get_object_or_404(queryset, id=id) 97 | 98 | post, owner = detail_setup(post) 99 | 100 | return render(request, 'post_detail.html', context={'post': post, 'owner': owner}) 101 | 102 | 103 | # MARK: - path() 104 | def path_demo_view(request, count, salute): 105 | count = count 106 | salute = salute 107 | first_name = request.GET.get('first_name') 108 | last_name = request.GET.get('last_name') 109 | return render(request, 110 | 'path.html', 111 | context={ 112 | 'count': count, 113 | 'salute': salute, 114 | 'first_name': first_name, 115 | 'last_name': last_name} 116 | ) 117 | 118 | 119 | # MARK: - 批量上传文件 120 | def uploads_files(request): 121 | if request.method == 'POST': 122 | 123 | # do validate here... 124 | 125 | files = request.FILES.getlist('file_field') 126 | for f in files: 127 | file = Image(image=f) 128 | file.save() 129 | 130 | return render(request, 'uploads_images.html', context={'images': Image.objects.all()}) 131 | 132 | 133 | # MARK: - Session 134 | def session_visits_count(request): 135 | count = request.session.get('visits_count', 0) 136 | count += 1 137 | request.session['visits_count'] = count 138 | 139 | # del request.session['deeper_count'] 140 | 141 | # session 保存字典数据 142 | if request.session.get('deeper_count'): 143 | num = request.session['deeper_count']['num'] 144 | # 此时 session 并未更新,因为更新的仅仅是字典中的数据 145 | request.session['deeper_count']['num'] = num + 1 146 | # 通知会话已修改 147 | # 注意:本视图中即使没有此指令也能正常保存,因为前面的 visits_count 已经触发了保存。 148 | request.session.modified = True 149 | else: 150 | num = 1 151 | request.session['deeper_count'] = {'num': num} 152 | 153 | 154 | return render(request, 'visits_count.html', context={'count': count, 'deeper_count': num}) 155 | 156 | 157 | 158 | # Helper 159 | 160 | def detail_setup(obj): 161 | # MARK: - update() 162 | obj.increase_view() 163 | # 刷新数据 164 | obj.refresh_from_db() 165 | return obj, obj.owner.get_owner() 166 | 167 | 168 | def get_name(): 169 | name1 = Person.objects.all().first().full_name 170 | name2 = Person.objects.all().first().full_name_with_midname('Wen') 171 | return name1, name2 172 | -------------------------------------------------------------------------------- /djangoKnowledgeBase/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/djangoKnowledgeBase/__init__.py -------------------------------------------------------------------------------- /djangoKnowledgeBase/__pycache__/__init__.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/djangoKnowledgeBase/__pycache__/__init__.cpython-38.pyc -------------------------------------------------------------------------------- /djangoKnowledgeBase/__pycache__/settings.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/djangoKnowledgeBase/__pycache__/settings.cpython-38.pyc -------------------------------------------------------------------------------- /djangoKnowledgeBase/__pycache__/urls.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/djangoKnowledgeBase/__pycache__/urls.cpython-38.pyc -------------------------------------------------------------------------------- /djangoKnowledgeBase/__pycache__/wsgi.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/djangoKnowledgeBase/__pycache__/wsgi.cpython-38.pyc -------------------------------------------------------------------------------- /djangoKnowledgeBase/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for djangoKnowledgeBase project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoKnowledgeBase.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /djangoKnowledgeBase/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangoKnowledgeBase project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | import json 15 | 16 | with open('env.json') as env: 17 | ENV = json.load(env) 18 | 19 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 20 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = ENV['SECRET_KEY'] 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = True 30 | 31 | ALLOWED_HOSTS = ['*'] 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.sessions', 40 | 'django.contrib.messages', 41 | 'django.contrib.staticfiles', 42 | 43 | 'demo', 44 | 'mig', 45 | 'transanction_demo', 46 | 'aggregation', 47 | 'middleware', 48 | 'mySignal', 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | 'django.middleware.security.SecurityMiddleware', 53 | 'django.contrib.sessions.middleware.SessionMiddleware', 54 | 'django.middleware.common.CommonMiddleware', 55 | 'django.middleware.csrf.CsrfViewMiddleware', 56 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 57 | 'django.contrib.messages.middleware.MessageMiddleware', 58 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 59 | 60 | # 'middleware.middlewares.Md1', 61 | # 'middleware.middlewares.Md2', 62 | # 'middleware.middlewares.Md3', 63 | 64 | # 'middleware.middlewares.NormalUserBlock', 65 | # 'middleware.middlewares.DebugOnlySuperUser', 66 | 'middleware.middlewares.ResponseTimer', 67 | ] 68 | 69 | ROOT_URLCONF = 'djangoKnowledgeBase.urls' 70 | 71 | TEMPLATES = [ 72 | { 73 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 74 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 75 | 'APP_DIRS': True, 76 | 'OPTIONS': { 77 | 'context_processors': [ 78 | 'django.template.context_processors.debug', 79 | 'django.template.context_processors.request', 80 | 'django.contrib.auth.context_processors.auth', 81 | 'django.contrib.messages.context_processors.messages', 82 | ], 83 | }, 84 | }, 85 | ] 86 | 87 | WSGI_APPLICATION = 'djangoKnowledgeBase.wsgi.application' 88 | 89 | # Database 90 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 91 | 92 | DATABASES = { 93 | 'default': { 94 | 'ENGINE': 'django.db.backends.sqlite3', 95 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 96 | # 'ATOMIC_REQUESTS': True, 97 | } 98 | } 99 | 100 | # Password validation 101 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 102 | 103 | AUTH_PASSWORD_VALIDATORS = [ 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 112 | }, 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 115 | }, 116 | ] 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 120 | 121 | LANGUAGE_CODE = 'en-us' 122 | 123 | TIME_ZONE = 'UTC' 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = True 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | # STATIC_ROOT = os.path.join(BASE_DIR, 'static') 136 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 137 | 138 | MEDIA_URL = '/media/' 139 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') 140 | 141 | AUTH_USER_MODEL = 'demo.MyUser' 142 | -------------------------------------------------------------------------------- /djangoKnowledgeBase/urls.py: -------------------------------------------------------------------------------- 1 | """djangoKnowledgeBase URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from demo.views import HomePageView 19 | from django.conf import settings 20 | from django.conf.urls.static import static 21 | 22 | from middleware.views import mid_test 23 | 24 | urlpatterns = [ 25 | path('admin/', admin.site.urls), 26 | # MARK: - include() 27 | path('demo/', include('demo.urls', namespace='demo')), 28 | path('', HomePageView.as_view(), name='home'), 29 | path('transanction/', include('transanction_demo.urls', namespace='transanction')), 30 | # MAR: - 中间件 demo 31 | path('middleware/',mid_test), 32 | # 信号 33 | path('signal/', include('mySignal.urls', namespace='signal')), 34 | ] 35 | # urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 36 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 37 | -------------------------------------------------------------------------------- /djangoKnowledgeBase/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djangoKnowledgeBase project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoKnowledgeBase.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /env.json: -------------------------------------------------------------------------------- 1 | { 2 | "SECRET_KEY": "ul#d-f%=-qal5fly=#_lwym@1koh-+ra1$6)z(3(a^r4is6n8s" 3 | } -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoKnowledgeBase.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /md/001-前言.md: -------------------------------------------------------------------------------- 1 | [![](https://img.shields.io/badge/python-3.8-orange.svg)](https://www.python.org) 2 | [![](https://img.shields.io/badge/django-3.0.5-green.svg)](https://docs.djangoproject.com) 3 | [![](https://img.shields.io/badge/license-MIT-000000.svg)](https://opensource.org/licenses/MIT) 4 | 5 | # Django知识库 6 | 7 | 这是关于 Django 框架的零散但有用知识点的 Handbook。 8 | 9 | 如果你在寻找一个完整的 Django 入门,请看我的**Django搭建个人博客**教程: 10 | 11 | - [GitHub](https://github.com/stacklens/django_blog_tutorial/tree/master/md) 12 | - [个人博客](https://www.dusaiphoto.com/article/detail/2/) 13 | 14 | > 注:两个版本是完全相同的。 15 | 16 | 然后,你可以快速读一遍此 Django 知识库,以便大脑中对部分重要的工具有大致印象;也可以在需要的时候再来查阅。请按实际情况食用。 17 | 18 | 如果你觉得这个专题对你有帮助,请在 [GitHub](https://github.com/stacklens/django-knowledge-base) 给个小小的 Star,让更多的人能看到它~(比心) 19 | 20 | **代码环境:** 21 | 22 | - Python 3.8 23 | - Django 3.0.5 24 | - Windows 10 25 | 26 | 想寻找学伴互相交流学习的,可以加 **Django交流QQ群**:107143175,或者**微信公众号**学习难免走弯路,有热心人帮忙就不再寂寞了。 27 | 28 | ![](https://blog.dusaiphoto.com/QR-0608.jpg) 29 | 30 | ## 许可协议 31 | 32 | 《Django 知识库》(包括且不限于文章、代码等内容)遵守 **署名-非商业性使用 4.0 国际 (CC BY-NC 4.0) 协议**。协议内容如下。 33 | 34 | **您可以自由地:** 35 | 36 | - **共享** — 在任何媒介以任何形式复制、发行本作品。 37 | - **演绎** — 修改、转换或以本作品为基础进行创作。 38 | 39 | 只要你遵守许可协议条款,许可人就无法收回你的这些权利。 40 | 41 | **惟须遵守下列条件:** 42 | 43 | - **署名** — 您必须给出**适当的署名**,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示许可人为您或您的使用背书。 44 | 45 | - **非商业性使用** — 您不得将本作品用于**商业目的**。 46 | 47 | - **没有附加限制** — 您不得适用法律术语或者技术措施从而限制其他人做许可协议允许的事情。 48 | 49 | > 适当的署名:您必须提供创作者和署名者的姓名或名称、版权标识、许可协议标识、免责标识和作品链接。 50 | > 51 | > 商业目的:主要目的为获得商业优势或金钱回报。 -------------------------------------------------------------------------------- /md/010-reverse()路由解析.md: -------------------------------------------------------------------------------- 1 | 假设已经有了这么一个路由: 2 | 3 | ```python 4 | path('foo/', some_view, name='foo_name'), 5 | ``` 6 | 7 | 想从**页面模板**某处链接跳转到**视图函数**中就非常容易了,用如下模板语法: 8 | 9 | ```html 10 | Jump 11 | ``` 12 | 13 | 现在问题来了,如果我想从**视图函数**中跳转到**另一个视图函数**该怎么办呢?这种情况是有可能发生的,比如某个视图会根据条件的不同而转换到不同的视图中去。 14 | 15 | 很简单,有现成的 `redirect()` 函数可使用: 16 | 17 | ```python 18 | return redirect('/foo/') 19 | ``` 20 | 21 | 但是这样把 `url` 硬编码到代码里了,不美。更好的写法就要用到主角 `reverse()` 了: 22 | 23 | ```python 24 | return redirect(reverse('foo_name')) 25 | ``` 26 | 27 | 这样写的好处是你可以任意更改 `url` 实际地址,只要路由的 `name` 不变,都是可以解析到正确的地址中去的。 28 | 29 | 带有参数的写法如下: 30 | 31 | ```python 32 | reverse('another_name', args=(id,)) 33 | ``` 34 | 35 | 因此带有参数的路由也可以正确解析了。简单又好用吧。 36 | 37 | 用之前记得导入: 38 | 39 | ```python 40 | from django.urls import reverse 41 | from django.shortcuts import redirect 42 | ``` 43 | 44 | -------------------------------------------------------------------------------- /md/020-redirect()视图重定向.md: -------------------------------------------------------------------------------- 1 | 上一节我们已经领教到 `redirect()` 和 `reverse()` 配合使用的强大威力了。 2 | 3 | 但 `redirect()` 的能力并不止于此。它可以接受 3 中不同的参数: 4 | 5 | ### Model 作为参数 6 | 7 | `redirect()` 可以接收模型作为第一个参数,像这样: 8 | 9 | ```python 10 | def redirect_view(request, id): 11 | ... 12 | obj = SomeModel.objects.get(id=id) 13 | return redirect(obj) 14 | ``` 15 | 16 | 此时 `redirect()` 会调用**模型**实例中的 `get_absolute_url()` 方法,所以你必须在模型中加上它: 17 | 18 | ```python 19 | class SomeModel(...) 20 | ... 21 | def get_absolute_url(self): 22 | return reverse('some_url', args=(self.id,)) 23 | ``` 24 | 25 | > `reverse()` 的用法上一节讲过了。 26 | 27 | **路由**部分的写法像这样: 28 | 29 | ```python 30 | ... 31 | path('...', redirect_view, name='...'), 32 | path('...', destination_view, name='some_url') 33 | ``` 34 | 35 | 所以当你请求 `redirect_view()` 时,`redirect()` 就帮你跳转到 `destination_view()` 视图中去了。 36 | 37 | ### View 作为参数 38 | 39 | 如果你在 `url` 中有如下需要跳转的地址: 40 | 41 | ```python 42 | path('...', another_view, name='another_url'), 43 | ``` 44 | 45 | 你还可以通过**视图的命名**作为参数: 46 | 47 | ```python 48 | return redirect('another_url', id=id) 49 | ``` 50 | 51 | 依然可以用关键字参数传递变量到被跳转的视图中。 52 | 53 | > 在 `reverse()` 的章节中我们是这样写的:`return redirect(reverse('foo_name'))`。两种写法本质上是一样的,路由都是由 `reverse()` 解析的,只不过 `Django` 隐式帮你处理了。 54 | 55 | ### URL 作为参数 56 | 57 | 第三种方式就更加粗暴了,把 `url` 字符串作为参数: 58 | 59 | ```python 60 | return redirect('/your_url/{}/'.format(post.id)) 61 | ``` 62 | 63 | 参数也可以通过字符串格式化传递进去,不过这种方式属于硬编码,还是少用为好。 64 | 65 | 导入路径在这里: 66 | 67 | ```python 68 | from django.urls import reverse 69 | from django.shortcuts import redirect 70 | ``` 71 | 72 | -------------------------------------------------------------------------------- /md/030-get_object_or_404()获取资源.md: -------------------------------------------------------------------------------- 1 | 从模型中取出某个特定内容,最简单的方式就是用模型管理器的 `get()` 方法了: 2 | 3 | ```python 4 | obj = SomeModel.objects.get(id=1) 5 | ``` 6 | 7 | 像上面的代码,如果数据库中没有 `id=1` 的数据条目时,`Django` 会抛出 `Error 500` 错误。但问题是大部分时候,没有相关条目仅仅是因为**资源不存在**,应该抛出万恶的 `Error 404` 才对。 8 | 9 | 因此就有了 `get_object_or_404()` : 10 | 11 | ```python 12 | obj = get_object_or_404(SomeModel, id=1) 13 | ``` 14 | 15 | 它其实就是下面这种写法的快捷方式: 16 | 17 | ```python 18 | try: 19 | obj = SomeModel.objects.get(id=1) 20 | except SomeModel.DoesNotExist: 21 | raise Http404("No SomeModel matches the given query.") 22 | ``` 23 | 24 | 除了上面这种最常用的写法, `get_object_or_404()` 还可以接受 `queryset` 作为第一个参数: 25 | 26 | ```python 27 | queryset = Post.objects.filter(title__startswith='V') 28 | post = get_object_or_404(queryset, id=id) 29 | ``` 30 | 31 | 当你需要把查询集反复筛选、传递时,这种写法还是很有用的。 32 | 33 | 最后还要注意, `get_object_or_404()` 和 `get()` 一样,只能返回单个结果,否则服务器将抛出错误。 34 | 35 | > `get_object_or_404()` 还可以接受管理器作为参数,有兴趣请去官方文档了解。 36 | 37 | 那要是我想获取多个结果呢?请用`get_list_or_404()`: 38 | 39 | ```python 40 | objs = get_list_or_404(SomeModel, isMale=True) 41 | ``` 42 | 43 | 它类似于如下代码: 44 | 45 | ```python 46 | objs = list(SomeModel.objects.filter(isMale=True)) 47 | if not objs: 48 | raise Http404("No SomeModel matches the given query.") 49 | ``` 50 | 51 | 用之前别忘了导入它们: 52 | 53 | ```python 54 | from django.shortcuts import get_object_or_404 55 | from django.shortcuts import get_list_or_404 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /md/040-path()路径映射.md: -------------------------------------------------------------------------------- 1 | 网站地址是由**统一资源定位符**表示的,也是就我们常说的 `url`。`Django` 中有非常强大的 `path()` 方法,可以动态构造出你想要的各种不同形态的 `url` 。 2 | 3 | 基本写法如下: 4 | 5 | ```python 6 | from django.urls import path 7 | 8 | urlpatterns = [ 9 | # 固定地址 10 | path('articles/2003/', ...), 11 | # 可传入 int 参数 12 | path('articles//', ...), 13 | # 可传入 int、str 等多个参数 14 | path('articles///', ...), 15 | ] 16 | ``` 17 | 18 | 可以看出 `path()` 中是可以传入动态参数的,比如上面的第三个 `path()` 可以匹配下面的 `url`: 19 | 20 | ```python 21 | '/articles/2020/awesome/' 22 | ``` 23 | 24 | 并且这些参数可以在**视图**中取得: 25 | 26 | ```python 27 | def some_view(request, year, title): 28 | date = year 29 | name = title 30 | ... 31 | ``` 32 | 33 | 你可以给这些参数指定默认值: 34 | 35 | ```python 36 | def some_view(request, year=2020, title='Django'): 37 | ... 38 | ``` 39 | 40 | 但是需要注意的是,`GET` 请求中**附带的参数**是不能够直接通过**视图函数的参数**取得的,比如下面这个地址: 41 | 42 | ```python 43 | '/articles/2020/awesome/?month=4&day=22' 44 | ``` 45 | 46 | 问号后面的参数不能作为**视图函数**的参数,否则你会得到无情的报错。 47 | 48 | 获取它们的方法是这样: 49 | 50 | ```python 51 | def some_view(request, ...): 52 | ... 53 | # month = 4 54 | month = request.GET.get('month') 55 | # day = 22 56 | day = request.GET.get('day') 57 | ... 58 | ``` 59 | 60 | 接下来就可以愉快的使用了,很简单吧。 61 | 62 | 顺带说一下,上面这个 `url` 在模板中应该这样子写: 63 | 64 | ```html 65 | {% url 'parse_name' 2020 'awesome' %}?month=4&day=22 66 | ``` 67 | 68 | 总结,`path()` 能接受的参数一共有**四种**: 69 | 70 | - `str` :匹配除路径分隔符 `'/'` 之外的非空字符串。 71 | - `int` :匹配零或正整数。 72 | - `slug` :匹配由ASCII字母、数字、连字符、下划线字符组成的字符串。例如, `building-your-1st-django-site`。 73 | - `uuid` :匹配格式化的UUID,如 `075194d3-6885-417e-a8a8-6c931e272f00` 。 74 | 75 | 合理运用吧。 76 | -------------------------------------------------------------------------------- /md/050-include()路径调度.md: -------------------------------------------------------------------------------- 1 | 路由中的 `include()` 方法非常的常用。它的作用是把 `url` 的剩余部分发配到另一个 `URLconfs` 处理,以免单个路由文件过于庞大和凌乱。 2 | 3 | 通常我们在**根路由**中使用它: 4 | 5 | ```python 6 | # root/urls.py 7 | 8 | from django.urls import path, include 9 | 10 | path('post/', include('post.urls', namespace='post')), 11 | ``` 12 | 13 | 后端在匹配到 `post/` 后,继续在 `post` 模块的 `urls.py` 中处理剩下的部分: 14 | 15 | ```python 16 | # post/urls.py 17 | ... 18 | 19 | path('john/', some_view, name='user') 20 | ``` 21 | 22 | 它两配合起来就可以匹配类似这样的地址: 23 | 24 | ```python 25 | '/post/john/' 26 | ``` 27 | 28 | 另外,你可能注意到了参数 `namespace` (应用程序命名空间)和 `name` (实例命名空间)这两兄弟了,他们是地址反向解析用的,比如在**模板**中: 29 | 30 | ```html 31 | {% url 'post:user' %} 32 | ``` 33 | 34 | 或者在**视图**中: 35 | 36 | ```python 37 | reverse('post:user') 38 | ``` 39 | 40 | 这样多级命名的好处是你可以在不同的 app 中重复的命名,它们是互不影响的。 41 | 42 | -------------------------------------------------------------------------------- /md/060-敏感信息保存与读取.md: -------------------------------------------------------------------------------- 1 | `Django` 中的 `settings.py` 是个宝库,基本上大部分的自定义配置都要在这里进行。所以问题就来了,这些配置里有相当多地方是涉及到账户、密码等**敏感信息**的。如果项目是开源的,怎么保证这些敏感信息不会泄露? 2 | 3 | 拿 `SECRET_KEY` 举例,它跟数据库中用户密码加盐等内容是挂钩的,听名字也知道是不能公开的内容。所以我们要在项目根目录创建一个环境文件 `env.json`,把 `SECRET_KEY` 给挪进去: 4 | 5 | ```python 6 | { 7 | "SECRET_KEY": "ul#d-f%=-.......z(3(a^r4is6n8s" 8 | } 9 | ``` 10 | 11 | 然后在 `settings.py` 中调用就好了: 12 | 13 | ```python 14 | import json 15 | 16 | with open('env.json') as env: 17 | ENV = json.load(env) 18 | 19 | SECRET_KEY = ENV['SECRET_KEY'] 20 | ``` 21 | 22 | 如果你用的 `GitHub` 进行的代码远程管理,**一定**记得要把环境文件从跟踪表中剔除: 23 | 24 | ```python 25 | # .gitignore 26 | 27 | env.json 28 | ... 29 | ``` 30 | 31 | 除了敏感信息,只要跟环境有关的变量都可以放到这个环境文件中。 32 | 33 | 比如下面这个: 34 | 35 | ```python 36 | # env.json 37 | { 38 | "env": "dev" 39 | } 40 | 41 | # ---------------------- 42 | 43 | # settings.py 44 | if ENV.get('env') == 'dev': 45 | DEBUG = True 46 | else: 47 | DEBUG = False 48 | ``` 49 | 50 | 这样就可以根据环境自动切换调试状态了。 51 | 52 | 能交给程序完成的事,坚决不能自己动手,麻烦又容易犯错。 -------------------------------------------------------------------------------- /md/070-用UUID作为模型主键.md: -------------------------------------------------------------------------------- 1 | 主键是数据库中每个条目的标识符,通常也是唯一的,用来索引到特定的数据条目。 2 | 3 | 如果你在定义模型时没有显式的指定主键,那么`Django` 会贴心的送你一个自增的 `id` 主键: 4 | 5 | ```python 6 | class SomeModel(model.Model): 7 | # 下面这个 id 字段是不需要写的,django 自动附送 8 | # id = models.AutoField(primary_key=True) 9 | 10 | ... 11 | ``` 12 | 13 | 这个 `id` 主键从 1 开始计数,每有一条新的数据则 +1,保证了主键不重复。 14 | 15 | 通常你用这个自增主键就够了,但是有些情况下用它又不合适:居心不良的黑客可以通过 `id` 的值轻易得知当前数据库中的数据总条目、各条数据的大致创建时间、甚至可以推断出相邻数据的主键。 16 | 17 | 如果你有这样的担心,那么用 `UUID` 作为主键非常合适。 `UUID` 是一种全局唯一标识符,通常用32位的字符串来表现,像这样:`9cd0c6fa-846e-11ea-8191-94e6f7639b8c` ,它可以保证全球范围内的唯一性。 18 | 19 | 方法是这样: 20 | 21 | ```python 22 | import uuid 23 | 24 | class SomeModel(models.Model): 25 | id = models.UUIDField(primary_key=True, default=uuid.uuid1, editable=False) 26 | ... 27 | ``` 28 | 29 | 从主键本身,基本看不出来任何有价值的信息,完美。它是 `python` 的标准库,记得导入。 -------------------------------------------------------------------------------- /md/080-模型save()方法更新部分字段.md: -------------------------------------------------------------------------------- 1 | 开发个人博客时,博客文章的模型通常包含有**浏览量计数**、**最近更新时间**两个字段,像这样: 2 | 3 | ```python 4 | class Post(models.Model): 5 | # 文章浏览量 6 | views = models.IntegerField(default=0) 7 | # 最近更新时间 8 | updated = models.DateTimeField(auto_now=True) 9 | 10 | # other fields... 11 | 12 | # 增加浏览量的方法 13 | def increase_view(self): 14 | self.views += 1 15 | self.save() 16 | ``` 17 | 18 | 每当访客打开文章详情页面时,浏览量需要 +1,所以在视图调用 `increase_view`: 19 | 20 | ```python 21 | def some_view(request, id): 22 | post = Post.objects.get(id=id) 23 | post.increase_view() 24 | ... 25 | ``` 26 | 27 | > 还有更好的自增方式,后面章节再讲。 28 | 29 | 这样弄的结果就是浏览量虽然正确的增加了,但是最近更新时间 `updated` 也一起更新了,这显然不是我们想要的。 30 | 31 | 正确的写法是要传入 `update_fields` 参数,控制需要更新的字段: 32 | 33 | ```python 34 | ... 35 | def increase_view(self): 36 | self.views += 1 37 | self.save(update_fields=['views']) 38 | ``` 39 | 40 | 这样就可以只更新 `views` 字段了,其他字段都保持原状。 -------------------------------------------------------------------------------- /md/090-F函数更新数据.md: -------------------------------------------------------------------------------- 1 | 上一章讲到,开发个人博客时,统计每篇文章浏览量的逻辑通常是这样写的: 2 | 3 | ```python 4 | post = Post.objects.get(...) 5 | post.views += 1 6 | post.save() 7 | ``` 8 | 9 | 上面的语句已经相当简短了,但实际上还有更好的办法,就是运用 `F` 函数: 10 | 11 | ```python 12 | from django.db.models import F 13 | 14 | post = Post.objects.get(...) 15 | post.views = F('views') + 1 16 | post.save() 17 | ``` 18 | 19 | 看起来似乎都差不多,但是用 `F` 函数有几个显著的好处: 20 | 21 | - **减少了操作次数**。`post.view += 1` 是 Python 在内存中操作的,然后再从内存把数据更新到数据库;而 `F('views') + 1` 是直接操作的数据库,减少了一个操作层级。 22 | - **避免竞争**。竞争是指多个 Python 线程同时对同一个数据进行更新,`post.view += 1` 就有可能丢失其中的某些更新操作,而 `F('views') + 1` 由于是直接操作数据库,不会有丢失数据的问题。 23 | 24 | **注意**,正因为 `F` 函数没有在内存中操作,因此更新完数据后需要重新刷新内存中的模型对象: 25 | 26 | ```python 27 | ... 28 | post.save() 29 | # 重新取值 30 | post = Post.objects.get(...) 31 | ``` 32 | 33 | 或者这样: 34 | 35 | ```python 36 | ... 37 | post.save() 38 | # 重新取值 39 | post.refresh_from_db() 40 | ``` 41 | 42 | Done! 43 | 44 | 除此之外,`F` 函数还支持跨字段的查找: 45 | 46 | ```python 47 | # models.py 48 | class Age(models.Model): 49 | year = models.IntegerField(default=6) 50 | month = models.IntegerField(default=10) 51 | 52 | # -------------- 53 | 54 | # 获取所有 year > month 的数据 55 | res = Age.objects.filter(year__gt=F('month')) 56 | ``` 57 | 58 | `F` 函数支持加,减,乘,除,取模和幂运算: 59 | 60 | ```python 61 | Age.objects.filter(year__gt=F('month') * 2) 62 | Age.objects.filter(year__gt=F('month') + F('year')) 63 | ``` 64 | 65 | 对于日期字段,也可以轻松处理: 66 | 67 | ```python 68 | >>> from datetime import timedelta 69 | >>> Entry.objects.filter(date__gt=F('pub_date') + timedelta(days=3)) 70 | ``` 71 | 72 | 跨关系的查找也是可以的: 73 | 74 | ```python 75 | # models.py 76 | class Person(...): 77 | name = ... 78 | 79 | class People(...): 80 | name = ... 81 | 82 | class Age(...): 83 | ... 84 | person = models.OneToOneField(Person, ...) 85 | people = models.OneToOneField(People, ...) 86 | 87 | # -------------- 88 | 89 | # 获取所有 person.name == user.name 的数据 90 | res = Age.objects.filter(person__name=F('people__name')) 91 | ``` 92 | 93 | > `F` 函数还有一些更高级的用法,如与聚合的配合,这里就不列举了,有兴趣的可以前往文档观摩。 94 | 95 | -------------------------------------------------------------------------------- /md/100-ForeignKey关联不同模型.md: -------------------------------------------------------------------------------- 1 | 有的时候我们需要将“一对多” `ForeignKey` 关联到**多个不同的模型**。什么意思?比如博客文章,它的作者既可以是独立的用户 `Person`,也可以是某一个群组 `Group`;但是 `ForeignKey` 显然只支持关联到单个模型的,怎么办? 2 | 3 | 解决方案有很多种,我比较倾向于这样: 4 | 5 | ```python 6 | # 群组 7 | class Group(models.Model): 8 | name = models.CharField(max_length=100) 9 | 10 | # 用户 11 | class Person(models.Model): 12 | name = models.CharField(max_length=100) 13 | 14 | # 起过渡作用的桥接模型 15 | class Owner(models.Model): 16 | # 用户 17 | person = models.OneToOneField( 18 | Person, 19 | null=True, 20 | blank=True, 21 | on_delete=models.CASCADE 22 | ) 23 | # 群组 24 | group = models.OneToOneField( 25 | Group, 26 | null=True, 27 | blank=True, 28 | on_delete=models.CASCADE 29 | ) 30 | 31 | # 文章 32 | class Post(models.Model): 33 | owner = models.ForeignKey(Owner, on_delete=models.CASCADE) 34 | ``` 35 | 36 | 关键点是 `ForeignKey` 关联了一个 `Owner` 桥接模型,再由 `Owner` 与实际的作者作“一对一”的关联。 37 | 38 | 在使用它时,可能还需要一个辅助函数确定 `Owner` 到底关联了谁: 39 | 40 | ```python 41 | class Owner(...): 42 | ... 43 | def get_owner(self): 44 | # 获取非空 Owner 对象 45 | if self.person is not None: 46 | return self.person 47 | elif self.group is not None: 48 | return self.group 49 | raise AssertionError("Neither is set") 50 | ``` 51 | 52 | 在视图中可以这样取得实际的 `owner` : 53 | 54 | ```python 55 | owner = post.owner.get_owner() 56 | ``` 57 | 58 | 这种方式有一些**缺点**,比如多了一个 `Owner` 桥接表、需要写更多辅助函数保证 `owner` 的正确性,但我觉得它在简洁、效率上是个不错的折中。 59 | 60 | 在个人博客的开发中,需要用到这种技巧的主要地方就是**评论模块**了,尝试去应用吧。 61 | 62 | > 实际上 Django 有一个专门处理对应不同模型的外键,叫 `GenericForeignKey`,但我不太喜欢,有兴趣的同学请读[官方文档](https://docs.djangoproject.com/en/3.0/ref/contrib/contenttypes/)。有关这个话题还有一篇经典的文章[为什么你应该避免使用GenericForeignKey](https://lukeplant.me.uk/blog/posts/avoid-django-genericforeignkey/),里面介绍了 5 种替代方案,值得一读。 -------------------------------------------------------------------------------- /md/110-@property与模型方法.md: -------------------------------------------------------------------------------- 1 | 我们在写 Django 程序时,很容易让**视图**承担了太多不应该的功能,以致让其快速膨胀。当你感觉某一个功能对另一个对象的关注度远远超过视图自身时,就应该考虑此功能是不是放错位置了。 2 | 3 | 在这个前提下,将适当的业务逻辑摆放到**模型**中就是宝贵的学问了。 4 | 5 | 比如下面展示的这两个模型方法: 6 | 7 | ```python 8 | class Person(models.Model): 9 | first_name = models.CharField(max_length=50) 10 | last_name = models.CharField(max_length=50) 11 | 12 | def full_name_with_midname(self, midname): 13 | return f"{self.first_name} {midname} {self.last_name}" 14 | 15 | @property 16 | def full_name(self): 17 | return f"{self.first_name} {self.last_name}" 18 | ``` 19 | 20 | 很显然这两个方法对 `Person` 的关注度很高,因此写在模型中是合适的。 21 | 22 | 在视图中这样子调用: 23 | 24 | ```python 25 | name1 = person.full_name # return 'Du Sai' 26 | name2 = person.full_name_with_midname('Trump') # return 'Du Trump Sai' 27 | ``` 28 | 29 | 因为用了 `@property` 装饰器,所以可以像获取变量一样调用 `def full_name()` 了,很适合用于计算型变量。 30 | 31 | 这个例子看起来不足为奇,但如果模块功能复杂、语句很多时,将逻辑从视图中抽离有助于保持视图的清爽,也方便复用。 -------------------------------------------------------------------------------- /md/120-模型的抽象基类和多表继承.md: -------------------------------------------------------------------------------- 1 | Django 中模型的继承大体上与 Python 原生的继承差不多,但又有些区别。 2 | 3 | 主要有下面三种形式。 4 | 5 | ### 抽象基类 6 | 7 | 抽象基类通常就是你想要的继承形式,像这样: 8 | 9 | ```python 10 | class Animal(models.Model): 11 | name = models.CharField(max_length=100) 12 | age = models.IntegerField(default=5) 13 | finger_count = models.IntegerField(default=10) 14 | 15 | class Meta: 16 | # 告诉 Django 这是个抽象基类 17 | abstract = True 18 | 19 | 20 | class Bird(Animal): 21 | flying_height = models.IntegerField(default=2000) 22 | # 覆写父类字段 23 | age = models.TextField(default='5 years old') 24 | # 删除父类字段 25 | finger_count = None 26 | ``` 27 | 28 | 上面代码中的 `Animal` 就是抽象基类,在迁移时它不会真的在数据库里生成一张 `Animal` 的表。它的作用就是把字段继承到 `Bird` 子类中,并且这些字段可以被覆写,也可以被删除。 29 | 30 | ### 多表继承 31 | 32 | 与抽象基类不同,多表继承的父类和子类都会生成在数据库中: 33 | 34 | ```python 35 | class Human(models.Model): 36 | name = models.CharField(max_length=100) 37 | 38 | 39 | class Baby(Human): 40 | age = models.IntegerField(default=0) 41 | ``` 42 | 43 | 现在数据库同时存在 `Human` 和 `Baby` 两张表了。**更重要的是这两张表并不是分离的,而是用外键链接起来的**。具体来说,当你保存了一个 `Baby` 表时,同时也创建了一个 `Human` 表,而且 `name` 字段是存储在 `Human` 表中的,像这样: 44 | 45 | ```python 46 | >>> human = Human.objects.get(id=1) 47 | >>> human.baby 48 | 49 | # 如果 .baby 不存在,则会报错 50 | ``` 51 | 52 | 也就是说,多表继承其实有一个隐藏的 `OneToOneField` 外键: 53 | 54 | ```python 55 | human_ptr = models.OneToOneField( 56 | Human, on_delete=models.CASCADE, 57 | parent_link=True, 58 | primary_key=True, 59 | ) 60 | ``` 61 | 62 | 在某些情况下,你可以声明 `parent_link` 来显示的覆写它。 63 | 64 | 注意,多表继承不允许你覆写父类中已有的字段,这和抽象基类也是不同的。 65 | 66 | ### 代理模式 67 | 68 | 有时候你可能并不想改变父类的字段内容,而仅仅是想改变模型的某些行为模式。这时候代理模式就是你的好选择: 69 | 70 | ```python 71 | class Car(models.Model): 72 | name = models.CharField(max_length=30) 73 | 74 | 75 | class MyCar(Car): 76 | class Meta: 77 | ordering = ["name"] 78 | proxy = True 79 | 80 | def do_something(self): 81 | # ... 82 | pass 83 | ``` 84 | 85 | `MyCar` 并不会在数据库中生成一张表,而仅仅是给父类 `Car` 添加了方法、改变了排序诸如此类的东西,并且你可以像真的有这张表一样去操作它: 86 | 87 | ```python 88 | >>> my_car = MyCar.objects.get(name='BiYaDi') 89 | >>> my_car 90 | 91 | >>> my_car.name 92 | 'BiYaDi' 93 | ``` 94 | 95 | 所有对 `MyCar` 的修改都会体现在 `Car` 的数据表中。垂帘听政,这就是 Django 界的慈禧太后啊。 96 | 97 | ### 多重继承 98 | 99 | Django 模型可以具有多个父类,这跟 Python 是一样的,并且解析规则也是相同的。也就是说,如果多个父类都包含一个 `Meta` 类,那么只有第一个会被使用,而其他的将被忽略。 100 | 101 | 多个父类同时拥有 `id` 字段将引发错误,你必须显式指定它们: 102 | 103 | ```python 104 | class Article(models.Model): 105 | article_id = models.AutoField(primary_key=True) 106 | 107 | class Book(models.Model): 108 | book_id = models.AutoField(primary_key=True) 109 | 110 | class BookReview(Book, Article): 111 | pass 112 | ``` 113 | 114 | 或者继承同一个祖先的 `AutoField` : 115 | 116 | ```python 117 | class Piece(models.Model): 118 | pass 119 | 120 | class Article(Piece): 121 | article_piece = models.OneToOneField( 122 | Piece, 123 | ... 124 | parent_link=True 125 | ) 126 | 127 | class Book(Piece): 128 | book_piece = models.OneToOneField( 129 | Piece, 130 | ... 131 | parent_link=True 132 | ) 133 | 134 | class BookReview(Book, Article): 135 | pass 136 | ``` 137 | 138 | 记住,除了抽象基类外,Django 的模型是不允许覆写父类的字段的。 -------------------------------------------------------------------------------- /md/130-User模型的扩展.md: -------------------------------------------------------------------------------- 1 | `Django` 内置了开箱即用的 `User` 用户模型,但是很多时候难免满足不了实际的开发需求:比方说国内总喜欢收集用户的手机号,这就需要扩展内置 `User` 模型了。 2 | 3 | 扩展 `User` 模型的途径很多,重点介绍最常用的两种。 4 | 5 | ### 外链扩展 6 | 7 | 这种方式完全不改变 `User` 本身的结构,而是用 `OneToOneField` 将扩展字段链接起来,像这样: 8 | 9 | ```python 10 | from django.db import models 11 | from django.contrib.auth.models import User 12 | from django.db.models.signals import post_save 13 | from django.dispatch import receiver 14 | 15 | 16 | class Profile(models.Model): 17 | user = models.OneToOneField(User, on_delete=models.CASCADE) 18 | phone_number = models.CharField(max_length=20) 19 | 20 | # And other fields you want... 21 | 22 | 23 | @receiver(post_save, sender=User) 24 | def create_user_profile(sender, instance, created, **kwargs): 25 | if created: 26 | Profile.objects.create(user=instance) 27 | 28 | 29 | @receiver(post_save, sender=User) 30 | def save_user_profile(sender, instance, **kwargs): 31 | instance.profile.save() 32 | ``` 33 | 34 | 代码中运用到了信号的概念,即每当 `User`调用 `save()` 方法时,都会自动调用下面的两个函数,从而确保每个 `User` 都对应了一个 `Profile`。如果你不想用信号,自己写逻辑保证它们的对应关系也是可以的,随便你。 35 | 36 | 这种方式的好处是不改变 `User` 本身的结构,做更改也很灵活。缺点是多了一个外链就多了一层查询,降低了效率。总的来说,一般的小网站对效率没有很高的要求,所以这种方法对大部分人是完全可以接受的。 37 | 38 | ### 扩展 AbstractUser 39 | 40 | `AbstractUser` 其实就是 `User` 的父类抽象模型,它提供了默认 `User` 的全部实现。 41 | 42 | 这种方式扩展起来也不难: 43 | 44 | ```python 45 | # models.py 46 | 47 | from django.contrib.auth.models import AbstractUser 48 | 49 | class MyUser(AbstractUser): 50 | phone_number = models.CharField(max_length=20) 51 | 52 | # And other fields you want... 53 | ``` 54 | 55 | 然后你还要让 Django 知道你现在用的是自定义的模型了,所以要在 `settings.py` 里加上这句: 56 | 57 | ```python 58 | # settings.py 59 | 60 | # xxx 是你自定义的 app 的名称 61 | AUTH_USER_MODEL = 'xxx.MyUser' 62 | ``` 63 | 64 | 然后你就发现内置后台中的 `User` 入口消失了,并且普通的 `admin.site.register(MyUser)` 还有问题,即密码居然是用明文存储的。这是因为 Django 不知道应该怎么去处理自定义的模型。因此要改一下 `admin.py` 的注册方式: 65 | 66 | ```python 67 | # admin.py 68 | 69 | from django.contrib.auth.admin import UserAdmin 70 | 71 | ADDITIONAL_FIELDS = ((None, {'fields': ('phone_number',)}),) 72 | 73 | class MyUserAdmin(UserAdmin): 74 | fieldsets = UserAdmin.fieldsets + ADDITIONAL_FIELDS 75 | add_fieldsets = UserAdmin.fieldsets + ADDITIONAL_FIELDS 76 | 77 | admin.site.register(MyUser, MyUserAdmin) 78 | ``` 79 | 80 | 这段代码的意思就是说,我现在要注册的这个定制的 `MyUser` 基本沿用默认的实现,并且添加了扩展的字段。再回到后台中,可以看到密码已经哈希过了,并且扩展的字段也都能正常显示了。 81 | 82 | 这种方式的好处就是把所有字段都整合到同一个表中,并且还具有默认 `User` 的完全实现。缺点就是会改变用户模型的结构,所以尽可能在项目开始时就谨慎考虑,谨慎使用。 83 | 84 | > 本文参考了 [How to Extend Django User Model](https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html),文章中还介绍了代理模式扩展和 `AbstractBaseUser` 扩展,有兴趣的读者可以研究一下。 -------------------------------------------------------------------------------- /md/140-管理静态文件.md: -------------------------------------------------------------------------------- 1 | Django 水友群里问得最多的,就是找不到静态文件的问题了,各种姿势的 `404 not found` 层出不穷。 2 | 3 | 到底应该怎么管理静态文件,请看下面的解答,希望对你有帮助。 4 | 5 | > Django Version >= 3.0 6 | 7 | ## 开发阶段 8 | 9 | ### 方案一 10 | 11 | 首先请保证打开调试模式: 12 | 13 | ```python 14 | # settings.py 15 | 16 | DEBUG = True 17 | ``` 18 | 19 | 开发阶段时这个选项通常都是 `True` ,以便获得框架提供的 `Debug` 功能。 20 | 21 | 然后请确保注册了如下应用: 22 | 23 | ```python 24 | # settings.py 25 | 26 | INSTALLED_APPS = [ 27 | ... 28 | 'django.contrib.staticfiles', 29 | ... 30 | ] 31 | ``` 32 | 33 | 这是 Django 内置的也是默认注册的 App,功能是帮你管理静态文件。 34 | 35 | 接下来,还需要配置这些: 36 | 37 | ```python 38 | # settings.py 39 | 40 | STATIC_URL = '/static/' 41 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 42 | ``` 43 | 44 | 这样就可以了,你甚至都不需要配置专门的路由,因为 `contrib.staticfiles` 帮你搞好了。 45 | 46 | 接下来就可以创建 `static/` 目录,并且把静态文件都放到这个目录下。注意这个 `static/` 目录直接位于项目的根目录下,与 `templates/` 、以及其他你创建的 `app` 是同一级的。 47 | 48 | 然后就可以愉快的在模板中引用静态文件了,比如: 49 | 50 | ```html 51 | 52 | 53 | 54 | 55 | 56 | ``` 57 | 58 | ### 方案二 59 | 60 | 另一个种方法是不依赖 `contrib.staticfiles` ,手动给静态文件提供服务: 61 | 62 | ```python 63 | # settings.py 64 | 65 | INSTALLED_APPS = [ 66 | ... 67 | # 注释掉它 68 | # 'django.contrib.staticfiles', 69 | ... 70 | ] 71 | 72 | DEBUG = True 73 | 74 | STATIC_URL = '/static/' 75 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 76 | # 注释掉它 77 | # STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 78 | ``` 79 | 80 | 所以此时你需要手动添加静态文件的路由解析: 81 | 82 | ```python 83 | # urls.py 84 | 85 | from django.conf import settings 86 | from django.conf.urls.static import static 87 | 88 | urlpatterns = [ 89 | ... 90 | ] 91 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 92 | ``` 93 | 94 | 这样子就可以了,也是同样的效果。 95 | 96 | > 这种方式的问题是后台 Admin 的静态文件都无法加载了,必须另外想办法手动管理起来,比如用部署时经常用到的 `collectstatic` 把相关文件收集起来,再配置路径。沿用了内置 admin 的同学还是尽量用第一种方案更方便。 97 | 98 | ## 部署阶段 99 | 100 | 部署阶段的套路就完全不同了,最主要的区别是这个东西: 101 | 102 | ```python 103 | # settings.py 104 | 105 | DEBUG = False 106 | ``` 107 | 108 | 此时 Django 就不再管理静态文件了,哪怕你配置了路由也不行。这是因为静态文件由 Django 来管理的效率实在是太低了,应该交由更高效的网络服务管理起来,如 `Nginx` 等。 109 | 110 | 即所有静态文件的请求都由 `Nginx` 直接处理,完全不经过 Django 了,所以此时不管你怎么折腾 Django 的配置都是没用的了。 111 | 112 | 部署阶段的静态文件管理在我之前的文章 [《将博客部署到线上》](https://www.dusaiphoto.com/article/detail/71/) 有过详细探讨了,有兴趣的朋友可以参照。 -------------------------------------------------------------------------------- /md/150-批量上传图片.md: -------------------------------------------------------------------------------- 1 | 批量上传文件、图片都是一样的,**第一步就是要把前端表单写对**: 2 | 3 | ```html 4 |
{% csrf_token %} 8 | 12 | 13 |
14 | ``` 15 | 16 | `enctype="multipart/form-data"` 允许表单提交文件,必须写这一项。`multiple="multiple"` 允许一次提交多个文件。 17 | 18 | 接下来配置文件路径: 19 | 20 | ```python 21 | # settings.py 22 | 23 | MEDIA_URL = '/media/' 24 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') 25 | ``` 26 | 27 | 注册到路由中去: 28 | 29 | ```python 30 | # urls.py 31 | 32 | from django.conf import settings 33 | from django.conf.urls.static import static 34 | 35 | 36 | urlpatterns = [ 37 | ... 38 | ] 39 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 40 | ``` 41 | 42 | 弄好了上面两步,Django 才知道去哪里找到上传的文件。 43 | 44 | > 不配置其实也是可以上传的,只不过读取这些文件需要自己配置路径。 45 | 46 | 接下来就是模型、视图、路由了: 47 | 48 | ```python 49 | # models.py 50 | class Image(models.Model): 51 | image = models.ImageField(upload_to='images/%Y%m%d') 52 | 53 | # ------------------ 54 | 55 | # views.py 56 | def uploads_files(request): 57 | if request.method == 'POST': 58 | files = request.FILES.getlist('file_field') 59 | for f in files: 60 | file = Image(image=f) 61 | file.save() 62 | return ... 63 | 64 | # ------------------ 65 | 66 | # app/urls.py 67 | path('uploads/', uploads_files, name='uploads') 68 | ``` 69 | 70 | > `ImageField` 字段依赖三方库 `Pillow` ,请先将它安装到项目中。 71 | 72 | 就这么简单,提交的图片就批量保存好了。比如今天是2020年5月11日,那么照片就会保存在 `media/images/20200511/` 目录里面。并且即使你上传重名的文件也没关系,Django 会妥善的帮你处理好文件重命名的问题,保证不会引用错误或者互相覆盖。 73 | 74 | 这里给出的是最小实现。实际开发中你还需要对上传的图片做一些校验,比如是否真的是图片、限制图片尺寸等等,都可以在视图里扩展相关功能。 75 | 76 | 上传成功后,你可以在模板上下文中愉快的引用它们了: 77 | 78 | ```html 79 | 80 | ``` 81 | 82 | 文件的上传也是一样的,把 `ImageField` 改成 `FileField` 就行了。 83 | 84 | 最后提醒一句,当你在数据库中删除 `Image` 模型时,仅仅只是删除了模型对图片的引用,图片本体还是安然无恙的躺在你的硬盘中的。你需要写额外的代码,手动管理文件本体的去留。 85 | 86 | ### 在后台中预览图片 87 | 88 | 这个话题虽然不太相关,但在这里也一并说了。 89 | 90 | 首先将图片模型改成这样: 91 | 92 | ```python 93 | class Image(models.Model): 94 | image = models.ImageField(upload_to='images/%Y%m%d') 95 | 96 | def admin_image(self): 97 | return '' % self.image 98 | 99 | admin_image.allow_tags = True 100 | ``` 101 | 102 | 然后将这个新函数注册到后台中: 103 | 104 | ```python 105 | # admin.py 106 | 107 | class ImageAdmin(admin.ModelAdmin): 108 | list_display = ('image', 'admin_image') 109 | 110 | admin.site.register(Image, ImageAdmin) 111 | ``` 112 | 113 | 现在你点击图片列表中某一个图片的名称,就直接进入预览界面了,体验稍稍提高了一点点。 -------------------------------------------------------------------------------- /md/160-QuerySet进行查询.md: -------------------------------------------------------------------------------- 1 | 以博客文章模型为例: 2 | 3 | ```python 4 | # models.py 5 | 6 | class Post(models.Model): 7 | owner = models.ForeignKey(User, ..., related_name='posts') 8 | title = models.CharField(...) 9 | body = models.TextField(...) 10 | created = models.DateTimeField(...) 11 | ``` 12 | 13 | 要从模型中检索对象,首先要构建一个管理器。默认情况下的管理器是这个: 14 | 15 | ```python 16 | >>> Post.objects 17 | 18 | 19 | >>> p = Post(title='...') 20 | >>> p.objects 21 | Traceback: 22 | ... 23 | AttributeError: "Manager isn't accessible via Post instances." 24 | ``` 25 | 26 | 管理器只能通过模型类获得,而不能通过实例获得。 27 | 28 | 最常用的检索就是获取所有对象了: 29 | 30 | ```python 31 | >>> all_posts = Post.objects.all() 32 | >>> all_posts 33 | , , ]> 34 | ``` 35 | 36 | 得到的结果是满足检索要求的集合,这就是通常说的 `QuerySet`。 37 | 38 | 检索特定对象用 `filter()`: 39 | 40 | ```python 41 | # 获取 2019 年发布的文章 42 | Post.objects.filter(created__year=2019) 43 | ``` 44 | 45 | 如果你要排除某些特定对象,可以用 `exclude()`: 46 | 47 | ```python 48 | # 获取 2019 年以外时间发布的文章 49 | Post.objects.exclude(created__year=2019) 50 | ``` 51 | 52 | 也可以做链式查询: 53 | 54 | ```python 55 | Post.objects.filter(created__year=2019).exclude(title__startswith='Foo') 56 | ``` 57 | 58 | 链多少层都可以,只要你喜欢。 59 | 60 | 此外,查询集在执行时不会原地修改,每次都会返回一个全新的子集: 61 | 62 | ```python 63 | a = Post.objects.all() 64 | b = a.exclude(created__year=2019) 65 | ``` 66 | 67 | 上面代码中的 `a` 、 `b` 都是独立分开的查询集,互不影响。 68 | 69 | `QuerySet` 在创建时是懒惰的,即并不会涉及数据库的操作: 70 | 71 | ```python 72 | >>> a = Post.objects.all() 73 | >>> b = a.exclude(created__year=2019) 74 | >>> c = b.filter(title__startswith='Foo') 75 | >>> print(c) 76 | ``` 77 | 78 | 上面的代码看起来像是对数据库查询了三次,但实际上只是在 `print(c)` 时才会真正执行查询。 79 | 80 | 稍有不同的是切片: 81 | 82 | ```python 83 | >>> d = Post.objects.all()[1:10] 84 | ``` 85 | 86 | 上面这句不会执行查询,道理相同。 87 | 88 | 下面这句就不一样了: 89 | 90 | ```python 91 | >>> e = Post.objects.all()[:10:2] 92 | ``` 93 | 94 | 这句是会执行查询的,Django 需要查询数据库以便返回带有间隔的列表。换句话说,带有步长的切片就会触发查询。 95 | 96 | 有一些管理器方法并不返回 `QuerySet` ,而是返回一个模型对象或者变量。比如 `get()` 方法可以取得单个确定的对象: 97 | 98 | ```python 99 | >>> Post.objects.get(id=1) 100 | ``` 101 | 102 | `.create()`、`.first()`、`last()`、`count()`、`exists()`都属于这一类。 103 | 104 | 跨关系的查找也可以: 105 | 106 | ```python 107 | >>> Post.objects.get(owner__username='dusai') 108 | ``` 109 | 110 | 如果 `get()` 返回了多个或者零个结果会报错。 111 | 112 | 我们已经多次用到如 `owner__username` 这种参数形式了。其实这里的 `__` 你理解成 `.` 就好了,因为语法规则里关键字不能用 `.` ,所以就用 `__` 来代替了。 113 | 114 | 还有 `title__startswith='Foo'` 这类用法,`startswith` 显然不是模型字段,而是指查找以 `Foo` 打头的 `title` 字段的相关数据。类似的查询方法还有: 115 | 116 | ```python 117 | exact # 完全匹配 118 | iexact # 不区分大小写的完全匹配 119 | contains # 包含 120 | icontains # 不区分大小写的包含 121 | in # 被包含在给定的集合中,如 Post.objects.filter(id__in=[2, 3, 4]) 122 | gt # 大于,如 Post.objects.filter(id__gt=3) 123 | gte # 大于或等于 124 | lt # 小于 125 | lte # 小于或等于 126 | startswith # 以 xx 开头 127 | istartswith # 不区分大小写的以 xx 开头 128 | endswith 129 | iendswith 130 | range # 在范围内 131 | date # 日期字段使用,如 Post.objects.filter(created__date=datetime.date(2020, 1, 1)) 132 | year, month, day... 133 | isnull 134 | regex # 匹配正则 135 | iregex 136 | ``` 137 | 138 | 有这么多,够你折腾了。 -------------------------------------------------------------------------------- /md/170-Manager()自定义模型方法.md: -------------------------------------------------------------------------------- 1 | 如果你需要在保存数据前先进行一些操作,那么需要覆写 `save()` 实例方法: 2 | 3 | ```python 4 | class Book(models.Model): 5 | title = models.CharField(max_length=100) 6 | 7 | def save(self, *args, **kwargs): 8 | # 在这里执行自定义逻辑 9 | ... 10 | 11 | super().save(*args, **kwargs) 12 | ``` 13 | 14 | 使用时就像 Python 那样,正常 `book.save()` 调用,很简单。 15 | 16 | 如果需要在创建新模型时塞点私货就有点点不同,因为创建这个动作由类本身来执行,而不是实例。 17 | 18 | 所以要这么写: 19 | 20 | ```python 21 | class Book(models.Model): 22 | title = models.CharField(max_length=100) 23 | 24 | @classmethod 25 | def create(cls, title): 26 | book = cls(title=title) 27 | # do something with the book 28 | return book 29 | ``` 30 | 31 | 这样子调用: 32 | 33 | ```python 34 | book = Book.create('Foo') 35 | book.save() 36 | ``` 37 | 38 | 还有一种更加推荐的方式: 39 | 40 | ```python 41 | class BookManager(models.Manager): 42 | def create_book(self, title): 43 | book = self.create(title=title) 44 | # do something with the book 45 | book.save() 46 | return book 47 | 48 | 49 | class Book(models.Model): 50 | title = models.CharField(max_length=100) 51 | 52 | objects = models.Manager() # 默认管理器 53 | custom = BookManager() # 自定义管理器 54 | ``` 55 | 56 | 这种方式就用到了**模型管理器**这个神奇的玩意了。 57 | 58 | 虽说神奇,但是你随时都在用到: 59 | 60 | ```python 61 | Obj.objects.create() # 中间那个 objects 就是友情赠送的默认管理器 62 | ``` 63 | 64 | 除了默认管理器,还自定义了一个新管理器 `custom`,它里面有一个 `create_book` 方法,是这样子使用的: 65 | 66 | ```python 67 | book = Book.custom.create_book('Bar') 68 | ``` 69 | 70 | 除此之外,你还可以修改管理器的初始查询集: 71 | 72 | ```python 73 | class BookManager(models.Manager): 74 | def get_queryset(self): 75 | return super().get_queryset().filter(title__contains='money') 76 | 77 | 78 | class Book(models.Model): 79 | title = models.CharField(max_length=100) 80 | 81 | objects = models.Manager() 82 | custom = BookManager() 83 | ``` 84 | 85 | 上面这坨代码,`Book.objects.all()` 返回所有的书籍,而 `Book.custom.all()` 仅返回标题里包含 `money` 的书籍。 86 | 87 | 你甚至可以在管理器里定义新方法: 88 | 89 | ```python 90 | class PollManager(models.Manager): 91 | def with_counts(self): 92 | from django.db import connection 93 | with connection.cursor() as cursor: 94 | cursor.execute(""" 95 | SELECT p.id, p.question, p.poll_date, COUNT(*) 96 | FROM polls_opinionpoll p, polls_response r 97 | WHERE p.id = r.poll_id 98 | GROUP BY p.id, p.question, p.poll_date 99 | ORDER BY p.poll_date DESC""") 100 | result_list = [] 101 | for row in cursor.fetchall(): 102 | p = self.model(id=row[0], question=row[1], poll_date=row[2]) 103 | p.num_responses = row[3] 104 | result_list.append(p) 105 | return result_list 106 | 107 | class OpinionPoll(models.Model): 108 | question = models.CharField(max_length=200) 109 | poll_date = models.DateField() 110 | objects = PollManager() 111 | 112 | class Response(models.Model): 113 | poll = models.ForeignKey(OpinionPoll, on_delete=models.CASCADE) 114 | person_name = models.CharField(max_length=50) 115 | response = models.TextField() 116 | ``` 117 | 118 | `with_counts()` 方法返回所有 `OpinionPoll` 对象,并且每个对象附带一个 `num_responses` 聚合查询属性。 -------------------------------------------------------------------------------- /md/180-Migrations数据迁移.md: -------------------------------------------------------------------------------- 1 | 如果你不熟悉 Web 开发,那你可能很难理解**数据迁移**为什么是一个强力的功能。 2 | 3 | ## 对象关系映射 4 | 5 | 通俗的讲,数据库是你存放数据的地方(废话)。关系型数据库又是数据库中的一种,其中的数据以表的形式组织,表具有一定数量的列、任意数量的行,每张表又可以通过外键连接其他的表。 6 | 7 | 表中每列都有特定的数据类型,这就是 Django 里常说的字段了。每一行就是表中的一条数据。比如下面这个: 8 | 9 | | id(integer) | title(string) | created(datetime) | 10 | | ----------- | ------------- | ------------------- | 11 | | 1 | Django | 2020-05-09 07:57:50 | 12 | | 2 | vs | 2020-05-10 09:58:05 | 13 | | 3 | Flask | 2020-05-17 17:00:13 | 14 | | ... | ... | ... | 15 | 16 | 关系型数据库的增删改查等操作,需要用到的是 SQL 语言。Django 为了保护程序员的头发,附带了一个对象关系映射器(简称 ORM),可以将数据库 SQL 映射到面向对象的 Python 中来,使得你可以在 Django 中像操作普通对象一样操作数据库。其直观表现就是模型 (Model)。 17 | 18 | 上面的表写成模型长这样: 19 | 20 | ```python 21 | class Post(models.Model): 22 | # id 字段不需要自己写 23 | title = models.TextField() 24 | created = models.DateTimeField() 25 | ``` 26 | 27 | 但是定义好了模型,数据库中的表并不会神奇的出现,你还需要把模型转化为对数据库的操作,这就是迁移 Migrations。 28 | 29 | ## 迁移工作流 30 | 31 | 新建一个项目,并在项目中创建一个叫 `mig` 的 app。 32 | 33 | 然后必须在 `INSTALLED_APPS` 配置中添加 `mig` ,并且 `mig` 还得带有 `migrations/` 目录以及目录下的 `__init__.py` 文件,否则 Django 不会为这个 app 创建任何迁移。 34 | 35 | 在 `models.py` 中创建如下模型: 36 | 37 | ```python 38 | # mig/models.py 39 | 40 | from django.db import models 41 | from django.utils import timezone 42 | 43 | class Pen(models.Model): 44 | price = models.IntegerField() 45 | color = models.CharField(default='black', max_length=20) 46 | purchase_date = models.DateTimeField(default=timezone.now) 47 | ``` 48 | 49 | 具有价格、颜色、购买日期的笔,很合理。 50 | 51 | 接下来在命令行执行 `makemigrations` 指令: 52 | 53 | ```python 54 | > python manage.py makemigrations 55 | # 下面是输出 56 | Migrations for 'mig': 57 | mig\migrations\0001_initial.py 58 | - Create model Pen 59 | Following files were affected 60 | D:\...\mig\migrations\0001_initial.py 61 | ``` 62 | 63 | 如上面的输出文字所述,指令执行完毕后会生成 `mig/migrations/0001_initial.py` 文件。在执行 `makemigrations` 指令时,Django 不会检查你的数据库,而是根据目前的模型的状态,创建一个操作列表,使项目状态与模型定义保持最新。 64 | 65 | 来看看这个文件的内容: 66 | 67 | ```python 68 | from django.db import migrations, models 69 | import django.utils.timezone 70 | 71 | class Migration(migrations.Migration): 72 | initial = True 73 | 74 | dependencies = [] 75 | 76 | operations = [ 77 | migrations.CreateModel( 78 | name='Pen', 79 | fields=[ 80 | ('id', models.AutoField(...)), 81 | ('price', models.IntegerField()), 82 | ('color', models.CharField(...)), 83 | ('purchase_date', models.DateTimeField(...)), 84 | ], 85 | ), 86 | ] 87 | ``` 88 | 89 | 就是一个普通的 Python 文件嘛: 90 | 91 | - `initial` :初次迁移。 92 | - `dependencies`:因为是初次迁移,没有依赖项,所以这里为空。 93 | - `operations`:迁移的具体操作就在这里了。`CreateModel` 表示创建新表,`name` 即表名,`fields` 则是表中的字段。 94 | 95 | 注意这个时候数据库是没有变化的。直到执行了 `migrate` 指令: 96 | 97 | ```python 98 | > python manage.py migrate 99 | # 下面是输出 100 | Operations to perform: 101 | Apply all migrations: admin, auth, contenttypes, mig, sessions 102 | Running migrations: 103 | Applying contenttypes.0001_initial... OK 104 | Applying auth.0001_initial... OK 105 | ... 106 | Applying mig.0001_initial... OK # mig 的迁移 107 | ... 108 | ``` 109 | 110 | 输出中似乎有很多不认识的迁移,不要虚,那些是 Django 自身运行所需要的表。关键是这个 `Applying mig.0001_initial... OK`,表示 `mig` 的迁移已经成功了。 111 | 112 | 打开数据库可以看到多了 `mig_pen` 表,并且里面的字段和模型是完全匹配的。 113 | 114 | ## 迁移文件 115 | 116 | 初次迁移完成后,你突然发现 `price` 字段不应该为整型,以便正确表示带小数的金额: 117 | 118 | ```python 119 | class Pen(models.Model): 120 | price = models.DecimalField(max_digits=7, decimal_places=2) 121 | ... 122 | ``` 123 | 124 | 执行完迁移后,又多出了 `mig\migrations\0002_auto_20200519_1659.py` 文件: 125 | 126 | ```python 127 | class Migration(migrations.Migration): 128 | dependencies = [ 129 | ('mig', '0001_initial'), 130 | ] 131 | 132 | operations = [ 133 | migrations.AlterField( 134 | model_name='pen', 135 | name='price', 136 | field=models.DecimalField(decimal_places=2, max_digits=7), 137 | ), 138 | ] 139 | ``` 140 | 141 | 此时 `dependencies` 列表不再为空了,里面是本次迁移所依赖的文件,即第一次迁移的 `0001_initial.py` 。由此的注意事项: 142 | 143 | - `migrations` 目录下的迁移文件非常重要并且**相互依赖**,一般情况下不要随意去修改(虽然 Django 允许你手动维护)。 144 | - 通常情况下,对数据库的操作尽可能通过迁移的方式。如果因为某些原因需要手动修改,那么你需要做好手动维护的准备。 145 | 146 | 继续回到代码。 `operations` 列表中的 `AlterField` 表示这次是更改操作。Django 内部有一套机制来尽可能的判断用户对模型的操作的具体类型,但是如果你一次进行了很多复杂的改动(比如同时进行多项修改、删除、新增),那么它也会犯糊涂,不知道你想干什么。为了避免这种尴尬的事情,对数据库下手不要太重。 147 | 148 | 再修改模型试试: 149 | 150 | ```python 151 | class Pen(models.Model): 152 | price = models.DecimalField(max_digits=7, decimal_places=2) 153 | # 我不想要 color 字段了 154 | # color = models.CharField(default='black', max_length=20) 155 | purchase_date = models.DateTimeField(default=timezone.now) 156 | ``` 157 | 158 | 新增的迁移文件如下: 159 | 160 | ```python 161 | # 0003_remove_pen_color.py 162 | 163 | class Migration(migrations.Migration): 164 | dependencies = [ 165 | ('mig', '0002_auto_20200519_1659'), 166 | ] 167 | 168 | operations = [ 169 | migrations.RemoveField( 170 | model_name='pen', 171 | name='color', 172 | ), 173 | ] 174 | 175 | ``` 176 | 177 | 你可以更清楚的看出迁移文件的工作模式了,即每个迁移文件记录的仅仅是和上一次的变化,每一次对数据库的操作是高度依赖的。 178 | 179 | 你还可以通过指令查看迁移文件将实际执行的 SQL 操作: 180 | 181 | ```python 182 | > python manage.py sqlmigrate mig 0003 183 | ... 184 | BEGIN; 185 | -- 186 | -- Remove field color from pen 187 | -- 188 | CREATE TABLE "new__mig_pen" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "price" decimal NOT NULL, "purchase_date" datetime NOT NULL); 189 | INSERT INTO "new__mig_pen" ("id", "price", "purchase_date") SELECT "id", "price", "purchase_date" FROM "mig_pen"; 190 | DROP TABLE "mig_pen"; 191 | ALTER TABLE "new__mig_pen" RENAME TO "mig_pen"; 192 | COMMIT; 193 | ``` 194 | 195 | ## 迁移记录表 196 | 197 | 很好,我们已经知道迁移文件的工作方式了。 198 | 199 | 现在我们尝试一下不修改模型,直接迁移: 200 | 201 | ```python 202 | > python manage.py makemigrations 203 | 204 | No changes detected 205 | 206 | > python manage.py migrate 207 | 208 | Operations to perform: 209 | Apply all migrations: ..., mig, ... 210 | Running migrations: 211 | No migrations to apply. 212 | ``` 213 | 214 | 没有任何迁移被执行。所以 Django 是如何得知哪些操作已经执行过了、哪些操作还没执行呢? 215 | 216 | 奥秘就在于数据库中的 `django_migrations` 表。这是由 Django 自动管理的表,里面记录了你每一次迁移的历史回溯: 217 | 218 | | id | app | name | applied | 219 | | ---- | ---- | ----------------------- | -------------- | 220 | | ... | ... | ... | ... | 221 | | 14 | mig | 0001_initial | 2020-05-19 ... | 222 | | 15 | mig | 0002_auto_20200519_1659 | 2020-05-19 ... | 223 | | 16 | mig | 0003_remove_pen_color | 2020-05-19 ... | 224 | | ... | ... | ... | ... | 225 | 226 | 表里的每一条记录都和迁移文件是对应的,如果这个表里已经有迁移记录了,那么对应的迁移文件中的指令就不再执行了。 227 | 228 | ### 作死1号 229 | 230 | 接下来我们来作个死,手动将最后一个迁移文件 `0003_remove_pen_color.py` 删除掉,再重新执行迁移: 231 | 232 | ```python 233 | > python manage.py makemigrations 234 | 235 | Migrations for 'mig': 236 | mig\migrations\0003_remove_pen_color.py 237 | - Remove field color from pen 238 | 239 | > python manage.py migrate 240 | 241 | Operations to perform: 242 | Apply all migrations: ...mig, ... 243 | Running migrations: 244 | No migrations to apply. 245 | ``` 246 | 247 | 除了 `0003_remove_pen_color.py` 文件被重新创建外,没有任何事情发生,因为迁移记录表中已经有对应的 0003 号记录了,数据库操作不会重复执行。 248 | 249 | ### 作死2号 250 | 251 | 再次手动将 `0003_remove_pen_color.py` 文件删除掉,并且新增一个模型字段: 252 | 253 | ```python 254 | class Pen(models.Model): 255 | price = models.DecimalField(max_digits=7, decimal_places=2) 256 | purchase_date = models.DateTimeField(default=timezone.now) 257 | 258 | # 上一次迁移时的删除更改 259 | # color = models.CharField(default='black', max_length=20) 260 | # 手动删除 0003 文件后,添加此字段 261 | length = models.IntegerField(default=10) 262 | ``` 263 | 264 | 再次迁移: 265 | 266 | ```python 267 | > python manage.py makemigrations 268 | 269 | Migrations for 'mig': 270 | mig\migrations\0003_auto_20200520_1051.py 271 | - Remove field color from pen 272 | - Add field length to pen 273 | 274 | > python manage.py migrate 275 | 276 | Operations to perform: 277 | Apply all migrations: admin, auth, contenttypes, demo, mig, sessions 278 | Running migrations: 279 | Applying mig.0003_auto_20200520_1051... OK 280 | ``` 281 | 282 | 虽然迁移内容不同,但是由于新增字段导致 0003 号文件名称发生了变化,数据库更改还是成功执行了。 283 | 284 | 但是这里是有坑的。让我们来看看实际的 SQL 指令: 285 | 286 | ```python 287 | > python manage.py sqlmigrate mig 0003 288 | 289 | BEGIN; 290 | -- 291 | -- Remove field color from pen 292 | -- 293 | ... 294 | -- 295 | -- Add field length to pen 296 | -- 297 | CREATE TABLE "new__mig_pen" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "length" integer NOT NULL, "price" decimal NOT NULL, "purchase_date" datetime NOT NULL); 298 | INSERT INTO "new__mig_pen" ("id", "price", "purchase_date", "length") SELECT "id", "price", "purchase_date", 10 FROM "mig_pen"; 299 | DROP TABLE "mig_pen"; 300 | ALTER TABLE "new__mig_pen" RENAME TO "mig_pen"; 301 | COMMIT; 302 | ``` 303 | 304 | 由于内部迁移机制,如果你之前的 `Pen` 表已经有数据了,那么这些数据中的 `length` 字段数据将全部被替换成默认值 10。 305 | 306 | ### 作死3号 307 | 308 | 这次我们不搞最后一条 0003 号文件了。把 0002 号文件删了,重新迁移试试... 309 | 310 | ```python 311 | > python manage.py makemigrations 312 | 313 | Traceback (most recent call last): 314 | File "D:\...\django_manage.py", line 43, in 315 | ... 316 | django...NodeNotFoundError: Migration mig.0003_auto_20200520_1115 dependencies reference nonexistent parent node ('mig', '0002_auto_20200519_1659') 317 | ``` 318 | 319 | 报错意思是说,我现在要迁移 0003 号文件了,但是发现居然找不到 0002 号文件,所以干不下去了。意料之中。怎么办? 320 | 321 | **第一种方式:**既然如此,那我把 0003 号文件的依赖改掉呢: 322 | 323 | ```python 324 | class Migration(migrations.Migration): 325 | dependencies = [ 326 | # ('mig', '0002_auto_20200519_1659'), 327 | ('mig', '0001_initial'), 328 | ] 329 | 330 | operations = [ 331 | ... 332 | ] 333 | 334 | ``` 335 | 336 | 这次迁移是可以成功的,而且 Django 还补了个 0004 号文件把缺失的操作给补上了。 337 | 338 | **第二种方式:**将缺失的依赖之后产生的迁移文件全部删除,也可以成功重新迁移。 339 | 340 | ### 作死4号 341 | 342 | 换一种更深入的作死姿势。假设现在最后一条迁移文件是 `0004_a.py` 。首先删掉它,然后对模型进行修改: 343 | 344 | ```python 345 | class Pen(models.Model): 346 | ... 347 | # 比方说,删除掉 length 字段 348 | # length = models.IntegerField(default=10) 349 | ``` 350 | 351 | 现在重新 `makemigrations` (注意不要 `migrate` ): 352 | 353 | ```python 354 | > python manage.py makemigrations 355 | 356 | Migrations for 'mig': 357 | mig\migrations\0004_b.py 358 | - Remove field length from pen 359 | ... 360 | ``` 361 | 362 | Django 自动生成了迁移文件 `0004_b.py`。精彩的来了,把这个 `0004_b.py` 的名称修改为 `0004_a.py`,然后执行 `migrate` : 363 | 364 | ```python 365 | > python manage.py migrate 366 | 367 | Operations to perform: 368 | Apply all migrations: ..., mig, ... 369 | Running migrations: 370 | No migrations to apply. 371 | ``` 372 | 373 | 删除 `length` 字段的指令没执行!这是因为数据库 `django_migrations` 表已经有同名记录了,Django 觉得这个文件里的操作都执行过了,就不再执行了。 374 | 375 | 这样子的结果就是 Model 和数据库字段不一致,在进行相关 ORM 操作时就会出现各种报错。 376 | 377 | 不要以为这种情况很少见,新手在不正常操作迁移的过程中是有可能发生的。 378 | 379 | ## 迁移伪造 380 | 381 | 如果你哪天真的手贱手动操作了与迁移相关的内容,遇到迁移表和数据库无法正常同步的问题,那么你可能会用到迁移伪造指令 `--fake`。这个指令根据 App 现有的迁移文件内容,伪造 `dango_migrations` 表中的内容,欺骗 Django 的迁移状态,从而帮助你从报错中解脱出来。 382 | 383 | 举个例子。某天你手贱将 `django_migrations` 表中有关于 `mig` App 的记录全删除了,那么就可以用: 384 | 385 | ```python 386 | > python manage.py migrate --fake mig 387 | ``` 388 | 389 | Django 会把 `mig` 中现有的迁移文件的记录全补到 `django_migrations` 。这样做能成功的前提是迁移文件本身没出问题。 390 | 391 | 又比如说因为某些骚操作,0003 号迁移文件中的 model 改动总是无法同步到数据库,那么你可以: 392 | 393 | ```python 394 | > python manage.py migrate --fake mig 0002 395 | ``` 396 | 397 | 可以将 `django_migrations` 表退回到 0002 号迁移文件的位置,然后你可以用重新执行 0003 号文件的迁移等方法进行恢复。(或者删除 0003 号迁移文件,重新 `makemigrations`) 398 | 399 | 又比如说你由于某些原因需要把 `mig` 的迁移记录全部清除,那么可以: 400 | 401 | ```python 402 | > python manage.py migrate --fake mig zero 403 | ``` 404 | 405 | 执行此句后有关 `mig` 的 `django_migrations` 记录将全部消失,你再根据具体情况,进行后续的迁移恢复。 406 | 407 | 也就是说,`migrate --fake` 指令可以修改 `django_migrations` 表中的记录,但并不会真正的修改数据库本身。 408 | 409 | 希望你永远都用不到 `--fake`。 410 | 411 | ## 迁移重建 412 | 413 | 如果经过你一顿骚操作,迁移文件、迁移记录表混乱不堪,并且无法正常迁移或者 ORM 频繁报错,有下面几种方法可以让迁移恢复正常。 414 | 415 | ### 方案1 416 | 417 | **项目在开发过程中,并且你不介意丢弃整个数据库。** 418 | 419 | - 删除每个 App 下的迁移文件,`__init__.py` 除外。 420 | - 删除当前数据库,或者根目录下的 `db.sqlite3` 文件。 421 | - 重新迁移。 422 | 423 | 胜败乃兵家常事,大侠请重新来过。这是最省事的方法。 424 | 425 | ### 方案2 426 | 427 | **你想保留数据,但是某个 App 的迁移文件和数据库未能同步(类似上面的作死4号)。** 428 | 429 | 举例如果 0003 号文件中的操作未能同步,那么执行下面的指令: 430 | 431 | ```python 432 | > python manage.py migrate --fake mig 0002 433 | 434 | Operations to perform: 435 | Target specific migration: 0002_xxx, from mig 436 | Running migrations: 437 | Rendering model states... DONE 438 | Unapplying mig.0003_auto_xxx... FAKED 439 | ``` 440 | 441 | `migrate --fake mig 0002` 指令将数据库中的 `django_migrations` 表回滚到 0002 号文件。 442 | 443 | 查看一下迁移状态: 444 | 445 | ```python 446 | > python manage.py showmigrations 447 | 448 | ... 449 | mig 450 | [X] 0001_initial 451 | [X] 0002_xxx 452 | [ ] 0003_auto_xxx 453 | ... 454 | ``` 455 | 456 | 表示 0003 号文件还未迁移。 457 | 458 | 然后重新迁移就好了: 459 | 460 | ```python 461 | > python manage.py migrate 462 | 463 | Operations to perform: 464 | Apply all migrations: ..., mig, ... 465 | Running migrations: 466 | Applying mig.0003_auto_xxx... OK 467 | ``` 468 | 469 | ### 方案3 470 | 471 | **如果你的数据库是现成的,但是 Django 中没有任何迁移文件。**(比如 Django 是数据库开发完成后才加入的) 472 | 473 | 首先在 `models.py` 中编写模型,确保模型和数据库中的表是完全一致的。 474 | 475 | 首先执行: 476 | 477 | ```python 478 | > python manage.py makemigrations 479 | ``` 480 | 481 | 创建初始迁移文件 `0001_initial.py`。 482 | 483 | 然后执行: 484 | 485 | ```python 486 | > python manage.py migrate --fake-initial mig 487 | ``` 488 | 489 | 这句的意思是:伪造一份 `mig` App 的迁移记录表(`django_migrations`),让 Django 误以为迁移已经完成了。(跟 `--fake` 指令类似) 490 | 491 | 顺利的话就已经搞定了: 492 | 493 | ```python 494 | > python manage.py makemigrations 495 | 496 | No changes detected 497 | 498 | > python manage.py migrate 499 | 500 | Operations to perform: 501 | Apply all migrations: ..., mig, ... 502 | Running migrations: 503 | No migrations to apply. 504 | ``` 505 | 506 | 除了上面三种方法外,前面还介绍了**迁移伪造**、**修改依赖**、**删除错误迁移文件**等方法,请量体裁衣,酌情使用。 507 | 508 | ## 总结 509 | 510 | 折腾这么一圈,你对 `Migrations` 也有一定的了解了。总结起来就是下面这张内涵丰富的图([@frostming](https://frostming.com/)提供): 511 | 512 | ![](http://blog.dusaiphoto.com/migration_workflow.jpg) 513 | 514 | - 数据迁移是一个很强大的功能,让完全不了解 SQL 的人可以以面向对象的方式管理数据库,保持 model 和数据库完全同步。 515 | - `makemigrations` 生成迁移文件是完全不管你的数据表实际什么样,全部是通过 `django_migrations` 的记录和 `migrations` 文件计算出来的。 516 | - 迁移文件是 Django 进行迁移的重要依据且互相依赖,不要随意改动,并应该纳入版本管理。虽然它可以手动修改,但前提是你完全了解它的工作原理。 517 | - 在迁移遭到破坏的情况下,不要想当然的去删表删文件瞎操作,而是利用好 Django 提供的方法,小心翼翼的恢复它。 518 | 519 | **祝迁移愉快!** -------------------------------------------------------------------------------- /md/190-类视图as_view()解析.md: -------------------------------------------------------------------------------- 1 | `Django` 有**函数视图**和**类视图**,分别是这样用的: 2 | 3 | ```python 4 | # 函数视图 5 | path(..., function_view, ...) 6 | # 类视图 7 | path(..., ClassView.as_view(), ...) 8 | ``` 9 | 10 | 这个 `as_view()` 很有意思,我们通过源码来看看它是如何把类转化成函数的。 11 | 12 | 源码不是很长,全贴出来如下所示: 13 | 14 | ```python 15 | class View: 16 | ... 17 | 18 | @classonlymethod 19 | def as_view(cls, **initkwargs): 20 | """Main entry point for a request-response process.""" 21 | for key in initkwargs: 22 | if key in cls.http_method_names: 23 | raise TypeError("You tried to pass in the %s method name as a " 24 | "keyword argument to %s(). Don't do that." 25 | % (key, cls.__name__)) 26 | if not hasattr(cls, key): 27 | raise TypeError("%s() received an invalid keyword %r. as_view " 28 | "only accepts arguments that are already " 29 | "attributes of the class." % (cls.__name__, key)) 30 | 31 | def view(request, *args, **kwargs): 32 | self = cls(**initkwargs) 33 | if hasattr(self, 'get') and not hasattr(self, 'head'): 34 | self.head = self.get 35 | self.setup(request, *args, **kwargs) 36 | if not hasattr(self, 'request'): 37 | raise AttributeError( 38 | "%s instance has no 'request' attribute. Did you override " 39 | "setup() and forget to call super()?" % cls.__name__ 40 | ) 41 | return self.dispatch(request, *args, **kwargs) 42 | view.view_class = cls 43 | view.view_initkwargs = initkwargs 44 | 45 | # take name and docstring from class 46 | update_wrapper(view, cls, updated=()) 47 | 48 | # and possible attributes set by decorators 49 | # like csrf_exempt from dispatch 50 | update_wrapper(view, cls.dispatch, assigned=()) 51 | return view 52 | ``` 53 | 54 | 来一步步分解。 55 | 56 | `as_view()` 是个类方法,它的第一个参数 `cls` 表示类本身,跟实例方法的 `self` 差不多,都是自动传入的。 57 | 58 | 进入 `as_view()` 后首先对传入的参数做简单的校验,避免传入的参数将类自己的关键函数名覆盖掉,或者传入类中没定义的属性。开头这个 `for` 循环就是干这个用的。 59 | 60 | 接着 `as_view()` 内部又定义了一个 `view()` 函数,它接收的参数和普通的**函数视图**是相同的: `request` 对象以及从 `url` 获取的 `args` 和 `kwargs` 参数。我们挑重点看它在干什么: 61 | 62 | ```python 63 | def view(request, *args, **kwargs): 64 | self = cls(**initkwargs) 65 | ... 66 | self.setup(request, *args, **kwargs) 67 | ... 68 | return self.dispatch(request, *args, **kwargs) 69 | ``` 70 | 71 | 首先实例化了类自己 `cls()`,并赋值给 `self` ,也就是你编写的类视图的实例。 72 | 73 | 接着调用 `self.setup()` 对实例的属性进行了初始化。`setup()` 方法非常简单: 74 | 75 | ```python 76 | def setup(self, request, *args, **kwargs): 77 | """Initialize attributes shared by all view methods.""" 78 | self.request = request 79 | self.args = args 80 | self.kwargs = kwargs 81 | ``` 82 | 83 | 把接收的参数原封不动的赋值到类实例中。 84 | 85 | > 这几个属性经常能用到,比如 `self.kwargs.get('id')` 获取 `url` 中传递的 `id` 值。 86 | 87 | `view()` 函数最后返回了 `dispatch()` ,它的源码是这样的: 88 | 89 | ```python 90 | class View: 91 | # dispatch 用到的http请求方法名 -- 杜赛注 92 | http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] 93 | ... 94 | 95 | def dispatch(self, request, *args, **kwargs): 96 | # Try to dispatch to the right method; if a method doesn't exist, 97 | # defer to the error handler. Also defer to the error handler if the 98 | # request method isn't on the approved list. 99 | if request.method.lower() in self.http_method_names: 100 | handler = getattr(self, request.method.lower(), self.http_method_not_allowed) 101 | else: 102 | handler = self.http_method_not_allowed 103 | return handler(request, *args, **kwargs) 104 | ``` 105 | 106 | `dispatch()` 非常简短,功能却非常重要:如果 `request.method` 是一个 `GET` 请求,则调用类视图 `self.get()` 方法,如果是 `POST` 请求,那就调用 `self.post()` 方法。这就起到根据 http 请求类型派发到不同函数的功能,这是类视图的核心了。 107 | 108 | 回到 `as_view()` 来,它最后做了属性赋值、修改函数签名等收尾工作后,返回了 `view` 函数闭包: 109 | 110 | ```python 111 | def as_view(cls, **initkwargs): 112 | ... 113 | 114 | view.view_class = cls 115 | view.view_initkwargs = initkwargs 116 | 117 | # take name and docstring from class 118 | update_wrapper(view, cls, updated=()) 119 | 120 | # and possible attributes set by decorators 121 | # like csrf_exempt from dispatch 122 | update_wrapper(view, cls.dispatch, assigned=()) 123 | 124 | return view 125 | ``` 126 | 127 | `as_view()` 方法就完成了,来总结一下它的核心流程: 128 | 129 | - `as_view()` 内部定义了 `view()` 函数。`view()` 函数对类视图进行初始化,返回并调用了 `dispatch()` 方法。 130 | - `dispatch()` 根据请求类型的不同,调用不同的函数(如 `get()` 、 `post()`),并将这些函数的 `response` 响应结果返回。 131 | - `as_view()` 返回了这个 `view` 函数闭包,供 `path()` 路由调用。 132 | 133 | 把核心部分拿出来就这样: 134 | 135 | ```python 136 | class View: 137 | ... 138 | @classonlymethod 139 | def as_view(cls, **initkwargs): 140 | ... 141 | def view(request, *args, **kwargs): 142 | self = cls(**initkwargs) 143 | ... 144 | self.setup(request, *args, **kwargs) 145 | ... 146 | return self.dispatch(request, *args, **kwargs) 147 | ... 148 | return view 149 | 150 | def setup(self, request, *args, **kwargs): 151 | self.request = request 152 | self.args = args 153 | self.kwargs = kwargs 154 | 155 | def dispatch(self, request, *args, **kwargs): 156 | if request.method.lower() in self.http_method_names: 157 | handler = getattr(self, request.method.lower(), ...) 158 | ... 159 | return handler(request, *args, **kwargs) 160 | ``` 161 | 162 | 结果就是 `as_view()` 返回了一个函数(携带着必要的参数),和你用视图函数时直接传递给路由一个函数的效果是相同的。 163 | 164 | 相当的神奇吧。 -------------------------------------------------------------------------------- /md/200-transaction事务.md: -------------------------------------------------------------------------------- 1 | 有些时候我们需要**对数据库进行一连串的操作**,如果其中某一个操作失败,那么其他的操作也要跟着回滚到操作以前的状态。 2 | 3 | 举个例子。某天你到银行存了 100 块钱,所以你的账户的数据库表就应该减去 100 块,而银行的账户上增加 100 块。但如果数据库在执行银行账户增加 100 块时操作失败了,岂不是平白无故损失掉 100 块钱,那你不得把银行屋顶给拆了。 4 | 5 | 这种情况下就需要用到**事务**这个概念了,即把一组操作捆绑到一起,大家生死与共,要么都成功,要么都失败,结成人民统一战线。 6 | 7 | Django 里如何实现事务?看下面的例子: 8 | 9 | ```python 10 | # models.py 11 | from django.db import models 12 | 13 | 14 | class Student(models.Model): 15 | """学生""" 16 | name = models.CharField(max_length=20) 17 | 18 | 19 | class Info(models.Model): 20 | """学生的基本情况""" 21 | age = models.IntegerField() 22 | 23 | 24 | class Address(models.Model): 25 | """学生的家庭住址""" 26 | home = models.CharField(max_length=100) 27 | ``` 28 | 29 | 有三个模型,`Student` 为学生、`Info` 为学生的基本情况、`Address` 为学生的住址。**假设这三个模型必须同时创建,否则数据就是不完整的。** 30 | 31 | 我们可以这样写视图: 32 | 33 | ```python 34 | def create_student(request): 35 | student = Student.objects.create(name='张三') 36 | info = Info.objects.create(age=19) 37 | address = Address.objects.create(home='北京') 38 | 39 | return HttpResponse('Create success...') 40 | ``` 41 | 42 | 很正常对吧。接下来让程序故意引发错误: 43 | 44 | ```python 45 | def create_student(request): 46 | student = Student.objects.create(name='张三') 47 | info = Info.objects.create(age=19) 48 | 49 | # 引发错误 50 | oh_my_god = int('abc') 51 | 52 | address = Address.objects.create(home='北京') 53 | 54 | return HttpResponse('Create success...') 55 | ``` 56 | 57 | 这就有问题了,前面的 `Student` 和 `Info` 都正常保存进数据库了,但是 `Address` 却由于前一句报错而没有执行创建,因此学生信息就变成了不完整的垃圾数据了。 58 | 59 | 解决办法就是把视图函数中的数据操作转化为**事务**: 60 | 61 | ```python 62 | from django.db import transaction 63 | 64 | # 注意这个装饰器 65 | @transaction.atomic 66 | def create_student(request): 67 | student = Student.objects.create(name='张三') 68 | info = Info.objects.create(age=19) 69 | 70 | oh_my_god = int('abc') 71 | 72 | address = Address.objects.create(home='北京') 73 | 74 | return HttpResponse('Create success...') 75 | 76 | ``` 77 | 78 | 这就非常不同了。无论视图里哪一个数据库操作失败或是没有执行,那么其他的操作也都会回滚到操作前的状态。也就是说上面这段代码中的三个模型,都没有保存成功。 79 | 80 | 有的时候视图里有很多的数据操作,如果我只想回滚其中一部分为事务也是有办法的: 81 | 82 | ```python 83 | from django.db import transaction 84 | 85 | @transaction.atomic 86 | def create_student(request): 87 | student = Student.objects.create(name='张三') 88 | 89 | # 回滚保存点 90 | save_tag = transaction.savepoint() 91 | 92 | try: 93 | info = Info.objects.create(age=19) 94 | 95 | # 引发错误 96 | oh_my_god = int('abc') 97 | 98 | address = Address.objects.create(home='北京') 99 | except: 100 | # 回滚到 save_tag 的位置 101 | transaction.savepoint_rollback(save_tag) 102 | 103 | return HttpResponse('Create success...') 104 | ``` 105 | 106 | 上面的代码运行之后,`Student` 表会成功保存,而另外两张表则都会失败。使用 `try` 的好处在于前端能正常运行。 107 | 108 | 除此之外,还有另一种方法可以将视图中的事务进行分组,实现更细腻的控制: 109 | 110 | ```python 111 | # 装饰器不要了 112 | # @transaction.atomic 113 | def create_student(request): 114 | student = Student.objects.create(name='张三') 115 | 116 | # 事务 117 | with transaction.atomic: 118 | info = Info.objects.create(age=19) 119 | 120 | # 引发错误 121 | oh_my_god = int('abc') 122 | 123 | address = Address.objects.create(home='北京') 124 | 125 | return HttpResponse('Create success...') 126 | ``` 127 | 128 | 效果是差不多的,仅有 `Student` 成功保存。 129 | 130 | 还有最后一个大杀器。如果你想让所有的数据库操作都是事务,那就在 `settings.py` 里配置: 131 | 132 | ```python 133 | # settings.py 134 | 135 | # 以 sqlite 为例 136 | DATABASES = { 137 | 'default': { 138 | 'ENGINE': ..., 139 | 'NAME': ..., 140 | # 加上这条 141 | 'ATOMIC_REQUESTS': True, 142 | } 143 | } 144 | 145 | ``` 146 | 147 | 然后可以用 `non_atomic_requests` 标记不需要成为事务的视图: 148 | 149 | ```python 150 | @transaction.non_atomic_requests 151 | def create_student(request): 152 | ... 153 | ``` 154 | 155 | 另外,**类视图**也是可以成为事务的: 156 | 157 | ```python 158 | class CreateStudent(View): 159 | @transaction.atomic 160 | def get(self, request): 161 | ... 162 | ``` 163 | 164 | 最后总结一下,并非任意对数据库的操作序列都是事务。数据库事务拥有 [ACID特性](https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1): 165 | 166 | - **原子性(Atomicity)**:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。 167 | - **一致性(Consistency)**:事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。 168 | - **隔离性(Isolation)**:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。 169 | - **持久性(Durability)**:已被提交的事务对数据库的修改应该永久保存在数据库中。 170 | 171 | > 关联官方文档:[Database transactions](https://docs.djangoproject.com/en/3.0/topics/db/transactions/) -------------------------------------------------------------------------------- /md/210-Aggregation聚合.md: -------------------------------------------------------------------------------- 1 | Django 的 filter、exclude 等方法使得对数据库的查询很方便了。这在数据量较小的时候还不错,但如果数据量很大,或者查询条件比较复杂,那么查询效率就会很低。 2 | 3 | 提高数据库查询效率可以通过原生 SQL 语句来实现,但是它的缺点就是需要开发者熟练掌握 SQL。倘若查询条件是动态变化的,则编写 SQL 会更加困难。 4 | 5 | 对于以便捷著称的 Django,怎么能忍受这样的事。于是就有了**Aggregation聚合**。 6 | 7 | 聚合最好的例子就是官网给的案例了: 8 | 9 | ```python 10 | # models.py 11 | 12 | from django.db import models 13 | 14 | class Author(models.Model): 15 | name = models.CharField(max_length=100) 16 | age = models.IntegerField() 17 | 18 | class Publisher(models.Model): 19 | name = models.CharField(max_length=300) 20 | 21 | class Book(models.Model): 22 | name = models.CharField(max_length=300) 23 | pages = models.IntegerField() 24 | price = models.DecimalField(max_digits=10, decimal_places=2) 25 | rating = models.FloatField() 26 | authors = models.ManyToManyField(Author) 27 | publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) 28 | pubdate = models.DateField() 29 | 30 | class Store(models.Model): 31 | name = models.CharField(max_length=300) 32 | books = models.ManyToManyField(Book) 33 | ``` 34 | 35 | 接下来可以这样求所有书籍的平均价格: 36 | 37 | ```python 38 | >>> from django.db.models import Avg, Max, Min 39 | 40 | >>> Book.objects.all().aggregate(Avg('price')) 41 | {'price__avg': Decimal('30.67')} 42 | ``` 43 | 44 | 实际上可以省掉 `all()` : 45 | 46 | ```python 47 | >>> Book.objects.aggregate(Avg('price')) 48 | {'price__avg': Decimal('30.67')} 49 | ``` 50 | 51 | 还可以指定返回的键名: 52 | 53 | ```python 54 | >>> Book.objects.aggregate(price_avg=Avg('price')) 55 | {'price_avg': Decimal('30.67')} 56 | ``` 57 | 58 | 如果要获取所有书籍中的最高价格: 59 | 60 | ```python 61 | >>> Book.objects.aggregate(Max('price')) 62 | {'price__max': Decimal('44')} 63 | ``` 64 | 65 | 获取所有书籍中的最低价格: 66 | 67 | ```python 68 | >>> Book.objects.aggregate(Min('price')) 69 | {'price__min': Decimal('12')} 70 | ``` 71 | 72 | `aggregate()` 方法返回的不再是 `QuerySet` 了,而是一个包含查询结果的字典。如果我要对 `QerySet` 中每个元素都进行聚合计算、并且返回的仍然是 `QuerySet` ,那就要用到 `annotate()` 方法了。 73 | 74 | `annotate` 翻译过来就是**注解**,它的作用有点像给 `QuerySet` 中的每个元素临时贴上一个临时的字段,字段的值是分组聚合运算的结果。 75 | 76 | 比方说要给查询集中的每本书籍都增加一个字段,字段内容是外链到书籍的作者的数量: 77 | 78 | ```python 79 | >>> from django.db.models import Count 80 | 81 | >>> q = Book.objects.annotate(Count('authors')) 82 | >>> q[0].authors__count 83 | 3 84 | ``` 85 | 86 | 与 `aggregate()` 的语法类似,也可以给这个字段自定义个名字: 87 | 88 | ```python 89 | >>> q = Book.objects.annotate(a_count=Count('authors')) 90 | ``` 91 | 92 | 跨外链查询字段也是可以的: 93 | 94 | ```python 95 | >>> s = Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price')) 96 | 97 | >>> s[0].min_price 98 | Decimal('12') 99 | >>> s[0].max_price 100 | Decimal('44') 101 | ``` 102 | 103 | 既然 `annotate()` 返回的是查询集,那么自然也可以和 `filter()`、`exclude()` 等查询方法组合使用: 104 | 105 | ```python 106 | >>> b = Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors')) 107 | >>> b[0].num_authors 108 | 4 109 | ``` 110 | 111 | 联用的时候 `filter` 、`annotate` 的顺序会影响返回结果,所以逻辑要想清楚。 112 | 113 | 也可以排序: 114 | 115 | ```python 116 | >>> Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors') 117 | ``` 118 | 119 | 总而言之,`aggregate` 和 `annotate` 用于组合查询。当你需要对某些字段进行聚合操作时(比如Sum, Avg, Max),请使用 `aggregate` 。如果你想要对数据集先进行分组(Group By)然后再进行某些聚合操作或排序时,请使用 `annotate` 。 120 | 121 | 进行此类查询有时候容易让人迷惑,如果你对查询的结果有任何的疑问,最好的方法就是直接查看它所执行的 SQL 原始语句,像这样: 122 | 123 | ```python 124 | >>> b = Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors') 125 | >>> print(b.query) 126 | SELECT "aggregation_book"."id", "aggregation_book"."name", 127 | "aggregation_book"."pages", "aggregation_book"."price", 128 | "aggregation_book"."rating", "aggregation_book"."publisher_id", 129 | "aggregation_book"."pubdate", COUNT("aggregation_book_authors"."author_id") 130 | AS "num_authors" FROM "aggregation_book" LEFT OUTER JOIN "aggregation_book_authors" 131 | ON ("aggregation_book"."id" = "aggregation_book_authors"."book_id") 132 | GROUP BY "aggregation_book"."id", "aggregation_book"."name", 133 | "aggregation_book"."pages", "aggregation_book"."price", 134 | "aggregation_book"."rating", "aggregation_book"."publisher_id", 135 | "aggregation_book"."pubdate" 136 | ORDER BY "num_authors" ASC 137 | ``` 138 | 139 | > 相关文档:[Aggregation](https://docs.djangoproject.com/en/3.0/topics/db/aggregation/) 140 | > 141 | > 复合使用聚合时的相互干扰问题:[Count and Sum annotations interfere with each other](https://stackoverflow.com/questions/56567841/django-count-and-sum-annotations-interfere-with-each-other) -------------------------------------------------------------------------------- /md/220-Session会话.md: -------------------------------------------------------------------------------- 1 | 浏览器和 Django 服务之间的通信采用 HTTP 协议,该协议是无状态的。也就是说,即使是同一个浏览器的请求也是完全独立的,服务器并不知道两次请求是否来自同一个用户。 2 | 3 | 会话(Session)就是来解决这类问题的。Session 为每个浏览器存储任意数据,并在浏览器连接时,将该数据提供给站点。Session 依赖 Cookie ,但 Cookie 中仅保存一个识别值,真正的数据是保存在数据库中的。 4 | 5 | Django 在创建时默认开启了 Session 功能: 6 | 7 | ```python 8 | # settings.py 9 | 10 | INSTALLED_APPS = [ 11 | ... 12 | 'django.contrib.sessions', 13 | ... 14 | ] 15 | 16 | MIDDLEWARE = [ 17 | ... 18 | 'django.contrib.sessions.middleware.SessionMiddleware', 19 | ... 20 | ] 21 | ``` 22 | 23 | 操作起来也很友好,跟 Python 的字典有点类似。 24 | 25 | 比如利用 Session 记录匿名用户的登录次数。写视图函数: 26 | 27 | ```python 28 | # views.py 29 | 30 | def session_visits_count(request): 31 | # 获取 visits_count 数据,若不存在则设置为 0 32 | count = request.session.get('visits_count', 0) 33 | count += 1 34 | # 保存 visits_count 进 session 35 | request.session['visits_count'] = count 36 | return render(request, 'visits_count.html', context={'count': count }) 37 | ``` 38 | 39 | 路由: 40 | 41 | ```python 42 | # urls.py 43 | 44 | ... 45 | urlpatterns = [ 46 | path('visits-count/', session_visits_count, name='visits_count'), 47 | ] 48 | ``` 49 | 50 | 模板: 51 | 52 | ```html 53 | # visits_count.html 54 | 55 | ... 56 | {% block content %} 57 |

58 | 您已经访问本页面:{{ count }} 次. 59 |

60 | {% endblock %} 61 | ``` 62 | 63 | Session 的默认保存时间为 2 周,如果你想手动删除也可以: 64 | 65 | ```python 66 | del request.session['xxx'] 67 | ``` 68 | 69 | 通常情况下对 session 的修改会自动保存。但如果你存的是某种嵌套结构(比如字典),那么需要手动保存: 70 | 71 | ```python 72 | def some_view(request): 73 | ... 74 | # session 保存字典数据 75 | if request.session.get('deeper_count'): 76 | num = request.session['deeper_count']['num'] 77 | # 此时 session 并未更新,因为更新的仅仅是字典中的数据 78 | request.session['deeper_count']['num'] = num + 1 79 | # 通知会话已修改 80 | request.session.modified = True 81 | else: 82 | num = 1 83 | request.session['deeper_count'] = {'num': num} 84 | return ... 85 | ``` 86 | 87 | > 关联文档:[How to use sessions](https://docs.djangoproject.com/en/3.0/topics/http/sessions/) -------------------------------------------------------------------------------- /md/230-Middleware中间件.md: -------------------------------------------------------------------------------- 1 | **中间件**是 Django 处理请求和响应的**钩子框架**。它是一个轻量级的、低层级的“插件”系统,用于**全局**改变 Django 的输入或输出。如果你需要在响应请求时插入一个自定义功能、参数的时候特别有用。 2 | 3 | ### 自定义中间件 4 | 5 | 假设你有一个叫 `middleware` 的 app 。在 app 中创建文件 `middlewares.py`,一会儿在这里面自定义中间件。 6 | 7 | 来看一下 Django 官方推荐的中间件写法是什么样子的: 8 | 9 | ```python 10 | # middleware/middlewares.py 11 | 12 | class Md1: 13 | def __init__(self, get_response): 14 | self.get_response = get_response 15 | # (0) 参数的配置与初始化。初始化只执行一次。 16 | 17 | def __call__(self, request): 18 | 19 | # (1) 这里写实际视图执行之前的逻辑 20 | print('Md1 视图执行前..') 21 | 22 | # (2) get_response 是下一个中间件或视图函数的处理程序 23 | response = self.get_response(request) 24 | 25 | # (3) 这里写实际视图执行之后的逻辑 26 | print('Md1 视图执行后..') 27 | 28 | return response 29 | ``` 30 | 31 | 这个类 `Md1` 就是一个最简单的中间件了,它最核心的部分就是实现了 `__call__` 方法,使得类变成可调用对象: 32 | 33 | - 方法里的 `request` 就是视图函数中传入的那个 `request` 。 34 | - `get_response` 是可调用对象,它处理的内容可以是下一个中间件,也可以是实际的视图函数,当前中间件并不关心。`request` 通过它传递到下一级,也就是说 `get_response` 到达的最底层就是视图函数了。 35 | - 因此,**序号(1)** 则是**请求到达视图前**需要自定义的逻辑,**序号(3)** 是**请求从视图出来后**需要自定义的逻辑。 36 | 37 | 来实际测试一下。 38 | 39 | 首先注册 app 和中间件: 40 | 41 | ```python 42 | # your_project/settings.py 43 | 44 | INSTALLED_APPS = [ 45 | ... 46 | 'middleware', 47 | ] 48 | 49 | MIDDLEWARE = [ 50 | # 这里是 Django 默认注册的中间件 51 | ... 52 | 53 | # 刚才自定义的中间件,注册规则:appName.fileName.className 54 | 'middleware.middlewares.Md1', 55 | ] 56 | ``` 57 | 58 | 在 app 中编写测试视图: 59 | 60 | ```python 61 | # middleware/views.py 62 | 63 | from django.http import HttpResponse 64 | 65 | def mid_test(request): 66 | print('--- 视图执行中...') 67 | return HttpResponse('中间件测试..') 68 | ``` 69 | 70 | 最后在项目根 `urls.py` 中添加路由: 71 | 72 | ```python 73 | # your_project/urls.py 74 | 75 | ... 76 | 77 | from middleware.views import mid_test 78 | 79 | urlpatterns = [ 80 | ... 81 | path('middleware/', mid_test), 82 | ] 83 | ``` 84 | 85 | 访问此路由,命令行打印结果如下: 86 | 87 | ```python 88 | Md1 视图执行前.. 89 | --- 视图执行中... 90 | Md1 视图执行后.. 91 | ``` 92 | 93 | 不仅视图 `mid_test`,项目中所有的请求都会执行中间件的代码,也就是说是**影响全局**的。 94 | 95 | ### 执行顺序 96 | 97 | Django 收到请求后,会根据配置文件中 `MIDDLEWARE` 列表挨个执行中间件,所以列表里中间件的**顺序就很重要**,有些是互相依赖的,比如 Django 默认开启的 `SessionMiddleware` 和 `AuthenticationMiddleware` ,调换执行顺序后功能就会不正常。 98 | 99 | 为了直观表现调用次序,再添加两个自定义中间件: 100 | 101 | ```python 102 | # middleware/middlewares.py 103 | 104 | class Md1: 105 | def __init__(self, get_response): 106 | self.get_response = get_response 107 | 108 | def __call__(self, request): 109 | 110 | print('Md1 视图执行前..') 111 | response = self.get_response(request) 112 | print('Md1 视图执行后..') 113 | 114 | return response 115 | 116 | 117 | class Md2: 118 | def __init__(self, get_response): 119 | self.get_response = get_response 120 | 121 | def __call__(self, request): 122 | 123 | print('Md2 视图执行前..') 124 | response = self.get_response(request) 125 | print('Md2 视图执行后..') 126 | 127 | return response 128 | 129 | 130 | class Md3: 131 | def __init__(self, get_response): 132 | self.get_response = get_response 133 | 134 | def __call__(self, request): 135 | 136 | print('Md3 视图执行前..') 137 | response = self.get_response(request) 138 | print('Md3 视图执行后..') 139 | 140 | return response 141 | ``` 142 | 143 | 注册到配置中: 144 | 145 | ```python 146 | # your_project/settings.py 147 | 148 | MIDDLEWARE = [ 149 | ... 150 | 'middleware.middlewares.Md1', 151 | 'middleware.middlewares.Md2', 152 | 'middleware.middlewares.Md3', 153 | ] 154 | ``` 155 | 156 | 刷新页面后,命令行打印如下: 157 | 158 | ```python 159 | Md1 视图执行前.. 160 | Md2 视图执行前.. 161 | Md3 视图执行前.. 162 | --- 视图执行中... 163 | Md3 视图执行后.. 164 | Md2 视图执行后.. 165 | Md1 视图执行后.. 166 | ``` 167 | 168 | 非常神奇的是,中间件调用的 `get_response()` 方法**之前**的逻辑是按照注册列表**顺序**执行,而**之后**的逻辑是**逆序**执行的。 169 | 170 | > 也就是说,中间件在传递请求的阶段顺序执行,在返回响应的阶段逆序执行。 171 | 172 | 为什么会这样?让我们再看一眼 `__call__` 方法: 173 | 174 | ```python 175 | def __call__(self, request): 176 | 177 | # (1) 这里的逻辑按中间件注册列表顺序执行 178 | 179 | # (2) 下一个中间件或视图 180 | response = self.get_response(request) 181 | 182 | # (3) 这里逆序执行 183 | 184 | return response 185 | ``` 186 | 187 | 请求通过 `__call__` 方法,首先执行了 **序号(1) ** 位置的代码后,通过 `get_response(request)` 传递到下一个中间件里后,又执行下一个 **序号(1)** 位置的代码并继续传递,直到到达了视图函数。 188 | 189 | 视图函数返回了响应体 `response` 后,整个中间件的调用从 `get_response(request)` 的位置一层层的往回翻,直到回到最初始的位置,整个调用才宣告结束。 190 | 191 | 所以中间件的执行顺序,**就像洋葱一样,请求通过洋葱的每一层直到核心的视图函数,再带着响应从里面反着出来**。 192 | 193 | ### 短路 194 | 195 | 中间件有种比较常用的用法,即不调用 `get_response()` 方法,手动返回一个 http 响应体,像这样: 196 | 197 | ```python 198 | # middleware/middlewares.py 199 | 200 | from django.http import HttpResponse 201 | 202 | 203 | class Md1: 204 | ... 205 | 206 | 207 | class Md2: 208 | ... 209 | 210 | def __call__(self, request): 211 | print('Md2 视图执行前..') 212 | 213 | # 新增代码 214 | if True: 215 | print('Md2 引发短路') 216 | return HttpResponse('Md2 引发短路') 217 | 218 | response = self.get_response(request) 219 | print('Md2 视图执行后..') 220 | return response 221 | 222 | 223 | class Md3: 224 | ... 225 | ``` 226 | 227 | 刷新页面,命令行打印如下: 228 | 229 | ```python 230 | Md1 视图执行前.. 231 | Md2 视图执行前.. 232 | Md2 引发短路 233 | Md1 视图执行后.. 234 | ``` 235 | 236 | `if` 语句中直接返回了 `HttpResponse`响应体,从而中断了中间件向下一级传播,直接从 `return` 位置返回了,甚至请求也不会进入视图函数。这就是中间件的**短路**,比较常用在权限控制等功能中。 237 | 238 | ### 更多钩子 239 | 240 | 除了上述最基础的模式之外,中间件类还提供了另外三种钩子方法。 241 | 242 | #### process_view() 243 | 244 | - `process_view(request, view_func, view_args, view_kwargs)` 245 | 246 | `request` 是一个 `HttpRequest` 对象。`view_func` 是实际要执行的视图函数。 `view_args` 是传递给视图的位置参数列表,`view_kwargs` 是传递给视图的关键字参数字典。 247 | 248 | `process_view()` 在**所有中间件的基础模式后、视图执行前**被调用。它返回 `None` 或 `HttpResponse`对象: 249 | 250 | - 返回 `None` ,Django 将按照规则处理这个请求并顺序执行接下来的中间件。 251 | - 返回 `HttpResponse` 对象,则 Django 不调用实际的视图,而是从已经调用的中间件开始逐层返回。 252 | 253 | #### process_exception() 254 | 255 | - `process_exception(request, exception)` 256 | 257 | `exception` 是视图函数引发的 `Exception` 对象。 258 | 259 | 当视图引发异常时,Django 会调用 `process_exception()`。它返回 `None` 或 `HttpResponse` 对象: 260 | 261 | - 如果返回 `HttpResponse`对象,中间件会直接将结果响应返回浏览器。 262 | - 否则就开始默认的异常处理。 263 | 264 | 也就是说,如果异常中间件返回一个响应,那么其他之后的中间件的 `process_exception` 方法将不会被调用。 265 | 266 | #### process_template_response() 267 | 268 | - `process_template_response(request, response)` 269 | 270 | `response` 是 `TemplateResponse` 对象。 271 | 272 | 它在视图被执行后调用,必须返回一个实现了 `render` 方法的响应对象。此钩子方法会在响应阶段按照相反的顺序运行。**也就是说,此方法仅当视图返回 `TemplateResponse` 对象才会被调用,通常用的 `render` 快捷方式不会触发它。** 273 | 274 | > 所有这些钩子方法组合到一起可以形成复杂的短路规则。推荐读者自行测试一下,印象会比较深刻。 275 | 276 | ### 案例 277 | 278 | #### 用户拦截 279 | 280 | 假设你有一个敏感路径,要求必须超级用户才能访问: 281 | 282 | ```python 283 | from django.http import HttpResponseForbidden 284 | 285 | class NormalUserBlock: 286 | def __init__(self, get_response): 287 | self.get_response = get_response 288 | 289 | def __call__(self, request): 290 | 291 | if (request.user.is_superuser != True) and (request.path == '/secret-url/'): 292 | return HttpResponseForbidden('

超级用户方可访问此页面!

') 293 | 294 | response = self.get_response(request) 295 | 296 | return response 297 | ``` 298 | 299 | 此功能可以非常容易扩展为 IP 拦截:通过 `request.META['REMOTE_ADDR'] ` 获取请求的 IP 地址,比对数据库中的黑名单进行拦截。 300 | 301 | #### Debug 页面优化 302 | 303 | 假设你的博客部署到线上了,并且理所当然的配置了 `DEBUG = False` 。当引发 500 错误后,你想让超级用户仍然看到 DEBUG 页面,而普通用户看不到,可以这样: 304 | 305 | ```python 306 | import sys 307 | from django.views.debug import technical_500_response 308 | 309 | class DebugOnlySuperUser(): 310 | def __init__(self, get_response): 311 | self.get_response = get_response 312 | 313 | def __call__(self, request): 314 | response = self.get_response(request) 315 | return response 316 | 317 | def process_exception(self, request, exception): 318 | if request.user.is_superuser: 319 | return technical_500_response(request, *sys.exc_info()) 320 | ``` 321 | 322 | 这样调试起来就方便了不少。 323 | 324 | #### CSRF 验证 325 | 326 | `process_view()` 方法和基础用法的主要区别之一,就是它带有与请求相关的视图的信息。 327 | 328 | Django 自带的 CSRF 中间件就是很好的例子: 329 | 330 | ```python 331 | ... 332 | 333 | def process_view(self, request, callback, callback_args, callback_kwargs): 334 | ... 335 | 336 | if getattr(callback, 'csrf_exempt', False): 337 | return None 338 | 339 | ... 340 | ``` 341 | 342 | 如果请求的视图上存在 `csrf_exempt` 装饰器,则本次请求不会实施 CSRF 保护。 343 | 344 | #### 响应计时器 345 | 346 | 自定义中间件,记录从收到请求到完成响应所花费的时间: 347 | 348 | ```python 349 | from datetime import datetime 350 | 351 | class ResponseTimer: 352 | def __init__(self, get_response): 353 | self.get_response = get_response 354 | 355 | def __call__(self, request): 356 | request._request_time = datetime.now() 357 | response = self.get_response(request) 358 | return response 359 | 360 | def process_template_response(self, request, response): 361 | response_time = request._request_time - datetime.now() 362 | response.context_data['response_time'] = abs(response_time) 363 | return response 364 | ``` 365 | 366 | 有了这个中间件,所有的模板都可以获取到 `{{ response_time }}` 这个变量了。 367 | 368 | **再重复一次**,此方法仅当视图返回 `TemplateResponse` 对象才会被调用,通常用的 `render` 快捷方式不会触发它。它两的区别是,`TemplateResponse` 会延迟渲染,它包含了呈现模板之前的上下文数据,因此让中间件有机会去修改里面的变量。而 `render` 会立即呈现模板并返回 `HttpResponse` ,无法唤起此钩子方法: 369 | 370 | ```python 371 | from django.shortcuts import render 372 | from django.template.response import TemplateResponse 373 | 374 | # 无法调用 process_template_response() 375 | def mid_test(request): 376 | return render(request, '....html', context={}) 377 | 378 | # 返回 TemplateResponse 才可调用 379 | def mid_test(request): 380 | return TemplateResponse(request, '....html', context={}) 381 | ``` 382 | 383 | -------------------------------------------------------------------------------- /md/240-信号的运用.md: -------------------------------------------------------------------------------- 1 | 在任何项目中,我们或多或少都需要一种能力,即:**当某个事件发生时,另一个对象也能够知晓此事**。 2 | 3 | 拿博客举个栗子。我希望有**用户在博客留言时,博主收到通知**。按照比较容易想到的方式,那就是在保存评论数据时,显式执行发送通知相关的代码。 4 | 5 | 比如下面这样: 6 | 7 | ```python 8 | from django.db import models 9 | from somewhere import post_notification 10 | 11 | class Comment(models.Model): 12 | # ... 13 | 14 | def save(self, *args, **kwargs): 15 | 16 | # 发送通知 17 | post_notification() 18 | 19 | # ... 20 | ``` 21 | 22 | 这样做的缺点就在于把**评论模块**和**通知模块**耦合到一起了。如果哪天我改动甚至删除了**通知模块**的对应函数,搞不好**评论模块**也无法正常工作了。 23 | 24 | 当**很多模块都关心评论模块的保存事件**时,代码就有可能变成了这样: 25 | 26 | ```python 27 | class Comment(models.Model): 28 | def save(self, *args, **kwargs): 29 | # 以下函数分属不同模块 30 | post_notification() 31 | save_log() 32 | increase_count() 33 | do_this() 34 | do_that() 35 | blablabla() 36 | do_this_again() 37 | do_that_again() 38 | blablabla_again() 39 | #... 40 | ``` 41 | 42 | 模块之间还可以互相调用,搅在一起,动了其中一个可能就引出一堆报错,不利于功能的扩展。 43 | 44 | ## 信号的作用 45 | 46 | 因此,像这种**许多代码段对同一事件感兴趣时,信号就特别有用**。 47 | 48 | Django 内置了对**信号**这个概念的支持。信号允许**发送器**通知**接收器**某些事件已经发生。当事件发生时,”发送器“只负责发出一个”信号“,提醒”接收器“该执行了;至于接收器具体是什么、有多少个,发送器就不关心了。 49 | 50 | > 这就有点像村里的村长拿个大喇叭,站在村口喊:”长得帅的人该起床了!“至于到底哪些村民长得帅、喊出去的话有没有人听到、听到的到底起不起床,村长就完全不管了。 51 | 52 | 反过来讲,接收器在很多时候也并不关心到底是谁发出的信号,反正只要接收到唤醒自己的信号,直接执行就万事大吉了。 53 | 54 | 这种近似匿名的机制,再加之发送器、接收器都可以有多个,使得模块可以很轻松的解耦和功能扩展。 55 | 56 | ## 内置信号 57 | 58 | Django 内置了几种常见的信号,开箱即用。 59 | 60 | 比如每当一个 HTTP 请求发起、结束时的信号: 61 | 62 | ```python 63 | from django.core.signals import request_finished, request_started 64 | from django.dispatch import receiver 65 | 66 | @receiver(request_finished) 67 | def signal_callback(sender, **kwargs): 68 | print('信号已接收..') 69 | ``` 70 | 71 | 上面的代码会在每个请求结束时执行。装饰器 `@receiver` 将函数标注为接收器,其参数 `request_finished` 指定了具体的信号类型。 72 | 73 | > `request_finished` 就是其中一个内置信号,在 http 请求结束时发送。 74 | 75 | 任何想成为接收器的函数必须包含下面两个参数: 76 | 77 | - `sender` 参数是发出信号的发送器。 78 | - `**kwargs` 关键字参数。之所以必须有它,是因为参数可能在任意时刻被添加到信号中,而接收器必须能够处理这些新的参数。 79 | 80 | 如前面说的,同一个信号的接收器可以有多个: 81 | 82 | ```python 83 | # from ... 84 | 85 | @receiver(request_finished) 86 | def signal_callback(sender, **kwargs): 87 | print('信号已接收1..') 88 | 89 | @receiver(request_finished) 90 | def signal_callback_2(sender, **kwargs): 91 | print('信号已接收2..') 92 | ``` 93 | 94 | 同一个接收器的信号也可以有多个: 95 | 96 | ```python 97 | @receiver([request_finished, request_started]) 98 | def signal_callback(sender, **kwargs): 99 | print('信号已接收..') 100 | ``` 101 | 102 | 有些时候你可能只对某一类信号中的**子集**感兴趣。比如说我只想在 `BookModel` 保存前触发接收器,而在 `PersonModel` 保存前不触发。于是你就可以这样做: 103 | 104 | ```python 105 | from django.db.models.signals import pre_save 106 | from django.dispatch import receiver 107 | from myapp.models import BookModel 108 | 109 | @receiver(pre_save, sender=BookModel) 110 | def my_handler(sender, **kwargs): 111 | # ... 112 | ``` 113 | 114 | 装饰器中的 `sender=BookModel` 就表明了此接收器只响应 `BookModel` 的信号。 115 | 116 | > 与 `pre_save` 对应的还有内置的 `post_save` 信号。 117 | 118 | 还有一个问题是:信号注册的代码有可能无意间被多次执行。为了防止重复注册导致的信号重复,可以给装饰器传递一个唯一的标识符,像这样: 119 | 120 | ```python 121 | @receiver(my_signal, dispatch_uid="my_unique_identifier") 122 | def my_signal_handler(sender, **kwargs): 123 | # ... 124 | ``` 125 | 126 | 标识符通常是字符串,但其实任何可散列的对象都可以。 127 | 128 | 以上就是内置信号的基础用法了。 129 | 130 | 更多内置信号,请见[Django内置信号](https://docs.djangoproject.com/zh-hans/3.2/ref/signals/)。 131 | 132 | ## 自定义信号 133 | 134 | 有时候内置信号可能无法满足需求,Django 也允许你自定义信号。下面用一个例子看看自定义信号是如何实现的。 135 | 136 | 假设我的项目中有一个叫 `mySignal` 的 App。新建 `mySignal/signals.py` 文件,注册一个自定义信号: 137 | 138 | ```python 139 | # mySignal/signals.py 140 | 141 | import django.dispatch 142 | # 注册信号 143 | view_done = django.dispatch.Signal() 144 | ``` 145 | 146 | 然后新建 `mySignal/handlers.py` ,编写接收器并把它和信号连接起来: 147 | 148 | ```python 149 | # mySignal/handlers.py 150 | 151 | from django.dispatch import receiver 152 | from mySignal.signals import view_done 153 | 154 | @receiver(view_done, dispatch_uid="my_signal_receiver") 155 | def my_signal_handler(sender, **kwargs): 156 | print(sender) 157 | print(kwargs.get('arg_1'), kwargs.get('arg_2')) 158 | ``` 159 | 160 | 虽然已经有了信号和接收器,但是项目运行时并没有运行这两段代码。因此下面两步的作用是加载它们。 161 | 162 | 修改 `mySignal/__init__.py` : 163 | 164 | ```python 165 | # mySignal/__init__.py 166 | 167 | default_app_config = "mySignal.apps.MysignalConfig" 168 | ``` 169 | 170 | 再修改 `mySignal/apps.py` : 171 | 172 | ```python 173 | # mySignal/apps.py 174 | 175 | from django.apps import AppConfig 176 | 177 | class MysignalConfig(AppConfig): 178 | name = 'mySignal' 179 | 180 | def ready(self): 181 | import mySignal.handlers 182 | ``` 183 | 184 | 差不多快完成了。接下来就可以在任意位置发送这个信号了。比如像这样: 185 | 186 | ```python 187 | # mySignal/views.py 188 | 189 | from mySignal.signals import view_done 190 | 191 | def some_view(request): 192 | # 发送信号 193 | view_done.send( 194 | sender='View function...', 195 | arg_1='My signal...', 196 | arg_2='received...' 197 | ) 198 | 199 | # 其他代码... 200 | ``` 201 | 202 | 重启服务器,访问此视图后,控制台将输出如下字符: 203 | 204 | ```python 205 | View function... 206 | My signal... received... 207 | ``` 208 | 209 | 接收器成功唤醒了,并且正确接收到信号携带的信息。 210 | 211 | ## 总结 212 | 213 | 信号的优点是让模块之间**解耦**。当不同模块的代码片段对同一事物感兴趣时,就是信号非常适合的应用场景。信号的缺点是它是**隐式执行**的,这使得调试变得更困难。 214 | 215 | 事物都有两面性,是否使用信号还得根据实际需求进行判断。 -------------------------------------------------------------------------------- /md/250-Python装饰器入门:从理解到应用.md: -------------------------------------------------------------------------------- 1 | **装饰器(Decorator)**是 Python 非常重要的组成部分,它可以修改或扩展其他函数的功能,并让代码保持简短。 2 | 3 | 装饰器对初学者来说,理解起来有些困难。 4 | 5 | 因此,让我们从 Python 最基础的知识讲起。 6 | 7 | ## 一切皆对象 8 | 9 | 在 Python 中,**函数可以根据给定的参数返回一个值**: 10 | 11 | ```python 12 | def hello(name): 13 | return 'Hello ' + name 14 | 15 | print(hello('Bob')) 16 | 17 | # 输出: 18 | # Hello Bob 19 | ``` 20 | 21 | 与 Python 的其他对象(如字符串、整数、列表等)一样,**函数也是对象**,也可以赋值给一个变量: 22 | 23 | ```python 24 | def hello(name): 25 | return 'Hello ' + name 26 | 27 | h = hello 28 | 29 | print(hello) 30 | # 输出: 31 | # 32 | 33 | print(h) 34 | # 输出: 35 | # 36 | 37 | print(h('Jack')) 38 | # 输出: 39 | # Hello Jack 40 | ``` 41 | 42 | 可以看到 `hello` 和 `h` 都指向同一个函数,而函数后加括号 `h('Jack')` 是对其进行了调用。 43 | 44 | ## 函数作为参数 45 | 46 | 既然函数是对象,那么当然也可以和其他 Python 对象一样,作为参数传递到另一个函数中去。 47 | 48 | 这种以其他函数作为参数的函数,又被称为**高阶函数**。 49 | 50 | 比如下面这个: 51 | 52 | ```python 53 | def hi(func): 54 | name = func() 55 | print('Hi ' + name) 56 | 57 | def bob(): 58 | return 'Bob' 59 | 60 | hi(bob) 61 | 62 | # 输出: 63 | # Hi Bob 64 | ``` 65 | 66 | 注意 `bob` 函数作为参数时并没有被调用(没加括号),而是作为函数被传递到 `hi` 函数里,才在 `name = func()` 这里被真正调用的。 67 | 68 | ## 函数里的函数 69 | 70 | 除此之外,函数里面还可以定义函数: 71 | 72 | ```python 73 | def hi(): 74 | def bob(): 75 | return 'Bob' 76 | print('Hi ' + bob()) 77 | 78 | hi() 79 | # 输出: 80 | # Hi Bob 81 | ``` 82 | 83 | 此时的 `bob` 函数的作用域在 `hi` 之内的。如果在全局调用 `bob()` 会引发错误: 84 | 85 | ```python 86 | >>> bob() 87 | NameError: name 'bob' is not defined 88 | ``` 89 | 90 | ## 函数作为返回值 91 | 92 | 很自然的,函数也可以作为其他函数的返回值,比如: 93 | 94 | ```python 95 | def cook(): 96 | def tomato(): 97 | print('I am Tomato') 98 | 99 | return tomato 100 | 101 | t = cook() 102 | t() 103 | # 输出: 104 | # I am Tomato 105 | ``` 106 | 107 | 函数可以作为参数、返回值,也可以内部定义。感觉很自然,对吧。 108 | 109 | ## 组合运用 110 | 111 | 接下来我们把前面的所有知识组合一下,像这样: 112 | 113 | ```python 114 | def outer(func): 115 | def inner(): 116 | print('Before func()..') 117 | func() 118 | print('After func()..') 119 | return inner 120 | 121 | 122 | def hi(): 123 | print('Hi World') 124 | 125 | 126 | h = outer(hi) 127 | h() 128 | 129 | # 输出: 130 | # Before func().. 131 | # Hi World 132 | # After func().. 133 | ``` 134 | 135 | - 函数 `outer` 的参数是函数 `hi` 136 | - `outer` 的返回值是函数 `inner` 137 | - `hi` 在 `inner` 中进行了调用 138 | 139 | `h = outer(hi)` 将 `outer` 的返回值(即 `inner` 函数)赋值给了 `h` 。 140 | 141 | 如果你不想赋值也可以,连起来写就是 `outer(hi)()` ,执行的效果是完全相同的。 142 | 143 | **这就是一个简单的装饰器了!** 144 | 145 | 原函数 `hi` 的功能不变,但又成功附加了两行打印的语句。 146 | 147 | ## 你的第一个装饰器 148 | 149 | 把上面的代码修改为**装饰器**的写法: 150 | 151 | ```python 152 | def outer(func): 153 | def inner(): 154 | print('Before func()..') 155 | func() 156 | print('After func()..') 157 | return inner 158 | 159 | @outer 160 | def hi(): 161 | print('Hi World') 162 | 163 | 164 | hi() 165 | 166 | # 输出: 167 | # Before func().. 168 | # Hi World 169 | # After func().. 170 | ``` 171 | 172 | 实际上 `@outer` 就等同于下面这一句: 173 | 174 | ```python 175 | hi = outer(hi) 176 | ``` 177 | 178 | 啊,这糖真甜。 179 | 180 | ## 装饰器的返回值 181 | 182 | 有时候原函数具有返回值,如果套用前面的装饰器: 183 | 184 | ```python 185 | def outer(func): 186 | def inner(): 187 | func() 188 | return inner 189 | 190 | @outer 191 | def one(): 192 | return 1 193 | 194 | print(one()) 195 | 196 | # 输出: 197 | # None 198 | ``` 199 | 200 | 因为装饰器返回的 `inner` 函数是不具有返回值的,因此原本函数的返回值就被”吃“掉了。 201 | 202 | 要解决此问题,就需要让 `inner` 函数把原函数的返回值丢出来,像这样: 203 | 204 | ```python 205 | def outer(func): 206 | def inner(): 207 | return func() 208 | return inner 209 | 210 | @outer 211 | def one(): 212 | return 1 213 | 214 | print(one()) 215 | 216 | # 输出: 217 | # 1 218 | ``` 219 | 220 | ## 带参数的原函数 221 | 222 | 原函数有可能带有参数: 223 | 224 | ```python 225 | def outer(func): 226 | def inner(): 227 | return func() 228 | return inner 229 | 230 | @outer 231 | def haha(name): 232 | return 'Haha ' + name 233 | ``` 234 | 235 | 不幸的是,这样调用会报错: 236 | 237 | ```python 238 | print(haha('Bob')) 239 | 240 | >>> TypeError: inner() takes 0 positional arguments but 1 was given 241 | ``` 242 | 243 | 你可以给 `inner` 函数加一个参数,但这样又不能适用无参数的函数了: 244 | 245 | ```python 246 | def outer(func): 247 | def inner(name): 248 | return func(name) 249 | return inner 250 | 251 | @outer 252 | def haha(name): 253 | return 'Haha ' + name 254 | 255 | @outer 256 | def hehe(): 257 | return 'Hehe' 258 | 259 | print(haha('Bob')) 260 | # 输出: 261 | # Haha Bob 262 | 263 | print(hehe()) 264 | # 输出报错: 265 | # TypeError: inner() missing 1 required positional argument: 'name' 266 | ``` 267 | 268 | 好在 Python 有 `*args` 和 `**kwargs` 可以接收任意数量的位置参数和关键字参数。 269 | 270 | 正确的解决方案是这样: 271 | 272 | ```python 273 | def outer(func): 274 | def inner(*args, **kwargs): 275 | return func(*args, **kwargs) 276 | return inner 277 | 278 | @outer 279 | def haha(name): 280 | return 'Haha ' + name 281 | 282 | @outer 283 | def hehe(): 284 | return 'Hehe' 285 | 286 | 287 | print(haha('Bob')) 288 | # 输出: 289 | # Haha Bob 290 | 291 | print(hehe()) 292 | # 输出: 293 | # Hehe 294 | ``` 295 | 296 | ## 你是谁 297 | 298 | Python 具有强大的 **自省能力**, 即对象在运行时了解自身属性的能力。 299 | 300 | 比如,函数知道自己的**名字**: 301 | 302 | ```python 303 | def my_func(): 304 | pass 305 | 306 | print(my_func.__name__) 307 | 308 | # 输出: 309 | # my_func 310 | ``` 311 | 312 | 但是由于装饰器包装后的返回值是 `inner` 函数,因此函数的身份就变得混乱了: 313 | 314 | ```python 315 | def outer(func): 316 | def inner(*args, **kwargs): 317 | return func(*args, **kwargs) 318 | return inner 319 | 320 | @outer 321 | def my_func(): 322 | pass 323 | 324 | print(my_func.__name__) 325 | # 输出: 326 | # inner 327 | ``` 328 | 329 | 虽然是正确的,但是却不怎么有用。大多数时候我们关心的是原函数的内在属性,特别是对于依赖函数签名的原函数。 330 | 331 | 好在 Python 有内置的解决方案: 332 | 333 | ```python 334 | import functools 335 | 336 | def outer(func): 337 | @functools.wraps(func) 338 | def inner(*args, **kwargs): 339 | return func(*args, **kwargs) 340 | return inner 341 | 342 | @outer 343 | def my_func(): 344 | pass 345 | 346 | print(my_func.__name__) 347 | 348 | # 输出: 349 | # my_func 350 | ``` 351 | 352 | 甚至解决方案本身就是个 `@wraps()` 装饰器。 353 | 354 | 具体实现就不用你过多操心了,总之函数的身份又修改正确了。 355 | 356 | ## 这里要考,划重点 357 | 358 | 经过上述一顿折腾,现在可以总结出一个非常**标准的装饰器模板**了: 359 | 360 | ```python 361 | import functools 362 | 363 | def decorator(func): 364 | @functools.wraps(func) 365 | def wrapper(*args, **kwargs): 366 | # 原函数运行前 367 | # Do something 368 | value = func(*args, **kwargs) 369 | # 原函数运行后 370 | # Do something 371 | return value 372 | return wrapper 373 | ``` 374 | 375 | 你可以在这个模板的基础上,衍生出功能复杂的装饰器。 376 | 377 | ## 一些例子 378 | 379 | ### 打印日志 380 | 381 | 装饰器非常经典的应用就是打印日志,比如打印时间、地点、访问记录等等。 382 | 383 | 拿前面的打印函数名举例: 384 | 385 | ```python 386 | import functools 387 | 388 | def log(func): 389 | @functools.wraps(func) 390 | def wrapper(*args, **kwargs): 391 | print('Calling: ' + func.__name__) 392 | return func(*args, **kwargs) 393 | return wrapper 394 | 395 | @log 396 | def some_func(): 397 | pass 398 | 399 | some_func() 400 | 401 | # 输出: 402 | # Calling: some_func 403 | ``` 404 | 405 | ### 计时器 406 | 407 | 一个简易的计时器装饰器: 408 | 409 | ```python 410 | import functools 411 | import time 412 | 413 | def time_it(func): 414 | @functools.wraps(func) 415 | def wrapper(*args, **kwargs): 416 | start = time.perf_counter() 417 | # 418 | value = func(*args, **kwargs) 419 | # 420 | end = time.perf_counter() 421 | duration = end - start 422 | print(f'Duration: {duration}') 423 | return value 424 | return wrapper 425 | 426 | @time_it 427 | def another_func(): 428 | time.sleep(1) 429 | 430 | another_func() 431 | 432 | # 输出: 433 | # Duration: 1.004140400000324 434 | ``` 435 | 436 | ### 减缓代码 437 | 438 | 下面这个装饰器可以让函数运行得更慢: 439 | 440 | ```python 441 | import functools 442 | import time 443 | 444 | def slow_down(func): 445 | @functools.wraps(func) 446 | def wrapper(*args, **kwargs): 447 | time.sleep(3) 448 | value = func(*args, **kwargs) 449 | print('Done.') 450 | return value 451 | return wrapper 452 | 453 | @slow_down 454 | def a_func(): 455 | pass 456 | ``` 457 | 458 | 为什么我要让代码运行得更慢?这样才方便以后帮雇主优化执行效率啊(这句划掉),也用于测试时模拟网络的卡顿环境。 459 | 460 | 总之装饰器的用法可以非常的花式,取决于你的业务需求。 461 | 462 | 下面让我们继续深入。 463 | 464 | ## 装饰器的参数 465 | 466 | 有的时候装饰器本身也需要接收参数,从而配置为不同的状态,比如打印日志时附带当前的用户名。 467 | 468 | 于是装饰器可能就变成了这样: 469 | 470 | ```python 471 | @logit(name='Dusai') 472 | ... 473 | ``` 474 | 475 | 但你要记得,不管怎么变化,**装饰器必须返回一个函数**。既然这里的装饰器多了一对括号,那就是多了一层调用,所以必须在之前无参数的情况下再增加一层的函数嵌套,也就是**三层嵌套的函数**: 476 | 477 | ```python 478 | import functools 479 | 480 | def logit(name): 481 | def decorator(func): 482 | @functools.wraps(func) 483 | def wrapper(*args, **kwargs): 484 | value = func(*args, **kwargs) 485 | print(f'{name} is calling: ' + func.__name__) 486 | return value 487 | return wrapper 488 | return decorator 489 | 490 | @logit(name='Dusai') 491 | def a_func(): 492 | pass 493 | 494 | a_func() 495 | 496 | # 输出: 497 | # Dusai is calling: a_func 498 | ``` 499 | 500 | 上面这个装饰器等效于: 501 | 502 | ```python 503 | a_func = log(name='Dusai')(a_func) 504 | ``` 505 | 506 | 开始有点烧脑了吧。 507 | 508 | ## 类作为装饰器 509 | 510 | 虽然前面例子里的装饰器都是函数,但是装饰器语法其实**并不要求**本身是函数,而只要是一个**可调用对象**即可。 511 | 512 | 既然如此,那我只要在**类**里实现了 `__call__()` 方法,岂不是类实例也可以做装饰器? 513 | 514 | 还是上面那个 `@logit()` 装饰器,试一下用类来实现: 515 | 516 | ```python 517 | import functools 518 | 519 | class Logit(): 520 | def __init__(self, name): 521 | self.name = name 522 | 523 | def __call__(self, func): 524 | @functools.wraps(func) 525 | def wrapper(*args, **kwargs): 526 | value = func(*args, **kwargs) 527 | print(f'{self.name} is calling: ' + func.__name__) 528 | return value 529 | return wrapper 530 | 531 | @Logit(name='Dusai') 532 | def a_func(): 533 | pass 534 | 535 | a_func() 536 | 537 | # 输出: 538 | # Dusai is calling: a_func 539 | ``` 540 | 541 | 万变不离其宗,感受一下。 542 | 543 | ## 闭包与装饰器 544 | 545 | 通常来说,函数中的变量为**局部变量**,一但函数执行完毕,其中的变量就不可用了: 546 | 547 | ```python 548 | def cook(): 549 | food = 'apple' 550 | 551 | cook() 552 | print(food) 553 | # 输出报错: 554 | # NameError: name 'food' is not defined 555 | ``` 556 | 557 | 但同样的情况到了**高阶函数**这里,就有点不对劲了。 558 | 559 | ```python 560 | def cook(): 561 | food = 'apple' 562 | def wrapper(): 563 | print(food) 564 | return wrapper 565 | 566 | value = cook() 567 | value() 568 | # 输出: 569 | # apple 570 | ``` 571 | 572 | 你发现 `cook()` 函数执行之后,按道理来说 `food` 变量就应该被销毁掉了。但实际上没有任何报错, `value()` 顺利的输出了 food 的值。 573 | 574 | 高阶函数中的内层函数携带外层函数中的参数、变量及其环境,一同存在的状态(即使已经离开了创造它的外层函数)被称之为**闭包**。被携带的外层变量被称为**自由变量**,有时候也被形容为外层变量被闭包**捕获**了。 575 | 576 | 发现没有,装饰器就是个天然的闭包。 577 | 578 | ## 带状态的装饰器 579 | 580 | 既然**装饰器就是闭包**,那么其中的**自由变量**就不会随着原函数的返回而销毁,而是伴随着原函数一直存在。利用这一点,装饰器就可以携带状态。 581 | 582 | 用下面这个计数器来理解一下: 583 | 584 | ```python 585 | import functools 586 | 587 | def counter(func): 588 | count = 0 589 | @functools.wraps(func) 590 | def wrapper(*args, **kwargs): 591 | nonlocal count 592 | count += 1 593 | print(count) 594 | return func(*args, **kwargs) 595 | return wrapper 596 | 597 | @counter 598 | def whatever(): 599 | pass 600 | 601 | whatever() 602 | whatever() 603 | whatever() 604 | 605 | # 输出: 606 | # 1 607 | # 2 608 | # 3 609 | ``` 610 | 611 | 通常闭包可以**使用**自由变量,但是不能**修改**其值。因此这里用 `nonlocal` 表明 `count` 不是内层函数的局部变量,并优先在与闭包作用域最近的自由变量中寻找 `count` 变量。 612 | 613 | 另一种带状态装饰器的解决方案是利用内层函数的属性: 614 | 615 | ```python 616 | import functools 617 | 618 | def counter(func): 619 | @functools.wraps(func) 620 | def wrapper(*args, **kwargs): 621 | wrapper.count += 1 622 | print(wrapper.count) 623 | return func(*args, **kwargs) 624 | wrapper.count = 0 625 | return wrapper 626 | 627 | @counter 628 | def whatever(): 629 | pass 630 | ``` 631 | 632 | 如果你的状态非常的复杂,那么可以考虑用**类装饰器**: 633 | 634 | ```python 635 | class Counter(): 636 | def __init__(self, start): 637 | self.count = start 638 | 639 | def __call__(self, func): 640 | @functools.wraps(func) 641 | def wrapper(*args, **kwargs): 642 | self.count += 1 643 | print(self.count) 644 | return func(*args, **kwargs) 645 | return wrapper 646 | 647 | @Counter(start=0) 648 | def whatever(): 649 | pass 650 | ``` 651 | 652 | 效果都差不多。 653 | 654 | ## 类的装饰器 655 | 656 | 实际上,装饰器不仅可以作用于函数,同样也可以作用于类: 657 | 658 | ```python 659 | import functools 660 | 661 | def logit(func): 662 | @functools.wraps(func) 663 | def wrapper(*args, **kwargs): 664 | print('-' * 10) 665 | print('Calling: ' + func.__name__) 666 | value = func(*args, **kwargs) 667 | print('-' * 10) 668 | return value 669 | return wrapper 670 | 671 | @logit 672 | class Tester(): 673 | def __init__(self): 674 | print('__init__ ended') 675 | 676 | def a_func(self): 677 | print('a_func ended') 678 | ``` 679 | 680 | 只不过效果可能和你预想的不太一样罢了: 681 | 682 | ```python 683 | tester = Tester() 684 | tester.a_func() 685 | 686 | # 输出 687 | # ---------- 688 | # Calling: Tester 689 | # __init__ ended 690 | # ---------- 691 | # a_func ended 692 | ``` 693 | 694 | **装饰器只在类实例化的时候起了效果**,而在调用其内部方法时并没有作用。 695 | 696 | 比较适合的用法是用装饰器实现**单例模式**: 697 | 698 | ```python 699 | import functools 700 | 701 | def singleton(cls): 702 | """使类只有一个实例""" 703 | @functools.wraps(cls) 704 | def wrapper(*args, **kwargs): 705 | if not wrapper.instance: 706 | wrapper.instance = cls(*args, **kwargs) 707 | return wrapper.instance 708 | wrapper.instance = None 709 | return wrapper 710 | 711 | @singleton 712 | class OnlyOne: 713 | pass 714 | 715 | first = OnlyOne() 716 | second = OnlyOne() 717 | 718 | print(id(first)) 719 | # 输出: 1964238157376 720 | print(id(second)) 721 | # 输出: 1964238157376 722 | ``` 723 | 724 | 不过单例模式在 Python 中并没有其他语言中那么常见。 725 | 726 | 如果你想类中的方法也附加装饰器的功能,只需要直接在方法上放置装饰器即可: 727 | 728 | ```python 729 | import functools 730 | 731 | def logit(func): 732 | @functools.wraps(func) 733 | def wrapper(*args, **kwargs): 734 | print('-' * 10) 735 | print('Calling: ' + func.__name__) 736 | value = func(*args, **kwargs) 737 | print('-' * 10) 738 | return value 739 | return wrapper 740 | 741 | class Tester(): 742 | def __init__(self): 743 | print('__init__ ended') 744 | 745 | @logit 746 | def a_func(self): 747 | print('a_func ended') 748 | 749 | tester = Tester() 750 | tester.a_func() 751 | # 输出: 752 | # __init__ ended 753 | # ---------- 754 | # Calling: a_func 755 | # a_func ended 756 | # ---------- 757 | ``` 758 | 759 | ## 叠加装饰器 760 | 761 | 装饰器可以叠加使用,像下面这样: 762 | 763 | ```python 764 | import functools 765 | 766 | def inc(func): 767 | @functools.wraps(func) 768 | def wrapper(*args, **kwargs): 769 | print('+' * 10) 770 | value = func(*args, **kwargs) 771 | print('+' * 10) 772 | return value 773 | return wrapper 774 | 775 | def dec(func): 776 | @functools.wraps(func) 777 | def wrapper(*args, **kwargs): 778 | print('-' * 5) 779 | value = func(*args, **kwargs) 780 | print('-' * 5) 781 | return value 782 | return wrapper 783 | 784 | 785 | @inc 786 | @dec 787 | def printer(): 788 | print('I am here!') 789 | 790 | printer() 791 | 792 | # 输出: 793 | # ++++++++++ 794 | # ----- 795 | # I am here! 796 | # ----- 797 | # ++++++++++ 798 | ``` 799 | 800 | 上面的语法相当于: 801 | 802 | ```python 803 | printer = inc(dec(printer)) 804 | ``` 805 | 806 | 这时候装饰器之间的顺序非常重要。 807 | 808 | 如果把两个装饰器位置互换: 809 | 810 | ```python 811 | @dec 812 | @inc 813 | def printer(): 814 | print('I am here!') 815 | 816 | printer() 817 | 818 | # 输出: 819 | # ----- 820 | # ++++++++++ 821 | # I am here! 822 | # ++++++++++ 823 | # ----- 824 | ``` 825 | 826 | 输出顺序改变,说明执行的顺序也改变了。 827 | 828 | ## 总结 829 | 830 | 以上就是**装饰器入门**所需的全部知识了: 831 | 832 | - 装饰器是闭包的一种应用,是返回值为函数的高阶函数; 833 | - 装饰器修饰可调用对象,也可以带有参数和返回值; 834 | - 装饰器中可以保持状态。 835 | 836 | 复杂的理论是建立在简单的规则之上的。 Python 的学习者们切忌浮躁,练好九阴真经,方得万剑归宗。 837 | 838 | --- 839 | 840 | 本文参考: 841 | 842 | - [Primer on Python Decorators](https://realpython.com/primer-on-python-decorators/#conclusion) 843 | - [Python Decorators](https://www.programiz.com/python-programming/decorator) 844 | 845 | > 作者杜赛,Python科普写手,著有 [Django搭建博客](https://www.dusaiphoto.com/topic/) 等系列教程。 846 | -------------------------------------------------------------------------------- /md/260-Python闭包概念入门.md: -------------------------------------------------------------------------------- 1 | **闭包**(Closure)是 Python 中一个重要的工具。 2 | 3 | 对于初学者来说,闭包是比较难懂的概念。因此让我们从 Python 基础开始,逐步理解闭包。 4 | 5 | ## 一切皆对象 6 | 7 | 在 Python 中,**函数可以根据给定的参数返回一个值**: 8 | 9 | ```python 10 | def hello(name): 11 | return 'Hello ' + name 12 | 13 | print(hello('Bob')) 14 | 15 | # 输出: 16 | # Hello Bob 17 | ``` 18 | 19 | 与 Python 的其他对象(如字符串、整数、列表等)一样,**函数也是对象**,也可以赋值给一个变量: 20 | 21 | ```python 22 | def hello(name): 23 | return 'Hello ' + name 24 | 25 | h = hello 26 | 27 | print(hello) 28 | # 输出: 29 | # 30 | 31 | print(h) 32 | # 输出: 33 | # 34 | 35 | print(h('Jack')) 36 | # 输出: 37 | # Hello Jack 38 | ``` 39 | 40 | 可以看到 `hello` 和 `h` 都指向同一个函数,而函数后加括号 `h('Jack')` 是对其进行了调用。 41 | 42 | ## 函数里的函数 43 | 44 | 同时,函数里面还可以定义函数: 45 | 46 | ```python 47 | def hi(): 48 | def bob(): 49 | return 'Bob' 50 | print('Hi ' + bob()) 51 | 52 | hi() 53 | # 输出: 54 | # Hi Bob 55 | ``` 56 | 57 | 此时的 `bob` 函数的作用域在 `hi` 之内的。如果在全局调用 `bob()` 会引发错误: 58 | 59 | ```python 60 | >>> bob() 61 | NameError: name 'bob' is not defined 62 | ``` 63 | 64 | ## 函数作为返回值 65 | 66 | 很自然的,函数也可以作为其他函数的返回值,比如: 67 | 68 | ```python 69 | def cook(): 70 | def tomato(): 71 | print('I am Tomato') 72 | return tomato 73 | 74 | t = cook() 75 | t() 76 | # 输出: 77 | # I am Tomato 78 | ``` 79 | 80 | 函数可以作为返回值,也可以内部定义。这种在函数里传递、嵌套、返回其他函数的情况,称之为**高阶函数**。 81 | 82 | > 除此之外,函数还可以作为其他函数的参数。 83 | 84 | ## 闭包与自由变量 85 | 86 | 通常来说,函数中的变量为**局部变量**,一但函数执行完毕,其中的变量就不可用了: 87 | 88 | ```python 89 | def cook(): 90 | food = 'apple' 91 | 92 | cook() 93 | print(food) 94 | # 输出报错: 95 | # NameError: name 'food' is not defined 96 | ``` 97 | 98 | 但同样的情况到了**高阶函数**这里,就有点不对劲了。 99 | 100 | ```python 101 | def cook(): 102 | food = 'apple' 103 | def wrapper(): 104 | print(food) 105 | return wrapper 106 | 107 | value = cook() 108 | value() 109 | # 输出: 110 | # apple 111 | ``` 112 | 113 | 你发现 `cook()` 函数执行之后,按道理来说 `food` 变量就应该被销毁掉了。但实际上没有任何报错, `value()` 顺利的输出了 food 的值。 114 | 115 | 甚至于,即使将 `cook()` 函数销毁了,`food` 的值都还存在: 116 | 117 | ```python 118 | def cook(): 119 | food = 'apple' 120 | def wrapper(): 121 | print(food) 122 | return wrapper 123 | 124 | value = cook() 125 | 126 | # 删除原函数 127 | del cook 128 | 129 | value() 130 | # 输出: 131 | # apple 132 | ``` 133 | 134 | 高阶函数中,内层函数携带外层函数中的参数、变量及其环境,一同存在的状态(即使已经离开了创造它的外层函数)被称之为**闭包**。被携带的外层变量被称为**自由变量**,有时候也被形容为外层变量被闭包**捕获**了。 135 | 136 | 理解了上面这段话,那么恭喜你,你已经理解闭包的大部分内容了。 137 | 138 | 为了把整个事情讲得更清楚,让我们继续深入。 139 | 140 | ## 参数捕获 141 | 142 | 很多时候,我们希望闭包所捕获的自由变量可以根据不同的情况有所区分。 143 | 144 | 很简单,把它作为外层函数的参数就可以了: 145 | 146 | ```python 147 | def cook(name): 148 | def wrapper(): 149 | print('I am cooking ' + name) 150 | return wrapper 151 | 152 | 153 | apple = cook('apple') 154 | pear = cook('pear') 155 | 156 | apple() 157 | # 输出: I am cooking apple 158 | pear() 159 | # 输出: I am cooking pear 160 | ``` 161 | 162 | 你看,外层函数的参数也可以成为自由变量,被封装到内层函数所在的环境中。 163 | 164 | 这种局部变量起作用的特定环境,有时候被称为**作用域**或者**域**。 165 | 166 | ## 函数生成 167 | 168 | 既然外层函数可以携带参数,那被返回的内层函数当然也可以带参数: 169 | 170 | ```python 171 | def outer(x): 172 | def inner(y): 173 | print(x + y) 174 | return inner 175 | 176 | outer(1)(2) 177 | # 输出: 178 | # 3 179 | ``` 180 | 181 | 看到两个括号就代表进行了两次函数调用。第一个括号对应 `outer` 的参数 `x` ,第二个括号里对应 `inner` 的参数 `y`。 182 | 183 | 因此,利用闭包携带参数并返回函数的这个特性,可以很方便的在一个底层的函数框架上,组装出不同的功能。 184 | 185 | 比如: 186 | 187 | ```python 188 | def add(x): 189 | def inner(y): 190 | print(x + y) 191 | return inner 192 | 193 | add_one = add(1) 194 | add_ten = add(10) 195 | 196 | add_one(5) 197 | # 输出: 198 | # 6 199 | add_ten(5) 200 | # 输出: 201 | # 15 202 | ``` 203 | 204 | 外层函数传递的参数甚至可以是个函数。你可以想象能玩出多少花样。 205 | 206 | ## 状态持有 207 | 208 | 闭包中的自由变量有两个神奇的特性。 209 | 210 | 第一个特性是,自由变量在闭包存在的期间,其中的值也会一直存在。因此闭包可以持有**状态**。 211 | 212 | 举个栗子: 213 | 214 | ```python 215 | # 记录每次取得的分数 216 | def make_score(): 217 | lis = [] 218 | def inner(x): 219 | lis.append(x) 220 | print(lis) 221 | return inner 222 | 223 | score = make_score() 224 | 225 | score(82) 226 | score(66) 227 | score(100) 228 | # 输出: 229 | # [82] 230 | # [82, 66] 231 | # [82, 66, 100] 232 | ``` 233 | 234 | 可以看出 `score` 闭包打印的列表记录了每次调用的结果。 235 | 236 | 另一个特性是,闭包与闭包之间的状态是**隔离**的。 237 | 238 | 还是举个栗子: 239 | 240 | ```python 241 | def make_score(): 242 | lis = [] 243 | def inner(x): 244 | lis.append(x) 245 | print(lis) 246 | return inner 247 | 248 | first = make_score() 249 | second = make_score() 250 | 251 | first(1) 252 | first(2) 253 | # 输出: 254 | # [1] 255 | # [1, 2] 256 | 257 | second(3) 258 | second(4) 259 | # 输出: 260 | # [3] 261 | # [3, 4] 262 | ``` 263 | 264 | 以上两个特性,使得闭包像一个微型的类,因为状态持有和数据隐藏是类的基本功能嘛。 265 | 266 | 它两在使用上的建议是:如果你的状态比较简单,那么可以用闭包来实现;相反则使用类。 267 | 268 | ## 不变量状态 269 | 270 | 在上面的例子里,闭包用 `lis.append()` 直接操作了自由变量。 271 | 272 | 但如果要操作的自由变量是个**不变量**,比如数值型、字符串等,那么记得加 **nonlocal** 关键字: 273 | 274 | ```python 275 | # 记录成绩总分 276 | def make_score(): 277 | total = 10 278 | def inner(x): 279 | nonlocal total 280 | total += x 281 | print(total) 282 | return inner 283 | 284 | total = make_score() 285 | total(5) 286 | # 输出: 287 | # 15 288 | ``` 289 | 290 | 此关键字就是在告诉解释器:接下来的 `total` 不是本函数里的局部变量,你最好去闭包或是别的地方找找。 291 | 292 | ## 延迟陷阱 293 | 294 | 有个跟闭包相关的常见陷阱: 295 | 296 | ```python 297 | funcs = [] 298 | for i in range(3): 299 | def inner(): 300 | print(i) 301 | funcs.append(inner) 302 | 303 | funcs[0]() 304 | funcs[1]() 305 | funcs[2]() 306 | # 输出: 307 | # 2 308 | # 2 309 | # 2 310 | ``` 311 | 312 | 直觉上好像应该输出 `0, 1, 2` ,但实际上是 `2, 2, 2` 。这是因为函数 `inner` 是延迟执行的,直到真正调用前,都是没进行内部操作的。 313 | 314 | 加上这句就清楚了: 315 | 316 | ```python 317 | funcs = [] 318 | for i in range(3): 319 | def inner(): 320 | print(i) 321 | funcs.append(inner) 322 | 323 | print(f'i is: {i}') 324 | # 输出: 325 | # i is: 2 326 | funcs[0]() 327 | funcs[1]() 328 | funcs[2]() 329 | # 输出: 330 | # 2 331 | # 2 332 | # 2 333 | ``` 334 | 335 | 解决方案就是用闭包将 `i` 的值立即捕获: 336 | 337 | ```python 338 | funcs = [] 339 | for i in range(3): 340 | def outer(a): 341 | def inner(): 342 | print(a) 343 | return inner 344 | funcs.append(outer(i)) 345 | 346 | print(f'i is: {i}') 347 | funcs[0]() 348 | funcs[1]() 349 | funcs[2]() 350 | # 输出: 351 | # i is: 2 352 | # 0 353 | # 1 354 | # 2 355 | ``` 356 | 357 | 对闭包的概念讲解基本上就这样。 358 | 359 | 接下来继续补充点闭包的常见应用。 360 | 361 | ## 组合函数 362 | 363 | 在上面**[函数生成]**的章节中,我们已经体验过利用闭包捕获参数、从而生成新函数的能力了。 364 | 365 | 更棒的是,被捕获的参数也可以是函数。 366 | 367 | 比如说,可以用闭包实现函数的拼接: 368 | 369 | ```python 370 | # 这个函数是重点 371 | def compose(g, f): 372 | def inner(*args, **kwargs): 373 | return g(f(*args, **kwargs)) 374 | return inner 375 | 376 | # 被拼接函数1 377 | def remove_first(lis): 378 | return lis[1:] 379 | 380 | # 被拼接函数2 381 | def remove_last(lis): 382 | return lis[:-1] 383 | 384 | # 这里进行了函数的合成 385 | middle = compose(remove_first, remove_last) 386 | new_lis = middle([1, 2, 3, 4, 5]) 387 | 388 | print(new_lis) 389 | # 输出: 390 | # [2, 3, 4] 391 | ``` 392 | 393 | 很方便的用两个简单的函数,合成出更复杂一点的新函数,提高代码复用的能力。 394 | 395 | ## 柯里化 396 | 397 | 最后,让我们来看个更复杂的应用: 398 | 399 | ```python 400 | # 柯里化闭包函数 401 | def curry(f): 402 | argc = f.__code__.co_argcount 403 | f_args = [] 404 | f_kwargs = {} 405 | def g(*args, **kwargs): 406 | nonlocal f_args, f_kwargs 407 | f_args += args 408 | f_kwargs.update(kwargs) 409 | if len(f_args)+len(f_kwargs) == argc: 410 | return f(*f_args, **f_kwargs) 411 | else: 412 | return g 413 | return g 414 | 415 | 416 | # 无关紧要的原函数 417 | def add(a, b, c): 418 | return a + b + c 419 | 420 | # c_add 是被柯里化的新函数 421 | c_add = curry(add) 422 | ``` 423 | 424 | 执行上面的代码后, `c_add` 这个函数就**非常魔性**了。来看看效果。 425 | 426 | 首先,原函数 `add` 必须接收3个参数,否则就会报错: 427 | 428 | ```python 429 | >>> add(1,2,3) 430 | 6 431 | >>> add(1) 432 | TypeError: add() missing 2 required positional arguments: 'b' and 'c' 433 | ``` 434 | 435 | 但是 `c_add` 是很有想法的函数,它居然可以**只接收部分参数**! 436 | 437 | 试一试,只提供一个参数: 438 | 439 | ```python 440 | >>> c_add(1) 441 | .g(*args, **kwargs)> 442 | ``` 443 | 444 | 没报错。 445 | 446 | 接着输入第二个参数: 447 | 448 | ```python 449 | >>> c_add(2) 450 | .g(*args, **kwargs)> 451 | ``` 452 | 453 | 然后输入最后一个参数: 454 | 455 | ```python 456 | >>> c_add(3) 457 | 6 458 | ``` 459 | 460 | 参数集齐后,终于得到了正确的结果。神奇吧。 461 | 462 | 像 `c_add` 这种函数,可以只接收一部分的参数(通常是每次只接收一个参数),并返回一个携带状态的新函数的函数,称为它被**柯里化**了。 463 | 464 | 前面已经多次说过了,更棒的是这些参数也可以是函数。 465 | 466 | 因此在有的函数式编程语言中,柯里化函数就像接龙一样,把很多简单函数组合为一个复杂的函数,比如在 Haskell 中: 467 | 468 | ```python 469 | fn = ceiling . negate . tan . cos . max 50 470 | ``` 471 | 472 | 这种被称为**无值风格**,有助于清晰表达函数的意义,以及将复杂问题分解为简单问题。 473 | 474 | > 柯里化和无值风格又是另一个大题目了。有兴趣的读者可以在评论区告诉我,是否要开一篇新文章探讨。 475 | 476 | 扯远了。让我们回到上面那个实现柯里化的 `curry` 函数的逻辑: 477 | 478 | - 它返回一个闭包,并且将闭包接收到的参数记录在自由变量中 479 | - 如果当前参数过少,则返回一个继续接收参数的新函数 480 | - 如果当前参数足够,则执行原函数并返回结果 481 | 482 | ## 结论 483 | 484 | 总结一下闭包的几个特性: 485 | 486 | - 它是一个嵌套函数 487 | - 它可以访问外部作用域中的自由变量 488 | - 它从外层函数返回 489 | 490 | 闭包的应用多样,但基本都是利用了其能够捕获自由变量的特点。除了以上所列举的以外,闭包另一个重要的应用就是**装饰器**。我写过一篇详细的[装饰器入门](https://www.dusaiphoto.com/article/139/)的文章,读者可结合闭包一起理解,也欢迎和我探讨。 491 | 492 | --- 493 | 494 | 本文参考: 495 | 496 | - [Python Closures](https://www.programiz.com/python-programming/closure) 497 | - [Closures and Decorators in Python](https://towardsdatascience.com/closures-and-decorators-in-python-2551abbc6eb6) 498 | 499 | > 作者杜赛,Python科普写手,著有 [Django搭建博客](https://www.dusaiphoto.com/topic/) 等系列教程。 500 | -------------------------------------------------------------------------------- /md/270-Python生成器.md: -------------------------------------------------------------------------------- 1 | Python 中的**生成器**(Generator)是十分有用的工具,它能够方便地生成迭代器(Iterator)。 2 | 3 | 这篇文章就来说说什么是生成器,它有什么作用以及如何使用。 4 | 5 | ## 普通函数 6 | 7 | Python 中的普通函数通常是这样的: 8 | 9 | ```python 10 | def normal(): 11 | print('Before return..') 12 | return 1 13 | ``` 14 | 15 | 函数里的语句将依次执行,并在遇到 `return` 时**立即终止**函数的运行并返回值。 16 | 17 | 因此,像下面这样连续写多个 `return` 是没有意义的: 18 | 19 | ```python 20 | def normal(): 21 | print('Before return..') 22 | return 1 23 | # 下面语句永远不会被执行 24 | print('After return..') 25 | return 2 26 | ``` 27 | 28 | ## 生成器函数 29 | 30 | 把普通函数修改为生成器函数很简单,只需要把 `return` 替换成 `yield` 就行了,像这样: 31 | 32 | ```python 33 | def gen(): 34 | print('yield 1..') 35 | yield 1 36 | 37 | print('yield 2..') 38 | yield 2 39 | 40 | print('yield 3..') 41 | yield 3 42 | ``` 43 | 44 | 试着来用用这个生成器: 45 | 46 | ```python 47 | >>> gen = my_gen() 48 | >>> gen 49 | 50 | ``` 51 | 52 | 可以看到,`gen` 变量被赋值为一个 `generator` ,即生成器。 53 | 54 | 生成器有一个最重要的特点,即当它执行到 `yield` 语句并返回后,生成器不会被立即终止,而是暂停在当前 `yield` 的位置,等待下一次调用: 55 | 56 | ```python 57 | >>> next(gen) 58 | yield 1.. 59 | 60 | >>> next(gen) 61 | yield 2.. 62 | 63 | >>> next(gen) 64 | yield 3.. 65 | 66 | >>> next(gen) 67 | Traceback (most recent call last): 68 | File "", line 1, in 69 | next(gen) 70 | StopIteration 71 | ``` 72 | 73 | 生成器用 `next()` 函数调用,每次调用都从上一次 `yield` 暂停的位置,继续向下执行。 74 | 75 | 当所有的 `yield` 执行完毕后,再一次调用 `next()` 就会抛出 `StopIteration` 错误,提示你生成器已经结束了。 76 | 77 | 既然会抛出错误,那就需要处理错误。用 `try` 语句完善一下就有: 78 | 79 | ```python 80 | def my_gen(): 81 | print('yield 1..') 82 | yield 1 83 | print('yield 2..') 84 | yield 2 85 | print('yield 3..') 86 | yield 3 87 | 88 | gen = my_gen() 89 | while True: 90 | try: 91 | next(gen) 92 | except StopIteration: 93 | print('Done..') 94 | break 95 | 96 | # 输出: 97 | # yield 1.. 98 | # yield 2.. 99 | # yield 3.. 100 | # Done.. 101 | ``` 102 | 103 | ## 迭代 104 | 105 | `next()` 调用太啰嗦,通常我们用迭代的方式获取生成器的值: 106 | 107 | ```python 108 | def my_gen(): 109 | yield 1 110 | yield 2 111 | yield 3 112 | 113 | gen = my_gen() 114 | for item in gen: 115 | print(item) 116 | 117 | # 输出: 118 | # 1 119 | # 2 120 | # 3 121 | ``` 122 | 123 | `for` 语句不仅简洁,还自动帮我们处理好了生成器的终止。 124 | 125 | 以上就是生成器的的基础知识了。下面看几个它的应用。 126 | 127 | ## 读取大文件 128 | 129 | 假设你需要读取并处理数据流或大文件(比如 txt/csv 文件),可能会这么写: 130 | 131 | ```python 132 | def csv_reader(file_name): 133 | file = open(file_name) 134 | result = file.read().split("\n") 135 | return result 136 | ``` 137 | 138 | 通常这都是没啥问题的。但如果这个文件非常非常大,那么将会得到内存溢出的报错: 139 | 140 | ```python 141 | Traceback (most recent call last): 142 | ... 143 | File "...", line 6, in csv_reader 144 | result = file.read().split("\n") 145 | MemoryError 146 | ``` 147 | 148 | 原因就在 `file.read().split("\n")` 一次性将所有内容加载到内存中,导致溢出。 149 | 150 | 解决此问题,用生成器可以这么写: 151 | 152 | ```python 153 | def csv_reader(file_name): 154 | for row in open(file_name, "r"): 155 | yield row 156 | ``` 157 | 158 | 由于这个版本的 `csv_reader()` 是个生成器,因此你可以通过遍历,加载一行、处理一行,从而避免了内存溢出的问题。 159 | 160 | ## 无限序列 161 | 162 | 理论上存储无限序列需要无限的空间,这是不可能的。 163 | 164 | 但是由于生成器一次只生成一个值,因此它可用于表示无限数据。(理论上) 165 | 166 | 比如生成所有偶数: 167 | 168 | ```python 169 | def all_even(): 170 | n = 0 171 | while True: 172 | yield n 173 | n += 2 174 | 175 | even = all_even() 176 | for i in even: 177 | print(i) 178 | ``` 179 | 180 | 这个程序将无限的运行下去,直到你手动打断它。 181 | 182 | ## 优化内存 183 | 184 | 假设你需要 1 到 10000 的序列,考虑用列表和生成器两种形式保存它: 185 | 186 | ```python 187 | def gen(): 188 | for x in range(10000): 189 | yield x 190 | 191 | # 生成器 192 | my_gen = gen() 193 | # 列表 194 | my_list = [x for x in range(10000)] 195 | ``` 196 | 197 | 来比较下它两的大小: 198 | 199 | ```python 200 | >>> import sys 201 | 202 | >>> sys.getsizeof(my_list) 203 | 87616 204 | >>> sys.getsizeof(my_gen) 205 | 112 206 | ``` 207 | 208 | 生成器有点像只是保存一个公式而已。而列表是老老实实的把数据计算并保存了。 209 | 210 | 实际上,生成器还有一种更简单的写法,像这样: 211 | 212 | ```python 213 | # 列表推导式 214 | my_list = [x for x in range(10000)] 215 | 216 | # 生成器表达式 217 | my_gen = (x for x in range(10000)) 218 | ``` 219 | 220 | 它与列表推导式的区别就在于是用圆括号。 221 | 222 | 需要说明的是,通常生成器的迭代速度会比列表更慢。这在逻辑上也说得通,毕竟生成器的值需要即时计算,而列表的值摆在那就能用。空间和时间,根据情况选用。 223 | 224 | ## 生成器组合 225 | 226 | 有时候你需要把两个生成器组合成一个新的生成器,比如: 227 | 228 | ```python 229 | gen_1 = (i for i in range(0,3)) 230 | gen_2 = (i for i in range(6,9)) 231 | 232 | def new_gen(): 233 | for x in gen_1: 234 | yield x 235 | for y in gen_2: 236 | yield y 237 | 238 | for x in new_gen(): 239 | print(x) 240 | 241 | # 输出: 242 | # 0 243 | # 1 244 | # 2 245 | # 6 246 | # 7 247 | # 8 248 | ``` 249 | 250 | 这种组合迭代的形式不太方便,因此 Python 3.3 引入新语法 `yield from` 后,可以改成这样: 251 | 252 | ```python 253 | def new_gen(): 254 | yield from gen_1 255 | yield from gen_2 256 | ``` 257 | 258 | 它代替了 `for` 循环,迭代并返回生成器的值。 259 | 260 | > `yield from` 感觉上像是语法糖,不过它主要的应用场景是在协程中,这里就不展开探讨了。 261 | 262 | ## 生成器进阶语法 263 | 264 | ### 使用 .send() 265 | 266 | 既然生成器允许我们暂停控制流并返回数据,那么就有可能需要将某些数据传回生成器。数据交流总是双向的嘛。 267 | 268 | 举个例子: 269 | 270 | ```python 271 | def gen(): 272 | count = 0 273 | while True: 274 | count += (yield count) 275 | ``` 276 | 277 | `yield` 变成个表达式了,并且可以通过 `.send()` 传回数据: 278 | 279 | ```python 280 | >>> g = gen() 281 | >>> g.send(None) 282 | 0 283 | >>> g.send(1) 284 | 1 285 | >>> g.send(2) 286 | 3 287 | >>> g.send(5) 288 | 8 289 | ``` 290 | 291 | 稍微要注意的是首次调用时,必须要先执行一次 `next()` 或者 `.send(None)` 使生成器到达 `yield` 位置。 292 | 293 | ### 使用 .throw() 294 | 295 | `.throw()` 允许用生成器抛出异常,像这样: 296 | 297 | ```python 298 | def my_gen(): 299 | count = 0 300 | while True: 301 | yield count 302 | count += 1 303 | 304 | gen = my_gen() 305 | for i in gen: 306 | print(i) 307 | if i == 3: 308 | gen.throw(ValueError('The number is 3...')) 309 | 310 | # 输出: 311 | # 0 312 | # 1 313 | # 2 314 | # 3 315 | # ValueError: The number is 3... 316 | ``` 317 | 318 | 这在任何需要捕获异常的领域都很有用。 319 | 320 | ### 使用 .close() 321 | 322 | `.close()` 可以停止生成器,比如把上面的例子改改: 323 | 324 | ```python 325 | def my_gen(): 326 | count = 0 327 | while True: 328 | yield count 329 | count += 1 330 | 331 | gen = my_gen() 332 | for i in gen: 333 | print(i) 334 | if i == 3: 335 | gen.close() 336 | ``` 337 | 338 | 这次就不会抛出异常了,而是在迭代完数字 3 之后,生成器就顺利地停止了。 339 | 340 | ## 结论 341 | 342 | 以上就是生成器的大致介绍了。它可以暂停控制流,并在你需要的时候随时回到控制流,从上一次暂停的位置继续执行。 343 | 344 | 生成器有助于你处理大型数据流或者表达无限序列,是生成迭代器的有用工具。 345 | 346 | --- 347 | 348 | 参考链接: 349 | 350 | - [Python Generators](https://www.programiz.com/python-programming/generator) 351 | - [How to Use Generators and yield in Python](https://realpython.com/introduction-to-python-generators/) 352 | 353 | > 作者杜赛,Python 科普写手,著有 [Django搭建个人博客](https://www.dusaiphoto.com/topic/) 等系列教程。 354 | -------------------------------------------------------------------------------- /md/280-Python黑魔法:元类与元编程.md: -------------------------------------------------------------------------------- 1 | **元类**(Metaclass)是面向对象编程中一个深奥的概念,它几乎隐藏在所有的 Python 代码后面,但通常你根本意识不到这点。 2 | 3 | 相比其他面向对象语言,Python 甚至允许你可以自定义元类。自定义元类的使用向来很有争议,就像下面这位大佬所说的名言: 4 | 5 | > “元类是99%的开发者都不需要用到的黑魔法。如果你在犹豫是否需要用到它,那答案就是不需要(真正需要的人肯定知道为什么要用,并且不需要解释原因)。” 6 | > 7 | > ​ — *Tim Peters* 8 | 9 | 虽然使用元类不是必需的,但理解它还是值得的,因为可以帮助更好地理解 Python 的奥妙。 10 | 11 | 谁还不希望掌握一点黑魔法呢? 12 | 13 | ## 一切皆对象 14 | 15 | 在我最近的文章中,**一切皆对象**都快被说烂了。但是 Python 中很多特性都跟这相关,因此让我们还是从一切皆对象开始说起。 16 | 17 | 随便定义几个对象: 18 | 19 | ```python 20 | a = 1 21 | b = {'x': 2, 'y': 3} 22 | c = [4, 5] 23 | 24 | class Foo: 25 | pass 26 | 27 | foo = Foo() 28 | ``` 29 | 30 | 对象需要从属某个**类型**,以表明自己是个什么样的对象。而用 `type()` 函数可以获取到某个对象的类型。 31 | 32 | 比如上面这些: 33 | 34 | ```python 35 | >>> type(a) 36 | int 37 | >>> type(b) 38 | dict 39 | >>> type(c) 40 | list 41 | >>> type(foo) 42 | __main__.Foo 43 | ``` 44 | 45 | a、b、c 的类型分别是整型、字典和列表,而 foo 实例的类型是 `Foo` 类。 46 | 47 | 让我们进一步思考:类也是对象,那**类的类型**是什么呢? 48 | 49 | 来试试: 50 | 51 | ```python 52 | >>> type(Foo) 53 | type 54 | ``` 55 | 56 | `Foo` 类的类型是 `type` 。 `type` 实际上就是个元类,也就是 Python 在幕后创建**所有类**的元类。 57 | 58 | 顺带一说,Python 中**所有**的对象都是从**类**派生出来的,包括内置的整型、字符串、列表等。因此: 59 | 60 | ```python 61 | >>> type(int) 62 | type 63 | >>> type(dict) 64 | type 65 | >>> type(list) 66 | type 67 | ``` 68 | 69 | 有点烧脑对吧,但其实元类并不复杂。换句话说,**元类只是用于创建类的东西**,也可以称为类工厂。 70 | 71 | > 所谓元类,就是创建类的类。类用于创建类实例;元类用于创建类。 72 | > 73 | > 即:元类 -> 类 -> 类实例 74 | 75 | ## type魔法 76 | 77 | `type()` 函数除了可以查看对象的类型外,更强大的是它还可以接收三个参数来**动态创建类**。 78 | 79 | 调用时 `type(name, bases, dct)` 三个参数分别是: 80 | 81 | - `name` 字符串类型,指定要创建的类名 82 | - `bases` 元组类型,指定该类的父类 83 | - `dct` 字典类型,存放该类的所有属性和方法 84 | 85 | 举个栗子: 86 | 87 | ```python 88 | Bar = type('Bar', (), {}) 89 | ``` 90 | 91 | 调用下试试: 92 | 93 | ```python 94 | >>> Bar 95 | __main__.Bar 96 | 97 | >>> b = Bar() 98 | >>> b 99 | <__main__.Bar at 0x253b636abe0> 100 | ``` 101 | 102 | 这就相当于下面的代码: 103 | 104 | ```python 105 | class Bar: 106 | pass 107 | ``` 108 | 109 | 它两是等价的。实际上 Python 解释器在遇到 `class` 定义时,幕后也是调用 `type()` 创建出类的。 110 | 111 | 再看一个例子: 112 | 113 | ```python 114 | Foo = type('Foo', (Bar,), {}) 115 | ``` 116 | 117 | 这就相当于: 118 | 119 | ```python 120 | class Foo(Bar): 121 | pass 122 | ``` 123 | 124 | 如果类里有属性和方法呢? 125 | 126 | 看最后一个例子: 127 | 128 | ```python 129 | Calc = type( 130 | 'Calc', 131 | (), 132 | { 133 | 'num': 100, 134 | 'half': lambda x: x.num / 2 135 | } 136 | ) 137 | ``` 138 | 139 | 试试调用其属性和方法: 140 | 141 | ```python 142 | >>> calc = Calc() 143 | 144 | >>> calc.num 145 | 100 146 | 147 | >>> calc.half() 148 | 50.0 149 | ``` 150 | 151 | 这就相当于: 152 | 153 | ```python 154 | class Calc: 155 | num = 100 156 | def half(self): 157 | return self.num / 2 158 | ``` 159 | 160 | 另外, `lambda` 表达式只能定义比较简单的函数。复杂函数你可以单独定义,然后赋值进去,比如: 161 | 162 | ```python 163 | def f(obj): 164 | return 1 165 | 166 | Yeah = type( 167 | 'Yeah', 168 | (), 169 | { 170 | 'hi': f 171 | } 172 | ) 173 | ``` 174 | 175 | ## 自定义元类 176 | 177 | 自定义元类的主要目的是在创建类时,动态更改类的某些行为。 178 | 179 | 回到这个例子: 180 | 181 | ```python 182 | class Foo: 183 | pass 184 | ``` 185 | 186 | 如果要在创建**类实例**时动态修改属性,可以修改类的 `__new__` 方法: 187 | 188 | ```python 189 | class Foo: 190 | pass 191 | 192 | def new(cls): 193 | obj = object.__new__(cls) 194 | obj.num = 100 195 | return obj 196 | 197 | Foo.__new__ = new 198 | ``` 199 | 200 | 测试下: 201 | 202 | ```python 203 | >>> foo = Foo() 204 | >>> foo.num 205 | 100 206 | ``` 207 | 208 | 那如果我创建**类**也想动态修改属性呢?由于类都是由 `type` 创建出来的,那我是否应该修改 `type` 的 `__new__` 方法? 209 | 210 | 试一下: 211 | 212 | ```python 213 | def new(cls): 214 | obj = type.__new__(cls) 215 | obj.num = 100 216 | return obj 217 | 218 | type.__new__ = new 219 | 220 | # 输出: 221 | # Traceback (most recent call last): 222 | # File "...", line 25, in 223 | # type.__new__ = new 224 | # TypeError: can't set attributes of built-in/extension type 'type' 225 | ``` 226 | 227 | type 是所有类的模板,修改它的特性非常危险。Python 不允许你这么瞎搞,直接报错了。 228 | 229 | 一种解决方案就是**自定义元类**: 230 | 231 | ```python 232 | class MyMeta(type): 233 | def __new__(cls, name, bases, dct): 234 | obj = super().__new__(cls, name, bases, dct) 235 | obj.num = 100 236 | return obj 237 | ``` 238 | 239 | 这就是个简单的自定义元类了。让我们一步步拆解: 240 | 241 | - 因为 `type` 是元类,因此继承它的子类也是元类。 242 | - 元类创建对象时会调用 `__new__` 方法,因此可以在这里对创建的类进行动态修改。 243 | 244 | 接着看 `__new__` 内部: 245 | 246 | - 第一句,原封不动调用父类 `type` 创建类时的动作。 247 | - 第二句,给创建出来的类附加额外的 `num` 属性。 248 | - 第三句,把创建好的类返回。 249 | 250 | 那么像这样去使用它: 251 | 252 | ```python 253 | class Foo(metaclass=MyMeta): 254 | pass 255 | ``` 256 | 257 | 测试下功能: 258 | 259 | ```python 260 | >>> Foo.num 261 | 100 262 | ``` 263 | 264 | 没想象中那么复杂,对吧?之所以觉得元类很复杂,是因为它被用到的场合通常都很纠结,比如库开发或者ORM设计等。 265 | 266 | 最后看几个自定义元类应用的例子。 267 | 268 | ### 元类应用 269 | 270 | ### 子类方法限制 271 | 272 | 假设你是个库的作者,你要求用户继承你的类**必须**实现特定的方法,比如下面这个: 273 | 274 | ```python 275 | # 库提供的父类 276 | class Father(): 277 | def foo(self): 278 | return self.bar() 279 | 280 | # 用户写的子类 281 | class Child(Father): 282 | def bar(self): 283 | return True 284 | ``` 285 | 286 | 该怎么办?用户会写出什么代码是无法预料的。可以强制子类必须实现某些方法吗? 287 | 288 | 元类就可以办到: 289 | 290 | ```python 291 | class Meta(type): 292 | def __new__(cls, name, bases, dct, **kwargs): 293 | if name != 'Father' and 'bar' not in dct: 294 | raise TypeError('Class must contain bar() method.') 295 | return super().__new__(cls, name, bases, dct, **kwargs) 296 | 297 | # 添加了元类 298 | class Father(metaclass=Meta): 299 | def foo(self): 300 | return self.bar() 301 | ``` 302 | 303 | 如果子类不实现 `bar()` 方法,运行则会立即报错: 304 | 305 | ```python 306 | # 用户写的子类 307 | class Child(Father): 308 | pass 309 | 310 | # 输出报错: 311 | # TypeError: Class must contain bar() method. 312 | ``` 313 | 314 | 稍微要注意的是实际上元类中的 `__new__` 方法还可以接收关键字参数。如果像这样定义基类: 315 | 316 | ```python 317 | # 库提供的父类 318 | class Father(metaclass=Meta, value=10): 319 | ... 320 | ``` 321 | 322 | 那么这个 `value` 就会成为关键字参数传递到 `__new__` 中。 323 | 324 | ### 动态添加方法 325 | 326 | 动态给子类添加属性或方法算是元类的基础用法了。 327 | 328 | 比如说,我只想让名叫 `Apple` 的子类具有 `sayHi()` 方法: 329 | 330 | ```python 331 | class Meta(type): 332 | def __new__(cls, name, bases, dct, **kwargs): 333 | if name == 'Apple': 334 | dct.update({ 335 | 'sayHi': lambda: 'Hi I am Apple' 336 | }) 337 | return super().__new__(cls, name, bases, dct, **kwargs) 338 | 339 | 340 | class Food(metaclass=Meta): 341 | pass 342 | 343 | class Apple(Food): 344 | pass 345 | 346 | class Pear(Food): 347 | pass 348 | ``` 349 | 350 | 调用下试试: 351 | 352 | ```python 353 | >>> Apple.sayHi() 354 | 'Hi I am Apple' 355 | 356 | >>> Pear.sayHi() 357 | AttributeError: type object 'Pear' has no attribute 'sayHi' 358 | ``` 359 | 360 | 除了判断类的名称外,你可以编写更加复杂的判据,来实现业务的要求。 361 | 362 | ### ORM 363 | 364 | 相比上面的例子,元类更多的被用到 API 的设计中,比较典型的就是 Web 框架的**对象关系映射**(ORM)中。 365 | 366 | 拿 Django 举例。Django 的 ORM 允许你这样定义与数据库映射的模型: 367 | 368 | ```python 369 | class Person(models.Model): 370 | name = models.CharField(max_length=30) 371 | age = models.IntegerField() 372 | ``` 373 | 374 | 如果你试图赋值并取得模型中的值: 375 | 376 | ```python 377 | person = Person(name='bob', age=35) 378 | 379 | print(person.age) 380 | # 输出: 381 | # 35 382 | ``` 383 | 384 | 这不是很奇怪吗? `age` 属性不管是赋值还是取值,都是一个普通的整型 `int` 。但在 `Person` 类中定义时它明明指定的是 `IntegerField` 对象,甚至还可以直接从数据库里取到 `age` 的值。 385 | 386 | 原因就是在于 `models.Model` 中定义的元类(以及其他辅助方法),将背后的复杂逻辑转化成了非常简单的语句,方便了框架的使用者。 387 | 388 | ## 写在最后 389 | 390 | 就如文章最开始说的,与其使用元类这种晦涩又容易出错的工具,大部分开发者至少有三种更好的替代方案: 391 | 392 | - 继承 393 | - 猴子补丁 394 | - 装饰器 395 | 396 | 90% 的情况下,你其实根本不需要动态修改类。 397 | 398 | 如果真的需要,那么 99% 的情况下,你不应该用元类,而是用上述的几种方法。 399 | 400 | 尽管如此,理解元类是有益的,可以让你对 Python 的理解更深刻,并且可以意识到何时它才应该成为你的工具。 401 | 402 | --- 403 | 404 | 本文参考: 405 | 406 | - [Python Metaclasses](https://realpython.com/python-metaclasses/) 407 | - [SO](https://stackoverflow.com/questions/100003/what-are-metaclasses-in-python) 408 | - [Understanding Python Metaclass](https://lotabout.me/2018/Understanding-Python-MetaClass/) 409 | 410 | > 作者杜赛,Python 科普写手,著有 [Django搭建个人博客](https://www.dusaiphoto.com/topic/) 等系列教程。 -------------------------------------------------------------------------------- /middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/middleware/__init__.py -------------------------------------------------------------------------------- /middleware/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /middleware/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MiddlewareConfig(AppConfig): 5 | name = 'middleware' 6 | -------------------------------------------------------------------------------- /middleware/middlewares.py: -------------------------------------------------------------------------------- 1 | # Django 1.11 以前的中间件 Mixin 2 | # 以后可能会废弃 3 | from django.utils.deprecation import MiddlewareMixin 4 | 5 | from django.http import HttpResponseForbidden 6 | import sys 7 | from django.views.debug import technical_500_response 8 | from datetime import datetime 9 | 10 | 11 | class ResponseTimer: 12 | def __init__(self, get_response): 13 | self.get_response = get_response 14 | 15 | def __call__(self, request): 16 | request._request_time = datetime.now() 17 | response = self.get_response(request) 18 | return response 19 | 20 | def process_template_response(self, request, response): 21 | response_time = request._request_time - datetime.now() 22 | response.context_data['response_time'] = abs(response_time) 23 | return response 24 | 25 | 26 | class NormalUserBlock: 27 | def __init__(self, get_response): 28 | self.get_response = get_response 29 | 30 | def __call__(self, request): 31 | if (request.user.is_superuser != True) and (request.path == '/middleware/'): 32 | return HttpResponseForbidden('

超级用户方可访问此页面!

') 33 | 34 | response = self.get_response(request) 35 | 36 | return response 37 | 38 | 39 | class DebugOnlySuperUser: 40 | 41 | def __init__(self, get_response): 42 | self.get_response = get_response 43 | 44 | def __call__(self, request): 45 | response = self.get_response(request) 46 | return response 47 | 48 | def process_exception(self, request, exception): 49 | if request.user.is_superuser: 50 | return technical_500_response(request, *sys.exc_info()) 51 | 52 | 53 | class Md1: 54 | def __init__(self, get_response): 55 | self.get_response = get_response 56 | # (0) 在这里进行某些自定义参数的初始化 57 | 58 | def __call__(self, request): 59 | # (1) 这里写实际视图执行之前的逻辑 60 | print('Md1 视图执行前..') 61 | 62 | # (2) response 是下一个中间件或视图函数的返回值 63 | response = self.get_response(request) 64 | 65 | # (3) 这里写实际视图执行之后的逻辑 66 | print('Md1 视图执行后..') 67 | 68 | return response 69 | 70 | 71 | class Md2: 72 | def __init__(self, get_response): 73 | self.get_response = get_response 74 | 75 | def __call__(self, request): 76 | print('Md2 视图执行前..') 77 | 78 | # if True: 79 | # print('Md2 引发短路') 80 | # return HttpResponse('Md2 引发短路') 81 | 82 | response = self.get_response(request) 83 | 84 | print('Md2 视图执行后..') 85 | 86 | return response 87 | 88 | 89 | class Md3: 90 | def __init__(self, get_response): 91 | self.get_response = get_response 92 | 93 | def __call__(self, request): 94 | print('Md3 视图执行前..') 95 | 96 | response = self.get_response(request) 97 | 98 | print('Md3 视图执行后..') 99 | 100 | return response 101 | -------------------------------------------------------------------------------- /middleware/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/middleware/migrations/__init__.py -------------------------------------------------------------------------------- /middleware/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /middleware/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /middleware/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse 3 | from django.template.response import TemplateResponse 4 | 5 | # Create your views here. 6 | 7 | def mid_test(request): 8 | print('--- 视图执行中...') 9 | # raise None 10 | # return HttpResponse('中间件测试..') 11 | return TemplateResponse(request, 'midware_demo.html', context={}) 12 | 13 | -------------------------------------------------------------------------------- /mig/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/mig/__init__.py -------------------------------------------------------------------------------- /mig/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Pen 3 | 4 | # Register your models here. 5 | 6 | admin.site.register(Pen) -------------------------------------------------------------------------------- /mig/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MigConfig(AppConfig): 5 | name = 'mig' 6 | -------------------------------------------------------------------------------- /mig/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-20 07:54 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Pen', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('price', models.DecimalField(decimal_places=2, max_digits=7)), 20 | ('color', models.CharField(default='black', max_length=20)), 21 | ('purchase_date', models.DateTimeField(default=django.utils.timezone.now)), 22 | ('length', models.IntegerField(default=10)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /mig/migrations/0002_remove_pen_purchase_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-22 00:33 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('mig', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='pen', 15 | name='purchase_date', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /mig/migrations/0003_pen_purchase_date.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-05-22 00:33 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('mig', '0002_remove_pen_purchase_date'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='pen', 16 | name='purchase_date', 17 | field=models.DateTimeField(default=django.utils.timezone.now), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /mig/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/mig/migrations/__init__.py -------------------------------------------------------------------------------- /mig/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | 5 | class Pen(models.Model): 6 | price = models.DecimalField(max_digits=7, decimal_places=2) 7 | color = models.CharField(default='black', max_length=20) 8 | purchase_date = models.DateTimeField(default=timezone.now) 9 | # 手动删除 0003 文件后,添加此字段 10 | length = models.IntegerField(default=10) -------------------------------------------------------------------------------- /mig/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /mig/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /mySignal/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "mySignal.apps.MysignalConfig" -------------------------------------------------------------------------------- /mySignal/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /mySignal/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MysignalConfig(AppConfig): 5 | name = 'mySignal' 6 | 7 | def ready(self): 8 | import mySignal.handlers -------------------------------------------------------------------------------- /mySignal/handlers.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import receiver 2 | from mySignal.signals import view_done 3 | 4 | 5 | @receiver(view_done, dispatch_uid="my_signal_receiver") 6 | def my_signal_handler(sender, **kwargs): 7 | print(sender) 8 | print(kwargs.get('arg_1'), kwargs.get('arg_2')) -------------------------------------------------------------------------------- /mySignal/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/mySignal/migrations/__init__.py -------------------------------------------------------------------------------- /mySignal/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /mySignal/signals.py: -------------------------------------------------------------------------------- 1 | import django.dispatch 2 | 3 | view_done = django.dispatch.Signal() -------------------------------------------------------------------------------- /mySignal/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /mySignal/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import some_view 3 | 4 | app_name = 'mySignal' 5 | 6 | urlpatterns = [ 7 | path('', some_view, name='some_view'), 8 | ] 9 | -------------------------------------------------------------------------------- /mySignal/views.py: -------------------------------------------------------------------------------- 1 | from django.http.response import HttpResponse 2 | from django.core.signals import request_finished, request_started 3 | from django.dispatch import receiver 4 | 5 | # @receiver([request_finished, request_started]) 6 | # def signal_callback(sender, **kwargs): 7 | # print('信号已接收..') 8 | # 9 | # 10 | # @receiver(request_finished) 11 | # def signal_callback_2(sender, **kwargs): 12 | # print('信号已接收2..') 13 | 14 | from mySignal.signals import view_done 15 | 16 | 17 | def some_view(request): 18 | 19 | view_done.send( 20 | sender='View function...', 21 | arg_1='My signal...', 22 | arg_2='received...' 23 | ) 24 | 25 | return HttpResponse('响应完毕..') 26 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.10 2 | Django==3.0.5 3 | Pillow==7.1.2 4 | pytz==2019.3 5 | sqlparse==0.3.1 6 | -------------------------------------------------------------------------------- /static/test.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/static/test.css -------------------------------------------------------------------------------- /static/test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/static/test.js -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %}{% endblock %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% include "header.html" %} 25 | 26 |
27 |
28 | {% block content %}{% endblock %} 29 |
30 |
31 | 32 | {% block script %}{% endblock %} 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /templates/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | 5 | 6 |

reverse()

7 |
8 | Home 9 | 13 | Url 14 | 15 | Reverse 16 | 20 | Reverse with params 21 | 22 | {{ content }} 23 |
24 | 25 |

redirect()

26 |
27 | {% for post in posts %} 28 |
29 | {{ post.title }} 30 |
31 | {% endfor %} 32 |
33 | 34 |

path()

35 | 36 |
37 | 41 |
42 | 43 |

@property 与模型方法

44 | 45 |
46 |
Full name: {{ name1 }}
47 |
Full name: {{ name2 }}
48 |
49 | 50 |

批量上传文件

51 |
52 |
{% csrf_token %} 56 |
57 | 66 |
67 |
68 | 69 |
70 |
71 |
72 | 73 |

Session

74 |
75 | Session记录 76 | 77 |
78 | 79 |
80 |
81 |
82 |
83 | {% endblock %} -------------------------------------------------------------------------------- /templates/midware_demo.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | 5 | {% block content %} 6 |

7 | 本次请求处理时间: {{ response_time }}. 8 |

9 | {% endblock %} -------------------------------------------------------------------------------- /templates/path.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | 5 | {% block content %} 6 |

7 | {{ salute }} {{ first_name }} {{ last_name }}! 您已经访问本站:{{ count }} 次. 8 |

9 | {% endblock %} -------------------------------------------------------------------------------- /templates/post_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

{{ post.title }}

5 |
6 | 阅读量:{{ post.views }}    7 | 更新时间:{{ post.updated | date:"Y/m/d H:m:s" }}    8 | 作者:{{ owner.username }} 9 |
10 |
{{ post.body }}
11 | {% endblock %} -------------------------------------------------------------------------------- /templates/uploads_images.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 | {% for image in images %} 5 |
6 | 7 |

{{ image.image.url }}

8 |
9 | {% endfor %} 10 | {% endblock %} -------------------------------------------------------------------------------- /templates/visits_count.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | 4 | {% block content %} 5 |

6 | 您已经访问本页面:{{ count }} 次. 7 | deeper_count: {{ deeper_count }} . 8 |

9 | {% endblock %} -------------------------------------------------------------------------------- /transanction_demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/transanction_demo/__init__.py -------------------------------------------------------------------------------- /transanction_demo/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /transanction_demo/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class TransanctionDemoConfig(AppConfig): 5 | name = 'transanction_demo' 6 | -------------------------------------------------------------------------------- /transanction_demo/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.5 on 2020-06-10 12:37 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Address', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('home', models.CharField(max_length=100)), 19 | ], 20 | ), 21 | migrations.CreateModel( 22 | name='Info', 23 | fields=[ 24 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 25 | ('age', models.IntegerField()), 26 | ], 27 | ), 28 | migrations.CreateModel( 29 | name='Student', 30 | fields=[ 31 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 32 | ('name', models.CharField(max_length=20)), 33 | ], 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /transanction_demo/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stacklens/django-knowledge-base/eed6eea3e667ff18cde1ff3382f58bf9f45ed7ca/transanction_demo/migrations/__init__.py -------------------------------------------------------------------------------- /transanction_demo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Student(models.Model): 5 | """学生""" 6 | name = models.CharField(max_length=20) 7 | 8 | 9 | class Info(models.Model): 10 | """学生的基本情况""" 11 | age = models.IntegerField() 12 | 13 | 14 | class Address(models.Model): 15 | """学生的家庭住址""" 16 | home = models.CharField(max_length=100) 17 | -------------------------------------------------------------------------------- /transanction_demo/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /transanction_demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import create_student, CreateStudent 3 | 4 | app_name = 'transanction_demo' 5 | 6 | urlpatterns = [ 7 | path('create/', create_student, name='create'), 8 | path('createbv/', CreateStudent.as_view(), name='createBV') 9 | ] 10 | -------------------------------------------------------------------------------- /transanction_demo/views.py: -------------------------------------------------------------------------------- 1 | from .models import Student, Info, Address 2 | from django.http import HttpResponse 3 | from django.views import View 4 | 5 | from django.db import transaction 6 | 7 | 8 | class CreateStudent(View): 9 | @transaction.atomic 10 | def get(self, request): 11 | student = Student.objects.create(name='张三') 12 | 13 | info = Info.objects.create(age=19) 14 | # 引发错误 15 | oh_my_god = int('abc') 16 | address = Address.objects.create(home='北京') 17 | 18 | return HttpResponse('Create success...') 19 | 20 | 21 | @transaction.atomic 22 | def create_student(request): 23 | student = Student.objects.create(name='张三') 24 | info = Info.objects.create(age=19) 25 | oh_my_god = int('abc') 26 | address = Address.objects.create(home='北京') 27 | 28 | return HttpResponse('Create success...') 29 | 30 | # @transaction.atomic 31 | # def create_student(request): 32 | # student = Student.objects.create(name='张三') 33 | # 34 | # # 回滚保存点 35 | # save_tag = transaction.savepoint() 36 | # 37 | # try: 38 | # info = Info.objects.create(age=19) 39 | # # 引发错误 40 | # oh_my_god = int('abc') 41 | # address = Address.objects.create(home='北京') 42 | # except: 43 | # # 回滚到 save_tag 的位置 44 | # transaction.savepoint_rollback(save_tag) 45 | # 46 | # return HttpResponse('Create success...') 47 | 48 | 49 | # def create_student(request): 50 | # student = Student.objects.create(name='张三') 51 | # 52 | # with transaction.atomic: 53 | # info = Info.objects.create(age=19) 54 | # # 引发错误 55 | # oh_my_god = int('abc') 56 | # address = Address.objects.create(home='北京') 57 | # 58 | # return HttpResponse('Create success...') 59 | 60 | # @transaction.non_atomic_requests 61 | # def create_student(request): 62 | # student = Student.objects.create(name='张三') 63 | # 64 | # info = Info.objects.create(age=19) 65 | # # 引发错误 66 | # oh_my_god = int('abc') 67 | # address = Address.objects.create(home='北京') 68 | # 69 | # return HttpResponse('Create success...') 70 | --------------------------------------------------------------------------------