Django unter dem Mikroskop

Wenn sie nach dem Bericht von Artyom Malyshev ( Proofit404 ) einen Film machen, dann ist der Regisseur Quentin Tarantino - er hat bereits einen Film über Django gedreht, er wird auch den zweiten drehen. Alle Details aus der Lebensdauer der internen Django-Mechanismen vom ersten Byte der HTTP-Anforderung bis zum letzten Byte der Antwort. Die Extravaganz von Parser-Formularen, die actionreiche Kompilierung von SQL, Spezialeffekte bei der Implementierung der Template-Engine für HTML. Wer verwaltet den Verbindungspool und wie? All dies in chronologischer Reihenfolge der Verarbeitung von WSGI-Objekten. Auf allen Bildschirmen des Landes - die Dekodierung "Django unter dem Mikroskop".



Über den Sprecher: Artyom Malyshev ist der Gründer des Dry Python-Projekts und der Hauptentwickler von Django Channels Version 1.0. Er schreibt seit 5 Jahren Python und hat bei der Organisation von Python Rannts Treffen in Nischni Nowgorod mitgewirkt. Artyom ist Ihnen möglicherweise unter dem Spitznamen PROOFIT404 bekannt . Die Präsentation des Berichts wird hier gespeichert.


Es war einmal, wir haben die alte Version von Django veröffentlicht. Dann sah sie unheimlich und traurig aus.



Sie haben gesehen, dass self_check bestanden hat, wir haben alles korrekt installiert, alles hat funktioniert und jetzt können Sie Code schreiben. Um dies alles zu erreichen, mussten wir den 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. 

Der Prozess startet, verarbeitet HTTP-Anforderungen und all die Magie geschieht im Inneren und der gesamte Code, den wir Benutzern als Site anzeigen möchten, wird ausgeführt.

Installation


django-admin auf dem System angezeigt, wenn wir Django beispielsweise mit pip, dem Paketmanager, installieren.

 $ 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 angezeigt, das auf die Funktion execute_from_command_line verweist. Diese Funktion ist ein Einstiegspunkt für jede Operation mit Django für jeden aktuellen Prozess.

Bootstrap


Was passiert innerhalb einer Funktion? Bootstrap , der in zwei Iterationen unterteilt ist.

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

Einstellungen konfigurieren


Das erste ist das Lesen von Konfigurationen :

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

Die Standardeinstellungen global_settings . Anschließend versuchen wir, aus der Umgebungsvariablen das Modul mit DJANGO_SETTINGS_MODULE zu finden, das der Benutzer geschrieben hat. Diese Einstellungen werden in einem Namensraum zusammengefasst.

Jeder, der in Django mindestens "Hallo Welt" schreibt, weiß, dass es INSTALLED_APPS - wo wir den Benutzercode schreiben.

Apps füllen


Im zweiten Teil werden alle diese Anwendungen, im Wesentlichen Pakete, einzeln iteriert. Wir erstellen für jede Konfiguration, importieren Modelle für die Arbeit mit einer Datenbank und überprüfen Modelle auf Integrität. Außerdem führt das Framework Check , dh es überprüft, ob jedes Modell einen Primärschlüssel hat, alle Fremdschlüssel auf vorhandene Felder verweisen und dass das Nullfeld nicht in das BooleanField geschrieben ist, sondern das NullBooleanField verwendet wird.

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

Dies ist die Mindestprüfung für Modelle, für das Admin-Panel und für alles andere - ohne Verbindung zur Datenbank, ohne etwas sehr Kompliziertes und Spezifisches. Zu diesem Zeitpunkt weiß Django noch nicht, welchen Befehl Sie ausführen sollen, runserver unterscheidet die migrate von runserver oder shell .

Dann befinden wir uns in einem Modul, das versucht, anhand von Befehlszeilenargumenten zu erraten, welchen Befehl wir ausführen möchten und in welcher Anwendung er liegt.

Verwaltungsbefehl


 # 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) 

