Django di bawah mikroskop

Jika menurut laporan Artyom Malyshev ( proofit404 ) mereka akan membuat film, maka sutradara akan menjadi Quentin Tarantino - dia sudah membuat satu film tentang Django, dia juga akan merekam yang kedua. Semua detail dari kehidupan mekanisme internal Django dari byte pertama permintaan HTTP ke byte terakhir dari respons. The extravaganza bentuk parser, kompilasi penuh aksi dari SQL, efek khusus dari implementasi mesin template untuk HTML. Siapa yang mengelola kumpulan koneksi dan bagaimana? Semua ini dalam urutan kronologis pemrosesan objek WSGI. Di semua layar negara - decoding "Django di bawah mikroskop".



Tentang pembicara: Artyom Malyshev adalah pendiri proyek Dry Python dan pengembang inti dari Django Channels versi 1.0. Dia telah menulis Python selama 5 tahun dan telah membantu mengatur pertemuan Python Rannts di Nizhny Novgorod. Artyom mungkin tidak asing bagi Anda dengan nama panggilan PROOFIT404 . Presentasi laporan disimpan di sini .


Sekali waktu, kami meluncurkan versi lama Django. Kemudian dia tampak menakutkan dan sedih.



Mereka melihat self_check berlalu, kami memasang semuanya dengan benar, semuanya berfungsi dan sekarang Anda dapat menulis kode. Untuk mencapai semua ini, kami harus menjalankan django-admin runserver .

 $ django-admin runserver Performing system checks… System check identified no issues (0 silenced). You have unapplied migrations; your app may not work properly until they are applied. Run 'python manage.py migrate1 to apply them. August 21, 2018 - 15:50:53 Django version 2.1, using settings 'mysite.settings' Starting development server at http://127.0.0.1:8000/Quit the server with CONTROL-C. 

Proses dimulai, memproses permintaan HTTP, dan semua keajaiban terjadi di dalam dan semua kode yang ingin kami tampilkan kepada pengguna saat situs dijalankan.

Instalasi


django-admin muncul di sistem ketika kami menginstal Django menggunakan, misalnya, pip, manajer paket .

 $ pip install Django # setup.py from setuptools import find_packages, setup setup( name='Django', entry_points={ 'console_scripts': [ 'django-admin = django.core.management:execute_from_command_line' ] }, ) 

entry_points setuptools muncul, yang menunjuk ke fungsi execute_from_command_line . Fungsi ini adalah titik masuk untuk operasi apa pun dengan Django, untuk setiap proses saat ini.

Bootstrap


Apa yang terjadi di dalam suatu fungsi? Bootstrap , yang terbagi menjadi dua iterasi.

 # django.core.management django.setup(). 

Konfigurasikan pengaturan


Yang pertama adalah membaca konfigurasi :

 import django.conf.global_settings import_module(os.environ["DJANGO_SETTINGS_MODULE"]) 

Pengaturan default global_settings , kemudian dari variabel lingkungan kami mencoba menemukan modul dengan DJANGO_SETTINGS_MODULE , yang ditulis pengguna. Pengaturan ini digabungkan menjadi satu ruang nama.

Siapa pun yang menulis di Django setidaknya "Halo, dunia" tahu bahwa ada INSTALLED_APPS - tempat kami menulis kode pengguna.

Mengisi aplikasi


Pada bagian kedua, semua aplikasi ini, pada dasarnya paket, diulang satu per satu. Kami membuat untuk setiap Konfigurasi, kami mengimpor model untuk bekerja dengan database dan kami memeriksa model untuk integritas. Lebih jauh lagi, kerangka kerjanya menjalankan Check , yaitu, memeriksa bahwa setiap model memiliki kunci utama, semua kunci asing menunjuk ke bidang yang ada dan bahwa bidang Null tidak ditulis dalam BooleanField, tetapi NullBooleanField digunakan.

 for entry in settings.INSTALLED_APPS: cfg = AppConfig.create(entry) cfg.import_models() 

