Hello World – Telegram
Hello World
1.68K subscribers
71 photos
6 videos
3 files
161 links
Be so good that you cannot be ignored. And then, go one step beyond.
Download Telegram
Generators & iterators (2/3)

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

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

Существует бесчисленное множество примеров использования итераторов. Например, все функции пакета itertools возвращают итераторы.

📌Некоторые из них генерируют бесконечные последовательности:
>>> from itertools import count
>>> counter = count(start=1)
>>> next(counter)
1
>>> next(counter)
2


📌Некоторые создают бесконечные последовательности из конечных:
>>> from itertools import cycle
>>> colors = cycle(['red', 'white', 'blue'])
>>> next(colors)
'red'
>>> next(colors)
'white'
>>> next(colors)
'blue'
>>> next(colors)
'red’


📌Или конечные последовательности из бесконечных:
>>> from itertools import islice
>>> colors = cycle(['red', 'white', 'blue'])
>>> limited = islice(colors, 0, 4)
>>> for x in limited:
... print(x)
red
white
blue
red


Генераторы
Итак, наконец-то мы добрались до самого интересного! Генераторы являются безумно интересной и полезной штукой в Python. Генератор — это особый, более изящный случай итератора.

Используя генератор, вы можете создавать итераторы, вроде того, что мы рассмотрели выше, используя более лаконичный синтаксис, и не создавая при этом отдельный класс с методами __iter__() и __next__().

Давайте внесём немного ясности:

любой генератор является итератором (но не наоборот!);
следовательно, любой генератор является "ленивой фабрикой", возвращающей значения последовательности по требованию.
Вот пример итератора последовательности чисел Фибоначчи в исполнении генератора:
>>> def fib():
... a, b = 0, 1
... while True:
... yield b
... a, b = b, a + b
...
>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]


Ну как? Не находите, что это выглядит намного элегантнее простого итератора? Весь секрет кроется в ключевом слове yield. Давайте разберёмся, что к чему.

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

Теперь, когда происходит вызов функции fib
f = fib()

будет создан и возвращён экземпляр генератора. К данному моменту ещё никакого кода внутри функции не выполняется и генератор ожидает вызова.

Дальше созданный экземпляр генератора передаётся в качестве аргумента функции islice, которая также возвращает итератор, следовательно также никакого кода функции fib пока что не выполняется.

И, наконец, происходит вызов list() с передачей в качестве аргумента итератора, возвращённого функцией islice(). Чтобы list() смогла построить объект списка на основе полученного аргумента, ей необходимо получить все значения из этого аргумента. Для этого list() выполняет последовательные вызовы метода next() итератора, возвращённого вызовом islice(), который, в свою очередь, выполняет последовательные вызовы next() в экземпляре итератора f.

#tips #generators
Python's list comprehensions

vals = [expression 
for value in collection
if condition]


Это эквивалентно следующему коду:
vals = []
for value in collection:
if condition:
vals.append(expression)


Пример:
>>> even_squares = [x * x for x in range(10) if not x % 2]
>>> even_squares
[0, 4, 16, 36, 64]


#tips
Generators & iterators (3/3)

Типы генераторов
В Python существует два типа генераторов: генераторные функции и генераторные выражения. Генератором является любая функция, содержащая yield в любом месте её кода. Пример такого генератора мы только что рассмотрели. Другой разновидностью генераторов в Python являются генераторные выражения, своим видом напоминающие списковые выражения. Использование генераторных выражений бывает очень хорошим решением в ряде случаев.

Предположим, вы используете следующую конструкцию, чтобы создать список квадратов чисел:
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> [x * x for x in numbers]
[1, 4, 9, 16, 25, 36]


Или, то же самое, но в виде множества:
>>> {x * x for x in numbers}
{1, 4, 36, 9, 16, 25}


Или в виде словаря:
>>> {x: x * x for x in numbers}
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}


Или, наконец, используя генераторное выражение (обратите внимание, это НЕ кортеж!):
>>> lazy_squares = (x * x for x in numbers)
>>> lazy_squares
at 0x10d1f5510>


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

