Python-generierter Code-Leitfaden
Unterschiede zwischen proto2, proto3 und Editions-generiertem Code werden hervorgehoben – beachten Sie, dass diese Unterschiede im generierten Code wie in diesem Dokument beschrieben sind, nicht in den Basisnachrichtenklassen/-schnittstellen, die in allen Versionen gleich sind. Sie sollten den Proto2-Sprachführer, Proto3-Sprachführer und/oder Editionsführer lesen, bevor Sie dieses Dokument lesen.
Die Python-Implementierung von Protokollpuffern unterscheidet sich geringfügig von C++ und Java. In Python gibt der Compiler nur Code aus, um Deskriptoren für die generierten Klassen zu erstellen, und eine Python-Metaklasse erledigt die eigentliche Arbeit. Dieses Dokument beschreibt, was Sie erhalten, *nachdem* die Metaklasse angewendet wurde.
Compiler-Aufruf
Der Protokollpuffer-Compiler erzeugt Python-Ausgaben, wenn er mit dem Befehlszeilenflag --python_out= aufgerufen wird. Der Parameter der Option --python_out= ist das Verzeichnis, in das der Compiler Ihre Python-Ausgabe schreiben soll. Der Compiler erstellt für jede .proto-Datei eine .py-Datei. Die Namen der Ausgabedateien werden berechnet, indem der Name der .proto-Datei genommen und zwei Änderungen vorgenommen werden.
- Die Erweiterung (
.proto) wird durch_pb2.pyersetzt. - Der Proto-Pfad (angegeben mit dem Befehlszeilenflag
--proto_path=oder-I) wird durch den Ausgabepfad (angegeben mit dem Flag--python_out=) ersetzt.
Nehmen wir also zum Beispiel an, Sie rufen den Compiler wie folgt auf:
protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto
Der Compiler liest die Dateien src/foo.proto und src/bar/baz.proto und erzeugt zwei Ausgabedateien: build/gen/foo_pb2.py und build/gen/bar/baz_pb2.py. Der Compiler erstellt automatisch das Verzeichnis build/gen/bar, falls erforderlich, aber er erstellt *nicht* build oder build/gen; diese müssen bereits existieren.
Protoc kann Python-Stubs (.pyi) mit dem Parameter --pyi_out generieren.
Beachten Sie, dass, wenn die .proto-Datei oder ihr Pfad Zeichen enthält, die in Python-Modulnamen nicht verwendet werden können (z. B. Bindestriche), diese durch Unterstriche ersetzt werden. So wird die Datei foo-bar.proto zur Python-Datei foo_bar_pb2.py.
Tipp
Wenn Python-Code ausgegeben wird, ist die Fähigkeit des Protokollpuffer-Compilers, direkt in ZIP-Archive zu schreiben, besonders praktisch, da der Python-Interpreter direkt aus diesen Archiven lesen kann, wenn sie inPYTHONPATH platziert werden. Um in eine ZIP-Datei zu schreiben, geben Sie einfach einen Ausgabepfad an, der auf .zip endet.Hinweis
Die Zahl 2 in der Erweiterung_pb2.py bezeichnet die Version 2 von Protokollpuffern. Version 1 wurde hauptsächlich intern bei Google verwendet, obwohl Sie möglicherweise Teile davon in anderem Python-Code finden, der vor Protokollpuffern veröffentlicht wurde. Da Version 2 von Python-Protokollpuffern eine völlig andere Schnittstelle hat und Python keine Laufzeit-Typüberprüfung hat, um Fehler zu erkennen, haben wir uns entschieden, die Versionsnummer zu einem prominenten Bestandteil der generierten Python-Dateinamen zu machen. Derzeit verwenden proto2, proto3 und Editions alle _pb2.py für ihre generierten Dateien.Pakete (Packages)
Der von Protokollpuffern generierte Python-Code wird vom Paketnamen, der in der .proto-Datei definiert ist, überhaupt nicht beeinflusst. Stattdessen werden Python-Pakete durch die Verzeichnisstruktur identifiziert.
Nachrichten
Angesichts einer einfachen Nachrichten Deklaration:
message Foo {}
Der Protokollpuffer-Compiler generiert eine Klasse namens Foo, die von google.protobuf.Message erbt. Die Klasse ist eine konkrete Klasse; keine abstrakten Methoden bleiben unbehandelt. Im Gegensatz zu C++ und Java wird der generierte Python-Code nicht von der Option optimize_for in der .proto-Datei beeinflusst; im Wesentlichen ist der gesamte Python-Code auf Code-Größe optimiert.
Wenn der Nachrichtenname ein Python-Schlüsselwort ist, dann ist seine Klasse nur über getattr() zugänglich, wie im Abschnitt Namen, die mit Python-Schlüsselwörtern kollidieren beschrieben.
Sie sollten *keine* eigenen Foo-Unterklassen erstellen. Generierte Klassen sind nicht für die Unterklassifizierung konzipiert und können zu Problemen mit "fragilen Basisklassen" führen. Außerdem ist Implementierungserbschaft schlechtes Design.
Python-Nachrichtenklassen haben keine besonderen öffentlichen Member außer denen, die von der Message-Schnittstelle definiert werden, und denen, die für verschachtelte Felder, Nachrichten und Enum-Typen generiert werden (unten beschrieben). Message bietet Methoden, mit denen Sie die gesamte Nachricht überprüfen, bearbeiten, lesen oder schreiben können, einschließlich des Parsens und Serialisierens in Binärzeichenfolgen. Zusätzlich zu diesen Methoden definiert die Klasse Foo die folgenden statischen Methoden:
FromString(s): Gibt eine neue Nachrichteninstanz zurück, die aus der gegebenen Zeichenfolge deserialisiert wurde.
Beachten Sie, dass Sie auch das Modul text_format verwenden können, um mit Protokollnachrichten im Textformat zu arbeiten: Zum Beispiel können Sie mit der Methode Merge() eine ASCII-Darstellung einer Nachricht in eine bestehende Nachricht einfügen.
Verschachtelte Typen
Eine Nachricht kann innerhalb einer anderen Nachricht deklariert werden. Zum Beispiel:
message Foo {
message Bar {}
}
In diesem Fall ist die Klasse Bar als statisches Mitglied von Foo deklariert, sodass Sie sie als Foo.Bar referenzieren können.
Gut bekannte Typen
Protokollpuffer bieten eine Reihe von gut bekannten Typen, die Sie in Ihren .proto-Dateien zusammen mit Ihren eigenen Nachrichtentypen verwenden können. Einige WKT-Nachrichten haben neben den üblichen Protokollpuffer-Nachrichtenmethoden spezielle Methoden, da sie sowohl von google.protobuf.Message als auch von einer WKT-Klasse erben.
Any
Für Any-Nachrichten können Sie Pack() aufrufen, um eine angegebene Nachricht in die aktuelle Any-Nachricht zu packen, oder Unpack(), um die aktuelle Any-Nachricht in eine angegebene Nachricht auszupacken. Zum Beispiel:
any_message.Pack(message)
any_message.Unpack(message)
Unpack() prüft auch den Deskriptor des übergebenen Nachrichtenobjekts gegen den gespeicherten und gibt False zurück, wenn sie nicht übereinstimmen, und versucht kein Auspacken; andernfalls gibt es True zurück.
Sie können auch die Methode Is() aufrufen, um zu prüfen, ob die Any-Nachricht den gegebenen Protokollpuffertyp darstellt. Zum Beispiel:
assert any_message.Is(message.DESCRIPTOR)
Verwenden Sie die Methode TypeName(), um den Protobuf-Typnamen einer inneren Nachricht abzurufen.
Timestamp
Timestamp-Nachrichten können mit den Methoden ToJsonString()/FromJsonString() in/aus dem RFC 3339-Datumszeichenfolgenformat (JSON-Zeichenfolge) konvertiert werden. Zum Beispiel:
timestamp_message.FromJsonString("1970-01-01T00:00:00Z")
assert timestamp_message.ToJsonString() == "1970-01-01T00:00:00Z"
Sie können auch GetCurrentTime() aufrufen, um die Timestamp-Nachricht mit der aktuellen Zeit zu füllen.
timestamp_message.GetCurrentTime()
Um zwischen anderen Zeiteinheiten seit der Epoche zu konvertieren, können Sie ToNanoseconds(), FromNanoseconds(), ToMicroseconds(), FromMicroseconds(), ToMilliseconds(), FromMilliseconds(), ToSeconds() oder FromSeconds() aufrufen. Der generierte Code hat auch die Methoden ToDatetime() und FromDatetime(), um zwischen Python-Datums- und Zeitobjekten und Timestamps zu konvertieren. Zum Beispiel:
timestamp_message.FromMicroseconds(-1)
assert timestamp_message.ToMicroseconds() == -1
dt = datetime(2016, 1, 1)
timestamp_message.FromDatetime(dt)
self.assertEqual(dt, timestamp_message.ToDatetime())
Duration
Dauer-Nachrichten haben die gleichen Methoden wie Timestamp, um zwischen JSON-Zeichenfolgen und anderen Zeiteinheiten zu konvertieren. Um zwischen Timedelta und Dauer zu konvertieren, können Sie ToTimedelta() oder FromTimedelta aufrufen. Zum Beispiel:
duration_message.FromNanoseconds(1999999999)
td = duration_message.ToTimedelta()
assert td.seconds == 1
assert td.microseconds == 999999
FieldMask
FieldMask-Nachrichten können mit den Methoden ToJsonString()/FromJsonString() in/aus JSON-Zeichenfolgen konvertiert werden. Darüber hinaus hat eine FieldMask-Nachricht die folgenden Methoden:
IsValidForDescriptor(message_descriptor): Prüft, ob die FieldMask für den Message Descriptor gültig ist.AllFieldsFromDescriptor(message_descriptor): Ruft alle direkten Felder des Message Descriptors in die FieldMask ab.CanonicalFormFromMask(mask): Konvertiert eine FieldMask in die kanonische Form.Union(mask1, mask2): Fügt zwei FieldMasks zu dieser FieldMask zusammen.Intersect(mask1, mask2): Schneidet zwei FieldMasks in dieser FieldMask.MergeMessage(source, destination, replace_message_field=False, replace_repeated_field=False): Fügt die in FieldMask angegebenen Felder von der Quelle zum Ziel zusammen.
Struct
Struct-Nachrichten ermöglichen es Ihnen, Elemente direkt abzurufen und festzulegen. Zum Beispiel:
struct_message["key1"] = 5
struct_message["key2"] = "abc"
struct_message["key3"] = True
Um eine Liste/ein Struct abzurufen oder zu erstellen, können Sie get_or_create_list()/get_or_create_struct() aufrufen. Zum Beispiel:
struct.get_or_create_struct("key4")["subkey"] = 11.0
struct.get_or_create_list("key5")
ListValue
Eine ListValue-Nachricht verhält sich wie eine Python-Sequenz, die Folgendes ermöglicht:
list_value = struct_message.get_or_create_list("key")
list_value.extend([6, "seven", True, None])
list_value.append(False)
assert len(list_value) == 5
assert list_value[0] == 6
assert list_value[1] == "seven"
assert list_value[2] == True
assert list_value[3] == None
assert list_Value[4] == False
Um eine ListValue/ein Struct hinzuzufügen, rufen Sie add_list()/add_struct() auf. Zum Beispiel:
list_value.add_struct()["key"] = 1
list_value.add_list().extend([1, "two", True])
Felder
Für jedes Feld in einem Nachrichtentyp hat die entsprechende Klasse eine Eigenschaft mit demselben Namen wie das Feld. Wie Sie die Eigenschaft manipulieren können, hängt von ihrem Typ ab.
Zusätzlich zu einer Eigenschaft generiert der Compiler eine Ganzzahlkonstante für jedes Feld, die seine Feldnummer enthält. Der Konstantenname ist der Feldname, konvertiert in Großbuchstaben, gefolgt von _FIELD_NUMBER. Zum Beispiel wird für das Feld int32 foo_bar = 5; die Konstante FOO_BAR_FIELD_NUMBER = 5 generiert.
Wenn der Feldname ein Python-Schlüsselwort ist, dann ist seine Eigenschaft nur über getattr() und setattr() zugänglich, wie im Abschnitt Namen, die mit Python-Schlüsselwörtern kollidieren beschrieben.
Protokollpuffer definieren zwei Modi der Feldpräsenz: explicit und implicit. Jeder davon wird in den folgenden Abschnitten beschrieben.
Singuläre Felder mit expliziter Anwesenheit
Singuläre Felder mit expliziter Präsenz können immer zwischen dem Zustand "nicht gesetzt" und dem Zustand "auf seinen Standardwert gesetzt" unterscheiden.
Wenn Sie ein singuläres Feld foo eines beliebigen Nicht-Nachrichtentyps haben, können Sie das Feld foo so bearbeiten, als wäre es ein reguläres Feld. Wenn zum Beispiel der Typ von foo int32 ist, können Sie sagen:
message.foo = 123
print(message.foo)
Beachten Sie, dass das Festlegen von foo auf einen Wert vom falschen Typ einen TypeError auslöst.
Wenn foo gelesen wird, wenn es nicht gesetzt ist, ist sein Wert der Standardwert für dieses Feld. Um zu prüfen, ob foo gesetzt ist, oder um den Wert von foo zu löschen, müssen Sie die Methoden HasField() oder ClearField() der Message-Schnittstelle aufrufen. Zum Beispiel:
assert not message.HasField("foo")
message.foo = 123
assert message.HasField("foo")
message.ClearField("foo")
assert not message.HasField("foo")
In Editions haben Felder standardmäßig eine explizite Präsenz. Das Folgende ist ein Beispiel für ein explizites Feld in einer Editions-.proto-Datei:
edition = "2023";
message MyMessage {
int32 foo = 1;
}
Singuläre Felder mit impliziter Anwesenheit
Singuläre Felder mit impliziter Präsenz haben keine HasField()-Methode. Ein implizites Feld ist immer "gesetzt" und das Lesen des Feldes gibt immer einen Wert zurück. Das Lesen eines impliziten Feldes, dem kein Wert zugewiesen wurde, gibt den Standardwert für diesen Typ zurück.
Wenn Sie ein singuläres Feld foo eines beliebigen Nicht-Nachrichtentyps haben, können Sie das Feld foo so bearbeiten, als wäre es ein reguläres Feld. Wenn zum Beispiel der Typ von foo int32 ist, können Sie sagen:
message.foo = 123
print(message.foo)
Beachten Sie, dass das Festlegen von foo auf einen Wert vom falschen Typ einen TypeError auslöst.
Wenn foo gelesen wird, wenn es nicht gesetzt ist, ist sein Wert der Standardwert für dieses Feld. Um den Wert von foo zu löschen und ihn auf den Standardwert für seinen Typ zurückzusetzen, rufen Sie die Methode ClearField() der Message-Schnittstelle auf. Zum Beispiel:
message.foo = 123
message.ClearField("foo")
Singuläre Nachrichtenfelder
Nachrichtentypen funktionieren etwas anders. Sie können einem eingebetteten Nachrichtenfeld keinen Wert zuweisen. Stattdessen impliziert die Zuweisung eines Wertes zu einem Feld innerhalb der Kindnachricht das Setzen des Nachrichtenfeldes im Elternteil. Unterklassen haben immer explizite Präsenz, sodass Sie auch die HasField()-Methode der übergeordneten Nachricht verwenden können, um zu prüfen, ob ein Nachrichtenfeldeswert gesetzt wurde.
Nehmen wir also zum Beispiel an, Sie haben die folgende .proto-Definition:
edition = "2023";
message Foo {
Bar bar = 1;
}
message Bar {
int32 i = 1;
}
Sie *können* nicht Folgendes tun:
foo = Foo()
foo.bar = Bar() # WRONG!
Stattdessen, um bar zu setzen, weisen Sie einfach direkt einen Wert einem Feld innerhalb von bar zu, und – presto! – foo hat ein bar-Feld.
foo = Foo()
assert not foo.HasField("bar")
foo.bar.i = 1
assert foo.HasField("bar")
assert foo.bar.i == 1
foo.ClearField("bar")
assert not foo.HasField("bar")
assert foo.bar.i == 0 # Default value
Ebenso können Sie bar mit der Methode CopyFrom() der Message-Schnittstelle setzen. Dies kopiert alle Werte aus einer anderen Nachricht desselben Typs wie bar.
foo.bar.CopyFrom(baz)
Beachten Sie, dass das einfache Lesen eines Feldes innerhalb von bar das Feld *nicht* setzt.
foo = Foo()
assert not foo.HasField("bar")
print(foo.bar.i) # Print i's default value
assert not foo.HasField("bar")
Wenn Sie die "has"-Bit für eine Nachricht benötigen, die keine Felder hat, die Sie setzen können oder wollen, können Sie die Methode SetInParent() verwenden.
foo = Foo()
assert not foo.HasField("bar")
foo.bar.SetInParent() # Set Foo.bar to a default Bar message
assert foo.HasField("bar")
Wiederholte Felder
Es gibt drei Arten von wiederholten Feldern: Skalar, Enum und Nachricht. Kartenfelder und Oneof-Felder können nicht wiederholt werden.
Wiederholte Skalar- und Enum-Felder
Wiederholte Felder werden als Objekt dargestellt, das sich wie eine Python-Sequenz verhält. Wie bei eingebetteten Nachrichten können Sie das Feld nicht direkt zuweisen, aber Sie können es manipulieren. Zum Beispiel bei dieser Nachrichten-Definition:
message Foo {
repeated int32 nums = 1;
}
Sie können Folgendes tun:
foo = Foo()
foo.nums.append(15) # Appends one value
foo.nums.extend([32, 47]) # Appends an entire list
assert len(foo.nums) == 3
assert foo.nums[0] == 15
assert foo.nums[1] == 32
assert foo.nums == [15, 32, 47]
foo.nums[:] = [33, 48] # Assigns an entire list
assert foo.nums == [33, 48]
foo.nums[1] = 56 # Reassigns a value
assert foo.nums[1] == 56
for i in foo.nums: # Loops and print
print(i)
del foo.nums[:] # Clears list (works just like in a Python list)
Die Methode ClearField() der Message-Schnittstelle funktioniert zusätzlich zur Verwendung von Python del.
Wenn Sie den Index verwenden, um einen Wert abzurufen, können Sie negative Zahlen verwenden, z. B. -1, um das letzte Element in der Liste abzurufen. Wenn Ihr Index außerhalb der Grenzen liegt, erhalten Sie eine IndexError: list index out of range.
Wiederholte Nachrichtenfelder
Wiederholte Nachrichtenfelder funktionieren ähnlich wie wiederholte Skalarfelder. Das entsprechende Python-Objekt hat jedoch auch eine add()-Methode, die ein neues Nachrichtenobjekt erstellt, es an die Liste anhängt und es dem Aufrufer zur Verfügung stellt, um es zu füllen. Außerdem erstellt die append()-Methode des Objekts eine *Kopie* der gegebenen Nachricht und hängt diese Kopie an die Liste an. Dies geschieht, damit Nachrichten immer von der übergeordneten Nachricht besessen werden, um zirkuläre Referenzen und andere Verwirrung zu vermeiden, die auftreten können, wenn eine veränderliche Datenstruktur mehrere Besitzer hat. Ebenso hängt die extend()-Methode des Objekts eine ganze Liste von Nachrichten an, erstellt aber eine *Kopie* jeder Nachricht in der Liste.
Zum Beispiel bei dieser Nachrichten-Definition:
edition = "2023";
message Foo {
repeated Bar bars = 1;
}
message Bar {
int32 i = 1;
int32 j = 2;
}
Sie können Folgendes tun:
foo = Foo()
bar = foo.bars.add() # Adds a Bar then modify
bar.i = 15
foo.bars.add().i = 32 # Adds and modify at the same time
new_bar = Bar()
new_bar.i = 40
another_bar = Bar()
another_bar.i = 57
foo.bars.append(new_bar) # Uses append() to copy
foo.bars.extend([another_bar]) # Uses extend() to copy
assert len(foo.bars) == 4
assert foo.bars[0].i == 15
assert foo.bars[1].i == 32
assert foo.bars[2].i == 40
assert foo.bars[2] == new_bar # The appended message is equal,
assert foo.bars[2] is not new_bar # but it is a copy!
assert foo.bars[3].i == 57
assert foo.bars[3] == another_bar # The extended message is equal,
assert foo.bars[3] is not another_bar # but it is a copy!
foo.bars[1].i = 56 # Modifies a single element
assert foo.bars[1].i == 56
for bar in foo.bars: # Loops and print
print(bar.i)
del foo.bars[:] # Clears list
# add() also forwards keyword arguments to the concrete class.
# For example, you can do:
foo.bars.add(i=12, j=13)
# Initializers forward keyword arguments to a concrete class too.
# For example:
foo = Foo( # Creates Foo
bars=[ # with its field bars set to a list
Bar(i=15, j=17), # where each list member is also initialized during creation.
Bar(i=32),
Bar(i=47, j=77),
]
)
assert len(foo.bars) == 3
assert foo.bars[0].i == 15
assert foo.bars[0].j == 17
assert foo.bars[1].i == 32
assert foo.bars[2].i == 47
assert foo.bars[2].j == 77
Im Gegensatz zu wiederholten Skalarfeldern unterstützen wiederholte Nachrichtenfelder *keine* Elementzuweisung (d. h. __setitem__). Zum Beispiel:
foo = Foo()
foo.bars.add(i=3)
# WRONG!
foo.bars[0] = Bar(i=15) # Raises an exception
# WRONG!
foo.bars[:] = [Bar(i=15), Bar(i=17)] # Also raises an exception
# WRONG!
# AttributeError: Cannot delete field attribute
del foo.bars
# RIGHT
del foo.bars[:]
foo.bars.extend([Bar(i=15), Bar(i=17)])
Gruppen (proto2)
Beachten Sie, dass Gruppen veraltet sind und nicht verwendet werden sollten, wenn neue Nachrichtentypen erstellt werden – verwenden Sie stattdessen verschachtelte Nachrichtentypen (proto2, proto3) oder begrenzte Felder (Editions).
Eine Gruppe kombiniert einen verschachtelten Nachrichtentyp und ein Feld in einer einzigen Deklaration und verwendet ein anderes Wire-Format für die Nachricht. Die generierte Nachricht hat denselben Namen wie die Gruppe. Der generierte Feldname ist der **kleingeschriebene** Name der Gruppe.
Zum Beispiel sind mit Ausnahme des Wire-Formats die folgenden beiden Nachrichten-Definitionen äquivalent:
// Version 1: Using groups
message SearchResponse {
repeated group SearchResult = 1 {
optional string url = 1;
}
}
// Version 2: Not using groups
message SearchResponse {
message SearchResult {
optional string url = 1;
}
repeated SearchResult searchresult = 1;
}
Eine Gruppe ist entweder required, optional oder repeated. Eine erforderliche oder optionale Gruppe wird mit derselben API wie ein reguläres singuläres Nachrichtenfeld manipuliert. Eine wiederholte Gruppe wird mit derselben API wie ein reguläres wiederholtes Nachrichtenfeld manipuliert.
Zum Beispiel können Sie bei der oben genannten SearchResponse-Definition Folgendes tun:
resp = SearchResponse()
resp.searchresult.add(url="https://blog.google")
assert resp.searchresult[0].url == "https://blog.google"
assert resp.searchresult[0] == SearchResponse.SearchResult(url="https://blog.google")
Map-Felder
Bei dieser Nachrichten-Definition:
message MyMessage {
map<int32, int32> mapfield = 1;
}
Die generierte Python-API für das Kartenfeld ist wie ein Python-dict:
# Assign value to map
m.mapfield[5] = 10
# Read value from map
m.mapfield[5]
# Iterate over map keys
for key in m.mapfield:
print(key)
print(m.mapfield[key])
# Test whether key is in map:
if 5 in m.mapfield:
print(“Found!”)
# Delete key from map.
del m.mapfield[key]
Wie bei eingebetteten Nachrichtenfeldern können Nachrichten nicht direkt einem Kartenwert zugewiesen werden. Um stattdessen eine Nachricht als Kartenwert hinzuzufügen, referenzieren Sie einen nicht definierten Schlüssel, der eine neue Untermeldung erstellt und zurückgibt.
m.message_map[key].submessage_field = 10
Sie erfahren im nächsten Abschnitt mehr über nicht definierte Schlüssel.
Referenzierung nicht definierter Schlüssel
Die Semantik von Protokollpuffer-Karten verhält sich geringfügig anders als Python-dicts, wenn es um nicht definierte Schlüssel geht. In einem regulären Python-dict löst die Referenzierung eines nicht definierten Schlüssels eine KeyError-Ausnahme aus:
>>> x = {}
>>> x[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 5
Bei Protokollpuffer-Karten erstellt die Referenzierung eines nicht definierten Schlüssels den Schlüssel in der Karte mit einem Null-/Falsch-/Leerwert. Dieses Verhalten ähnelt eher der defaultdict-Bibliothek von Python.
>>> dict(m.mapfield)
{}
>>> m.mapfield[5]
0
>>> dict(m.mapfield)
{5: 0}
Dieses Verhalten ist besonders praktisch für Karten mit Nachrichtenwerttyp, da Sie die Felder der zurückgegebenen Nachricht direkt aktualisieren können.
>>> m.message_map[5].foo = 3
Beachten Sie, dass die Untermeldung auch dann in der Karte erstellt wird, wenn Sie keine Werte für Nachrichtenfelder zuweisen:
>>> m.message_map[10]
<test_pb2.M2 object at 0x7fb022af28c0>
>>> dict(m.message_map)
{10: <test_pb2.M2 object at 0x7fb022af28c0>}
Dies *unterscheidet* sich von regulären eingebetteten Nachrichtenfeldern, bei denen die Nachricht erst erstellt wird, wenn Sie einem ihrer Felder einen Wert zuweisen.
Da es für jemanden, der Ihren Code liest, möglicherweise nicht sofort offensichtlich ist, dass z. B. allein m.message_map[10] eine Untermeldung erstellen kann, stellen wir auch eine Methode get_or_create() bereit, die dasselbe tut, aber deren Name die mögliche Nachrichtenerstellung expliziter macht.
# Equivalent to:
# m.message_map[10]
# but more explicit that the statement might be creating a new
# empty message in the map.
m.message_map.get_or_create(10)
Aufzählungen (Enums)
In Python sind Enums einfach Ganzzahlen. Eine Reihe von Ganzzahlkonstanten werden definiert, die den definierten Werten des Enums entsprechen. Zum Beispiel, gegeben:
message Foo {
enum SomeEnum {
VALUE_A = 0;
VALUE_B = 5;
VALUE_C = 1234;
}
SomeEnum bar = 1;
}
Die Konstanten VALUE_A, VALUE_B und VALUE_C werden mit den Werten 0, 5 bzw. 1234 definiert. Sie können SomeEnum bei Bedarf zugreifen. Wenn ein Enum im äußeren Geltungsbereich definiert ist, sind die Werte Modulkonstanten; wenn es innerhalb einer Nachricht (wie oben) definiert ist, werden sie zu statischen Mitgliedern dieser Nachrichtenklasse.
Sie können beispielsweise die Werte auf die folgenden drei Arten für das folgende Enum in einem Proto zugreifen:
enum SomeEnum {
VALUE_A = 0;
VALUE_B = 5;
VALUE_C = 1234;
}
value_a = myproto_pb2.SomeEnum.VALUE_A
# or
myproto_pb2.VALUE_A
# or
myproto_pb2.SomeEnum.Value('VALUE_A')
Ein Enum-Feld funktioniert wie ein Skalarfeld.
foo = Foo()
foo.bar = Foo.VALUE_A
assert foo.bar == 0
assert foo.bar == Foo.VALUE_A
Wenn der Enum-Name (oder ein Enum-Wert) ein Python-Schlüsselwort ist, dann ist sein Objekt (oder die Eigenschaft des Enum-Wertes) nur über getattr() zugänglich, wie im Abschnitt Namen, die mit Python-Schlüsselwörtern kollidieren beschrieben.
Mit proto2 sind Enums geschlossen und mit proto3 sind Enums offen. In Editions bestimmt das Feature enum_type das Verhalten eines Enums.
OPENEnums können jedenint32-Wert haben, auch wenn er nicht in der Enum-Definition angegeben ist. Dies ist die Standardeinstellung in Editions.CLOSEDEnums dürfen keinen numerischen Wert enthalten, der nicht für den Enum-Typ definiert ist. Wenn Sie einen Wert zuweisen, der nicht im Enum enthalten ist, löst der generierte Code eine Ausnahme aus. Dies entspricht dem Verhalten von Enums in proto2.
Enums haben eine Reihe von Hilfsmethoden, um Feldnamen aus Werten und umgekehrt abzurufen, Listen von Feldern usw. – diese sind in enum_type_wrapper.EnumTypeWrapper (die Basisklasse für generierte Enum-Klassen) definiert. Wenn Sie also zum Beispiel dieses eigenständige Enum in myproto.proto haben:
enum SomeEnum {
VALUE_A = 0;
VALUE_B = 5;
VALUE_C = 1234;
}
…können Sie Folgendes tun:
self.assertEqual('VALUE_A', myproto_pb2.SomeEnum.Name(myproto_pb2.VALUE_A))
self.assertEqual(5, myproto_pb2.SomeEnum.Value('VALUE_B'))
Für ein Enum, das innerhalb einer Protokollnachricht deklariert ist, wie z. B. Foo oben, ist die Syntax ähnlich:
self.assertEqual('VALUE_A', myproto_pb2.Foo.SomeEnum.Name(myproto_pb2.Foo.VALUE_A))
self.assertEqual(5, myproto_pb2.Foo.SomeEnum.Value('VALUE_B'))
Wenn mehrere Enum-Konstanten denselben Wert haben (Aliase), wird die zuerst definierte Konstante zurückgegeben.
enum SomeEnum {
option allow_alias = true;
VALUE_A = 0;
VALUE_B = 5;
VALUE_C = 1234;
VALUE_B_ALIAS = 5;
}
Im obigen Beispiel gibt myproto_pb2.SomeEnum.Name(5) "VALUE_B" zurück.
Oneof
Bei einer Nachricht mit einem Oneof:
message Foo {
oneof test_oneof {
string name = 1;
int32 serial_number = 2;
}
}
Die Python-Klasse, die Foo entspricht, hat Eigenschaften namens name und serial_number, genau wie reguläre Felder. Im Gegensatz zu regulären Feldern kann jedoch höchstens eines der Felder in einem Oneof gleichzeitig gesetzt sein, was durch die Laufzeit sichergestellt wird. Zum Beispiel:
message = Foo()
message.name = "Bender"
assert message.HasField("name")
message.serial_number = 2716057
assert message.HasField("serial_number")
assert not message.HasField("name")
Die Nachrichtenklasse hat auch eine Methode WhichOneof, mit der Sie herausfinden können, welches Feld (falls vorhanden) im Oneof gesetzt wurde. Diese Methode gibt den Namen des Feldes zurück, das gesetzt ist, oder None, wenn nichts gesetzt wurde.
assert message.WhichOneof("test_oneof") is None
message.name = "Bender"
assert message.WhichOneof("test_oneof") == "name"
HasField und ClearField akzeptieren zusätzlich zu Feldnamen auch Oneof-Namen.
assert not message.HasField("test_oneof")
message.name = "Bender"
assert message.HasField("test_oneof")
message.serial_number = 2716057
assert message.HasField("test_oneof")
message.ClearField("test_oneof")
assert not message.HasField("test_oneof")
assert not message.HasField("serial_number")
Beachten Sie, dass das Aufrufen von ClearField für ein Oneof nur das aktuell gesetzte Feld löscht.
Namen, die mit Python-Schlüsselwörtern kollidieren
Wenn der Name einer Nachricht, eines Feldes, eines Enums oder eines Enum-Wertes ein Python-Schlüsselwort ist, dann ist der Name seiner entsprechenden Klasse oder Eigenschaft gleich, aber Sie können darauf nur über die integrierten Funktionen getattr() und setattr() von Python zugreifen und nicht über die normale Attributreferenzsyntax von Python (d. h. den Punktoperator).
Wenn Sie zum Beispiel die folgende .proto-Definition haben:
message Baz {
optional int32 from = 1
repeated int32 in = 2;
}
Sie würden auf diese Felder wie folgt zugreifen:
baz = Baz()
setattr(baz, "from", 99)
assert getattr(baz, "from") == 99
getattr(baz, "in").append(42)
assert getattr(baz, "in") == [42]
Im Gegensatz dazu führt der Versuch, die obj.attr-Syntax zur Adressierung dieser Felder zu verwenden, dazu, dass Python beim Parsen Ihres Codes Syntaxfehler auslöst.
# WRONG!
baz.in # SyntaxError: invalid syntax
baz.from # SyntaxError: invalid syntax
Erweiterungen (Extensions)
Bei einer proto2- oder Editions-Nachricht mit einem Erweiterungsbereich:
edition = "2023";
message Foo {
extensions 100 to 199;
}
Die Python-Klasse, die Foo entspricht, hat ein Mitglied namens Extensions, das ein Wörterbuch ist, das Erweiterungsidentifikatoren ihren aktuellen Werten zuordnet.
Gegeben eine Erweiterungsdefinition
extend Foo {
int32 bar = 123;
}
Der Protokollpuffer-Compiler generiert einen "Erweiterungsidentifikator" namens bar. Der Identifikator fungiert als Schlüssel für das Extensions-Wörterbuch. Das Ergebnis des Nachschlagens eines Wertes in diesem Wörterbuch ist genau dasselbe, als ob Sie auf ein normales Feld desselben Typs zugreifen würden. Angenommen, im obigen Beispiel könnten Sie also sagen:
foo = Foo()
foo.Extensions[proto_file_pb2.bar] = 2
assert foo.Extensions[proto_file_pb2.bar] == 2
Beachten Sie, dass Sie die Erweiterungsidentifikator-Konstante angeben müssen und nicht nur einen String-Namen: Dies liegt daran, dass es möglich ist, dass mehrere Erweiterungen mit demselben Namen in verschiedenen Bereichen angegeben werden.
Analog zu normalen Feldern gibt Extensions[...] ein Nachrichtenobjekt für singuläre Nachrichten und eine Sequenz für wiederholte Felder zurück.
Die Methoden HasField() und ClearField() der Message-Schnittstelle funktionieren nicht mit Erweiterungen. Sie müssen stattdessen HasExtension() und ClearExtension() verwenden. Um die Methoden HasExtension() und ClearExtension() zu verwenden, übergeben Sie den field_descriptor für die Erweiterung, deren Existenz Sie überprüfen.
Services
Wenn die .proto-Datei die folgende Zeile enthält:
option py_generic_services = true;
Dann generiert der Protokollpuffer-Compiler Code basierend auf den in der Datei gefundenen Service-Definitionen, wie in diesem Abschnitt beschrieben. Der generierte Code ist jedoch möglicherweise unerwünscht, da er nicht an ein bestimmtes RPC-System gebunden ist und daher mehr Indirektionsstufen erfordert als Code, der auf ein System zugeschnitten ist. Wenn Sie *nicht* möchten, dass dieser Code generiert wird, fügen Sie diese Zeile zur Datei hinzu:
option py_generic_services = false;
Wenn keine der oben genannten Zeilen angegeben ist, ist die Option standardmäßig false, da generische Dienste veraltet sind. (Beachten Sie, dass vor 2.4.0 die Option standardmäßig true ist)
RPC-Systeme, die auf .proto-Sprachservice-Definitionen basieren, sollten Plugins bereitstellen, um Code zu generieren, der für das System geeignet ist. Diese Plugins erfordern wahrscheinlich, dass abstrakte Dienste deaktiviert werden, damit sie eigene Klassen mit denselben Namen generieren können.
Der Rest dieses Abschnitts beschreibt, was der Protocol Buffer Compiler generiert, wenn abstrakte Dienste aktiviert sind.
Schnittstelle
Gegeben eine Service-Definition
service Foo {
rpc Bar(FooRequest) returns(FooResponse);
}
Der Protokollpuffer-Compiler generiert eine Klasse Foo, um diesen Dienst darzustellen. Foo hat eine Methode für jede in der Dienstdefinition definierte Methode. In diesem Fall ist die Methode Bar wie folgt definiert:
def Bar(self, rpc_controller, request, done)
Die Parameter sind äquivalent zu den Parametern von Service.CallMethod(), mit dem Unterschied, dass das Argument method_descriptor impliziert ist.
Diese generierten Methoden sind dazu bestimmt, von Unterklassen überschrieben zu werden. Die Standardimplementierungen rufen einfach controller.SetFailed() mit einer Fehlermeldung auf, die angibt, dass die Methode nicht implementiert ist, und rufen dann den done-Callback auf. Bei der Implementierung Ihres eigenen Dienstes müssen Sie diese generierte Dienstklasse unterklassifizieren und ihre Methoden entsprechend implementieren.
Foo unterklassifiziert die Service-Schnittstelle. Der Protocol Buffer Compiler generiert automatisch Implementierungen der Methoden von Service wie folgt:
GetDescriptor: Gibt denServiceDescriptordes Dienstes zurück.CallMethod: Ermittelt, welche Methode basierend auf dem bereitgestellten Methoden-Deskriptor aufgerufen wird, und ruft sie direkt auf.GetRequestClassundGetResponseClass: Gibt die Klasse der Anfrage oder Antwort des richtigen Typs für die gegebene Methode zurück.
Stub
Der Protokollpuffer-Compiler generiert auch eine "Stub"-Implementierung jeder Service-Schnittstelle, die von Clients verwendet wird, die Anfragen an Server senden möchten, die den Dienst implementieren. Für den Foo-Dienst (oben) wird die Stub-Implementierung Foo_Stub definiert.
Foo_Stub ist eine Unterklasse von Foo. Sein Konstruktor nimmt einen RpcChannel als Parameter entgegen. Der Stub implementiert dann jede der Methoden des Dienstes, indem er die CallMethod()-Methode des Kanals aufruft.
Die Protokollpuffer-Bibliothek enthält keine RPC-Implementierung. Sie enthält jedoch alle Werkzeuge, die Sie benötigen, um eine generierte Dienstklasse an jede beliebige RPC-Implementierung Ihrer Wahl anzubinden. Sie müssen lediglich Implementierungen von RpcChannel und RpcController bereitstellen.
Plugin-Einfügepunkte
Code-Generator-Plugins, die die Ausgabe des Python-Code-Generators erweitern möchten, können Code der folgenden Typen unter Verwendung der angegebenen Einfügepunkt-Namen einfügen.
imports: Importanweisungen.module_scope: Deklarationen auf oberster Ebene.
Warnung
Generieren Sie keinen Code, der von privaten Klassenmitgliedern abhängt, die vom Standard-Code-Generator deklariert werden, da sich diese Implementierungsdetails in zukünftigen Versionen von Protocol Buffers ändern können.Gemeinsame Nutzung von Nachrichten zwischen Python und C++
Vor der Version 4.21.0 der Protobuf Python API konnten Python-Apps Nachrichten mit einer nativen Erweiterung mit C++ teilen. Ab der API-Version 4.21.0 wird das Teilen von Nachrichten zwischen Python und C++ von der Standardinstallation nicht unterstützt. Um diese Funktion zu aktivieren, wenn Sie mit den Versionen 4.x und höher der Protobuf Python API arbeiten, definieren Sie die Umgebungsvariable PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp und stellen Sie sicher, dass die Python/C++-Erweiterung installiert ist.