Это десятая подборка советов про Python и программирование из моего авторского канала @pythonetc.
Хранение и отправка объектов по сети в виде байтов — это очень большая тема. Для этих целей в Python обычно используется ряд инструментов, давайте обсудим их достоинства и недостатки.
В качестве примера я попробую сериализовать объект Cities, который содержит объекты City, расположенные в определённом порядке. Можно воспользоваться четырьмя подходами:
1. JSON. Человекочитаемый, простой в использовании, но потребляет много памяти. То же самое справедливо в отношении форматов YAML и XML.
class City:
def to_dict(self):
return dict(
name=self._name,
country=self._country,
lon=self._lon,
lat=self._lat,
)
class Cities:
def __init__(self, cities):
self._cities = cities
def to_json(self):
return json.dumps([
c.to_dict() for c in self._cities
]).encode('utf8')
2. Pickle. Это нативный инструмент Python, настраиваемый, потребляет меньше памяти, чем JSON. Недостаток: для извлечения данных нужно использовать Python.
class Cities:
def pickle(self):
return pickle.dumps(self)
3. Protobuf (и прочие бинарные сериализаторы, например, msgpack). Потребляет ещё меньше памяти, может использоваться из любого языка программирования, но требует написания явной схемы:
syntax = "proto2";
message City {
required string name = 1;
required string country = 2;
required float lon = 3;
required float lat = 4;
}
message Cities {
repeated City cities = 1;
}
class City:
def to_protobuf(self):
result = city_pb2.City()
result.name = self._name
result.country = self._country
result.lon = self._lon
result.lat = self._lat
return result
class Cities:
def to_protobuf(self):
result = city_pb2.Cities()
result.cities.extend([
c.to_protobuf() for c in self._cities
])
return result
4. Вручную. Вы можете вручную упаковывать и распаковывать данные с помощью модуля struct
. Так можно добиться минимально возможного потребления памяти, однако иногда лучше воспользоваться protobuf
, поскольку он поддерживает версионирование и явные схемы.
class City:
def to_bytes(self):
name_encoded = self._name.encode('utf8')
name_length = len(name_encoded)
country_encoded = self._country.encode('utf8')
country_length = len(country_encoded)
return struct.pack(
'BsBsff',
name_length, name_encoded,
country_length, country_encoded,
self._lon, self._lat,
)
class Cities:
def to_bytes(self):
return b''.join(
c.to_bytes() for c in self._cities
)
Если аргумент функции имеет значение по умолчанию None
и аннотирован как T
, тогда mypy
автоматически будет считать его Optional[T]
(то есть Union[T, None]
).
С другими типами это не работает, так что у вас не получится написать что-нибудь вроде f(x: A = B())
. Также этот трюк не работает с присвоением переменной: a: A = None
приведёт к ошибке.
def f(x: int = None):
reveal_type(x)
def g(y: int = 'x'):
reveal_type(y)
z: int = None
reveal_type(z)
$ mypy test.py
test.py:2: error: Revealed type is 'Union[builtins.int, None]'
test.py:4: error: Incompatible default for argument "y" (default has type "str", argument has type "int")
test.py:5: error: Revealed type is 'builtins.int'
test.py:7: error: Incompatible types in assignment (expression has type "None", variable has type "int")
test.py:8: error: Revealed type is 'builtins.int'
***
В Python 3, при выходе из блока except
переменные, хранящие пойманные исключения, удаляются из locals()
, даже если они уже существовали:
>>> e = 2
>>> try:
... 1/0
... except Exception as e:
... pass
...
>>> e
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'e' is not defined
Если хотите сохранить ссылку на исключение, нужно использовать другую переменную:
>>> error = None
>>> try:
... 1/0
... except Exception as e:
... error = e
...
>>> error
ZeroDivisionError('division by zero',)
В Python 2, однако, этого не происходит.
Вы можете легко сделать свой собственный pypi
-репозиторий. Он позволяет выпускать пакеты внутри вашего проекта и устанавливать их с помощью pip
, как если бы они были обычными пакетами.
Важно отметить, что вам не нужно устанавливать какое-то особое ПО, вы можете использовать обычный HTTP-сервер. Вот как это работает у меня.
Возьмём примитивный пакет pythonetc
.
setup.py:
from setuptools import setup, find_packages
setup(
name='pythonetc',
version='1.0',
packages=find_packages(),
)
pythonetc.py:
def ping():
return 'pong'
Сделаем его релиз в директорию ~/pypi
:
$ python setup.py sdist bdist_wheel
…
$ mv dist ~/pypi/pythonetc
И начнём предоставлять пакет с домена pypi.pushtaev.ru
с помощью nginx:
$ cat /etc/nginx/sites-enabled/pypi
server {
listen 80;
server_name pypi.pushtaev.ru;
root /home/vadim/pypi;
index index.html index.htm index.nginx-debian.html;
location / {
autoindex on;
try_files $uri $uri/ =404;
}
}
Теперь пакет можно установить:
$ pip install -i http://pypi.pushtaev.ru --trusted-host pypi.pushtaev.ru pythonetc
…
Collecting pythonetc
Downloading http://pypi.pushtaev.ru/pythonetc/pythonetc-1.0-py3-none-any.whl
Installing collected packages: pythonetc
Successfully installed pythonetc-1.0
$ python
Python 3.7.0+ (heads/3.7:0964aac, Mar 29 2019, 00:40:55)
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pythonetc
>>> pythonetc.ping()
'pong'
Часто нужно объявлять словарь с ключами, одноименными локальным переменным. Например:
dict(
context=context,
mode=mode,
action_type=action_type,
)
В ECMAScript для таких случаев даже есть специальная форма литерала object
(называется Object Literal Property Value Shorthand):
> var a = 1;
< undefined
> var b = 2;
< undefined
> {a, b}
< {a: 1, b: 2}
Можно создать такой же помощник и в Python (увы, он вовсе не так хорош, как нотация в ECMAScript):
def shorthand_dict(lcls, names):
return {k: lcls[k] for k in names}
context = dict(user_id=42, user_ip='1.2.3.4')
mode = 'force'
action_type = 7
shorthand_dict(locals(), [
'context',
'mode',
'action_type',
])
Вы можете спросить, зачем передавать locals()
в качестве параметра? А можно получать locals
вызывающего объекта в вызываемом? Можно, но придётся воспользоваться модулем inspect
:
import inspect
def shorthand_dict(names):
lcls = inspect.currentframe().f_back.f_locals
return {k: lcls[k] for k in names}
context = dict(user_id=42, user_ip='1.2.3.4')
mode = 'force'
action_type = 7
shorthand_dict([
'context',
'mode',
'action_type',
])
Можно пойти ещё дальше и применить такое решение — https://github.com/alexmojaki/sorcery:
from sorcery import dict_of
dict_of(context, mode, action_type)
Автор: Пуштаев Вадим