In diesem Fall verfügt das Runserver-Modul über ein integriertes django.core.management.commands.runserver Modul. Nach dem Import des Moduls wird gemäß Konvention die globale Klasse Command aufgerufen, instanziiert und wir sagen: " Ich habe Sie gefunden, hier haben Sie die Befehlszeilenargumente, die der Benutzer übergeben hat, machen Sie etwas mit ihnen ."

Als nächstes gehen wir zum Runserver-Modul und sehen, dass Django aus "Regexp und Sticks" besteht , worüber ich heute ausführlich sprechen werde:

 # 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) 

Befehle


Scrollen Sie eineinhalb Bildschirme nach unten - schließlich kommen wir zur Definition unseres Teams, das den Server startet.

 # 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 führt eine minimale BaseCommand von Operationen aus, damit Befehlszeilenargumente zu Argumenten für den Aufruf der **options *args und **options . Wir sehen, dass hier die WSGI-Serverinstanz erstellt wird, der globale WSGIHandler auf diesem WSGI-Server installiert ist - genau das ist God Object Django . Wir können sagen, dass dies die einzige Instanz des Frameworks ist. Die Instanz wird global auf dem Server installiert - über die set application und lautet: "In Ereignisschleife drehen, Anforderungen ausführen."

Es gibt immer irgendwo eine Ereignisschleife und einen Programmierer, der ihm Aufgaben gibt.

WSGI-Server


Was ist WSGIHandler ? WSGI ist eine Schnittstelle, mit der Sie HTTP-Anforderungen mit einem Mindestabstraktionsgrad verarbeiten können. Sie sieht aus wie eine Funktion.

WSGI-Handler


 # 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 

Hier ist es beispielsweise eine Instanz einer Klasse, für die ein call definiert ist. Er wartet auf seinen Wörterbucheintrag, in dem Header als Bytes und als File-Handler dargestellt werden. Der Handler wird benötigt, um den <body> der Anforderung zu lesen. Der Server selbst gibt auch start_response damit wir response.headers und seinen Header, z. B. status, in einem Bundle senden können.

Außerdem können wir den Antworttext über das Antwortobjekt an den Server übergeben. Response ist ein Generator, über den Sie iterieren können.

Alle Server, die für WSGI geschrieben wurden - Gunicorn, uWSGI, Waitress - arbeiten an dieser Schnittstelle und sind austauschbar. Wir ziehen jetzt einen Server für die Entwicklung in Betracht, aber jeder Server kommt zu dem Punkt, dass er in Django die Umgebung und den Rückruf durchbricht.

Was ist in Gott Objekt?


Was passiert in dieser globalen Gottobjektfunktion in Django?

  • ANFRAGE.
  • MITTELWAREN.
  • ROUTING-Anfrage zum Anzeigen.
  • VIEW - Benutzercode-Verarbeitung in der Ansicht.
  • FORMULAR - mit Formularen arbeiten.
  • ORM.
  • SCHABLONE
  • ANTWORT.

Alle Maschinen, die wir von Django wollen, finden in einer einzigen Funktion statt, die über das gesamte Framework verteilt ist.

Anfrage


Wir verpacken die WSGI-Umgebung, bei der es sich um ein einfaches Wörterbuch handelt, in ein spezielles Objekt, um die Arbeit mit der Umgebung zu vereinfachen. Zum Beispiel ist es bequemer, die Länge einer Benutzeranforderung durch Arbeiten mit etwas ähnlichem wie einem Wörterbuch herauszufinden, als mit einer Byte-Zeichenfolge, die analysiert werden muss, und darin nach Schlüsselwerteinträgen zu suchen. Wenn ich mit Cookies arbeite, möchte ich auch nicht manuell berechnen, ob die Speicherdauer abgelaufen ist oder nicht, und sie irgendwie interpretieren.

 # 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']) 