Ini adalah pemeriksaan kewarasan minimum untuk model, untuk panel admin, untuk apa saja - tanpa terhubung ke database, tanpa sesuatu yang sangat rumit dan spesifik. Pada tahap ini, Django belum tahu perintah mana yang Anda minta untuk dieksekusi, yaitu, tidak membedakan migrate dari runserver atau shell .

Kemudian kita menemukan diri kita dalam modul yang mencoba menebak dengan argumen baris perintah perintah mana yang ingin kita jalankan dan di mana aplikasi itu berada.

Perintah manajemen


 # django.core.management subcommand = sys.argv[1] app_name = find(pkgutils.iter_modules(settings.INSTALLED_APPS)) module = import_module( '%s.management.commands.%s' % (app_name, subcommand) ) cmd = module.Command() cmd.run_from_argv(self.argv) 

Dalam hal ini, modul runserver akan memiliki modul django.core.management.commands.runserver . Setelah mengimpor modul, dengan konvensi, Command kelas global dipanggil di dalam, dipakai, dan kami berkata: " Saya menemukan Anda, di sini Anda memiliki argumen baris perintah yang dilewati pengguna, melakukan sesuatu dengan mereka ."

Selanjutnya, kita pergi ke modul runserver dan melihat bahwa Django terbuat dari "regexp and sticks" , yang akan saya bicarakan secara rinci hari ini:

 # django.core.management.commands.runserver naiveip_re = re.compile(r"""^(?: (?P<addr> (?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address (?P<ipv6>\[[a-fA-F0-9:]+\]) | # IPv6 address (?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN ):)?(?P<port>\d+)$""", re.X) 

Perintah


Gulir ke bawah satu setengah layar - akhirnya kita masuk ke definisi tim kami yang memulai server.

 # django.core.management.commands.runserver class Command(BaseCommand): def handle(self, *args, **options): httpd = WSGIServer(*args, **options) handler = WSGIHandler() httpd.set_app(handler) httpd.serve_forever() 

BaseCommand melakukan serangkaian operasi minimal agar argumen baris perintah menghasilkan argumen untuk memanggil fungsi *args dan **options . Kita melihat bahwa instance server WSGI sedang dibuat di sini, WSGIHandler global diinstal di server WSGI ini - ini persisnya God Object Django . Kita dapat mengatakan bahwa ini adalah satu-satunya contoh kerangka kerja. Instance diinstal pada server secara global - melalui set application dan mengatakan: "Putar di Event Loop, jalankan permintaan."

Selalu ada Perulangan Acara di suatu tempat dan seorang programmer yang memberinya tugas.

Server WSGI


Apa itu WSGIHandler ? WSGI adalah antarmuka yang memungkinkan Anda untuk memproses permintaan HTTP dengan tingkat abstraksi minimum, dan terlihat seperti sesuatu dalam bentuk fungsi.

Handler WSGI


 # django.core.handlers.wsgi class WSGIHandler: def __call__(self, environ, start_response): signals.request_started.send() request = WSGIRequest(environ) response = self.get_response(request) start_response(response.status, response.headers) return response 

Sebagai contoh, ini adalah instance dari kelas yang memiliki call ditentukan. Dia menunggu entri kamusnya, di mana header akan disajikan sebagai byte dan file-handler. Handler diperlukan untuk membaca <body> permintaan. Server itu sendiri juga memberikan callback start_response sehingga kami dapat mengirim response.headers dan header-nya, misalnya, status, dalam satu bundel.

Lebih lanjut, kita bisa meneruskan tubuh respons ke server melalui objek respons. Respons adalah generator yang bisa Anda ulangi.

Semua server yang ditulis untuk WSGI - Gunicorn, uWSGI, Waitress, bekerja pada antarmuka ini dan dapat dipertukarkan. Kami sekarang mempertimbangkan server untuk pengembangan, tetapi server mana pun sampai pada titik di Django itu mengetuk lingkungan dan panggilan balik.

Apa yang ada di dalam Objek Tuhan?


Apa yang terjadi di dalam fungsi Objek Dewa global ini di dalam Django?

  • PERMINTAAN.
  • MIDDLEWARES.
  • Permintaan ROUTING untuk melihat.
  • LIHAT - pemrosesan kode pengguna tampilan dalam.
  • BENTUK - bekerja dengan formulir.
  • ORM.
  • TEMPLATE
  • TANGGAPAN.

