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

Все что будет рассматриваться в статье проверялось и относится к python3 и python3.4 в частности. Тут не указаны отличия от этого всего в python2 чтобы не тратить ни чье время, но пару таких отличий все же есть.

В python есть только объекты, изменяемые и неизменяемые

Так как пайтон основан на идеях Алану Кэю - автора языка программирования Smalltalk, в нем все объект, то есть вообще все.

У каждого объекта есть id. Для CPython это адресс в памяти, который никогда не меняется после создания объекта. Когда объекты сравниваются через is то на самом деле сравниваются эти id. id можно узнать функцией id().

>>> i = 1
>>> id(i)
25769969528

None, кстати, тоже объект, просто он уже заранее создан.

>>> id(None)
22477911248
>>> n = None
>>> id(n)
22477911248
>>> n is None
True 

Так как все объект - функции тоже объекты, так что:

>>> id(id)
7696581375616

Второе после id что есть у каждого объекита это его неизменный тип который можно узнать функцией type, он не меняется после создания объекта, как и id :

>>> type(i)
<class 'int'>
>>> type(id)
<class 'builtin_function_or_method'>
>>> type(None)
<class 'NoneType'>
>>> type(n)
<class 'NoneType'>

Вы можете спросить но что если я сделаю

>>> type(n)
<class 'NoneType'>
>>> n = 1
>>> type(n)
<type 'int'>

Тип поменялся, но ведь написано что он неизменный? Нет, тип не поменялся, просто вы создали новый объект и присвоели его в переменную в которой раньше был объект NoneType. Не верите? Проверьте id до и после присвоения - естественно что у разных объектов они разные.

В питоне есть объекты которые содержат ссылки на другие объекты, они называются контейнеры (list, tuple, dict...).

Итак, третье что есть у объекта это его значение. В зависимости от того может ли оно менятся объекты бывают:

  • mutable (пример - контейнеры dict и list, set)
  • immutable (пример числа - numbers, строки - stringstuple, bytes). Сами объекты в tuple могут быть мутебл.

Что бы все встало на свои места вот пример который покажет вам всю суть вещей:

>>> t = ([1,2], [11,12])  # объявил тупл листов
>>> id(t) # выведем id что бы потом знать тот ли это объект
7696577748648
>>> id(t[0]) # и посмотрем id первого листа
7696577905872
>>> t[0].append(3) # добавим в первый лист число
>>> t   # вывидем t что бы узнать получилось ли
([1, 2, 3], [11, 12]) # да то что нужно
>>> id(t[0])  # опять проверим id листа
7696577905872  # не поменялся!, а сам лист изменился, в него добавилась 3ка.
>>> id(t) # а ну ка, а что с туплом
7696577748648 # тоже не поменялся, но мы его еще и не изменели
>>> t.append([21, 22]) # а теперь добавим в тупл еще лист
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'tuple' object has no attribute 'append'
# да, так не получится, у тупла нет метода что бы добавить что-то
>>> t += ([21, 22],) # ну ладно сделаем так
>>> t
([1, 2, 3], [11, 12], [21, 22]) # да, у нас получилось
>>> id(t) 
7696577836848 # но объект тупла уже не тот! то есть мы просто пересоздали его
выражением t = t + ([21, 22],), но до сих пор не смогли изменить страрый! >>> id(t[0]) # а что с листом? 7696577905872 # он тот же >>> t[0] += [4] # а что если мы попробуем пересоздать лист в тупле, то есть по сути все таки изменить тупл Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment # да, тут мы попытались выполить t[0] = новый_список, но туплы то нельзя менять

Очень важно понимать что, скажем int-ы в пайтоне не меняют свое значение, из-за этого при всяких арифметических операциях объект меняется:

>>> c = 1
>>> id(c)
25769969528
>>> c = c + 1
>>> id(c)
25769969504

Даже когда вы итерируетесь, скажем по ренджу, каждую итерацию происходит пересоздание объекта i:

