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


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


  • Билл Любанович «Простой 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, а потом, другой добрый человек, указал, что это неизбежно следует из самого математического определения, по которому, остаток не может быть отрицательным, что и приводит к такому необычному поведению для отрицательных чисел.

Комментарии (22)


  1. Xalium
    13.10.2018 17:04

    nonlocal и global для модификации переменных на один уровень выше

    не на один уровень выше, а ближайший выше, исключая глобальную


    1. worldmind Автор
      13.10.2018 17:31

      ну это и имелось ввиду, я не знаю какая формулировка понятнее


  1. barker
    13.10.2018 17:47
    +1

    такой код <> приводит к тому, что переменные x и y ссылаются на различные данные, а такой <> нет
    Может потому что в первом случае вы присваиваете значение переменной(==ссылке) повторно, а во втором э… нет? Ничего себе идентичные ситуации. Причём тут вообще мутабельность?


    1. worldmind Автор
      13.10.2018 17:59
      -2

      Да, надо придумать пример получше


  1. DollaR84
    13.10.2018 20:07

    >>> можно догадаться, что передача осуществляется по ссылке
    Почти, но не всегда.
    Мутабельные элементы, типа списка, да передаются по ссылке.
    А иммутабельные, например строка, передаются по значению.
    Такая вот гибридная система.
    А насчет многопоточности.
    В multiprocessing есть потоки, а есть процессы.
    У каждого свое применение.
    Для задач io лучше применять потоки, а для cpu вычислений лучше работают процессы.


    1. andreymal
      14.10.2018 00:18

      А иммутабельные, например строка, передаются по значению.

      Нет, строки тоже передаются по ссылке, это элементарно проверяется с помощью is или id() в CPython


      1. DollaR84
        14.10.2018 01:19

        Вот тут вы не правы. Да id функция показывает один id, но это не из-за того, что переменная передалась по ссылке.
        В python все есть объекты, даже простые числа, например 1 — это тоже объект. А переменные не хранят никаких значений, python связывает их с объектами, точнее хранят ссылку на объект.
        Поэтому когда вы передаете строку в функцию, передается сам объект.
        Например:
        mylist = [1, 2]
        def func(x):
        x.append(3)
        print(mylist) # [1, 2, 3]
        s = 'stroka_1'
        def func2(x):
        x = 'stroka_2'
        print(s) # stroka_1
        Может несколько сумбурно объяснил, но почитайте про объекты в python. На эту тему очень много публикаций.


        1. andreymal
          14.10.2018 01:27

          это не из-за того, что переменная передалась по ссылке.

          Я не просто так упомянул CPython — в нём реализация id() для большинства объектов такова, что она возвращает именно что адрес в памяти, то есть по сути значение ссылки. Которая одинаковая, да.


          Поэтому когда вы передаете строку в функцию, передается сам объект.

          Когда я передаю строку в функцию, она принимает ссылку на объект. На тот же самый объект. Переменная — это по сути ссылка с именем. Разные переменные могут ссылаться на один и тот же объект.


          Когда вы пишете x = чтототам, вы меняете ссылку у переменной, но совершенно никак не трогаете сами объекты. Так же вы не трогаете все остальные переменные, которые ссылаются на этот объект — вы просто меняете значение этой одной переменной, и больше ничего.


          Вы сами-то свой код запускали? Мало того что вы забыли вызвать функции, так даже после их вызова код будет вести себя совершенно не так, как вы показали. Вам стоит подтянуть основы питона.


          1. DollaR84
            14.10.2018 02:53

            Да, вызовы функций случайно упустил, каюсь.
            Вы конечно правы, и я с вами согласен что передаются ссылки на объекты. Вот только статья называется Что нужно запомнить программисту, переходящему на Python. Ключевое слово переходящему с других языков. Такая система с объектами, что переменные не хранят никаких значений кроме ссылок на объекты вот и вводит в путаность переходящих.
            Например, в C++ переменная является именованной областью памяти, хранящей значение. Поэтому передача переменной по ссылки в функцию подразумевает передачу ссылки именно на переменную, значение которой потом в функции можно изменить. В python же передается значение переменной, то есть непосредственно ссылка на объект. Из-за чего и возникает вопрос как переменные передаются в функцию, по значению или по ссылке.
            Наверно хорошо сказали тут:
            jeffknupp.com/blog/2012/11/13/is-python-callbyvalue-or-callbyreference-neither


            1. khim
              14.10.2018 04:20

              Для того, чтобы не расщеплять сущности это назвали call by sharing и поскольку оно же используется в большинстве распространённых языков (Java, JavaScript и т.д.), то непонятно кого это может смущать…


              1. DollaR84
                14.10.2018 10:31

                Ну я с вами согласен, вот только языков программирования много, а не только Java и JavaScript. я, например, их не знаю вовсе, а в Python перешел с C++, где система несколько иная: call-by-value.
                Я в первом своем комментарии неправильно выразился, надо было написать, что если сравнивать с языками по типу C/C++.


    1. mcferden
      14.10.2018 12:42
      +1

      А иммутабельные, например строка, передаются по значению.

      Иммутабельность тут ни при чем. Все передается по ссылке.
      >>> s = 'abc' * 1024 * 1024
      >>> id(s)
      2381293081856
      >>> def foo(s):
      ...     return id(s)
      ...
      >>> foo(s)
      2381293081856


  1. DaneSoul
    13.10.2018 23:52

    в реальной жизни ключом может быть только число или строка
    Кортеж координат как ключ тоже вполне востребованное решение, когда мы описываем объекты на игровой доске, например.
    А вообще вопрос про ключи словаря он на понимание хешируемости, там есть не совсем очевидные вещи, когда (1, 2) — допустимый ключ, а (1, [2]) — нет, хотя оба являются кортежем!
    a = (1, [2, 3], 4)
    print(type(a))   # <type 'tuple'>
    b = {a: 1}       # TypeError: unhashable type: 'list'


  1. DaneSoul
    14.10.2018 00:12

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

    def try_del(mylist):
        mylist = []
        return mylist
    
    mylist = [1,2]
    try_del(mylist)
    print(mylist)
    В Вашем примере создалась новая локальная переменная внутри функции, из функции она сама не вернется если нет присвоения при вызове функции (последний мой пример).

    Можно так, даже без возврата обнулить коллекцию по ссылке:
    def try_del(mylist):
        mylist.clear()
    
    mylist = [1,2]
    try_del(mylist)
    print(mylist)  # []

    А если Вы хотите возвращаемое из функции значение, нужно обязательно присвоить переменной, «в никуда» оно не вернется.
    def try_del(mylist):
        mylist = [4, 5]
        return mylist
    
    mylist = [1,2]
    mylist = try_del(mylist)
    print(mylist)  # [4, 5]


    1. Xalium
      14.10.2018 09:02
      +2

      Именно " нельзя затереть исходную ссылку в функции" как раз и выполняется. Т.е. — передачей значения какой-то переменной — изнутри функции нельзя изменить адрес объекта, на который ссылается эта переменная, если конечно ее не указать в ф-и как global (выделал с помощью тире, чтобы путаницы не было).


  1. Viacheslav01
    14.10.2018 13:07

    «Оставь надежду всяк сюда входящий» больше ничего не надо :)


  1. Barafu_Albino_Cheetah
    14.10.2018 14:23

    А вот мне кажется неправильным описывать поведение функции, в точности соответствующее шикарной документации, и дополнять эпитетами «неожиданно» и «внезапно».


    1. worldmind Автор
      14.10.2018 14:30

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


  1. shoorick
    14.10.2018 18:55
    +1

    в какой-то момент перл исчерпал себя

    Это как? Надоело писать доллары перед именами скалярных переменных?
    А если серьёзно, чего не хватило в перле, из-за чего вы перешли на питон?


    1. cynovg
      15.10.2018 18:27

      У автора есть эпичный пост на эту тему, посмотрите в его профиле.


    1. worldmind Автор
      15.10.2018 20:43

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


  1. SlavikMIPT
    14.10.2018 21:24

    Спасибо — недавно перешел с C/C++ embedded в python и веб хайлоад)
    Открыл некоторые вещи незнание которых было бомбой замедленного действия — например про ссылки, формально знал об этом, но так как в основном поведение было как у переменных — бдительность усыпило