UWAGA: witryna została wycofana. Po 31 stycznia 2023 roku witryna zostanie wyłączona, a ruch będzie kierowany do nowej witryny na https://protobuf.dev. Do tego czasu aktualizacje będą dotyczyć tylko protobuf.dev.

Podstawy bufora protokołów: Python

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Ten samouczek zawiera podstawowe informacje na temat pracy z buforami protokołów w języku Python. Dzięki prostej przykładowej aplikacji dowiesz się, jak

  • Zdefiniuj formaty wiadomości w pliku .proto.
  • Użyj kompilatora bufora protokołu.
  • Do zapisywania i odczytywania wiadomości używaj interfejsu API Pythona do buforowania protokołu.

Nie jest to wyczerpujący przewodnik dotyczący korzystania z buforów protokołów w Pythonie. Szczegółowe informacje znajdziesz w przewodniku po języku bufora protokołu (proto2), przewodniku po języku bufora protokołu (proto3), przewodniku po kodzie wygenerowanym w języku Python i przewodniku po kodowaniu w języku Python.

Domena problemu

W tym przykładzie wykorzystamy prostą aplikację typu „książka adresowa”, która pozwala odczytywać i zapisywać dane kontaktowe osób z pliku. Każda osoba w książce adresowej ma imię i nazwisko, identyfikator, adres e-mail i kontaktowy numer telefonu.

Jak serializujesz i pobierasz takie uporządkowane dane? Istnieje kilka sposobów rozwiązania tego problemu:

  • Użyj języka Pythona. Jest to metoda domyślna, ponieważ jest wbudowana w język, ale nie sprawdza się w przypadku ewolucji schematu. Nie sprawdza się też zbyt dobrze, gdy trzeba udostępniać dane aplikacjom napisanym w języku C++ lub Java.
  • Możesz wymyślić sposób doraźny, który pozwoli Ci zakodować elementy danych w jeden ciąg znaków, na przykład zakodować 4 cale jako „12:3:-23:67”. Jest to proste i elastyczne podejście, chociaż wymaga jednorazowego kodowania i analizy kodu, a jego pobieranie wiąże się z niewielkim kosztem. To rozwiązanie najlepiej sprawdza się w przypadku kodowania bardzo prostych danych.
  • Serializuj dane do formatu XML. Ta metoda może być bardzo atrakcyjna, ponieważ kod XML jest (w pewnym stopniu) zrozumiały dla ludzi i istnieją biblioteki wiążące dla wielu języków. Może to być dobry wybór do udostępniania danych innym aplikacjom i projektom. Jednak plik XML jest często zajmuje dużo miejsca, a jego kodowanie/dekodowanie może wiązać się z dużą wydajnością aplikacji. Nawigacja w drzewie DOM XML jest znacznie bardziej skomplikowana niż normalnie w przypadku poruszania się po prostych polach w klasie.

Zamiast tych opcji możesz korzystać z buforów protokołów. Bufory protokołów to elastyczne, efektywne, zautomatyzowane rozwiązanie pozwalające rozwiązać dokładnie ten problem. Korzystając z buforów protokołu, wpisujesz .proto opis struktury danych, którą chcesz przechowywać. Kompilator bufora protokołu tworzy z kolei klasy, które implementują automatyczne kodowanie i analizują dane bufora protokołu w efektywnym formacie binarnym. Wygenerowana klasa udostępnia metody pobierania i pobierania pól, które tworzą bufor protokołu, i dba o szczegóły odczytu i zapisu bufora protokołu w formie jednostki. Co ważne, format bufora protokołu umożliwia z czasem rozbudowywanie formatu w taki sposób, aby kod nadal mógł odczytywać dane zakodowane w starym formacie.

Gdzie znaleźć przykładowy kod

Przykładowy kod jest zawarty w pakiecie kodu źródłowego w katalogu „examples”. Pobierz tutaj

Definiowanie formatu protokołu

Aby utworzyć aplikację do obsługi książek adresowych, musisz zacząć od pliku .proto. Definicje w pliku .proto są proste – dodaj wiadomość dla każdej struktury danych, którą chcesz zserializować, a potem określ nazwę i typ każdego pola w wiadomości. Oto plik .proto, który definiuje Twoje wiadomości: addressbook.proto.

