- Appel du compilateur
- Colis
- Messages
- Types connus
- Fields
- Énumérations
- Une
- Extensions
- Services
- Points d'insertion de plug-in
- Partager des messages entre Python et C++
Cette page décrit précisément les définitions de Python générées par le compilateur de tampon de protocole pour une définition de protocole donnée. Toute différence entre le code généré proto2 et le code généré proto3 est mise en évidence. Notez que ces différences résident dans le code généré tel que décrit dans ce document et non dans les classes/interfaces de messages de base, qui sont identiques dans les deux versions. Lisez le guide du langage proto2 et/ou le guide du langage proto3 avant de lire ce document.
L'implémentation de Protocol Buffers Python est légèrement différente de C++ et Java. En Python, le compilateur ne génère que le code permettant de créer des descripteurs pour les classes générées. Une métaclasse Python fait le travail. Ce document décrit ce que vous obtenez après l'application de la métaclasse.
Appel du compilateur
Le compilateur de tampon de protocole génère une sortie Python lorsqu'elle est appelée avec l'option de ligne de commande --python_out=
. Le paramètre de l'option --python_out=
correspond au répertoire dans lequel vous souhaitez que le compilateur écrive votre sortie Python. Le compilateur crée un fichier .py
pour chaque entrée de fichier .proto
. Les noms des fichiers de sortie sont calculés en prenant le nom du fichier .proto
et en apportant deux modifications:
- L'extension (
.proto
) est remplacée par_pb2.py
. - Le chemin d'accès proto (spécifié avec l'option de ligne de commande
--proto_path=
ou-I
) est remplacé par le chemin de sortie (spécifié avec l'option--python_out=
).
Par exemple, imaginons que vous invoquez le compilateur comme suit :
protoc --proto_path=src --python_out=build/gen src/foo.proto src/bar/baz.proto
Le compilateur lit les fichiers src/foo.proto
et src/bar/baz.proto
, puis produit deux fichiers de sortie : build/gen/foo_pb2.py
et build/gen/bar/baz_pb2.py
. Le compilateur crée automatiquement le répertoire build/gen/bar
si nécessaire, mais il ne crée pas build
ni build/gen
. Ils doivent déjà exister.
Notez que si le fichier .proto
ou son chemin contient des caractères qui ne peuvent pas être utilisés dans les noms de modules Python (comme des traits d'union), ils sont remplacés par des traits de soulignement. Ainsi, le fichier foo-bar.proto
devient le fichier Python foo_bar_pb2.py
.
Lorsque vous générez du code Python, la capacité du compilateur de tampon de protocole à générer directement des archives ZIP est particulièrement pratique, car l'interpréteur Python est capable de lire directement à partir de ces archives si elles sont placées dans PYTHONPATH
. Pour générer la sortie dans un fichier ZIP, il vous suffit de fournir un emplacement de sortie se terminant par .zip
.
Le chiffre 2 dans l'extension _pb2.py
désigne la version 2 des Protocol Buffers. La version 1 a été principalement utilisée dans Google, mais vous pouvez peut-être trouver certaines parties de ce code dans d'autres codes Python publiés avant Protocol Buffers. Étant donné que la version 2 de Protocoles Buffers Python a une interface complètement différente et que Python ne dispose pas de vérification du type au moment de la compilation pour détecter les erreurs, nous avons choisi de faire du numéro de version un composant proéminent des noms de fichiers Python générés. Actuellement, les protocoles générés par proto2 et proto3 utilisent _pb2.py
.
Colis
Le code Python généré par le compilateur de tampon de protocole n'est pas affecté par le nom du package défini dans le fichier .proto
. À la place, les packages Python sont identifiés par une structure de répertoires.
Messages
Voici une déclaration de message simple :
message Foo {}
Le compilateur de tampon de protocole génère une classe appelée Foo
, qui sous-classe google.protobuf.Message
. La classe est une classe concrète. Aucune méthode abstraite n'est laissée en œuvre. Contrairement à C++ et Java, le code généré par Python n'est pas affecté par l'option optimize_for
dans le fichier .proto
. En fait, tout le code Python est optimisé pour la taille du code.
Si le nom du message est un mot clé Python, sa classe ne sera accessible que via getattr()
, comme décrit dans la section Noms en conflit avec des mots clés Python.
Vous ne devez pas créer vos propres sous-classes Foo
. Les classes générées ne sont pas conçues pour le sous-classement et peuvent entraîner des problèmes de "classe fragile". De plus, l'héritage d'implémentation est de mauvaise qualité.
Les classes de message Python n'ont pas de membres publics autres que ceux définis par l'interface Message
et générés pour les champs imbriqués, les messages et les types d'énumération (décrits ci-dessous). Message
fournit des méthodes que vous pouvez utiliser pour vérifier, manipuler, lire ou écrire l'intégralité du message, y compris l'analyse et la sérialisation dans des chaînes binaires. En plus de ces méthodes, la classe Foo
définit les méthodes statiques suivantes:
FromString(s)
: renvoie une nouvelle instance de message désérialisée à partir de la chaîne donnée.
Notez que vous pouvez également utiliser le module text_format
pour traiter des messages de protocole au format texte. Par exemple, la méthode Merge()
vous permet de fusionner la représentation ASCII d'un message dans un message existant.
Types imbriqués
Un message peut être déclaré dans un autre message. Exemple :
message Foo {
message Bar {
}
}
Dans ce cas, la classe Bar
est déclarée en tant que membre statique de Foo
. Vous pouvez donc l'appeler Foo.Bar
.
Types bien connus
Protocol Buffers fournit un certain nombre de types connus que vous pouvez utiliser dans vos fichiers .proto avec vos propres types de messages. Certains messages WKT utilisent des méthodes spéciales en plus des méthodes habituelles pour les messages Protocol Buffer, car ils sous-classe google.protobuf.Message
et une classe WKT.
Tout
Pour tous les messages, vous pouvez appeler Pack()
pour empaqueter un message spécifié dans le message "Tous" actuel ou Unpack()
pour décompresser le message "Any" actuel dans un message donné. Exemple :
any_message.Pack(message) any_message.Unpack(message)
Unpack()
compare également le descripteur de l'objet message transmis avec celui stocké et renvoie False
s'il ne correspond pas et ne tente pas de décompresser ; True
dans le cas contraire.
Vous pouvez également appeler la méthode Is()
pour vérifier si le message "Any" représente le type de tampon de protocole donné. Exemple :
assert any_message.Is(message.DESCRIPTOR)
Code temporel
Les messages d'horodatage peuvent être convertis au format de chaîne de date RFC 3339 (chaîne JSON) à l'aide des méthodes ToJsonString()
/FromJsonString()
. Par exemple,
timestamp_message.FromJsonString("1970-01-01T00:00:00Z") assert timestamp_message.ToJsonString() == "1970-01-01T00:00:00Z".
Vous pouvez également appeler GetCurrentTime()
pour remplir le message Timestamp avec l'heure actuelle :
timestamp_message.GetCurrentTime()
Pour convertir d'autres unités de temps depuis l'époque, vous pouvez appeler ToNanoseconds(), FromNanoseconds(), ToMicroseconds(), FromMicroseconds() ,ToMilliseconds(), FromMilliseconds(), ToSeconds()
ou FromSeconds()
. Le code généré comporte également les méthodes ToDatetime()
et FromDatetime()
pour convertir les objets datetime Python et les horodatages. Exemple :
timestamp_message.FromMicroseconds(-1) assert timestamp_message.ToMicroseconds() == -1 dt = datetime(2016, 1, 1) timestamp_message.FromDatetime(dt) self.assertEqual(dt, timestamp_message.ToDatetime())
Durée
Les messages de durée utilisent les mêmes méthodes que l'horodatage pour la conversion entre une chaîne JSON et d'autres unités de temps. Pour convertir une valeur en temps et en durée, vous pouvez appeler ToTimedelta()
ou FromTimedelta
. Exemple :
duration_message.FromNanoseconds(1999999999) td = duration_message.ToTimedelta() assert td.seconds == 1 assert td.microseconds == 999999
FieldMask
Les messages FieldMask peuvent être convertis en chaînes JSON à l'aide des méthodes ToJsonString()
/FromJsonString()
. De plus, un message FieldMask comprend les méthodes suivantes:
IsValidForDescriptor
: vérifie si le FieldMask est valide pour Message Descriptor.AllFieldsFromDescriptor
: récupère tous les champs directs du descripteur de message vers FieldMask.CanonicalFormFromMask
: convertit un FieldMask au format canonique.Union
: fusionne deux FieldMasks dans ce FieldMask.Intersect
: recoupe deux FieldMasks dans ce FieldMask.MergeMessage
: fusionne les champs spécifiés dans FieldMask de la source vers la destination.
Struct
Les messages structurés vous permettent d'obtenir et de définir les éléments directement. Par exemple :
struct_message["key1"] = 5 struct_message["key2"] = "abc" struct_message["key3"] = True
Pour obtenir ou créer une liste ou une structure, vous pouvez appeler get_or_create_list()
/get_or_create_struct()
. Exemple :
struct.get_or_create_struct("key4")["subkey"] = 11.0 struct.get_or_create_list("key5")
ListValue
Un message ListValue agit comme une séquence Python et vous permet d'effectuer les opérations suivantes :
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
Pour ajouter une valeur ListValue/Struct, appelez add_list()
/add_struct()
. Exemple :
list_value.add_struct()["key"] = 1 list_value.add_list().extend([1, "two", True])
Champs
Pour chaque champ d'un type de message, la classe correspondante possède une propriété portant le même nom que le champ. La façon dont vous pouvez manipuler la propriété dépend de son type.
En plus d'une propriété, le compilateur génère une constante entière pour chaque champ contenant son numéro de champ. Le nom constant correspond au nom du champ converti en majuscules suivi de _FIELD_NUMBER
. Par exemple, pour le champ optional int32 foo_bar = 5;
, le compilateur générera la constante FOO_BAR_FIELD_NUMBER = 5
.
Si le nom du champ est un mot clé Python, sa propriété ne sera accessible que via getattr()
et setattr()
, comme décrit dans la section Noms en conflit avec des mots clés Python.
Champs singuliers (proto2)
Si vous disposez d'un champ unique (facultatif ou obligatoire) foo
de tout type autre qu'un message, vous pouvez manipuler le champ foo
comme s'il s'agissait d'un champ standard. Par exemple, si le type de foo
est int32
, vous pouvez dire :
message.foo = 123 print(message.foo)
Notez que la définition de foo
sur une valeur d'un type incorrect génère une TypeError
.
Si foo
est lu alors qu'il n'est pas défini, sa valeur est la valeur par défaut de ce champ. Pour vérifier si foo
est défini ou pour effacer la valeur de foo
, vous devez appeler les méthodes HasField()
ou ClearField()
de l'interface Message
. Exemple :
assert not message.HasField("foo") message.foo = 123 assert message.HasField("foo") message.ClearField("foo") assert not message.HasField("foo")
Champs singuliers (proto3)
Si vous possédez un champ unique foo
de tout type autre qu'un message, vous pouvez manipuler le champ foo
comme s'il s'agissait d'un champ standard. Par exemple, si le type de foo
est int32
, vous pouvez dire :
message.foo = 123 print(message.foo)
Notez que la définition de foo
sur une valeur d'un type incorrect génère une TypeError
.
Si foo
est lu alors qu'il n'est pas défini, sa valeur est la valeur par défaut de ce champ. Pour effacer la valeur de foo
et rétablir sa valeur par défaut pour son type, appelez la méthode ClearField()
de l'interface Message
. Exemple :
message.foo = 123 message.ClearField("foo")
Contrairement à proto2, vous ne pouvez pas appeler HasField()
pour un champ unique sans message dans proto3 et la bibliothèque renverra une exception si vous essayez de le faire.
Champs de message singuliers
Les types de messages fonctionnent légèrement différemment. Vous ne pouvez pas attribuer de valeur à un champ de message intégré. Au lieu de cela, attribuer une valeur à un champ du message enfant implique de définir le champ du message dans le parent. Dans proto3, vous pouvez également utiliser la méthode HasField()
du message parent pour vérifier si une valeur de champ de type de message a été définie, ce que vous ne pouvez pas faire avec d'autres types de champ singulier de proto3.
Par exemple, supposons que vous ayez la définition .proto
suivante :
message Foo { optional Bar bar = 1; } message Bar { optional int32 i = 1; }
Vous ne pouvez pas effectuer les opérations suivantes :
foo = Foo() foo.bar = Bar() # WRONG!
Pour définir bar
, il vous suffit d'attribuer une valeur directement à un champ de bar
. - foo
comporte un champ bar
:
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
De même, vous pouvez définir bar
à l'aide de la méthode CopyFrom()
de l'interface Message
. Cette opération permet de copier toutes les valeurs d'un autre message du même type que bar
.
foo.bar.CopyFrom(baz)
Notez que lire un champ dans bar
ne le définit pas :
foo = Foo() assert not foo.HasField("bar") print(foo.bar.i) # Print i's default value assert not foo.HasField("bar")
Si vous avez besoin de l'extrait "comporte" sur un message qui ne comporte aucun champ que vous pouvez définir ou souhaitez définir, vous pouvez utiliser la méthode SetInParent()
.
foo = Foo() assert not foo.HasField("bar") foo.bar.SetInParent() # Set Foo.bar to a default Bar message assert foo.HasField("bar")
Champs répétés
Les champs répétés sont représentés sous la forme d'un objet qui agit comme une séquence Python. Comme pour les messages intégrés, vous ne pouvez pas attribuer le champ directement, mais vous pouvez le manipuler. Par exemple, avec la définition de message suivante :
message Foo { repeated int32 nums = 1; }
Vous pouvez effectuer les opérations suivantes :
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)
La méthode ClearField()
de l'interface Message
fonctionne en plus de l'del
Python.
Champs de message répétés
Les champs de message répétés fonctionnent de la même manière que les champs scalaires répétés. Toutefois, l'objet Python correspondant comporte également une méthode add()
qui crée un objet de message, l'ajoute à la liste et le renvoie à l'appelant pour le renseigner. De plus, la méthode append()
de l'objet crée une copie du message donné et l'ajoute à la liste. Ainsi, les messages appartiennent toujours au message parent afin d'éviter les références circulaires et toute autre confusion pouvant se produire lorsqu'une structure de données modifiable a plusieurs propriétaires. De même, la méthode extend()
de l'objet ajoute une liste complète de messages, mais crée une copie de chaque message de la liste.
Par exemple, avec la définition de message suivante :
message Foo { repeated Bar bars = 1; } message Bar { optional int32 i = 1; optional int32 j = 2; }
Vous pouvez effectuer les opérations suivantes :
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
Contrairement aux champs scalaires répétés, les champs de message répétés ne sont pas compatibles avec l'attribution d'éléments (par exemple, __setitem__
). Par exemple:
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
Groupes (proto2)
Notez que les groupes sont obsolètes et ne doivent pas être utilisés lors de la création de types de messages. Utilisez plutôt des types de messages imbriqués.
Un groupe combine un type de message imbriqué et un champ dans une même déclaration, et utilise un format de fil différent pour le message. Le message généré porte le même nom que le groupe. Le nom du champ généré est le nom en minuscules du groupe.
Par exemple, à l'exception du format de communication par filaire, les deux définitions de message suivantes sont équivalentes :
// Version 1: Using groups message SearchResponse { repeated group SearchResult = 1 { required string url = 2; } } // Version 2: Not using groups message SearchResponse { message SearchResult { required string url = 2; } repeated SearchResult searchresult = 1; }
Un groupe peut être required
, optional
ou repeated
.
Un groupe obligatoire ou facultatif est manipulé à l'aide de la même API qu'un champ de message au singulier standard.
Un groupe répété est manipulé à l'aide de la même API qu'un champ de message répété standard.
Par exemple, avec la définition SearchResponse
ci-dessus, vous pouvez effectuer les opérations suivantes :
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")
Champs de carte
Selon la définition de ce message :
message MyMessage { map<int32, int32> mapfield = 1; }
L'API Python générée pour le champ de mappage est semblable à un dict
Python :
# 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]
Comme pour les champs de messages intégrés, les messages ne peuvent pas être directement attribués à une valeur de mappage. Pour ajouter un message en tant que valeur de mappage, vous devez référencer une clé non définie, qui construit et renvoie un nouveau sous-message :
m.message_map[key].submessage_field = 10
Pour en savoir plus sur les clés non définies, consultez la section suivante.
Référencer des clés non définies
La sémantique des mappages de Protocol Buffer se comporte légèrement différemment des dict
Python en ce qui concerne les clés non définies. Dans un dict
Python standard, le référencement d'une clé non définie génère une exception KeyError :
>>> x = {} >>> x[5] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 5
Toutefois, dans les mappages de Protocol Buffers, le référencement d'une clé non définie crée la clé dans le mappage avec une valeur nulle/faux/vide. Ce comportement est semblable à celui de la bibliothèque Python standard defaultdict
.
>>> dict(m.mapfield) {} >>> m.mapfield[5] 0 >>> dict(m.mapfield) {5: 0}
Ce comportement est particulièrement pratique pour les mappages avec des valeurs de type de message, car vous pouvez directement mettre à jour les champs du message renvoyé.
>>> m.message_map[5].foo = 3
Notez que même si vous n'attribuez aucune valeur aux champs du message, le sous-message est toujours créé dans le mappage :
>>> m.message_map[10] <test_pb2.M2 object at 0x7fb022af28c0> >>> dict(m.message_map) {10: <test_pb2.M2 object at 0x7fb022af28c0>}
Elle est différente des champs de message intégrés standards, dans lesquels le message n'est créé qu'une fois que vous avez attribué une valeur à l'un de ses champs.
Il n'est pas forcément évident de lire immédiatement le code que m.message_map[10]
peut, par exemple, créer un sous-message. C'est pourquoi nous fournissons également une méthode get_or_create()
qui fait la même chose, mais dont le nom rend la création de message plus explicite :
# 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)
Énumérations
En Python, les énumérations ne sont que des entiers. Un ensemble de constantes d'intégration correspond aux valeurs définies de l'énumération. Exemple :
message Foo { enum SomeEnum { VALUE_A = 0; VALUE_B = 5; VALUE_C = 1234; } optional SomeEnum bar = 1; }
Les constantes VALUE_A
, VALUE_B
et VALUE_C
sont définies avec les valeurs 0, 5 et 1 234, respectivement. Vous pouvez accéder à SomeEnum
si vous le souhaitez. Si une énumération est définie dans le champ d'application externe, les valeurs sont des constantes de module. Si elles sont définies dans un message (comme ci-dessus), elles deviennent des membres statiques de cette classe de message.
Par exemple, dans un proto, vous pouvez accéder aux valeurs des trois manières suivantes pour l'énumération suivante:
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')
Un champ d'énumération fonctionne comme un champ scalaire.
foo = Foo() foo.bar = Foo.VALUE_A assert foo.bar == 0 assert foo.bar == Foo.VALUE_A
Si le nom de l'énumération (ou une valeur d'énumération) est un mot clé Python, son objet (ou la propriété de la valeur d'énumération) ne sera accessible que via getattr()
, comme décrit dans la section Noms en conflit avec des mots clés Python.
Les valeurs que vous pouvez définir dans une énumération dépendent de votre version de Protocol Buffers:
- Dans proto2, une énumération ne peut pas contenir de valeur numérique autre que celles définies pour le type d'énumération. Si vous attribuez une valeur qui ne figure pas dans l'énumération, le code généré génère une exception.
- proto3 utilise une sémantique d'énumération ouverte: les champs enum peuvent contenir n'importe quelle valeur
int32
.
Les énumérations ont plusieurs méthodes utilitaires pour obtenir les noms de champs à partir de valeurs et inversement, des listes de champs, etc. Elles sont définies dans enum_type_wrapper.EnumTypeWrapper
(la classe de base pour les classes d'énumération générées). Par exemple, si vous disposez de l'énumération autonome suivante dans myproto.proto
:
enum SomeEnum { VALUE_A = 0; VALUE_B = 5; VALUE_C = 1234; }
Vous pouvez procéder comme suit :
self.assertEqual('VALUE_A', myproto_pb2.SomeEnum.Name(myproto_pb2.VALUE_A)) self.assertEqual(5, myproto_pb2.SomeEnum.Value('VALUE_B'))Pour une énumération déclarée dans un message de protocole (par exemple, Foo), la syntaxe est similaire :
self.assertEqual('VALUE_A', myproto_pb2.Foo.SomeEnum.Name(myproto_pb2.Foo.VALUE_A)) self.assertEqual(5, myproto_pb2.Foo.SomeEnum.Value('VALUE_B'))Si plusieurs constantes d'énumération ont la même valeur (alias), la première constante définie est renvoyée.
enum SomeEnum { option allow_alias = true; VALUE_A = 0; VALUE_B = 5; VALUE_C = 1234; VALUE_B_ALIAS = 5; }Dans l'exemple ci-dessus,
myproto_pb2.SomeEnum.Name(5)
renvoie "VALUE_B"
.
Un
Message reçu avec l'un des éléments suivants :
message Foo { oneof test_oneof { string name = 1; int32 serial_number = 2; } }
La classe Python correspondant à Foo
aura des propriétés appelées name
et serial_number
, tout comme les champs standards. Toutefois, contrairement aux champs standards, un seul des champs d'un champ peut être défini à la fois, ce qui est assuré par l'environnement d'exécution. Par exemple,
message = Foo() message.name = "Bender" assert message.HasField("name") message.serial_number = 2716057 assert message.HasField("serial_number") assert not message.HasField("name"):
La classe de message dispose également d'une méthode WhichOneof
qui vous permet de savoir quel champ (le cas échéant) a été défini. Cette méthode renvoie le nom du champ défini, ou None
si rien n'a été défini :
assert message.WhichOneof("test_oneof") is None message.name = "Bender" assert message.WhichOneof("test_oneof") == "name"
HasField
et ClearField
acceptent également l'un des noms en plus des noms de champs :
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")
Notez que si vous appelez ClearField
sur l'un de ces champs, le champ actuellement défini est effacé.
Noms en conflit avec des mots clés Python
Si le nom d'un message, champ, énumération ou valeur d'énumération est un mot clé Python, le nom de la classe ou de la propriété correspondante sera le même, mais vous ne pourrez y accéder qu'à l'aide des fonctions intégrées getattr()
et setattr()
de Python, et non via la syntaxe de référence d'attribut normale de Python (l'opérateur point).
Par exemple, si vous avez la définition .proto
suivante :
message Baz { optional int32 from = 1 repeated int32 in = 2; }
Vous devez accéder à ces champs comme suit :
baz = Baz() setattr(baz, "from", 99) assert getattr(baz, "from") == 99 getattr(baz, "in").append(42) assert getattr(baz, "in") == [42]
En revanche, si vous utilisez la syntaxe obj.attr
pour accéder à ces champs, Python génère des erreurs de syntaxe lors de l'analyse de votre code :
# WRONG! baz.in # SyntaxError: invalid syntax baz.from # SyntaxError: invalid syntax
Extensions (proto2 uniquement)
Message reçu avec une plage d'extensions :
message Foo { extensions 100 to 199; }
La classe Python correspondant à Foo
comportera un membre appelé Extensions
, qui est un identifiant d'extension de dictionnaire mappant ses valeurs actuelles.
Selon une définition d'extension :
extend Foo { optional int32 bar = 123; }
Le compilateur de tampon de protocole génère un "identifiant d'extension" appelé bar
. L'identifiant agit comme une clé du dictionnaire Extensions
. La recherche d'une valeur dans ce dictionnaire est exactement la même que si vous accédiez à un champ normal du même type. Ainsi, avec l'exemple ci-dessus, vous pouvez faire :
foo = Foo() foo.Extensions[proto_file_pb2.bar] = 2 assert foo.Extensions[proto_file_pb2.bar] == 2
Notez que vous devez spécifier la constante de l'identifiant d'extension, et pas seulement un nom de chaîne: en effet, il est possible de spécifier plusieurs extensions portant le même nom dans des champs d'application différents.
Comme pour les champs normaux, Extensions[...]
renvoie un objet message pour les messages au singulier et une séquence pour les champs répétés.
Les méthodes HasField()
et ClearField()
de l'interface Message
ne fonctionnent pas avec les extensions. Vous devez utiliser HasExtension()
et ClearExtension()
à la place. Pour utiliser les méthodes HasExtension()
et ClearExtension()
, transmettez field_descriptor
à l'extension dont vous souhaitez vérifier l'existence.
Services
Si le fichier .proto
contient la ligne suivante:
option py_generic_services = true;
Le compilateur de tampon de protocole générera ensuite du code en fonction des définitions de service trouvées dans le fichier, comme décrit dans cette section. Cependant, le code généré peut ne pas être souhaitable, car il n'est lié à aucun système RPC particulier et nécessite donc plus de niveaux d'indirection que le code personnalisé pour un seul système. Si vous NE souhaitez PAS que ce code soit généré, ajoutez la ligne suivante au fichier:
option py_generic_services = false;
Si aucune de ces lignes n'est indiquée, l'option par défaut est false
, car les services génériques sont obsolètes.
Notez qu'avant la version 2.4.0, l'option est définie par défaut sur true
.
Les systèmes RPC basés sur les définitions de service en .proto
doivent fournir des plug-ins permettant de générer un code adapté au système. Ces plug-ins sont susceptibles d'exiger que les services abstraits soient désactivés pour pouvoir générer leurs propres classes du même nom. Les plug-ins sont nouveaux dans la version 2.3.0 (janvier 2010).
Le reste de cette section décrit ce que le compilateur de tampon de protocole génère lorsque les services abstraits sont activés.
Interface
Selon une définition de service :
service Foo { rpc Bar(FooRequest) returns(FooResponse); }
Le compilateur de tampon de protocole générera une classe Foo
pour représenter ce service. Foo
comporte une méthode pour chaque méthode définie dans la définition de service. Dans ce cas, la méthode Bar
est définie comme suit :
def Bar(self, rpc_controller, request, done)
Les paramètres sont équivalents aux paramètres de Service.CallMethod()
, sauf que l'argument method_descriptor
est implicite.
Les méthodes générées sont destinées à être remplacées par les sous-classes. Les implémentations par défaut appellent simplement controller.SetFailed()
avec un message d'erreur indiquant que la méthode n'est pas mise en œuvre, puis appelle le rappel done
. Lorsque vous implémentez votre propre service, vous devez sous-classer ce service généré et implémenter ses méthodes en conséquence.
Foo
sous-classe l'interface Service
. Le compilateur de tampon de protocole génère automatiquement des implémentations des méthodes de Service
comme suit:
GetDescriptor
: renvoie la valeurServiceDescriptor
du service.CallMethod
: détermine quelle méthode est appelée en fonction du descripteur de méthode fourni et l'appelle directement.GetRequestClass
etGetResponseClass
: affiche la classe de la requête ou de la réponse du type approprié pour la méthode donnée.
Stub
Le compilateur de tampon de protocole génère également une implémentation « bouchon » de chaque interface de service, qui est utilisée par les clients souhaitant envoyer des requêtes aux serveurs mettant en œuvre le service. Pour le service Foo
(ci-dessus), l'implémentation de bouchon Foo_Stub
sera définie.
Foo_Stub
est une sous-classe de Foo
. Son constructeur utilise un RpcChannel
comme paramètre. La simulation implémente ensuite chacune des méthodes du service en appelant la méthode CallMethod()
du canal.
La bibliothèque de tampons de protocole n'inclut pas de mise en œuvre RPC. Toutefois, elle inclut tous les outils dont vous avez besoin pour relier une classe de service générée à l'implémentation RPC arbitraire de votre choix. Il vous suffit de fournir des implémentations de RpcChannel
et RpcController
.
Points d'insertion des plug-ins
Les plug-ins de génération de code qui souhaitent étendre la sortie du générateur de code Python peuvent insérer du code des types suivants en utilisant les noms de point d'insertion donnés.
imports
: importer des instructions.module_scope
: déclarations de niveau supérieur.
Ne générez pas de code reposant sur des membres de classe privée déclarés par le générateur de code standard, car ces détails d'implémentation peuvent changer dans les futures versions de Protocol Buffers.
Partager des messages entre Python et C++
Avant la version 4.21.0 de l'API Python Protobuf, les applications Python pouvaient partager des messages avec C++ à l'aide d'une extension native. À partir de la version 4.21.0 de l'API, l'installation par défaut n'est pas compatible avec le partage de messages entre Python et C++. Pour activer cette fonctionnalité lorsque vous utilisez les versions 4.x et ultérieures de l'API Python Protobuf, définissez la variable d'environnement PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=cpp
et assurez-vous que l'extension Python/C++ est installée.