Что нужно запомнить программисту, переходящему на Python

в 12:50, , рубрики: python, tips, Программирование

Когда-то давно, в студенческие годы, меня укусил питон, правда инкубационный период затянулся и получилось так, что я стал программистом на перле.

Однако в какой-то момент перл исчерпал себя и я решил заняться питоном, сначала просто делал что-то и разбирался с тем, что нужно для данной задачи, а потом понял, что нужны какие-то систематизированные знания и прочитал несколько книг:

  • Билл Любанович «Простой Python. Современный стиль программирования»
  • Дэн Бейдер «Чистый Python. Тонкости программирования для профи»
  • Бретт Слаткин «Секреты Python: 59 рекомендаций по написанию эффективного кода»

Которые мне показались вполне подходящими для понимания основных тонкостей языка, хотя не помню, чтобы в них упоминалось про slots, но и не уверен, что это реально нужная фича — если уже по памяти прижало, то скорее всего одного этого способа будет недостаточно, но это всё зависит от ситуации.

В итоге у меня накопились какие-то заметки об особенностях питона, которые, как мне кажется, могут быть полезны тому кто пожелает мигрировать на него с других языков.

Я заметил, что на собеседованиях по питону достаточно часто задают вопросы про вещи не имеющие отношения к реальной разработке, типа того, что может быть ключом словаря, ну чуваки, в реальной жизни ключом может быть только число или строка, в тех уникальных случаях когда это не так, можно почитать документацию и разобраться, зачем спрашивать такое? Чтобы найти чего собеседуемый не знает? Так в итоге все запомнят ответ именно на этот вопрос и это перестанет работать.

Актуальными я считаю питон версий выше 3.5 (про второй питон давно пора забыть) т.к. именно такая версия в стабильном дебиане, а значит во всех остальных местах более свежие версии )

Так как я вовсе не гуру питона, то надеюсь меня поправят в комментариях если я вдруг сморозил какую-то глупость.

Типизация

Питон динамически типизированный язык т.е. он проверяет соответствие типов в процессе выполнения, например:

cat type.py

a=5
b='5'
print(a+b)

выполняем:

python3 type.py
... TypeError: unsupported operand type(s) for +: 'int' and 'str'

Однако, если ваш проект дозрел до необходимости статической типизации, то питон предоставляет и такую возможность путём использования статического анализатора mypy:

mypy type.py
type.py:3: error: Unsupported operand types for + ("int" and "str")

Правда так ловятся не все ошибки:

cat type2.py

def greeting(name):
    return 'Hello ' + name

greeting(5)

mypy тут не ругнётся, а при выполнении случится ошибка, поэтому актуальные версии питона поддерживают специальный синтаксис для указания типов аргументов функций:

cat type3.py
def greeting(name: str) -> str:
    return 'Hello ' + name

greeting(5)

а теперь:

mypy type3.py
type3.py:4: error: Argument 1 to "greeting" has incompatible type "int"; expected "str"

Переменные и данные

Переменные в питоне не хранят данные, а лишь ссылаются на них, а данные бывают изменяемые (мутабельные) и неизменяемые (иммутабельные).
Это приводит к различному поведению в зависимости от типа данных в практически идентичных ситуациях, например такой код:

x = 1
y = x
x = 2
print(y)

приводит к тому, что переменные x и y ссылаются на различные данные, а такой:

x = [1, 2, 3]
y = x
x[0] = 7
print(y)

нет, x и y остаются ссылками на один и тот же список, что кстати в питоне можно проверить оператором is (я уверен что создатель джавы навсегда лишился хорошего сна от стыда когда узнал про этот оператор в питоне).

Строки хотя и похожи на список являются иммутабельным типом данных, это значит, что саму строку изменить нельзя, можно лишь породить новую, но переменной можно присвоить другое значение, хотя исходные данные при этом не изменятся:

>>> mystr = 'sss'
>>> str = mystr  # делаем ссылку на те же данные
>>> mystr[0] = 'a'
...
  TypeError: 'str' object does not support item assignment
>>> mystr = 'ssa'  # меняем исходную переменную
>>> str  # данные не изменились и доступны по второй ссылке
  'sss'