Semua mesin yang kita inginkan dari Django berlangsung dalam satu fungsi, yang tersebar di seluruh kerangka kerja.

Minta


Kami membungkus lingkungan WSGI, yang merupakan kamus sederhana, di beberapa objek khusus, untuk kenyamanan bekerja dengan lingkungan. Misalnya, lebih mudah untuk mengetahui panjang permintaan pengguna melalui bekerja dengan sesuatu yang mirip dengan kamus daripada dengan string byte yang perlu diuraikan dan mencari entri nilai kunci di dalamnya. Saat bekerja dengan cookie, saya juga tidak ingin menghitung secara manual apakah periode penyimpanan telah kedaluwarsa atau belum, dan entah bagaimana menafsirkannya.

 # django.core.handlers.wsgi class WSGIRequest(HttpRequest): @cached_property def GET(self): return QueryDict(self.environ['QUERY_STRING']) @property def POST(self): self._load_post_and_files() return self._post @cached_property def COOKIES(self): return parse_cookie(self.environ['HTTP_COOKIE']) 

Permintaan berisi parser, serta satu set penangan untuk mengontrol pemrosesan tubuh permintaan POST: apakah itu file dalam memori atau sementara dalam penyimpanan pada disk. Semuanya diputuskan di dalam Permintaan. Permintaan di Django juga merupakan objek agregator di mana semua middlewares dapat menaruh informasi yang kita butuhkan tentang sesi, otentikasi, dan otorisasi pengguna. Kita dapat mengatakan bahwa ini juga merupakan Obyek Tuhan, tetapi lebih kecil.

Permintaan Lebih Lanjut sampai ke middleware.

Middlewares


Middleware adalah pembungkus yang membungkus fungsi lain seperti dekorator. Sebelum melepaskan kendali middleware, dalam metode panggilan kami memberikan respons atau memanggil middleware yang sudah dibungkus.

Inilah yang tampak seperti middleware dari sudut pandang programmer.

Pengaturan


 # settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ] 

Tentukan


 class Middleware: def __init__(self, get_response=None): self.get_response = get_response def __call__(self, request): return self.get_response(request) 

Dari sudut pandang Django, middlewares terlihat seperti tumpukan:

 # django.core.handlers.base def load_middleware(self): handler = convert_exception_to_response(self._get_response) for middleware_path in reversed(settings.MIDDLEWARE): middleware = import_string(middleware_path) instance = middleware(handler) handler = convert_exception_to_response(instance) self._middleware_chain = handler 

Terapkan


 def get_response(self, request): set_urlconf(settings.ROOT_URLCONF) response = self._middleware_chain(request) return response 

Kami mengambil fungsi get_response awal, membungkusnya dengan handler, yang akan menerjemahkan, misalnya, permission error dan not found error ke dalam kode HTTP yang benar. Kami membungkus semuanya dalam middleware itu sendiri dari daftar. Tumpukan middlewares tumbuh, dan masing-masing berikutnya membungkus yang sebelumnya. Ini sangat mirip dengan menerapkan tumpukan dekorator yang sama untuk semua tampilan dalam suatu proyek, hanya secara terpusat. Tidak perlu berkeliling dan mengatur pembungkus dengan tangan Anda sesuai dengan proyek, semuanya nyaman dan logis.

Kami melewati 7 lingkaran middlewares, permintaan kami bertahan dan memutuskan untuk memprosesnya. Selanjutnya kita sampai pada modul routing.

Routing


Di sinilah kami memutuskan penangan mana yang akan dipanggil untuk permintaan tertentu. Dan ini diselesaikan:

  • berdasarkan url;
  • dalam spesifikasi WSGI, di mana request.path_info dipanggil.

 # django.core.handlers.base def _get_response(self, request): resolver = get_resolver() view, args, kwargs = resolver.resolve(request.path_info) response = view(request, *args, **kwargs) return response 

Url


