본문 바로가기

Programming

Protocol Buffers - Python (2/3)

반응형

 

 

Protocol Buffer Basics: Python  |  Protocol Buffers  |  Google Developers

This tutorial provides a basic Python programmer's introduction to working with protocol buffers. By walking through creating a simple example application, it shows you how to Define message formats in a .proto file. Use the protocol buffer compiler. Use t

developers.google.com

위 링크의 내용을 기반으로 작성했습니다.

이 문서는 proto2 버전을 사용합니다.

proto3 도 곧 다뤄보겠습니다. (아마도...)

 

Protocol Buffers 를 사용하는 방법은 간단합니다. 다음 방법만 따르면 됩니다.

  1. .proto file 에 message 구조를 정의하라
  2. protocol buffer compiler 를 이용하라
  3. 메시지를 읽고 쓰기 위해서 python protocol buffer api 를 이용하라

이전까지 서로 다른 플랫폼, 언어에서 데이터를 주고 받기 위해서는 이런 방법들을 썼었습니다.

  • 간단한 방법은 여러 데이터를 하나의 문자열로 구분하는 것입니다. "chope:123-4567-8901:ABC:12" 이렇게 구분자로 합쳐서 encode, decode 하는 것입니다.
  • XML 을 많이 이용했습니다. 이미 수많은 언어들로 라이브러리가 있어서 쉽게 접근할 수 있습니다. 하지만 공백을 많이 사용하고 encode/decode 포퍼먼스가 좋지 못합니다.

protocol buffer 는 이러한 문제를 해결하기 위한 유연하고 효과적이고 자동화된 설루션입니다.

compiler 는 효율적인 바이너리 포맷의 protocol buffer data 를 파싱하고 자동 encoding 을 구현한 클래스를 생성한다.

protocol buffer compiler 는 결과 바이너리 파일로 encode 하는 코드와 decode 하는 코드를 생성합니다. 그리고 그 값들을 가지고 있는 data type 또한 생성합니다.

생성된 class 는 각 필드의 getter, setter 를 제공합니다.

중요한 것은 포맷이 확장됨에도 여전히 예전 포맷으로부터 읽을 수 있다는 것입니다. 호환성이 좋다는 얘기겠네요.

 

Protocol buffers 를 사용하려면 가장 먼저 해야 할 것은 .proto 파일을 작성하는 것입니다.

 

proto 파일의 시작은 package 로 시작한다. package 는 namespace 를 지정할 수 있게 해 줍니다.

message 는 type 이 정해진 여러 필드의 모음입니다. struct 와 유사합니다.

bool, int32, float, double, string 등의 기본 type 이 사용 가능합니다.

다른 message 를 type 으로 사용 가능합니다.

message 는 nested 선언이 가능합니다.

enum type 도 지원합니다. 정의 시 지정하는 값은 tag 이자 유니크한 식별자로 바이너리 인코딩 때 사용합니다. tag 는 한 바이트 값으로 해야 합니다. 1-15 

만약 한 바이트로 하지 않으면 최적화 관련 경고가 뜬다고 합니다.

 

공식 문서와 같이 아래와 같이 .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;
}

 

모든 필드는 3가지 수식어 중 하나를 표시해야 합니다.

 

optional

  • 값이 있거나 없을 수 있습니다.
  • 값이 설정되지 않으면 기본값을 사용하고 정의된 기본값이 없으면 시스템 기본값을 사용합니다.
  • 숫자형은 0, 문자열은 빈 문자열, boolean 은 false

repeated

  • 필드가 0번 이상 반복될 수 있습니다.
  • 순서는 protocol buffer 에서 보존됩니다.
  • repeated fields 는 동적 크기 배열입니다.

required

  • 값이 필수로 존재해야 합니다. 그렇지 않으면 초기화되지 않고 exception 이 발생합니다.

required 는 조심해서 사용해야 합니다. required 로 사용하다가 optional 로 변경할 때 문제가 발생합니다. old reader 는 이러한 변화를 거부하거나 무시할 수 있습니다.

protocol buffer 가 아닌 application 에서 validation 을 추가하는 것을 고려하는 것이 좋습니다.

구글에서도 required 를 탐탁지 않게 여기며 대부분의 메시지를 optional, repeated 를 사용하고 proto3 에서는 require 를 지원하지 않습니다.

 