Die Anforderung enthält Parser sowie eine Reihe von Handlern zur Steuerung der Verarbeitung des Hauptteils der POST-Anforderung: ob es sich um eine Datei im Speicher oder um eine temporäre Datei im Speicher auf der Festplatte handelt. Alles wird innerhalb der Anfrage entschieden. Request in Django ist auch ein Aggregatorobjekt, in das alle Middlewares die Informationen einfügen können, die wir über die Sitzung, Authentifizierung und Benutzerautorisierung benötigen. Wir können sagen, dass dies auch ein Gottobjekt ist, aber kleiner.

Weitere Anfrage geht an Middleware.

Middlewares


Middleware ist ein Wrapper, der andere Funktionen wie einen Dekorateur umschließt. Bevor wir die Kontrolle über die Middleware aufgeben, geben wir in der Aufrufmethode eine Antwort oder rufen eine bereits verpackte Middleware auf.

So sieht Middleware aus Sicht eines Programmierers aus.

Einstellungen


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

Definieren


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

Aus der Sicht von Django sehen Middlewares wie eine Art Stapel aus:

 # 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 

Bewerben


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

Wir nehmen die anfängliche Funktion get_response und verpacken sie in einen Handler, der beispielsweise permission error und not found error in den richtigen HTTP-Code übersetzt. Wir verpacken alles in die Middleware selbst aus der Liste. Der Middlewares-Stapel wächst und jeder nächste schließt den vorherigen ein. Dies ist sehr ähnlich zum Anwenden des gleichen Stapels von Dekorateuren auf alle Ansichten in einem Projekt, nur zentral. Sie müssen nicht herumgehen und die Wrapper mit Ihren Händen entsprechend dem Projekt anordnen, alles ist bequem und logisch.

Wir gingen durch 7 Kreise von Middlewares, unsere Anfrage überlebte und beschloss, sie im Blick zu verarbeiten. Weiter kommen wir zum Routing-Modul.

Routing


Hier entscheiden wir, welcher Handler für eine bestimmte Anfrage aufgerufen werden soll. Und das ist gelöst:

  • basierend auf URL;
  • in der WSGI-Spezifikation, in der request.path_info aufgerufen wird.

 # 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 

Urls


Wir nehmen den Resolver, geben ihm die aktuelle Anforderungs-URL und erwarten, dass er die Ansichtsfunktion selbst zurückgibt, und von derselben URL erhalten wir die Argumente, mit denen view aufgerufen werden soll. Dann ruft get_response view auf, behandelt Ausnahmen und macht etwas damit.

 # 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) ] 

Resolver


So sieht der Resolver aus:

 # 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}) 

Dies ist ebenfalls regulär, aber rekursiv. Es geht in Teile der URL, sucht nach dem, was der Benutzer will: andere Benutzer, Beiträge, Blogs oder ist es eine Art Konverter, zum Beispiel ein bestimmtes Jahr, das aufgelöst, Argumente eingegeben und in int umgewandelt werden muss.

Es ist charakteristisch, dass die Rekursionstiefe der Auflösungsmethode immer gleich der Anzahl der Argumente ist, mit denen view aufgerufen wird. Wenn ein Fehler aufgetreten ist und wir keine bestimmte URL gefunden haben, tritt ein nicht gefundener Fehler auf.

Dann kommen wir endlich in Sicht - der Code, den der Programmierer geschrieben hat.

Anzeigen


In seiner einfachsten Darstellung ist es eine Funktion, die eine Anfrage von einer Antwort zurückgibt, aber darin führen wir logische Aufgaben aus: "für, wenn, eines Tages" - viele sich wiederholende Aufgaben. Django bietet uns eine klassenbasierte Ansicht, in der Sie bestimmte Details angeben können. Das gesamte Verhalten wird von der Klasse selbst im richtigen Format interpretiert.

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

Methodenflussdiagramm


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

Die dispatch dieser Instanz befindet sich bereits in der URL-Zuordnung anstelle einer Funktion. Der Versand basierend auf dem HTTP-Verb versteht, welche Methode aufgerufen werden soll: POST kam zu uns und wir möchten höchstwahrscheinlich das Formularobjekt instanziieren. Wenn das Formular gültig ist, speichern Sie es in der Datenbank und zeigen Sie die Vorlage an. Dies alles geschieht durch die große Anzahl von Mixins, aus denen diese Klasse besteht.