Kami mengambil resolver, memberi makan url permintaan saat ini dan mengharapkannya untuk mengembalikan fungsi tampilan itu sendiri, dan dari url yang sama kami mendapatkan argumen yang dapat digunakan untuk memanggil tampilan. Kemudian get_response panggilan tampilan, menangani pengecualian dan melakukan sesuatu dengannya.

 # urls.py urlpatterns = [ path('articles/2003/', views.special_case_2003), path('articles/<int:year>/', views.year_archive), path('articles/<int:year>/<int:month>/', views.month_archive) ] 

Penyelesai


Beginilah bentuk resolver:

 # django.urls.resolvers _PATH_RE = re.compile( r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>' ) def resolve(self, path): for pattern in self.url_patterns: match = pattern.search(path) if match: return ResolverMatch( self.resolve(match[0]) ) raise Resolver404({'path': path}) 

Ini juga regexp, tetapi rekursif. Ini masuk di bagian url, mencari apa yang diinginkan pengguna: pengguna lain, posting, blog, atau semacam konverter, misalnya, tahun tertentu yang perlu diselesaikan, dimasukkan ke dalam argumen, dilemparkan ke argumen, dilemparkan ke int.

Merupakan karakteristik bahwa kedalaman rekursi dari metode tekad selalu sama dengan jumlah argumen yang disebut tampilan. Jika terjadi kesalahan dan kami tidak menemukan url tertentu, kesalahan tidak ditemukan terjadi.

Lalu kami akhirnya melihat - kode yang ditulis oleh programmer.

Lihat


Dalam representasi yang paling sederhana, ini adalah fungsi yang mengembalikan permintaan dari respons, tetapi di dalamnya kami melakukan tugas-tugas logis: "untuk, jika, suatu hari" - banyak tugas berulang. Django memberi kami tampilan berbasis kelas di mana Anda dapat menentukan detail spesifik, dan semua perilaku akan ditafsirkan dalam format yang benar oleh kelas itu sendiri.

 # django.views.generic.edit class ContactView(FormView): template_name = 'contact.html' form_class = ContactForm success_url = '/thanks/' 

Bagan alur metode


 self.dispatch() self.post() self.get_form() self.form_valid() self.render_to_response() 

Metode dispatch instance ini sudah dalam pemetaan url alih-alih fungsi. Pengiriman berdasarkan kata kerja HTTP mengerti metode mana yang harus dihubungi: POST datang kepada kami dan kami kemungkinan besar ingin instantiate objek form, jika form valid, simpan ke database dan perlihatkan templat. Ini semua dilakukan melalui sejumlah besar mixin yang membentuk kelas ini.

Formulir


Formulir harus dibaca dari soket sebelum masuk ke tampilan Django - melalui penangan file yang sama yang terletak di lingkungan WSGI. form-data adalah aliran byte, di mana pemisah dijelaskan - kita dapat membaca blok ini dan membuat sesuatu darinya. Ini bisa menjadi korespondensi nilai kunci, jika itu adalah bidang, bagian dari file, lalu lagi beberapa bidang - semuanya dicampur.

 Content-Type: multipart/form-data;boundary="boundary" --boundary name="field1" value1 --boundary name="field2"; value2 

Parser


Parser terdiri dari 3 bagian.

Chunk iterator yang menciptakan pembacaan yang diharapkan dari aliran byte berubah menjadi iterator yang dapat menghasilkan boundaries . Ini menjamin bahwa jika sesuatu kembali, itu akan menjadi batas. Hal ini diperlukan agar di dalam pengurai tidak perlu menyimpan keadaan koneksi, membaca dari soket atau tidak membaca untuk meminimalkan logika pemrosesan data.

Selanjutnya, generator membungkus di LazyStream , yang lagi-lagi membuat file objek dari itu, tetapi dengan pembacaan yang diharapkan. Jadi parser sudah bisa berjalan melalui potongan-potongan byte dan membangun nilai kunci dari mereka.