proto 파일을 작성하면 다음은 class 를 생성해야 합니다.

protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/data.proto

 

실행하면 다음과 같이 data_pb2.py 파일이 하나 생성됩니다.

protoc

data_pb2.py 파일을 열어 보면 다음과 같이 코드가 생성되어 있습니다.

# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: data.proto
"""Generated protocol buffer code."""
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
# @@protoc_insertion_point(imports)

_sym_db = _symbol_database.Default()




DESCRIPTOR = _descriptor.FileDescriptor(
  name='data.proto',
  package='tutorial',
  syntax='proto2',
  serialized_options=None,
  create_key=_descriptor._internal_create_key,
  serialized_pb=b'\n\ndata.proto\x12\x08tutorial\"\xdb\x01\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\x1aM\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12.\n\x04type\x18\x02 \x01(\x0e\x32\x1a.tutorial.Person.PhoneType:\x04HOME\"+\n\tPhoneType\x12\n\n\x06MOBILE\x10\x00\x12\x08\n\x04HOME\x10\x01\x12\x08\n\x04WORK\x10\x02\"/\n\x0b\x41\x64\x64ressBook\x12 \n\x06people\x18\x01 \x03(\x0b\x32\x10.tutorial.Person'
)



_PERSON_PHONETYPE = _descriptor.EnumDescriptor(
  name='PhoneType',
  full_name='tutorial.Person.PhoneType',
  filename=None,
  file=DESCRIPTOR,
  create_key=_descriptor._internal_create_key,
  values=[
    _descriptor.EnumValueDescriptor(
      name='MOBILE', index=0, number=0,
      serialized_options=None,
      type=None,
      create_key=_descriptor._internal_create_key),
    _descriptor.EnumValueDescriptor(
      name='HOME', index=1, number=1,
      serialized_options=None,
      type=None,
      create_key=_descriptor._internal_create_key),
    _descriptor.EnumValueDescriptor(
      name='WORK', index=2, number=2,
      serialized_options=None,
      type=None,
      create_key=_descriptor._internal_create_key),
  ],
  containing_type=None,
  serialized_options=None,
  serialized_start=201,
  serialized_end=244,
)
_sym_db.RegisterEnumDescriptor(_PERSON_PHONETYPE)