Кстати, о строках, из-за их иммутабельности конкатенация очень большого списка строк сложением или append'ом в цикле может быть не очень эффективной (зависит от рализации в конкретном компиляторе/версии), обычно для таких случаев рекомендуют использовать метод join, который ведёт себя немного неожиданно:

>>> str_list = ['ss', 'dd', 'gg']
>>> 'XXX'.join(str_list)
'ssXXXddXXXgg'
>>> str = 'hello'
>>> 'XXX'.join(str)
'hXXXeXXXlXXXlXXXo'

Во-первых, строка у которой вызывается метод становиться разделителем, а не началом новой строки как можно было бы подумать, а во-вторых, передавать нужно список (итерируемый объект), а не отдельную строку ибо таковая тоже является итерируемым объектом и будет сджойнена посимвольно.

Так как переменные это ссылки, то вполне нормальным является желание сделать копию объекта, чтобы не ломать исходный объект, однако тут есть подводный камень — функция copy копирует только один уровень, что явно не то, что ожидается от функции с таким именем, поэтому используете deepcopy.

Аналогичная проблема с копированием может возникать при умножении коллекции на скаляр, как недавно разбиралось тут.

Область видимости

Область видимости переменных в питоне ограничена модулем/функцией в которых она определена и вложенными функциями, но есть тонкость — переменная по умолчанию доступна для чтения во вложенных пространствах имён, но модификация требует использования специальных ключевых слов nonlocal и global для модификации переменных на один уровень выше или глобальной видимости соответственно.

Например, такой код:

x = 7
print(id(x))

def func():
    print(id(x))
    return x

print(func())

Работает с одной глобальной переменной, а такой:

x = 7
print(id(x))

def func():
    x = 1
    print(id(x))
    return x

print(func())
print(x)

уже порождает локальную.
С моей точки зрения это не очень хорошо, по идее любое использование нелокальных переменных в функции это часть публичного интерфейса функции, её сигнатуры, а значит должно объявляться явно и видимо в начале функции.

Аргументы функций

Питон предоставляет просто шикарные возможности за заданию аргументов функций — позиционные, именованные аргументы и их комбинации.

Но нужно понимать как осуществляется передача аргументов — т.к. в питоне все переменные это ссылки на данные, то можно догадаться, что передача осуществляется по ссылке, однако тут есть особенность — сама ссылка передаётся по значению т.е. вы можете модифицировать мутабельное значение по ссылке:

def add_element(mylist):
    mylist.append(3)

mylist = [1,2]
add_element(mylist)
print(mylist)

выполняем:

python3 arg_modify.py
[1, 2, 3]

однако нельзя затереть исходную ссылку в функции:

def try_del(mylist):
    mylist = []
    return mylist

mylist = [1,2]
try_del(mylist)
print(mylist)

исходная ссылка жива и работает:

python3 arg_kill.py
[1, 2]

Также для аргументов можно задавать значения по умолчанию, но с этим есть одна неочевидная вещь которую нужно запомнить — значения по умолчанию вычисляются один раз при определении функции, это не создаёт никаких проблем, если вы в качестве значения по умолчанию передаёте неизменяемые данные, а если передаются изменяемые данные или динамическое значение, то результат будем чуток неожиданным:

изменяемые данные:

cat arg_list.py

def func(arg = []):
    arg.append('x')
    return arg

print(func())
print(func())
print(func())

результат:

python3 arg_list.py
['x']
['x', 'x']
['x', 'x', 'x']

динамическое значение:

cat arg_now.py

from datetime import datetime

def func(arg = datetime.now()):
    return arg

print(func())
print(func())
print(func())

получаем:

python3 arg_now.py
2018-09-28 10:28:40.771879
2018-09-28 10:28:40.771879
2018-09-28 10:28:40.771879

ООП