Formular


Das Formular muss aus dem Socket gelesen werden, bevor es in die Django-Ansicht gelangt - über denselben Datei-Handler, der sich in der WSGI-Umgebung befindet. Formulardaten sind ein Bytestream, in dem Trennzeichen beschrieben werden - wir können diese Blöcke lesen und etwas daraus machen. Es kann eine Schlüssel-Wert-Entsprechung sein. Wenn es sich um ein Feld, einen Teil einer Datei oder ein anderes Feld handelt, ist alles gemischt.

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

Parser


Der Parser besteht aus 3 Teilen.

Der Chunk-Iterator, der die erwarteten Messwerte aus dem Bytestream erstellt, wird zu einem Iterator, der boundaries erzeugen kann. Es garantiert, dass wenn etwas zurückkehrt, es eine Grenze ist. Dies ist erforderlich, damit im Parser der Verbindungsstatus nicht gespeichert, vom Socket gelesen oder nicht gelesen werden muss, um die Logik der Datenverarbeitung zu minimieren.

Als nächstes wird der Generator in LazyStream eingeschlossen , der erneut eine Objektdatei daraus erstellt, jedoch mit dem erwarteten Messwert. Der Parser kann also bereits Byte-Teile durchlaufen und daraus einen Schlüsselwert erstellen.

Feld und Daten hier sind immer Zeichenfolgen . Wenn wir eine Datenzeit im ISO-Format erhalten haben, erhält das Django-Formular (das vom Programmierer geschrieben wurde) unter Verwendung bestimmter Felder, z. B. Zeitstempel.

 # 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)) 

Außerdem möchte sich das Formular höchstwahrscheinlich in einer Datenbank speichern, und hier beginnt Django ORM.

ORM


Ungefähr durch solche DSL werden Anforderungen für ORM ausgeführt:

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

Mit Schlüsseln können Sie ähnliche SQL-Ausdrücke erfassen:

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

Wie läuft das

Abfragesatz


Die exclude hat ein Query unter der Haube. Dem Objekt werden Argumente an die Funktion übergeben, und es wird eine Hierarchie von Objekten erstellt, von denen jedes sich selbst als Zeichenfolge in ein separates Teil der SQL-Abfrage verwandeln kann.

Beim Durchlaufen des Baums fragt jeder der Abschnitte seine untergeordneten Knoten ab, empfängt verschachtelte SQL-Abfragen, und als Ergebnis können wir SQL als Zeichenfolge erstellen. Beispielsweise ist der Schlüsselwert kein separates SQL-Feld, sondern wird mit dem Wertwert verglichen. Die Verkettung und Ablehnung von Abfragen funktioniert genauso wie eine rekursive Baumdurchquerung, für die für jeden Knoten eine Umwandlung in SQL aufgerufen wird.

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

Compiler


 # 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') 

Ausgabe


 >>> 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 … 

An diese Methode wird ein kleiner Helfer-Compiler übergeben, der den MySQL-Dialekt von PostgreSQL unterscheiden und den syntaktischen Zucker, der im Dialekt einer bestimmten Datenbank verwendet wird, korrekt anordnen kann.

DB-Routing


Wenn wir die SQL-Abfrage erhalten haben, klopft das Modell an das DB-Routing und fragt, in welcher Datenbank es sich befindet. In 99% der Fälle handelt es sich um die Standarddatenbank, in den restlichen 1% um eine eigene.

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

Durch das Umschließen eines Datenbanktreibers mit einer bestimmten Bibliotheksschnittstelle wie Python MySQL oder Psycopg2 wird ein universelles Objekt erstellt, mit dem Django arbeiten kann. Es gibt einen Wrapper für Cursor, einen Wrapper für Transaktionen.

Pool verbinden


 # 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() 