syntax = "proto2";

package tutorial;

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    optional string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

Jak widać, składnia jest podobna do C++ lub Java. Przyjrzyjmy się poszczególnym elementom pliku i zobaczmy, do czego służy.

Plik .proto zaczyna się od deklaracji pakietu, która pomaga zapobiegać konfliktom nazw między różnymi projektami. W Pythonie pakiety są zwykle określane przez strukturę katalogu, więc element package zdefiniowany w pliku .proto nie ma wpływu na wygenerowany kod. Musisz jednak zadeklarować taką nazwę, aby uniknąć konfliktów nazw w obszarze nazw Bufora protokołów oraz w językach innych niż Python.

Następnie masz definicje wiadomości. Wiadomość to po prostu agregacja zawierająca zestaw podanych pól. Wiele standardowych typów danych jest dostępnych jako typy pól, np. bool, int32, float, double i string. Możesz też dodać więcej struktury do wiadomości, używając innych typów wiadomości jako typów pól. W powyższym przykładzie wiadomość Person zawiera wiadomości z PhoneNumber, a wiadomość AddressBook zawiera wiadomości z Person. Możesz nawet określić typy wiadomości zagnieżdżonych w innych wiadomościach – typ PhoneNumber jest zdefiniowany w pliku Person. Możesz też zdefiniować typy enum, jeśli chcesz, aby jedno z pól miało jedną ze wstępnie zdefiniowanych list wartości. W tym miejscu możesz określić, czy numer telefonu może być jednym z tych typów telefonów: MOBILE, HOME czy WORK.

Znaczniki „ = 1”, „ = 2” w każdym elemencie określają unikalny „tag”, którego to pole używa w kodowaniu binarnym. Kodowanie tagów od 1 do 15 wymaga o jeden bajt mniej niż kodowanie wyższych wartości. W celu optymalizacji możesz ich użyć w przypadku często wybieranych lub powtórzonych elementów, a rzadko używane elementy opcjonalne – od 16. Każdy element w powtarzającym się polu wymaga ponownego kodowania numeru tagu, dlatego pola te szczególnie dobrze nadają się do optymalizacji.

W każdym polu musi znaleźć się jeden z tych modyfikatorów:

  • optional: to pole może być ustawione lub nie. Jeśli wartość opcjonalnego pola nie została ustawiona, używana jest wartość domyślna. W przypadku prostych typów możesz określić własną wartość domyślną, tak jak w przykładzie pod numerem telefonu type. W przeciwnym razie używana jest wartość domyślna systemu: zero dla typów liczbowych, pusty ciąg znaków dla ciągów, false dla wartości logicznych. W przypadku wiadomości umieszczonych na stronie wartością domyślną jest „instancja domyślna” lub „prototyp” wiadomości, która nie ma żadnego z ustawionych pól. Wywołanie metody dostępu w celu uzyskania wartości opcjonalnego (lub wymaganego) pola, które nie zostało ustawione wprost, zawsze zwraca wartość domyślną tego pola.
  • repeated: to pole może się powtarzać dowolną liczbę razy (również zero). Kolejność powtarzających się wartości zostanie zachowana w buforze protokołu. Potraktuj pola powtarzane jak tablice o dynamicznym rozmiarze.
  • required: trzeba podać wartość w tym polu. W przeciwnym razie wiadomość zostanie uznana za „nie zainicjowaną”. Serializowanie niezainicjowanej wiadomości spowoduje zgłoszenie wyjątku. Analizowanie niezainicjowanej wiadomości zakończy się niepowodzeniem. Poza tym wymagane pole działa dokładnie tak samo jak pole opcjonalne.