_PERSON_PHONENUMBER = _descriptor.Descriptor(
  name='PhoneNumber',
  full_name='tutorial.Person.PhoneNumber',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  create_key=_descriptor._internal_create_key,
  fields=[
    _descriptor.FieldDescriptor(
      name='number', full_name='tutorial.Person.PhoneNumber.number', index=0,
      number=1, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=b"".decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
    _descriptor.FieldDescriptor(
      name='type', full_name='tutorial.Person.PhoneNumber.type', index=1,
      number=2, type=14, cpp_type=8, label=1,
      has_default_value=True, default_value=1,
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto2',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=122,
  serialized_end=199,
)

_PERSON = _descriptor.Descriptor(
  name='Person',
  full_name='tutorial.Person',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  create_key=_descriptor._internal_create_key,
  fields=[
    _descriptor.FieldDescriptor(
      name='name', full_name='tutorial.Person.name', index=0,
      number=1, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=b"".decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
    _descriptor.FieldDescriptor(
      name='id', full_name='tutorial.Person.id', index=1,
      number=2, type=5, cpp_type=1, label=1,
      has_default_value=False, default_value=0,
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
    _descriptor.FieldDescriptor(
      name='email', full_name='tutorial.Person.email', index=2,
      number=3, type=9, cpp_type=9, label=1,
      has_default_value=False, default_value=b"".decode('utf-8'),
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
    _descriptor.FieldDescriptor(
      name='phones', full_name='tutorial.Person.phones', index=3,
      number=4, type=11, cpp_type=10, label=3,
      has_default_value=False, default_value=[],
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
  ],
  extensions=[
  ],
  nested_types=[_PERSON_PHONENUMBER, ],
  enum_types=[
    _PERSON_PHONETYPE,
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto2',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=25,
  serialized_end=244,
)


_ADDRESSBOOK = _descriptor.Descriptor(
  name='AddressBook',
  full_name='tutorial.AddressBook',
  filename=None,
  file=DESCRIPTOR,
  containing_type=None,
  create_key=_descriptor._internal_create_key,
  fields=[
    _descriptor.FieldDescriptor(
      name='people', full_name='tutorial.AddressBook.people', index=0,
      number=1, type=11, cpp_type=10, label=3,
      has_default_value=False, default_value=[],
      message_type=None, enum_type=None, containing_type=None,
      is_extension=False, extension_scope=None,
      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
  ],
  extensions=[
  ],
  nested_types=[],
  enum_types=[
  ],
  serialized_options=None,
  is_extendable=False,
  syntax='proto2',
  extension_ranges=[],
  oneofs=[
  ],
  serialized_start=246,
  serialized_end=293,
)

_PERSON_PHONENUMBER.fields_by_name['type'].enum_type = _PERSON_PHONETYPE
_PERSON_PHONENUMBER.containing_type = _PERSON
_PERSON.fields_by_name['phones'].message_type = _PERSON_PHONENUMBER
_PERSON_PHONETYPE.containing_type = _PERSON
_ADDRESSBOOK.fields_by_name['people'].message_type = _PERSON
DESCRIPTOR.message_types_by_name['Person'] = _PERSON
DESCRIPTOR.message_types_by_name['AddressBook'] = _ADDRESSBOOK
_sym_db.RegisterFileDescriptor(DESCRIPTOR)

Person = _reflection.GeneratedProtocolMessageType('Person', (_message.Message,), {

  'PhoneNumber' : _reflection.GeneratedProtocolMessageType('PhoneNumber', (_message.Message,), {
    'DESCRIPTOR' : _PERSON_PHONENUMBER,
    '__module__' : 'data_pb2'
    # @@protoc_insertion_point(class_scope:tutorial.Person.PhoneNumber)
    })
  ,
  'DESCRIPTOR' : _PERSON,
  '__module__' : 'data_pb2'
  # @@protoc_insertion_point(class_scope:tutorial.Person)
  })
_sym_db.RegisterMessage(Person)
_sym_db.RegisterMessage(Person.PhoneNumber)

AddressBook = _reflection.GeneratedProtocolMessageType('AddressBook', (_message.Message,), {
  'DESCRIPTOR' : _ADDRESSBOOK,
  '__module__' : 'data_pb2'
  # @@protoc_insertion_point(class_scope:tutorial.AddressBook)
  })
_sym_db.RegisterMessage(AddressBook)


# @@protoc_insertion_point(module_scope)

Python 에 익숙하지 않은 저는 자세히 이해하기가 어려웠습니다.

역시 그냥 이용 방법이나 알아봐야겠네요.

 

Write 하는 방법입니다. 

공식 문서는 python2 여서 python3 로 변경했습니다.

#! /Users/chope/.pyenv/shims/python

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

    type = 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()

실제 동작해서 2개의 전화번호를 입력했습니다.

위 결과를 저장한 address_book 을 vscode 로 열어 보았습니다.

text file 이 아니어서 경고가 한번 뜨네요. 

그래도 열어 보자를 선택해 보면 아래와 같습니다.

 

저장한 파일을 읽기 위해 문서에 나온 예제를 이용합니다.

#! /Users/chope/.pyenv/shims/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 #: ", end='')
      elif phone_number.type == addressbook_pb2.Person.PhoneType.HOME:
        print("  Home phone #: ", end='')
      elif phone_number.type == addressbook_pb2.Person.PhoneType.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.
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()

ListPeople(address_book)

여러 데이터가 나오는 것을 보기 위해서 위에서 언급하지 않은 데이터를 3개 더 추가하고 실행했습니다.

정말 쉽게 serialization/deserialization 이 가능하다.

 

python 으로 생성된 코드는 java, c++ 과 조금 다르다고 합니다. 자세히 알고 싶은 분은 공식문서를 더 참고하세요.

 

 

Protocol Buffers - protobuf 는 무엇인가? (1/3)

반응형

'Programming' 카테고리의 다른 글

Protocol Buffers - protobuf 는 무엇인가? (1/3)  (0) 2020.12.01
무료 SSL 인증서 발급 적용  (0) 2020.11.27
GitHub Free vs Pro  (0) 2020.07.10
[Perfect] Dependency 추가  (2) 2016.10.18
[Perfect] 프로젝트 생성하기  (1) 2016.10.15