>>> for i in range(3):
...    id(i)
...
25769969552
25769969528
25769969504

Еще один интерестный экмперемент как объекты создаются при присвоении:

# числа
>>> a = 1 >>> b = 1 >>> id(a) 25769969528 >>> id(b) 25769969528 >>> a is b True
# туплы >>> a = (1,) >>> b = (1,) >>> id(a) 7696577855376 >>> id(b) 7696577855184 >>> a is b False

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

Встроенные типы объектов

Рассмотри подробнее контейнеры. Начнем с контейнеров относящихся к сиквенсам (Sequences). Сиквенс - это упорядеченное индексируемое множество объектов. Самого типа Sequance в python нет, это термин лишь используемый в документации при классификации. В сиквенсы входят list, tuple, bytearray, str, и т.п. Все сиквенсы поддерживают слайсы типо:

>>> a = [1, 2, 3, 4, 5, 6]
>>> a[1: -1]
[2, 3, 4, 5]

Также не забывайте про extended slicing:

>>> a[0: 6: 2]
[1, 3, 5]

Из личного опыта скажу что с помощью ексиендед слайсинга можно, например быстро конвертировать bytes с двухбайтными словами в противоположный endian.

Иммутебл сиквенсы это str, tuple и bytes.

Строки - это сиквенс из юникодных символов (от U+0000 до U+10FFFF). В пайтоне нет отдельного типа чар и каждый символ это просто строка размером 1.

У строки есть метод encode, который приобразует ее в bytes в указанной кодировке. Байты имеют метод decode что бы получить строку:

>>> s = "abc"
>>> type(s)
<class 'str'>
>>> b = s.encode("UTF-8")
>>> type(b)
<class 'bytes'>
>>> b[0], b[1]
(97, 98)
>>> b
b'abc'
>>> s1 = b.decode("UTF-8")
>>> s1
'abc'
>>> s1[0]
'a'

Что касается UTF-8 то обратите внимание что хоть каждый символ может иметь значение до 0x10FFFF что вмещается в 3 байта, на самом деле символ может занимать и по байту так и по 2 байта, конечно в зависимости от символов которые используются:

>>> len("абв".encode("UTF-8"))
6
>>> len("abc".encode("UTF-8"))
3

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

>>> type((1,2)) 
<class 'tuple'>
>>> type(()) # пустые скобки тоже тупл, правда бессмысленный
<class 'tuple'>
>>> type((1)) 
<class 'int'> # а вот тут пайтон думает что мы использовали скобки для групировки
>>> type((1,)) # надо так
<class 'tuple'>

Байтс тоже неизменяемые. Байтс могут задаваться с помощью b'сткрока':

>>> b'ab'
b'ab'
>>> b'\x61\x62' # тут же можем сразу прописовать хекс-байты
b'ab'

Еще его можно сделать функцией bytes(): 

>>> bytes([0x61, 0x62])
b'ab'

Там может быть не только лист а строки (тогда еще нужно передать encoding), любо любой итерейбл. Вообще у функции такой прототип:

class bytes([source[, encoding[, errors]]])

Мутебл сиквенсы это list и bytearray. Первый - изменяемая версия tuple. А второй это изменяемая версия bytes. Изменяемость означает что туда могут быть добавлены либо удалены элементы:

>>> a = bytearray([1])
>>> a.append(2)
>>> a
bytearray(b'\x01\x02')
>>> del a[0]
>>> a
bytearray(b'\x02')

С сиквенсами все. 

Дальше сеты.

set - это неупорядоченые множества иммутебл объектов. Неупорядоченные значит что индексироваться не могут. Сеты изменяемые, но могут содержать только неизменяемые объекты (об этом чуть позже). Суть в том что в них нет порядка и что в них не может быть повторяемых элементов, в омбщем реализация классического математического множества:  

Вот примеры:

>>> a = set([1, 2, 3])
>>> b = set([3, 4, 5])
>>> type(a)
<class 'set'>
>>> a.union(b)
{1, 2, 3, 4, 5}
>>> a.intersection(b)
{3}
>>> a.difference(b)
{1, 2}
>>> b.difference(a)
{4, 5}
>>> a.symmetric_difference(b)
{1, 2, 4, 5}
>>> b.symmetric_difference(a)
{1, 2, 4, 5}
>>> set([1,2]) < a # сет {1,2} есть подмножеством сета a потому что все его элементы есть в а
True
>>> set([1,2]) < b # а вот в b нет всех его элементов по этом это не подмножество
False
>>> set([1,2,2])  # в сете не может быть одинаковых значений
{1, 2}

Также есть frozenset - такие же как set, только immutable.

Интерестный вопрос, зачем вообще нужны мутбл и иммутбл версии вроде бы одних и тех же типов одновременно? То есть:

Mutable версия Immutable версия
List Tuple
Bytearray Bytes
Sets Frozen Sets

Все дело в том что все иммутебл built-in типы в пайтоне являются также hashable что дает им некоторые возможности. И аналогично все Мутебл не хешебл (но заметьте что это касается именно только built-in-ов а не ваших классов, там вы сами решаете каким будет тип). Что такое hashable нужно пояснить подробнее.

Чтобы объект был hashable он должен внутри иметь метод __hash__(), который у двух одинаковых объектов должен быстро пытаться возвращать одинаковые значения, а также он должен быть сравним с другими объектами а для этого должен реализововать метод __eq__().

Стоит подробнее объяснить почему нужно две функции а не одна - ведь вроде они одинаковые (обе предназначены для сравнения), дак зачем дублировать друг друга? Как известно хеш-функция это функция которая кадому значению из огромного множества данных сопоставляет значение из маленького множества. Использовать такие маленькие значения очень удобно - их можно легко разместить в памяти и эффективно сравнивать. То есть вместро того чтобы пробегать по двум огромным строкам и сравнивать каждый символ в памяти конечно же проще сравнить два инта которые занимают несколько байт и были вычеслены один раз при создании строки. Но тут есть одна проблемка. Так как в большом множестве объектов слишком много для того что бы всем им выдать уникальные значения из маленького множества, то хеши двух разных объектов могут совпасть (такая ситуация называется коллизией). Поэтому если пайтон найдет коллизию между двумя объектами, например в set-e, он будет вынужден использовать __eq__ функцию, которая вот уже на 100% ов должна правильно ответить на вопрос одинаковы ли объекты. Да строку в этом случае придеться пройти полностью и это тормоза. То есть по сути пайтон знает:

  • если хеши разные то объекты 100%ов разные
  • если хеши одинаковые то точно не известно одинаковые ли данные, возможно да а возможно и нет, нужно сравнить методом __eq__()

Давайте вернемся к туплам и их сравнению. Так как они immutable они также hashable .

>>> a = (1,) 
>>> b = (1,)
>>> id(a), id(b)
(7696578199680, 7696578054632)  # как видим объекты разные, лужат в разной памяти
>>> a is b    # что можно конечно просто проверить через is
False 
>>> a.__hash__(), b.__hash__()
(3430019387558, 3430019387558) # а вот хеши одинаковые
>>> a.__eq__(b) # вот так вызывается метод сравнения
True 
>>> a == b  # кстати когда вы делаете так по факту пайтон вызывает a.__eq__(b)
True
 

Хешебл объекты используются как ключи в dict-ах либо например как те же элементы в set либо frozenset. Думаю вы согласитись что так как в множестве не может быть одинаковых элементов пайтону надо это както определить. Определять по id-шникам нет смысла, потому что скажем две строки "стр" и "стр" могут быть абсолютно разными объектами. А значит надо сравнивать что-то другое, вот по этому тут и нужен хешебл объект. Его метод __hash__ пайтон сможет использовать что бы получить сравнимое представление таких элементов и метод __eq__ чтобы точно знать уникален ли элемент в множестве.

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

