Подборка @pythonetc, апрель 2019

469cfe43068ff61bcd8eb6f3737c8bb7.png

Это десятая подборка советов про 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 "", line 1, in 
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)

© Habrahabr.ru