📌Если вы всё ещё не используете генераторы и хотели бы начать делать это, попробуйте начать с того, что обратите внимание на все участки вашего кода, имеющие вид:
def something():
result = []
for ... in ...:
result.append(x)
return result


И замените их генераторами:
def iter_something():
for ... in ...:
yield x


#tips #generators
uuid

Стандартный модуль uuid — быстрый и простой способ сгенерировать UUID (universally unique identifier, глобально уникальный идентификатор).
import uuid

user_id = uuid.uuid4()
print(user_id)


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

Существует более 2¹²² возможных UUID. Это более 5 ундециллионов или 5,000,000,000,000,000,000,000,000,000,000,000,000.

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

Вполне недурно для двух строк кода.

#uuid
Global Variables

В Python любая переменная, задекларированная вне функции, по-умолчанию определена как глобальная.
c = 1

def add():
print(c)

add()


#выведет
1


Но если попытаться изменить эту переменную внутри функции, мы наткнемся на ошибку:
c = 1

def add():
c = c + 1
print(c)

add()


#выведет
UnboundLocalError: local variable ‘c’ referenced before assignment


Поэтому существует специальное слово global:
c = 1

def add()
global c
c = c + 1
print(‘Inside:’, c)

add()
print(‘Outside:’, c)


#выведет
Inside: 2
Outside: 2


📌Любая переменная внутри функции по-умолчанию определена как локальная
📌Любая переменная снаружи функции по-умолчанию определена как глобальная
📌Мы используем ключевое слово global для того, чтобы модифицировать глобальную переменную внутри функции
📌Использование global снаружи функции не даст никакого эффекта

#tips #global
type annotations (1/2)

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

Сохраняя идею динамической утиной типизации в современных версиях Python (3.6+) поддерживает аннотации типов переменных, полей класса, аргументов и возвращаемых значений функций.

Пример:
def my_add(a: int, b: int) -> int:
return a + b


def greet(name: str) -> str:
return ‘Hello ‘ + name


Стрелочка после аргументов функции показывает возвращаемый тип.

#typing
Python’s slices

Срезы в листах Python могут быть использованы и без индексов
Вот несколько забавных и полезных вещей:

# Очистка всех элементов листа:
>>> lst = [1, 2, 3, 4, 5]
>>> del lst[:]
>>> lst
[]


# Замена всех элементов листа
# без создания нового объекта:
>>> a = lst
>>> lst[:] = [7, 8, 9]
>>> lst
[7, 8, 9]
>>> a
[7, 8, 9]
>>> a is lst
True


# Создание копии листа:
>>> b = lst[:]
>>> b
[7, 8, 9]
>>> b is lst
False


#tips #list
Argument unpacking

Небольшой пример того, почему пайтон так хорош:
def myfunc(x, y, z):
print(x, y, z)


tuple_vec = (1, 0, 1)
dict_vec = {'x': 1, 'y': 0, 'z': 1}


Теперь мы можем распаковать кортеж (да и любой другой итерируемый объект) с помощью оператора *
>>> myfunc(*tuple_vec)
1, 0, 1


Распаковка словарей происходит с помощью **
>>> myfunc(**dict_vec)
1, 0, 1


#tips
Collections

Находим наиболее часто встречающийся элемент в контейнере:

>>> import collections
>>> c = collections.Counter('helloworld’)


>>> c
Counter({'l': 3, 'o': 2, 'e': 1, 'd': 1, 'h': 1, 'r': 1, 'w': 1})


>>> c.most_common(3)
[('l', 3), ('o', 2), ('e', 1)]


#tips #collections
Permutations

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

>>> import itertools
>>> for p in itertools.permutations('ABCD'):
... print(p)

('A', 'B', 'C', 'D')
('A', 'B', 'D', 'C')
('A', 'C', 'B', 'D')
('A', 'C', 'D', 'B')
('A', 'D', 'B', 'C')
('A', 'D', 'C', 'B')
('B', 'A', 'C', 'D')
('B', 'A', 'D', 'C')
('B', 'C', 'A', 'D')
('B', 'C', 'D', 'A')
('B', 'D', 'A', 'C')
('B', 'D', 'C', 'A')
('C', 'A', 'B', 'D')
('C', 'A', 'D', 'B')
('C', 'B', 'A', 'D')
('C', 'B', 'D', 'A')
('C', 'D', 'A', 'B')
('C', 'D', 'B', 'A')
('D', 'A', 'B', 'C')
('D', 'A', 'C', 'B')
('D', 'B', 'A', 'C')
('D', 'B', 'C', 'A')
('D', 'C', 'A', 'B')
('D', 'C', 'B', ‘A’)


4! = 24

#tips #permutations
itertools

Допустим, мы хотим получить какой то конечный список элементов из бесконечного генератора. В модуле itertools есть удобная функция islice, позволяющая осуществить срез генератора fib.

from itertools import islice

def fib():
a, b = 0, 1
while True:
yield b
a, b = b, a + b

In : list(islice(fib(), 6))
Out: [1, 1, 2, 3, 5, 8]


#itertools
reduce

Продолжая тему itertools. Например, нужно написать функцию, которая принимает список чисел и перемножает их. То есть [1,2,3,4,5,6] даст 1*2*3*4*5*6.

📌Способ 1.

from functools import reduce

In : reduce(lambda x, y: x*y, [1, 2, 3, 4, 5, 6])
Out: 720


📌Способ 2.

import operator
import functools

In : functools.reduce(operator.mul, [1, 2, 3, 4, 5, 6], 1)
Out: 720


📌Способ 3.

import numpy as np

In : np.prod(np.array([1, 2, 3, 4, 5, 6]))
Out: 720


#tips #itertools #reduce
Что выведет следующий код?

In[1]: qwe = ‘qwe’
In[2]: qwe[0] = ‘a’
In[3]: print(qwe)
Правильный ответ

Строки — это неизменяемый тип данных, поэтому произойдет ошибка в строке 2 (простите за тавтологию). Когда вы пытаетесь сделать например вот это:
a = ‘a’
a += ‘b’


переменная a на второй строчке заменяется уже другой переменной.

📎изменять строки нельзя, можно только заменять их новыми

#string
Целые числа.

Пользователей Python зачастую привлекает его простота, важной частью которой является динамическая типизация. В то время как в языках со статической типизацией, таких как С, необъодимо явным образом объявлять все переменные, языки с динамической типизацией этого не требуют.
Например, в языке С можно описать операцию так:
int result = 0;
for(int i=0; i<100; ++i){
result += i;
}


На языке Python это запишется так:
result = 0
for i in range(100):
result += i


Главное отличие: в языке С типы данных каждой переменной объявлены явным образом.
В Python мы можем сделать так:
x = 4
x = “four”


В С это могло бы привести к ошибке компиляции или же неопределенному поведению:
int x = 4;
x = “four” // сбой


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

Стандартная реализация языка Python написана на С. Это значит, что каждый объект Python — замаскированная структура С. Посмотрев на исходный код Python 3.4, можно узнать что описание целого типа (long), фактически выглядит так:
struct _longobject {
long ob_refcnt;
PyTypeObject *ob_type;
size_t ob_size;
long ob_digit[1];


Отдельное целое число в языке Python 3.4 состоит из 4 частей:
📌 ob_refcnt - счетчик ссылок, с помощью которого происходит выделение и освобождение памяти
📌 ob_type - тип переменной
📌 ob_size - задает размер следующих элементов данных
📌 ob_digit - содержит фактическое целочисленное значение

Это значит, что существует некоторая избыточность при хранении целого числа в языке Python по сравнению с целыми числами в компилируемых языках.
📎 Целое число в Python — указатель на место в памяти, где хранится вся информация об объекте, включая байты, содержащие само целочисленное значение.
📎 Это влечет за собой последствия в виде дополнительных расходов памяти и/или вычислительного времени, что становится заметно в структурах, объединяющих значительное количество таких объектов.
# Python 3 имеет модуль
# для работы с ip адресами:

>>> import ipaddress

>>> ipaddress.ip_address('192.168.1.2')
IPv4Address('192.168.1.2')

>>> ipaddress.ip_address('2001:af3::')
IPv6Address('2001:af3::’)


# Подробности здесь:
# https://docs.python.org/3/library/ipaddress.html
Теоретический_миниму.pdf
10.3 MB
Отличная книга по CS, рекомендую к прочтению.