Wymagane na zawsze Zachowaj ostrożność podczas oznaczania pól jako required. Jeśli w którymś momencie przestaniesz pisać lub wyślesz wymagane pole, zmiana hasła na pole opcjonalne nie będzie możliwe — osoby, które już to zrobiły, uznają je za niekompletne i mogą nieumyślnie odrzucić je lub odrzucić. Zamiast tego rozważ utworzenie dla buforów niestandardowych procedur weryfikacji aplikacji. W Google pola required są zdecydowanie odrzucane. Większość wiadomości zdefiniowanych w składni proto2 korzysta tylko z optional i repeated. (Proto3 w ogóle nie obsługuje pól required).

Pełny przewodnik po tworzeniu plików .proto (wraz ze wszystkimi możliwymi typami pól) znajdziesz w przewodniku po buforach protokołów. Nie szukaj obiektów podobnych do dziedziczenia klas, ale nie buforują ich protokoły.

Kompilowanie buforów protokołów

Skoro masz już .proto, następnym krokiem jest wygenerowanie klas, które musisz przeczytać i napisać AddressBook (a tym samym Person i PhoneNumber). Aby to zrobić, musisz uruchomić kompilator protoc bufora protokołu na urządzeniu .proto:

  1. Jeżeli kompilator nie jest jeszcze zainstalowany, pobierz pakiet „protoc” i postępuj zgodnie z instrukcjami w README.
  2. Teraz uruchom kompilator, określając katalog źródłowy (gdzie znajduje się kod źródłowy aplikacji – bieżący katalog jest używany, jeśli nie podasz wartości), katalog docelowy (gdzie chcesz umieścić wygenerowany kod, często taki sam jak $SRC_DIR) oraz ścieżkę do .proto. W tym przypadku:
    protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
    Aby używać zajęć w języku Python, używaj opcji --python_out – podobne opcje są dostępne w przypadku innych obsługiwanych języków.

W określonym katalogu docelowym zostanie wygenerowany addressbook_pb2.py.

Interfejs API Buffer

W przeciwieństwie do generowania kodu bufora protokołu Java i C++ kod kompilatora bufora Pythona nie generuje bezpośrednio Twojego kodu dostępu do danych. Zamiast tego jak zobaczysz, jeśli spojrzysz na pole addressbook_pb2.py, zostanie wygenerowany specjalny deskryptor dla wszystkich wiadomości, wyliczenia i pól oraz niektóre zagadkowo puste klasy, po jednym dla każdego typu wiadomości:

class Person(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType

  class PhoneNumber(message.Message):
    __metaclass__ = reflection.GeneratedProtocolMessageType
    DESCRIPTOR = _PERSON_PHONENUMBER
  DESCRIPTOR = _PERSON

class AddressBook(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType
  DESCRIPTOR = _ADDRESSBOOK

Ważny wiersz w każdych zajęciach to __metaclass__ = reflection.GeneratedProtocolMessageType. Szczegółowe informacje o działaniu metaklasy w Pythonie wykraczają poza zakres tego samouczka, ale możesz traktować je jak szablony do tworzenia zajęć. Podczas ładowania metaklasa GeneratedProtocolMessageType korzysta z określonych deskryptorów, aby utworzyć wszystkie metody języka Python potrzebne do pracy z każdym typem wiadomości i dodaje je do odpowiednich zajęć. Następnie możesz użyć w pełni uzupełnionych kodów w kodzie.

Ostatnim efektem jest to, że możesz używać klasy Person tak, jakby wszystkie pola klasy podstawowej Message były zdefiniowane jako zwykłe pola. Możesz na przykład napisać:

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.HOME

Pamiętaj, że te przypisania nie powodują tylko dodawania nowych pól do ogólnego obiektu Pythona. Jeśli spróbujesz przypisać pole, które nie zostało zdefiniowane w pliku .proto, zwróci się wartość AttributeError. Jeśli przypiszesz pole do wartości nieprawidłowego typu, zostanie zwrócony element TypeError. Ponadto czytanie wartości pola przed jego ustawieniem zwraca wartość domyślną.

person.no_such_field = 1  # raises AttributeError
person.id = "1234"        # raises TypeError

Więcej informacji o tym, jakie dokładnie elementy generuje protokół kompilatora w przypadku poszczególnych definicji pól, znajdziesz w dokumentacji kodu wygenerowanej w języku Python.

Wartości w polu enum

Enum jest rozszerzany przez metaklasy do zbioru symboli symboli stałych z wartościami całkowitymi. Na przykład stała addressbook_pb2.Person.PhoneType.WORK ma wartość 2.

Standardowe metody komunikacji

Każda klasa wiadomości zawiera też kilka innych metod pozwalających sprawdzić lub zmodyfikować całą wiadomość, w tym:

  • IsInitialized(): sprawdza, czy wszystkie wymagane pola są ustawione.
  • __str__(): zwraca zrozumiały dla człowieka komunikat, szczególnie przydatny do debugowania. (zwykle wywoływane jako str(message) lub print message).
  • CopyFrom(other_msg): powoduje zastąpienie wiadomości wartościami danej wiadomości.
  • Clear(): czyści wszystkie elementy do pustego stanu.

Te metody wdrażają interfejs Message. Więcej informacji znajdziesz w pełnej dokumentacji interfejsu API Message.

Analizowanie i serializacja

W każdej klasie bufora protokołu są zapisane i odczytywane wiadomości dowolnego typu przy użyciu formatu binarnego bufora protokołu. Są to między innymi:

  • SerializeToString(): serializuje wiadomość i zwraca ją jako ciąg znaków. Bajty są binarne, nie tekstowe. Typ str jest używany tylko jako wygodny kontener.
  • ParseFromString(data): analizuje wiadomość na podstawie podanego ciągu.

To tylko kilka opcji analizowania i serializacji. Pełną listę znajdziesz w dokumentacji interfejsu API Message.

Klasy buforów protokołów i projektu zorientowanego obiektu to zasadniczo właściciele danych (np. konstrukcja C), które nie zapewniają dodatkowych funkcji; nie zapewniają dobrych obywateli pierwszej klasy w modelu obiektu. Jeśli chcesz dodać bogatsze zachowanie do wygenerowanej klasy, najlepiej umieścić w niej klasę bufora protokołu w ramach klasy aplikacji. Dobrym pomysłem jest też buforowanie protokołu opakowania, jeśli nie masz kontroli nad projektowaniem pliku .proto (jeśli na przykład wykorzystujesz projekt z innego projektu). W takim przypadku możesz użyć klasy kodu, aby utworzyć interfejs lepiej dopasowany do niepowtarzalnego środowiska aplikacji: ukrycie niektórych danych i metod, ujawnienie danych funkcji itp. Nie należy dodawać zachowania do wygenerowanych klas przez ich dziedziczenie. Zepsuje to mechanizmy wewnętrzne i nie jest to dobre ćwiczenie zorientowane na obiekty.

Pisanie wiadomości

Spróbujmy teraz użyć klas bufora protokołu. Pierwszą rzeczą, jaką musisz zrobić, aby aplikacja do książki adresowej była dostępna, jest zapisanie danych osobowych w pliku książki adresowej. Aby to zrobić, musisz utworzyć i uzupełnić wystąpienia klas bufora protokołu, a następnie zapisać je w strumieniu wyjściowym.

Oto program, który odczytuje AddressBook z pliku, dodaje do pliku 1 nowy element Person na podstawie danych wejściowych użytkownika, i ponownie zapisuje nowy plik AddressBook w pliku. Fragmenty, które bezpośrednio wywołują kod referencyjny wygenerowany przez kompilator protokołu, są wyróżnione.

#! /usr/bin/python

import addressbook_pb2
import sys

# This function fills in a Person message based on user input.
def PromptForAddress(person):
  person.id = int(raw_input("Enter person ID number: "))
  person.name = raw_input("Enter name: ")

  email = raw_input("Enter email address (blank for none): ")
  if email != "":
    person.email = email

  while True:
    number = raw_input("Enter a phone number (or leave blank to finish): ")
    if number == "":
      break

    phone_number = person.phones.add()
    phone_number.number = number

    type = raw_input("Is this a mobile, home, or work phone? ")
    if type == "mobile":
      phone_number.type = addressbook_pb2.Person.PhoneType.MOBILE
    elif type == "home":
      phone_number.type = addressbook_pb2.Person.PhoneType.HOME
    elif type == "work":
      phone_number.type = addressbook_pb2.Person.PhoneType.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:
  f = open(sys.argv[1], "rb")
  address_book.ParseFromString(f.read())
  f.close()
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.
f = open(sys.argv[1], "wb")
f.write(address_book.SerializeToString())
f.close()

Odczytywanie wiadomości

Oczywiście książki adresowe byłyby nieprzydatne, jeśli nie można było pobrać żadnych informacji. Ten przykład odczytuje plik utworzony w powyższym przykładzie i drukuje wszystkie zawarte w nim informacje.

#! /usr/bin/python

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.MOBILE:
        print "  Mobile phone #: ",
      elif phone_number.type == addressbook_pb2.Person.PhoneType.HOME:
        print "  Home phone #: ",
      elif phone_number.type == addressbook_pb2.Person.PhoneType.WORK:
        print "  Work phone #: ",
      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.
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()

ListPeople(address_book)

Przedłużanie bufora protokołu

Niedługo potem lub później, po wprowadzeniu kodu korzystającego z bufora protokołu, prawdopodobnie zechcesz „poprawić” definicję bufora protokołu. Jeśli chcesz, aby Twoje nowe bufory były zgodne wstecznie, a stare – zgodne. W nowej wersji bufora protokołu:

  • nie możesz zmieniać numerów tagów w istniejących polach.
  • nie możesz dodawać ani usuwać wymaganych pól.
  • możesz usunąć pola opcjonalne lub powtarzające się.
  • możesz dodać nowe opcjonalne lub powtarzające się pola, ale musisz użyć nowych numerów tagów (czyli tych, które nigdy nie były używane w tym buforze protokołu, nawet przez usunięte pola).

(Istnieją pewne wyjątki od tych reguł, ale są one rzadko używane).

Jeśli będziesz przestrzegać tych reguł, stary kod z łatwością będzie odczytywać nowe wiadomości i ignorować wszystkie nowe pola. W przypadku starego kodu usunięte pola opcjonalne miały po prostu wartość domyślną, a usunięte pola powtarzane były puste. Nowy kod będzie przezroczyście odczytywał też stare wiadomości. Pamiętaj jednak, że w starszych wiadomościach nie będzie nowych pól opcjonalnych, więc musisz sprawdzić, czy są one skonfigurowane jako has_, lub podać odpowiednią wartość domyślną w pliku .proto za pomocą tagu [default = value] po numerze tagu. Jeśli element opcjonalny nie jest określony, zamiast niego używana jest wartość domyślna konkretnego typu: w przypadku ciągów znaków wartość domyślna jest pustym ciągiem. W przypadku wartości logicznych wartość domyślna to fałsz. W przypadku typów liczbowych wartością domyślną jest 0. Pamiętaj też, że po dodaniu nowego pola powtarzanego nowy kod nie będzie wiedzieć, czy jest on pusty (przez nowy kod), czy też nie zostanie ustawiony (przez stary kod), ponieważ nie ma dla niego flagi has_.

Do wymagających zadań

Bufory protokołu mają więcej zastosowań niż tylko proste akcesory i seriaryzacja. Zapoznaj się z informacjami o interfejsie API języka Python, aby zobaczyć, co jeszcze możesz zrobić z tymi interfejsami.

Jedną z kluczowych funkcji zapewnianych przez klasy komunikatów protokołu jest odbicie. Możesz iterować pola w wiadomości i manipulować ich wartościami bez konieczności pisania kodu względem określonego typu wiadomości. Bardzo przydatnym sposobem użycia funkcji odbicia jest przekonwertowanie komunikatów protokołu na inny format, np. XML lub JSON. Bardziej zaawansowanym sposobem korzystania z odbicia może być znalezienie różnic między 2 wiadomościami tego samego typu lub opracowanie rodzaju „wyrażeń regularnych dla komunikatów protokołu”, w których można napisać wyrażenia pasujące do określonej treści wiadomości. Jeśli używasz własnej wyobraźni, możesz stosować bufory protokołów w przypadku znacznie większego zakresu niż można sobie wyobrazić!

Odbicie jest dostarczane w interfejsie Message.