In diesem speziellen Zusammenhang senden wir Anforderungen an den Socket, der an die Datenbank klopft, und warten auf die Ausführung. Der Wrapper über der Bibliothek liest die menschliche Antwort aus der Datenbank in Form eines Datensatzes, und Django sammelt die Modellinstanz aus diesen Daten in Python-Typen. Dies ist keine komplizierte Iteration.

Wir haben etwas in die Datenbank geschrieben, etwas gelesen und beschlossen, dem Benutzer über die HTML-Seite davon zu erzählen. Zu diesem Zweck verfügt Django über eine Community-unbeliebte Vorlagensprache, die nur in einer HTML-Datei wie eine Programmiersprache aussieht.

Vorlage


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

Code


 <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)))) 

Überraschung - wieder regexp. Erst am Ende sollte ein Komma stehen, und die Liste wird weit unten stehen. Dies ist wahrscheinlich der schwierigste reguläre Ausdruck, den ich in diesem Projekt gesehen habe.

Lexer


Der Template-Handler und der Interpreter sind ziemlich einfach. Es gibt einen Lexer, der Regexp verwendet, um Text in eine Liste kleiner Token zu übersetzen.

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

Wir durchlaufen die Liste der Token und schauen: „Wer bist du? Wickle dich in einen Tag-Knoten. “ Wenn dies beispielsweise der Beginn eines if oder for oder for , übernimmt der Tag-Handler den entsprechenden Handler. Der for Handler selbst teilt dem Parser erneut mit: "Lesen Sie mir eine Liste der Token bis zum schließenden Tag vor."

Die Operation geht wieder zum Parser.

Ein Knoten, ein Tag und ein Parser sind gegenseitig rekursive Elemente, und die Tiefe der Rekursion entspricht normalerweise der Verschachtelung der Vorlage selbst durch Tags.

Parser


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

Der Tag-Handler gibt uns einen bestimmten Knoten, beispielsweise mit einer for Schleife, für die die render angezeigt wird.

Für Schleife


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

Für Knoten


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

Die render ist ein Renderbaum. Jeder obere Knoten kann zu einem Tochterknoten gehen und ihn zum Rendern auffordern. Programmierer sind es gewohnt, einige Variablen in dieser Vorlage anzuzeigen. Dies geschieht im context - es wird in Form eines regulären Wörterbuchs dargestellt. Dies ist ein Stapel von Wörterbüchern zum Emulieren eines Bereichs, wenn wir ein Tag eingeben. Wenn der context selbst ein anderes Tag in der for Schleife ändert, werden die Änderungen beim Verlassen der Schleife rückgängig gemacht. Dies ist praktisch, da es schwierig ist, zu arbeiten, wenn alles global ist.

Antwort


Endlich haben wir unsere Zeile mit der HTTP-Antwort erhalten:

Hallo Welt!

Wir können dem Benutzer die Leitung geben.

  • Gibt diese Antwort aus der Ansicht zurück.
  • View listet Middleware auf.
  • Middlewares diese Reaktion modifizieren, ergänzen und verbessern.
  • Die Antwort beginnt innerhalb von WSGIHandler zu iterieren, wird teilweise in den Socket geschrieben und der Browser erhält eine Antwort von unserem Server.

Alle berühmten Startups, die in Django geschrieben wurden, wie Bitbucket oder Instagram, begannen mit einem so kleinen Zyklus, den jeder Programmierer durchlief.

All dies und eine Präsentation bei Moscow Python Conf ++ sind erforderlich, damit Sie besser verstehen, was sich in Ihren Händen befindet und wie Sie es verwenden. In jeder Magie gibt es einen großen Teil des regulären Ausdrucks, den Sie kochen müssen.

Artyom Malyshev und 23 weitere großartige Redner werden uns am 5. April auf der Moskauer Python Conf ++ - Konferenz erneut viele Denkanstöße und Diskussionen zum Thema Python geben. Studieren Sie den Zeitplan und nehmen Sie am Erfahrungsaustausch teil, um eine Vielzahl von Problemen mit Python zu lösen.

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


All Articles