Grundlagen von Protocol Buffers: Python
Dieses Tutorial bietet eine grundlegende Einführung für Python-Programmierer in die Arbeit mit Protocol Buffers. Anhand eines einfachen Beispielanwendung zeigt es Ihnen, wie Sie
- Nachrichtenformate in einer
.proto-Datei definieren. - Den Protocol Buffer-Compiler verwenden.
- die Python-Protocol-Buffer-API zum Schreiben und Lesen von Nachrichten verwenden.
Dies ist kein umfassender Leitfaden zur Verwendung von Protocol Buffers in Python. Ausführlichere Referenzinformationen finden Sie im Protocol Buffer Language Guide (proto2), im Protocol Buffer Language Guide (proto3), im Python API-Referenz, im Leitfaden für generierten Python-Code und im Encoding-Referenz.
Das Problemfeld
Das Beispiel, das wir verwenden werden, ist eine sehr einfache "Adressbuch"-Anwendung, die Kontaktdaten von Personen lesen und in eine Datei schreiben kann. Jede Person im Adressbuch hat einen Namen, eine ID, eine E-Mail-Adresse und eine Telefonnummer.
Wie serialisiert und ruft man strukturierte Daten wie diese ab? Es gibt mehrere Möglichkeiten, dieses Problem zu lösen
- Python-Pickling verwenden. Dies ist der Standardansatz, da er in die Sprache integriert ist, aber er kommt nicht gut mit Schemaevolution zurecht und funktioniert auch nicht gut, wenn Sie Daten mit Anwendungen teilen müssen, die in C++ oder Java geschrieben sind.
- Man kann sich einen Ad-hoc-Weg ausdenken, um die Datenpunkte in einen einzigen String zu kodieren – z. B. 4 ganze Zahlen als „12:3:-23:67“ kodieren. Dies ist ein einfacher und flexibler Ansatz, obwohl er das Schreiben von einmaligen Kodierungs- und Parsing-Codes erfordert und das Parsen einen geringen Laufzeitaufwand verursacht. Dies funktioniert am besten für die Kodierung sehr einfacher Daten.
- Serialisieren Sie die Daten in XML. Dieser Ansatz kann sehr attraktiv sein, da XML (sozusagen) menschenlesbar ist und es Bindungsbibliotheken für viele Sprachen gibt. Dies kann eine gute Wahl sein, wenn Sie Daten mit anderen Anwendungen/Projekten teilen möchten. XML ist jedoch bekanntermaßen speicherintensiv, und die Kodierung/Dekodierung kann Anwendungen eine enorme Leistungseinbuße bescheren. Außerdem ist die Navigation durch einen XML-DOM-Baum erheblich komplizierter als die normale Navigation durch einfache Felder in einer Klasse.
Anstelle dieser Optionen können Sie Protocol Buffers verwenden. Protocol Buffers sind die flexible, effiziente und automatisierte Lösung für genau dieses Problem. Mit Protocol Buffers schreiben Sie eine .proto-Beschreibung der Datenstruktur, die Sie speichern möchten. Daraus erstellt der Protocol Buffer-Compiler eine Klasse, die eine automatische Kodierung und ein Parsing der Protocol Buffer-Daten mit einem effizienten Binärformat implementiert. Die generierte Klasse bietet Getter und Setter für die Felder, aus denen ein Protocol Buffer besteht, und kümmert sich um die Details des Lesens und Schreibens des Protocol Buffers als Einheit. Wichtig ist, dass das Protocol Buffer-Format die Idee unterstützt, das Format im Laufe der Zeit so zu erweitern, dass der Code Daten, die mit dem alten Format kodiert wurden, weiterhin lesen kann.
Wo man den Beispielcode findet
Der Beispielcode ist im Quellcode-Paket im Verzeichnis „examples“ enthalten. Laden Sie ihn hier herunter.
Definieren Ihres Protokollformats
Um Ihre Adressbuchanwendung zu erstellen, müssen Sie mit einer .proto-Datei beginnen. Die Definitionen in einer .proto-Datei sind einfach: Sie fügen eine Nachricht für jede Datenstruktur hinzu, die Sie serialisieren möchten, und geben dann einen Namen und einen Typ für jedes Feld in der Nachricht an. Hier ist die .proto-Datei, die Ihre Nachrichten definiert, addressbook.proto.
edition = "2023";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2 [default = PHONE_TYPE_HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
Wie Sie sehen, ist die Syntax ähnlich wie bei C++ oder Java. Lassen Sie uns jeden Teil der Datei durchgehen und sehen, was er tut.
Die Datei .proto beginnt mit einer Paketdeklaration, die dazu beiträgt, Namenskonflikte zwischen verschiedenen Projekten zu vermeiden. In Python werden Pakete normalerweise durch die Verzeichnisstruktur bestimmt, sodass das von Ihnen in Ihrer .proto-Datei definierte package keine Auswirkung auf den generierten Code hat. Sie sollten jedoch dennoch eines deklarieren, um Namenskollisionen im Namensraum von Protocol Buffers sowie in nicht-Python-Sprachen zu vermeiden.
Als nächstes haben Sie Ihre Nachrichtendefinitionen. Eine Nachricht ist einfach eine Aggregation, die eine Reihe von typisierten Feldern enthält. Viele standardmäßige einfache Datentypen sind als Feldtypen verfügbar, einschließlich bool, int32, float, double und string. Sie können Ihren Nachrichten auch weitere Struktur verleihen, indem Sie andere Nachrichtentypen als Feldtypen verwenden – im obigen Beispiel enthält die Nachricht Person PhoneNumber-Nachrichten, während die Nachricht AddressBook Person-Nachrichten enthält. Sie können sogar Nachrichtentypen definieren, die innerhalb anderer Nachrichten verschachtelt sind – wie Sie sehen können, ist der Typ PhoneNumber innerhalb von Person definiert. Sie können auch enum-Typen definieren, wenn Sie möchten, dass eines Ihrer Felder einen vordefinierten Wert aus einer Liste hat – hier möchten Sie angeben, dass eine Telefonnummer einer der folgenden Telefonarten sein kann: PHONE_TYPE_MOBILE, PHONE_TYPE_HOME oder PHONE_TYPE_WORK.
Die Markierungen " = 1", " = 2" bei jedem Element identifizieren den eindeutigen "Tag", den dieses Feld in der Binärkodierung verwendet. Tag-Nummern von 1-15 erfordern ein Byte weniger zur Kodierung als höhere Nummern. Daher können Sie diese Tags als Optimierung für häufig verwendete oder wiederholte Elemente verwenden und die Tags 16 und höher für weniger häufig verwendete optionale Elemente übrig lassen. Jedes Element in einem wiederholten Feld erfordert eine erneute Kodierung der Tag-Nummer. Wiederholte Felder sind daher besonders gute Kandidaten für diese Optimierung.
Eine vollständige Anleitung zum Schreiben von .proto-Dateien – einschließlich aller möglichen Feldtypen – finden Sie im Protocol Buffer Language Guide. Suchen Sie jedoch nicht nach Einrichtungen, die der Klassenerbschaft ähneln – Protocol Buffers können das nicht.
Kompilieren Ihrer Protocol Buffers
Nachdem Sie nun eine .proto-Datei haben, müssen Sie als Nächstes die Klassen generieren, die Sie zum Lesen und Schreiben von AddressBook (und damit Person und PhoneNumber) Nachrichten benötigen. Dazu müssen Sie den Protocol Buffer-Compiler protoc auf Ihrer .proto-Datei ausführen.
Wenn Sie den Compiler noch nicht installiert haben, laden Sie das Paket herunter und befolgen Sie die Anweisungen in der README-Datei.
Führen Sie nun den Compiler aus und geben Sie das Quellverzeichnis an (wo sich der Quellcode Ihrer Anwendung befindet – das aktuelle Verzeichnis wird verwendet, wenn Sie keinen Wert angeben), das Zielverzeichnis (wo der generierte Code hingehen soll; oft dasselbe wie
$SRC_DIR) und den Pfad zu Ihrer.proto-Datei. In diesem Fall werden Sie…protoc --proto_path=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.protoDa Sie Python-Klassen wünschen, verwenden Sie die Option
--python_out– ähnliche Optionen sind für andere unterstützte Sprachen verfügbar.Protoc kann mit
--pyi_outauch Python-Stubs (.pyi) generieren.
Dies generiert addressbook_pb2.py (oder addressbook_pb2.pyi) in Ihrem angegebenen Zielverzeichnis.
Die Protocol Buffer API
Im Gegensatz zur Generierung von Java- und C++-Protocol-Buffer-Code generiert der Python-Protocol-Buffer-Compiler Ihren Datenzugriffscode nicht direkt für Sie. Stattdessen (wie Sie sehen werden, wenn Sie sich addressbook_pb2.py ansehen) generiert er spezielle Deskriptoren für alle Ihre Nachrichten, Aufzählungen und Felder sowie einige mysteriös leere Klassen, eine für jeden Nachrichtentyp.
import google3
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.GOOGLE_INTERNAL,
0,
20240502,
0,
'',
'main.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nmain.proto\x12\x08tutorial\"\xa3\x02\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12,\n\x06phones\x18\x04 \x03(\x0b\x32\x1c.tutorial.Person.PhoneNumber\x1aX\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12\x39\n\x04type\x18\x02 \x01(\x0e\x32\x1a.tutorial.Person.PhoneType:\x0fPHONE_TYPE_HOME\"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03\"/\n\x0b\x41\x64\x64ressBook\x12 \n\x06people\x18\x01 \x03(\x0b\x32\x10.tutorial.Person')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google3.main_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_PERSON']._serialized_start=25
_globals['_PERSON']._serialized_end=316
_globals['_PERSON_PHONENUMBER']._serialized_start=122
_globals['_PERSON_PHONENUMBER']._serialized_end=210
_globals['_PERSON_PHONETYPE']._serialized_start=212
_globals['_PERSON_PHONETYPE']._serialized_end=316
_globals['_ADDRESSBOOK']._serialized_start=318
_globals['_ADDRESSBOOK']._serialized_end=365
# @@protoc_insertion_point(module_scope)
Die wichtige Zeile in jeder Klasse ist __metaclass__ = reflection.GeneratedProtocolMessageType. Während die Details, wie Python-Metaklassen funktionieren, den Rahmen dieses Tutorials sprengen, können Sie sie sich wie eine Vorlage zum Erstellen von Klassen vorstellen. Zur Ladezeit verwendet die Metaklasse GeneratedProtocolMessageType die angegebenen Deskriptoren, um alle Python-Methoden zu erstellen, die Sie zum Arbeiten mit jedem Nachrichtentyp benötigen, und fügt sie den entsprechenden Klassen hinzu. Sie können dann die vollständig ausgefüllten Klassen in Ihrem Code verwenden.
Das Endergebnis all dessen ist, dass Sie die Klasse Person so verwenden können, als ob sie jedes Feld der Basisklasse Message als reguläres Feld definiert hätte. Sie könnten zum Beispiel schreiben:
import addressbook_pb2
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phones.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.Person.PHONE_TYPE_HOME
Beachten Sie, dass diese Zuweisungen nicht einfach beliebige neue Felder zu einem generischen Python-Objekt hinzufügen. Wenn Sie versuchen würden, ein Feld zuzuweisen, das nicht in der .proto-Datei definiert ist, würde ein AttributeError ausgelöst. Wenn Sie einem Feld einen Wert des falschen Typs zuweisen, wird ein TypeError ausgelöst. Das Lesen des Werts eines Feldes, bevor es gesetzt wurde, gibt den Standardwert zurück.
person.no_such_field = 1 # raises AttributeError
person.id = "1234" # raises TypeError
Weitere Informationen darüber, welche Mitglieder der Protokollcompiler genau für eine bestimmte Felddefinition generiert, finden Sie in der Referenz für generierten Python-Code.
Aufzählungen
Aufzählungen werden von der Metaklasse in eine Reihe symbolischer Konstanten mit ganzzahligen Werten erweitert. So hat beispielsweise die Konstante addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK den Wert 2.
Standard-Nachrichtenmethoden
Jede Nachrichtenklasse enthält auch eine Reihe anderer Methoden, mit denen Sie die gesamte Nachricht überprüfen oder manipulieren können, darunter:
IsInitialized(): prüft, ob alle erforderlichen Felder gesetzt wurden.__str__(): gibt eine menschenlesbare Darstellung der Nachricht zurück, die besonders für die Fehlersuche nützlich ist. (Wird normalerweise alsstr(message)oderprint messageaufgerufen.)CopyFrom(other_msg): überschreibt die Nachricht mit den Werten der gegebenen Nachricht.Clear(): löscht alle Elemente zurück in den leeren Zustand.
Diese Methoden implementieren die Message-Schnittstelle. Weitere Informationen finden Sie in der vollständigen API-Dokumentation für Message.
Parsen und Serialisieren
Schließlich verfügt jede Protocol Buffer-Klasse über Methoden zum Schreiben und Lesen von Nachrichten Ihres gewählten Typs unter Verwendung des Binärformats von Protocol Buffers. Dazu gehören:
SerializeToString(): serialisiert die Nachricht und gibt sie als String zurück. Beachten Sie, dass die Bytes binär und nicht Text sind; wir verwenden nur denstr-Typ als praktischen Container.ParseFromString(data): parst eine Nachricht aus dem gegebenen String.
Dies sind nur einige der verfügbaren Optionen für das Parsen und Serialisieren. Sehen Sie auch hier in der Message API-Referenz nach einer vollständigen Liste.
Sie können Nachrichten auch einfach von und zu JSON serialisieren. Das Modul json_format bietet dafür Hilfsmittel.
MessageToJson(message): serialisiert die Nachricht in einen JSON-String.Parse(json_string, message): parst einen JSON-String in die gegebene Nachricht.
Zum Beispiel
from google.protobuf import json_format
import addressbook_pb2
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
# Serialize to JSON
json_string = json_format.MessageToJson(person)
# Parse from JSON
new_person = addressbook_pb2.Person()
json_format.Parse(json_string, new_person)
Wichtig
Protocol Buffers und objektorientiertes Design Protocol Buffer-Klassen sind im Grunde Datenhalter (wie Strukturen in C), die keine zusätzliche Funktionalität bieten; sie eignen sich nicht gut als erstklassige Objekte in einem Objektmodell. Wenn Sie einer generierten Klasse reichhaltigere Funktionalität hinzufügen möchten, ist es am besten, die generierte Protocol Buffer-Klasse in eine anwendungsspezifische Klasse zu verpacken. Das Verpacken von Protocol Buffers ist auch eine gute Idee, wenn Sie keinen Einfluss auf das Design der.proto-Datei haben (wenn Sie beispielsweise eine aus einem anderen Projekt wiederverwenden). In diesem Fall können Sie die Wrapper-Klasse verwenden, um eine Schnittstelle zu erstellen, die besser auf die einzigartige Umgebung Ihrer Anwendung zugeschnitten ist: Ausblenden einiger Daten und Methoden, Bereitstellen von Komfortfunktionen usw. Sie sollten niemals Verhalten zu den generierten Klassen hinzufügen, indem Sie von ihnen erben. Dies würde interne Mechanismen brechen und ist ohnehin keine gute objektorientierte Praxis.Eine Nachricht schreiben
Lassen Sie uns nun versuchen, Ihre Protocol Buffer-Klassen zu verwenden. Das Erste, was Ihre Adressbuchanwendung tun können sollte, ist das Schreiben von persönlichen Details in Ihre Adressbuchdatei. Dazu müssen Sie Instanzen Ihrer Protocol Buffer-Klassen erstellen und befüllen und sie dann in einen Ausgabestrom schreiben.
Hier ist ein Programm, das ein AddressBook aus einer Datei liest, basierend auf Benutzereingaben eine neue Person hinzufügt und das neue AddressBook wieder in die Datei schreibt. Die Teile, die direkt Code aufrufen oder darauf verweisen, der vom Protocol Compiler generiert wurde, sind hervorgehoben.
#!/usr/bin/env python3
import addressbook_pb2
import sys
# This function fills in a Person message based on user input.
def PromptForAddress(person):
person.id = int(input("Enter person ID number: "))
person.name = input("Enter name: ")
email = input("Enter email address (blank for none): ")
if email != "":
person.email = email
while True:
number = input("Enter a phone number (or leave blank to finish): ")
if number == "":
break
phone_number = person.phones.add()
phone_number.number = number
phone_type = input("Is this a mobile, home, or work phone? ")
if phone_type == "mobile":
phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_MOBILE
elif phone_type == "home":
phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME
elif phone_type == "work":
phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK
else:
print("Unknown phone type; leaving as default value.")
# Main procedure: Reads the entire address book from a file,
# adds one person based on user input, then writes it back out to the same
# file.
if len(sys.argv) != 2:
print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Read the existing address book.
try:
with open(sys.argv[1], "rb") as f:
address_book.ParseFromString(f.read())
except IOError:
print(sys.argv[1] + ": Could not open file. Creating a new one.")
# Add an address.
PromptForAddress(address_book.people.add())
# Write the new address book back to disk.
with open(sys.argv[1], "wb") as f:
f.write(address_book.SerializeToString())
Eine Nachricht lesen
Natürlich wäre ein Adressbuch nicht sehr nützlich, wenn man keine Informationen daraus abrufen könnte! Dieses Beispiel liest die Datei, die vom obigen Beispiel erstellt wurde, und gibt alle darin enthaltenen Informationen aus.
#!/usr/bin/env python3
import addressbook_pb2
import sys
# Iterates though all people in the AddressBook and prints info about them.
def ListPeople(address_book):
for person in address_book.people:
print("Person ID:", person.id)
print(" Name:", person.name)
if person.HasField('email'):
print(" E-mail address:", person.email)
for phone_number in person.phones:
if phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_MOBILE:
print(" Mobile phone #: ", end="")
elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME:
print(" Home phone #: ", end="")
elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK:
print(" Work phone #: ", end="")
print(phone_number.number)
# Main procedure: Reads the entire address book from a file and prints all
# the information inside.
if len(sys.argv) != 2:
print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Read the existing address book.
with open(sys.argv[1], "rb") as f:
address_book.ParseFromString(f.read())
ListPeople(address_book)
Erweitern eines Protocol Buffers
Früher oder später, nachdem Sie den Code veröffentlicht haben, der Ihre Protocol Buffer verwendet, werden Sie zweifellos die Definition des Protocol Buffers „verbessern“ wollen. Wenn Sie möchten, dass Ihre neuen Buffer abwärtskompatibel sind und Ihre alten Buffer vorwärtskompatibel sind – und das wollen Sie fast sicherlich –, dann gibt es einige Regeln, die Sie befolgen müssen. In der neuen Version des Protocol Buffers
- Sie dürfen die Tag-Nummern vorhandener Felder nicht ändern.
- Sie dürfen keine erforderlichen Felder hinzufügen oder löschen.
- Sie dürfen optionale oder wiederholte Felder löschen.
- Sie dürfen neue optionale oder wiederholte Felder hinzufügen, müssen aber frische Tag-Nummern verwenden (d. h. Tag-Nummern, die noch nie in diesem Protocol Buffer verwendet wurden, nicht einmal von gelöschten Feldern).
(Es gibt einige Ausnahmen von diesen Regeln, aber sie werden selten verwendet.)
Wenn Sie diese Regeln befolgen, wird alter Code neue Nachrichten problemlos lesen und neue Felder einfach ignorieren. Für den alten Code haben gelöschte optionale Felder einfach ihren Standardwert, und gelöschte wiederholte Felder sind leer. Neuer Code wird auch alte Nachrichten transparent lesen. Beachten Sie jedoch, dass neue optionale Felder in alten Nachrichten nicht vorhanden sind. Sie müssen also entweder explizit prüfen, ob sie mit HasField('field_name') gesetzt sind, oder einen angemessenen Standardwert in Ihrer .proto-Datei mit [default = value] nach der Tag-Nummer angeben. Wenn für ein optionales Element kein Standardwert angegeben ist, wird stattdessen ein typspezifischer Standardwert verwendet: für Strings ist der Standardwert der leere String. Für Booleans ist der Standardwert false. Für numerische Typen ist der Standardwert null. Beachten Sie auch, dass Ihr neuer Code, wenn Sie ein neues wiederholtes Feld hinzugefügt haben, nicht erkennen kann, ob es leer gelassen wurde (von neuem Code) oder gar nicht gesetzt wurde (von altem Code), da es keine HasField-Prüfung dafür gibt.
Erweiterte Nutzung
Protocol Buffers haben Verwendungszwecke, die über einfache Accessoren und Serialisierung hinausgehen. Erkunden Sie unbedingt die Python API-Referenz, um zu sehen, was Sie damit noch alles tun können.
Ein Schlüsselmerkmal, das von Protokollnachrichtenklassen bereitgestellt wird, ist die *Reflection*. Sie können die Felder einer Nachricht durchlaufen und ihre Werte manipulieren, ohne Ihren Code gegen einen bestimmten Nachrichtentyp zu schreiben. Eine sehr nützliche Anwendung der Reflection ist die Konvertierung von Protokollnachrichten in und aus anderen Encodings wie XML oder JSON (siehe Parsen und Serialisieren für ein Beispiel). Eine fortgeschrittenere Verwendung der Reflection könnte darin bestehen, Unterschiede zwischen zwei Nachrichten desselben Typs zu finden oder eine Art "reguläre Ausdrücke für Protokollnachrichten" zu entwickeln, mit denen Sie Ausdrücke schreiben können, die zu bestimmten Nachrichten-Inhalten passen. Wenn Sie Ihre Vorstellungskraft einsetzen, ist es möglich, Protocol Buffers auf eine viel breitere Palette von Problemen anzuwenden, als Sie anfangs erwarten würden!
Reflection wird als Teil der Message Schnittstelle bereitgestellt.