Питонячьи хитрости
Расскажу о всяких хитростях Питона, с которыми сталкивался в работе. Долгое время записывал их на бумажку, а теперь решил скомпоновать. Думаю, будет любопытно опытным питонщикам. Сказанное ниже носит познавательный характер. Не призываю использовать в проде.
Многоточие
Неожиданно, при определенных условиях многоточие ...
становится правильной
лексемой! Внутри оператора []
многоточие вырождается в объект Ellipsis
:
>>> d = {(1, Ellipsis, 2): 42}
>>> d[1, ..., 2]
42
Если переопределить метод __getitem__
и проверять аргументы на многоточие,
можно добиться интересных результатов. Так работает библиотека
hask. В ней с помощью многоточия
определяются бесконечные списки:
# бесконечный ленивый список от единицы
List[1, ...]
# ленивый список от 1 до 100
List[1, ..., 100]
# аналогично, но с шагом в 2
List[1, 3, ...]
В другом месте многоточие вызовет ошибку синтаксиса.
Срезы в аргументах
В оператор индекса []
можно передать синтаксис словаря: key: value,
...
. Тогда в методе __getitem__
мы получим кортеж объектов slice
. Ключ и
значение хранятся в полях .start
и .stop
. Выглядит так:
def __getitem__(cls, slices):
if isinstance(slices, tuple):
slice_tuple = slices
else:
slice_tuple = (slices, )
keys = (sl.start for sl in slice_tuple)
vals = (sl.stop for sl in slice_tuple)
return dict(zip(keys, vals))
YourClass["foo": 42, "bar": "test"]
>>> {"foo": 42, "bar": "test"}
Деструктивный синтаксис
Внезапно, деструктивный синтаксис, о котором я уже писал, выпилен в тройке: не проходит проверку синтаксиса. Печаль моя не знает границ.
Это объективный минус третьего Питона. Зачем лишать разработчиков удобной
возможности? Я постоянно работаю с парами. Синтаксиса def action((key, val)):
теперь будет не хватать.
Погружение в словарь
Предположим, есть вложенные словари, ответ какого-то микросервиса. Нужно взять поле из глубины. Структура ответа меняется. Часто разработчики пишут следующее:
data.get('result', {}).get('user', {}).get('name', 'unknown')
Минусы в том, что код плохо читается и созданы 2 лишних словаря. Аргументы вычисляются до шага в функцию, поэтому словари будут созданы даже когда ключи есть.
Неочевидный минус, из-за которого придется чинить прод в пятницу, кроется в
методе .get
. Он возвращает дефолт только если ключа нет. А если ключ есть и
равен None
, то вернется None
вне зависимости от того, что передано в дефолт.
data = {"result": {"user": None}}
data.get('result', {}).get('user', {}).get('name', 'unknown')
>>> AttributeError: 'NoneType' object has no attribute 'get'
В библиотеке f я предложил нормальный способ работы со вложенными словарями:
f.ichain(data, 'result', 'user', 'name')
>>> None
Конечное приведение типов
Приводить типы в конце каждой операции – здравая мысль. Идея в том, чтобы
добавлять к концу вычислений кусочек ... or <default>
, например:
get_users_count() or 0
>>> 0
get_account_list(id) or []
>>> []
f.ichain(data, 'result', 'user', 'name') or 'dunno'
>>> dunno
Этот прием спасает злополучного None
, который лезет изо всех щелей. Беда в
том, что в скриптовых языках нет четких правил для пустых значений. Например,
функция может отдать или пустой список, или None
. Запомнить невозможно. Рано
или поздно None
провалится туда, где ожидают список или число.
Решение – выводить типы самостоятельно. Поскольку оператор or
ленив во всех
языках, выражение справа не будет вычислено без необходимости. Следующий код:
data = {"user": None}
(data.get("user") or {}).get("name") or "dunno"
>>> "dunno"
вернет ожидаемый дефолт, даже когда ключ есть и равен None
. При этом если в
юзере лежат актуальные данные, пустой словарь не будет создан.
Правые методы
Еще одна особенность классов в Питоне – правые волшебные методы. Они начинаются
с префикса r
: __radd__
, __rsub__
и т.д. Как следует из названия, эти
методы вызываются для правого операнда в тех случаях, когда для левого операнда
метод не определен.
Другими словами, рассмотрим выражение a + b
. Сперва Питон попытается сделать
так: a.__add__(b)
. Если для a
метод __add__
неопределен, будет предпринята
другая попытка: b.__radd__(a)
. И если __radd__
тоже не определен для b
,
вылезет ошибка.
Правые методы появились в третьей версии Питона. Попробуем реализовать следующее поведение: к списку добавляем число. Если прибавляем слева, число становится в начало списка. Если справа – в конец.
class List(list):
def __add__(self, other):
if isinstance(other, list):
return super(List, self).__add__(other)
else:
return self + [other]
def __radd__(self, other):
if isinstance(other, list):
return other.__add__(self)
else:
return [other].__add__(self)
>>> 99 + List([1, 2, 3])
[99, 1, 2, 3]
>>> List([1, 2, 3]) + 99
[1, 2, 3, 99]
Особенности среза в тройке
Во втором Питоне у классов был особый метод __getslice__
для получения
среза. В тройке его объединили с __getitem__
. Теперь интерпретатор проверяет,
что передано в __getitem__
. Если написать foo[1:2]
, будет передан экземпляр
класса slice
, а если foo[1, 2]
– кортеж.
Этот пункт я привожу к тому, что переопределив метод __getitem__
, мы по запаре
можем сломать операцию среза. Приходится добавлять условие: если передан срез –
вызывать предка, если нет – своя логика:
def __getitem__(self, item):
if isinstance(item, slice):
return super(MyClass, self).__getitem__(item)
else:
...
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter
Олег Комков, 27th Jul 2016, link
"Поскольку оператор or ленив во всех языках"
В Erlang-е они жадные, а ленивые сделаны отдельным orelse
Ivan Grishaev, 27th Jul 2016, link , parent
Да, когда писил, вертелось в голове, что есть исключение.