Дальше дикты. 

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

Но на самом деле для ключей есть тип:

>>> type({1:1, 2:2}.keys())
<class 'dict_keys'>

Ключи в дикте не упорядочены, если нужно чтобы дикт запоменал в каком порядке ключи были добавлены в модуле collections есть OrderedDict.

import collections
>>> a = collections.OrderedDict()
>>> a['w'] = {}
>>> a['a'] = {}
>>> a['s'] = {}
>>> a
OrderedDict([('w', {}), ('a', {}), ('s', {})])
>>> dict(a)
{'a': {}, 's': {}, 'w': {}}

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

Методы инстанса - это обекты которые соединяют в себе три вещи - класс, инстанса класса и колебл функцию.

class Class:
    class_attr = "class attr"
    def __init__(self):
        self.inst_attr = "inst attr"

    def method(self):
        print("Hi")

inst = Class()

Посмотрем что есть в классе и инстансе:

>>> type(inst.method) # посмотрим тип метода
<class 'method'>
>>> inst.method # выведем объект метода, заметьте что без скобок он не вызывается а просто отображается
<bound method Class.method of <__main__.Class object at 0x6ffffcd0a58>>
>>> type(Class.method) # у самого класса ведь тоже есть этот метод
<class 'function'> # но это обычная функция (она не связана - bound с инстансом)
>>> Class.method
<function Class.method at 0x6ffffcd7268>

>>> inst.method.__func__  # у метода есть ссылка на функцию которая есть в методе
<function Class.method at 0x6ffffcd7268>

>>> inst # выведем инстанс
<__main__.Class object at 0x6ffffcd0a58>
>>> inst.method.__self__ # также мы можем понять какой конкретно инстанс связан с методом
<__main__.Class object at 0x6ffffcd0a58>

По сути вызов inst.method() эквивалентен Class.method(inst) . 

Некоторые (не все) объекты имеют метод __dict__ , который возвращает список его аттрибутов.

inst.__dict__
{'inst_attr': 'inst attr'}

Так как класс - тожe объект, можем попробовать посмотреть его __dict__:

Class.__dict__
mappingproxy({
'__dict__': <attribute '__dict__' of 'Class' objects>,
'__weakref__': <attribute '__weakref__' of 'Class' objects>,
'__module__': '__main__',
'__init__': <function Class.__init__ at 0x6ffffcd71e0>,
'method': <function Class.method at 0x6ffffcd7268>,
'__doc__': None})

Функции - генераторы.

Это фнукции в которых пресутсвиет оператор yeld. Такие функции при вызове всегда сразу возвращают итератор, при этом тело функции не выполняется. При вызове iterator.__next__() итератора будет выполнятся тело функции пока не достигнит yeld который должен вернуть очередное значение. Если функция закончится ретурном или просто то итератор достигнет конца.

Собственно пример из документации:

>>> def echo(value=None):
                print("Generator are going to return value {}".format(value))
...     print("Generator start executing")
...     try:
...         while True:
...             try:
...                 print("Generator are going to return value {}".format(value))
...                 value = (yield value)
...                 print("yeld returned value to generator {}".format(value))
...             except Exception as e:
...                 print("Exception throwed in generator {}".format(e))
...                 value = e
...     finally:
...         print("Cleanup something here if you need, files, sockets etc")
...
>>>
>>> generator = echo(1)

>>> print(next(generator))
Generator start executing
Generator are going to return value 1
1

>>> print(next(generator))
yeld returned value to generator None
Generator are going to return value None
None

>>> print(generator.send(2))
yeld returned value to generator 2
Generator are going to return value 2
2

>>> print(generator.throw(TypeError, "spam"))
Exception throwed in generator spam
Generator are going to return value spam
spam

>>> generator.close()
Cleanup something here if you need, files, sockets etc