bidang dan data di sini akan selalu berupa string . Jika kami menerima datatime dalam format ISO, formulir Django (yang ditulis oleh programmer) akan menerima, menggunakan bidang-bidang tertentu, misalnya cap waktu.

 # django.http.multipartparser self._post = QueryDict(mutable=True) stream = LazyStream(ChunkIter(self._input_data)) for field, data in Parser(stream): self._post.append(field, force_text(data)) 

Lebih jauh, bentuknya, kemungkinan besar, ingin menyimpan dirinya sendiri dalam basis data, dan di sini Django ORM dimulai.

ORM


Sekitar melalui permintaan DSL untuk ORM dijalankan:

 # models.py Entry.objects.exclude( pub_date__gt=date(2005, 1, 3), headline='Hello', ) 

Menggunakan kunci, Anda dapat mengumpulkan ekspresi SQL yang serupa:

 SELECT * WHERE NOT (pub_date > '2005-1-3' AND headline = 'Hello') 

Bagaimana kabarnya?

Queryset


Metode exclude memiliki objek Query bawah tenda. Objek dilewatkan argumen ke fungsi, dan itu menciptakan hirarki objek, yang masing-masing dapat mengubah dirinya menjadi bagian terpisah dari permintaan SQL sebagai string.

Ketika melintasi pohon, masing-masing bagian melakukan polling pada node anaknya, menerima query SQL yang bersarang, dan sebagai hasilnya, kita dapat membangun SQL sebagai string. Sebagai contoh, nilai-kunci tidak akan menjadi bidang SQL yang terpisah, tetapi akan dibandingkan dengan nilai-nilai. Gabungan dan penolakan query bekerja dengan cara yang sama dengan traversal pohon rekursif, untuk setiap node yang disebut sebagai pemeran SQL.

 # django.db.models.query sql.Query(Entry).where.add( ~Q( Q(F('pub_date') > date(2005, 1, 3)) & Q(headline='Hello') ) ) 

Kompiler


 # django.db.models.expressions class Q(tree.Node): AND = 'AND' OR = 'OR' def as_sql(self, compiler, connection): return self.template % self.field.get_lookup('gt') 

Keluaran


 >>> Q(headline='Hello') # headline = 'Hello' >>> F('pub_date') # pub_date >>> F('pub_date') > date(2005, 1, 3) # pub_date > '2005-1-3' >>> Q(...) & Q(...) # ... AND ... >>> ~Q(...) # NOT … 

Kompiler pembantu kecil dilewatkan ke metode ini, yang dapat membedakan dialek MySQL dari PostgreSQL dan mengatur gula sintaksis yang digunakan dalam dialek database tertentu dengan benar.

Routing DB


Ketika kami menerima permintaan SQL, model mengetuk pada perutean DB dan menanyakan basis data yang mana. Dalam 99% kasus, ini akan menjadi basis data default, sisanya 1% - semacam miliknya sendiri.

 # django.db.utils class ConnectionRouter: def db_for_read(self, model, **hints): if model._meta.app_label == 'auth': return 'auth_db' 

Membungkus driver basis data dari antarmuka pustaka tertentu, seperti Python MySQL atau Psycopg2, menciptakan objek universal yang dapat digunakan Django. Ada pembungkus untuk kursor, pembungkus untuk transaksi.

Kolam renang penghubung


 # django.db.backends.base.base class BaseDatabaseWrapper: def commit(self): self.validate_thread_sharing() self.validate_no_atomic_block() with self.wrap_database_errors: return self.connection.commit() 

Dalam koneksi khusus ini, kami mengirim permintaan ke soket yang mengetuk database dan menunggu eksekusi. Pembungkus perpustakaan akan membaca respon manusia dari database dalam bentuk catatan, dan Django mengumpulkan contoh model dari data ini dalam tipe Python. Ini bukan iterasi yang rumit.

Kami menulis sesuatu ke dalam basis data, membaca sesuatu, dan memutuskan untuk memberi tahu pengguna tentang hal itu menggunakan halaman HTML. Untuk melakukan ini, Django memiliki bahasa templat yang tidak disukai komunitas yang terlihat seperti bahasa pemrograman, hanya dalam file HTML.

Templat


 from django.template.loader import render_to_string render_to_string('my_template.html', {'entries': ...}) 