ООП в питоне сделано весьма интересно (одни property чего стоят) и это большая тема, однако сапиенс знакомый с ООП вполне может нагуглить всё (или найти на хабре), что ему захочется, поэтому нет смысла повторяться, единственный минус стандартных классов — шаблонный код во всяких дандер методах, лично мне нравится библиотека attrs, она значительно более питоническая.
Стоит упомянуть, что так в питоне всё объекты, включая функции и классы, то классы можно создавать динамически (без использования eval) функцией type.
Также стоит почитать про метаклассы (на хабре) и дескрипторы (хабр).
Особенность, которую стоит запомнить — атрибуты класса и объекта это не одно и тоже, в случае неизменяемых атрибутов это не вызывает проблем так как атрибуты "затеняются" (shadowing) — создаются автоматически атрибуты объекта с таким же именем, а вот в случае изменяемых атрибутов можно получить не совсем то, что ожидалось:

cat class_attr.py
class MyClass:
    storage = [7,]
    def __init__(self, number):
        self.number = number

obj = MyClass(1)
obj2 = MyClass(2)

obj.number = 5
obj.storage.append(8)

print(obj2.storage, obj2.number)

получаем:

python3 class_attr.py
[7, 8] 2

как можно увидеть — изменяли obj, а storage изменился и в obj2 т.к. этот атрибут (в отличии от number) принадлежит не экземпляру, а классу.

Стандартная библиотека

Обычно стандартная библиотека питона включает отличные решения типовых проблем, однако стоит подходить критически, ибо хватает и странностей. Правда бывает и так, что то, что на первый взгляд кажется странным, в итоге оказывается наилучшим решением, просто нужно знать все условия (см. далее про range), но всё же есть и странности.

Например, идущий в комплекте модуль для модульного тестирования unittest не имеет никакого отношения к питону и сильно воняет джавой, поэтому, как говорит автор питона: "Eveybody is using py.test ...".

На замену стандартному модулю сериализации pickle делают dill, тут кстати стоит запомнить, что эти модули не подходят для обмена данными в внешними системами т.к. восстанавливать произвольные объекты полученные из неконтролируемого источника небезопасно, для таких случаев ест json (для REST) и gRPC (для RPC).

Вот другой пример — человек сделал свой модуль in-place, чтобы пофиксить кривизну и неполноту API стандартного модуля fileinput в части in place редактирования файлов.

Ну и таких случаев думаю много, раз даже мне попался не один, так что будьте бдительны и не забывайте заглядывать во всякие списки полезняшек типа awesome, я думаю, что у хорошего питонщика есть чуйка на меру питоничности решения, это кстати тема для отдельного разговора — по моим ощущениям (конечно статистика никакой на эту тему не может быть) в питон-мире уровень специалистов выше среднего, ибо часто хорошие софтины оказываются написанными на питоне, напишите в комментариях, что вы думаете по этому поводу.

Параллелизм и конкурентность

Питон предоставляет широкие возможности как для параллельного, так и для конкурентного программирования, однако не обходиться без особенностей.

Если вам нужен параллелизм, а это бывает когда ваши задачи требуют вычислений, то вам стоит обратить внимание на модуль multiprocessing.

А если в ваших задачах много ожидания IO, то питон предоставляет массу вариантов на выбор, от тредов и gevent, до asyncio.
Все эти варианты выглядят вполне пригодными для использования (хотя треды значительно больше ресурсов требуют), но есть ощущение, что asyncio потихоньку выдавливает остальных, в том числе благодаря всяким плюшками типа uvloop.

Если кто не заметил — в питоне треды это не про параллельность, я недостаточно компетентен, чтобы хорошо рассказать про GIL, но по это теме достаточно материалов, поэтому и нет такой необходимости, главное, что нужно запомнить это то, что треды в питоне (точнее в CPython) ведут себя не так как это принято в других языках программирования — они исполняются только на одном ядре, а значит не подходят для случаев когда вам нужна настоящая параллельность, однако, выполнение тредов приостанавливается при ожидании ввода-вывода, поэтому их можно использовать для конкурентности.

Странности, которые не странности

На первый взгляд мне показалось странным, что тип range не включает правую границу, но потом добрый человек подсказал мне неучу где мне нужно поучиться и оказалось, что всё вполне логично.

Также я не мог понять почему -22//10=-3, а потом, другой добрый человек, указал, что это неизбежно следует из самого математического определения, по которому, остаток не может быть отрицательным, что и приводит к такому необычному поведению для отрицательных чисел.

Автор: Alexey Shrub

Источник

Поделиться