Kode


 <ul> {% for entry in entries %} <li>{{ entry.name }}</li> {% endfor %} </ul> 

Parser


 # django.template.base BLOCK_TAG_START = '{%' BLOCK_TAG_END = '%}' VARIABLE_TAG_START = '{{' VARIABLE_TAG_END = '}}' COMMENT_TAG_START = '{#' COMMENT_TAG_END = '#}' tag_re = (re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END), re.escape(COMMENT_TAG_START), re.escape(COMMENT_TAG_END)))) 

Kejutan - regexp lagi. Hanya pada bagian akhir harus ada koma, dan daftar akan jauh ke bawah. Ini mungkin regexp paling sulit yang pernah saya lihat dalam proyek ini.

Lexer


Handler template dan interpreter sangat sederhana. Ada lexer yang menggunakan regexp untuk menerjemahkan teks ke dalam daftar token kecil.

 # django.template.base def tokenize(self): for bit in tag_re.split(template_string): lineno += bit.count('\n') yield bit 

Kami beralih ke daftar token, lihat: "Siapa kamu? Membungkus Anda dalam simpul tag. " Misalnya, jika ini adalah awal dari beberapa if atau for atau for , penangan tag akan mengambil penangan yang sesuai. For handler itu sendiri memberi tahu parser: "Baca saya daftar token sampai ke tag penutup."

Operasi kembali ke parser.

Node, tag, dan parser adalah hal yang saling rekursif, dan kedalaman rekursi biasanya sama dengan bersarangnya template itu sendiri dengan tag.

Parser


 def parse(): while tokens: token = tokens.pop() if token.startswith(BLOCK_TAG_START): yield TagNode(token) elif token.startswith(VARIABLE_TAG_START): ... 

Penangan tag memberi kita simpul tertentu, misalnya, dengan loop for, yang muncul metode render .

Untuk loop


 # django.template.defaulttags @register.tag('for') def do_for(parser, token): args = token.split_contents() body = parser.parse(until=['endfor']) return ForNode(args, body) 

Untuk simpul


 class ForNode(Node): def render(self, context): with context.push(): for i in self.args: yield self.body.render(context) 

Metode render adalah pohon render. Setiap simpul atas dapat pergi ke simpul anak, memintanya untuk membuat. Pemrogram digunakan untuk menunjukkan beberapa variabel dalam templat ini. Ini dilakukan melalui context - disajikan dalam bentuk kamus reguler. Ini adalah tumpukan kamus untuk meniru lingkup ketika kita memasukkan tag. Misalnya, jika context sendiri mengubah beberapa tag lain di dalam loop for , maka ketika kita keluar dari loop perubahan akan dibatalkan. Ini nyaman karena ketika semuanya bersifat global, sulit untuk bekerja.

Tanggapan


Akhirnya, kami mendapat tanggapan dari respons HTTP:

Halo Dunia!

Kami dapat memberikan garis ke pengguna.

  • Kembalikan respons ini dari tampilan.
  • Lihat daftar middlewares.
  • Middlewares menanggapi, memodifikasi, melengkapi, dan meningkatkan ini.
  • Respons mulai beralih di dalam WSGIHandler, sebagian ditulis ke soket, dan browser menerima respons dari server kami.

Semua startup terkenal yang ditulis dalam Django, seperti Bitbucket atau Instagram, dimulai dengan siklus kecil yang dilalui setiap programmer.

Semua ini, dan presentasi di Moscow Python Conf ++, perlu bagi Anda untuk lebih memahami apa yang ada di tangan Anda dan bagaimana menggunakannya. Dalam sihir apa pun, ada sebagian besar regexp yang harus Anda masak.

Artyom Malyshev dan 23 pembicara hebat lainnya pada tanggal 5 April akan kembali memberi kita banyak bahan untuk dipikirkan dan dibahas tentang topik Python pada konferensi Moskow Python Conf ++ . Pelajari jadwal dan bergabunglah dalam pertukaran pengalaman dalam menyelesaikan berbagai masalah menggunakan Python.

Source: https://habr.com/ru/post/id433482/


All Articles