<jupyter_start><jupyter_text>`Практикум по программированию на языке Python` `Занятие 2: Пользовательские и встроенные функции, итераторы и генераторы` `<NAME> (<EMAIL>)` `Москва, 2021`  `Функции range и enumerate`<jupyter_code>r = range(2, 10, 3)
print(type(r))

for e in r:
    print(e, end=' ')
for index, element in enumerate(list('abcdef')):
    print(index, element, end='   ')<jupyter_output>0 a   1 b   2 c   3 d   4 e   5 f<jupyter_text>`Функция zip`<jupyter_code>z = zip([1, 2, 3], 'abc')
print(type(z))

for a, b in z:
    print(a, b, end='  ')
for e in zip('abcdef', 'abc'):
    print(e)
for a, b, c, d in zip('abc', [1,2,3], [True, False, None], 'xyz'):
    print(a, b, c, d)<jupyter_output>a 1 True x
b 2 False y
c 3 None z<jupyter_text>`Определение собственных функций`<jupyter_code>def function(arg_1, arg_2=None):
    print(arg_1, arg_2)

function(10)
function(10, 20)<jupyter_output>10 None
10 20<jupyter_text>Функция - это тоже объект, её имя - просто символическая ссылка:<jupyter_code>f = function
f(10)

print(function is f)<jupyter_output>10 None
True<jupyter_text>`Определение собственных функций`<jupyter_code>retval = f(10)
print(retval)
def factorial(n):
    return n * factorial(n - 1) if n > 1 else 1  # recursion

print(factorial(1))
print(factorial(2))
print(factorial(4))<jupyter_output>1
2
24<jupyter_text>`Передача аргументов в функцию`Параметры в Python всегда передаются по ссылке<jupyter_code>def function(scalar, lst):
    scalar += 10
    print(f'Scalar in function: {scalar}')

    lst.append(None)
    print(f'Scalar in function: {lst}')
s, l = 5, []
function(s, l)

print(s, l)<jupyter_output>Scalar in function: 15
Scalar in function: [None]
5 [None]<jupyter_text>`Передача аргументов в функцию`<jupyter_code>def f(a, *args):
    print(type(args))
    print([v for v in [a] + list(args)])
    
f(10, 2, 6, 8)
def f(*args, a):
    print([v for v in [a] + list(args)])
    print()

f(2, 6, 8, a=10)
def f(a, *args, **kw):
    print(type(kw))
    print([v for v in [a] + list(args) + [(k, v) for k, v in kw.items()]])

f(2, *(6, 8), **{'arg1': 1, 'arg2': 2})<jupyter_output><class 'dict'>
[2, 6, 8, ('arg1', 1), ('arg2', 2)]<jupyter_text>`Области видимости переменных`В Python есть 4 основных уровня видимости:- Встроенная (buildins) - на этом уровне находятся все встроенные объекты (функции, классы исключений и т.п.)- Глобальная в рамках модуля (global) - всё, что определяется в коде модуля на верхнем уровне- Объемлюшей функции (enclosed) - всё, что определено в функции верхнего уровня- Локальной функции (local) - всё, что определено в функции нижнего уровняЕсть ещё области видимости переменных циклов, списковых включений и т.п.  `Правило разрешения области видимости LEGB при чтении`<jupyter_code>def outer_func(x):
    def inner_func(x):
        return len(x)
    return inner_func(x)
print(outer_func([1, 2]))<jupyter_output>2<jupyter_text>Кто определил имя `len`?- на уровне вложенной функции такого имени нет, смотрим выше- на уровне объемлющей функции такого имени нет, смотрим выше- на уровне модуля такого имени нет, смотрим выше- на уровне builtins такое имя есть, используем его  `На builtins можно посмотреть`<jupyter_code>import builtins

counter = 0
lst = []
for name in dir(builtins):
    if name[0].islower():
        lst.append(name)
        counter += 1
    
    if counter == 5:
        break

lst<jupyter_output><empty_output><jupyter_text>Кстати, то же самое можно сделать более pythonic кодом:<jupyter_code>list(filter(lambda x: x[0].islower(), dir(builtins)))[: 5]<jupyter_output><empty_output><jupyter_text>`Локальные и глобальные переменные`<jupyter_code>x = 2
def func():
    print('Inside: ', x)  # read
    
func()
print('Outside: ', x)
x = 2
def func():
    x += 1  # write
    print('Inside: ', x)
    
func()  # UnboundLocalError: local variable 'x' referenced before assignment
print('Outside: ', x)
x = 2
def func():
    x = 3
    x += 1
    print('Inside: ', x)
    
func()
print('Outside: ', x)<jupyter_output>Inside:  4
Outside:  2<jupyter_text>`Ключевое слово global`<jupyter_code>x = 2
def func():
    global x
    x += 1  # write
    print('Inside: ', x)
    
func()
print('Outside: ', x)
x = 2
def func(x):
    x += 1
    print('Inside: ', x)
    return x
    
x = func(x)
print('Outside: ', x)<jupyter_output>Inside:  3
Outside:  3<jupyter_text>`Ключевое слово nonlocal`<jupyter_code>a = 0
def out_func():
    b = 10
    def mid_func():
        c = 20
        def in_func():
            global a
            a += 100
            
            nonlocal c
            c += 100
            
            nonlocal b
            b += 100

            print(a, b, c)
            
        in_func()
    mid_func()

out_func()<jupyter_output>100 110 120<jupyter_text>__Главный вывод:__ не надо злоупотреблять побочными эффектами при работе с переменными верхних уровней  `Пример вложенных функций: замыкания`- В большинстве случаев вложенные функции не нужны, плоская иерархия будет и проще, и понятнее- Одно из исключений - фабричные функции (замыкания)<jupyter_code>def function_creator(n):
    def function(x):
        return x ** n

    return function

f = function_creator(5)
f(2)<jupyter_output><empty_output><jupyter_text>Объект-функция, на который ссылается `f`, хранит в себе значение `n`  `Анонимные функции`- `def` - не единственный способ объявления функции- `lambda` создаёт анонимную (lambda) функциюТакие функции часто используются там, где синтаксически нельзя записать определение через `def`<jupyter_code>def func(x): return x ** 2
func(6)
lambda_func = lambda x: x ** 2  # should be an expression
lambda_func(6)
def func(x): print(x)
func(6)
lambda_func = lambda x: print(x ** 2)  # as print is function in Python 3.*
lambda_func(6)<jupyter_output>36<jupyter_text>`Встроенная функция sorted`<jupyter_code>lst = [5, 2, 7, -9, -1]
def abs_comparator(x):
    return abs(x)

print(sorted(lst, key=abs_comparator))
sorted(lst, key=lambda x: abs(x))
sorted(lst, key=lambda x: abs(x), reverse=True)<jupyter_output><empty_output><jupyter_text>`Встроенная функция filter`<jupyter_code>lst = [5, 2, 7, -9, -1]
f = filter(lambda x: x < 0, lst)  # True condition
type(f)  # iterator
list(f)<jupyter_output><empty_output><jupyter_text>`Встроенная функция map`<jupyter_code>lst = [5, 2, 7, -9, -1]
m = map(lambda x: abs(x), lst)
type(m)  # iterator
list(m)<jupyter_output><empty_output><jupyter_text>`Ещё раз сравним два подхода`Напишем функцию скалярного произведения в императивном и функциональном стилях:<jupyter_code>def dot_product_imp(v, w):
    result = 0
    for i in range(len(v)):
        result += v[i] * w[i]
    return result
dot_product_func = lambda v, w: sum(map(lambda x: x[0] * x[1], zip(v, w)))
print(dot_product_imp([1, 2, 3], [4, 5, 6]))
print(dot_product_func([1, 2, 3], [4, 5, 6]))<jupyter_output>32
32<jupyter_text>`Функция reduce``functools` - стандартный модуль с другими функциями высшего порядка.Рассмотрим пока только функцию `reduce`:<jupyter_code>from functools import reduce

lst = list(range(1, 10))

reduce(lambda x, y: x * y, lst)<jupyter_output><empty_output><jupyter_text>`Итерирование, функции iter и next`<jupyter_code>r = range(3)

for e in r:
    print(e)
it = iter(r)  # r.__iter__() - gives us an iterator

print(next(it))
print(it.__next__())
print(next(it))
print(next(it))<jupyter_output>0
1
2<jupyter_text>`Итераторы часто используются неявно`Как выглядит для нас цикл `for`:<jupyter_code>for i in 'seq':
    print(i)<jupyter_output>s
e
q<jupyter_text>Как он работает на самом деле:<jupyter_code>iterator = iter('seq')
while True:
    try:
        i = next(iterator)
        print(i)
    except StopIteration:
        break<jupyter_output>s
e
q<jupyter_text>`Генераторы`- Генераторы, как и итераторы, предназначены для итерирования по коллекции, но устроены несколько иначе- Они определяются с помощью функций с оператором `yield` или генераторов списков, а не вызовов `iter()` и `next()`- В генераторе есть внутреннее изменяемое состояние в виде локальных переменных, которое он хранит автоматически- Генератор - более простой способ создания собственного итератора, чем его прямое определение- Все генераторы являются итераторами, но не наоборот - Примеры функций-генераторов:    - `zip`    - `enumerate`    - `reversed`    - `map`    - `filter`  `Ключевое слово yield`- `yield` - это слово, по смыслу похожее на `return`- Но используется в функциях, возвращающих генераторы- При вызове такой функции тело не выполняется, функция только возвращает генератор- В первых запуск функция будет выполняться от начала и до `yield`- После выхода состояние функции сохраняется- На следующий вызов будет проводиться итерация цикла и возвращаться следующее значение- И так далее, пока не кончится цикл каждого `yield` в теле функции- После этого генератор станет пустым  `Пример генератора`<jupyter_code>def my_range(n):
    yield 'You really want to run this generator?'

    i = -1
    while i < n:
        i += 1
        yield i
gen = my_range(3)
while True:
    try:
        print(next(gen), end='   ')
    except StopIteration:  # we want to catch this type of exceptions
        break
for e in my_range(3):
    print(e, end='   ')<jupyter_output>You really want to run this generator?   0   1   2   3<jupyter_text>`Особенность range``range` не является генератором, хотя и похож, поскольку не хранит всю последовательность<jupyter_code>print('__next__' in dir(zip([], [])))
print('__next__' in dir(range(3)))<jupyter_output>True
False<jupyter_text>Полезные особенности:- объекты `range` неизменяемые (могут быть ключами словаря)- имеют полезные атрибуты (`len`, `index`, `__getitem__`)- по ним можно итерироваться многократно  `Модуль itetools`- Модуль представляет собой набор инструментов для работы с итераторами и последовательностями- Содержит три основных типа итераторов:    - бесконечные итераторы    - конечные итераторы    - комбинаторные итераторы- Позволяет эффективно решать небольшие задачи вида:    - итерирование по бесконечному потоку    - слияние в один список вложенных списков    - генерация комбинаторного перебора сочетаний элементов последовательности    - аккумуляция и агрегация данных внутри последовательности  `Модуль itetools: примеры`<jupyter_code>from itertools import count

for i in count(start=0):
    print(i, end='  ')
    if i == 5:
        break
from itertools import cycle
 
count = 0
for item in cycle('XYZ'):
    if count > 4:
        break
    print(item, end='  ')
    count += 1<jupyter_output>X  Y  Z  X  Y<jupyter_text>`Модуль itetools: примеры`<jupyter_code>from itertools import accumulate

for i in accumulate(range(1, 5), lambda x, y: x * y):
    print(i)
from itertools import chain

for i in chain([1, 2], [3], [4]):
    print(i)<jupyter_output>1
2
3
4<jupyter_text>`Модуль itetools: примеры`<jupyter_code>from itertools import groupby
 
vehicles = [('Ford', 'Taurus'), ('Dodge', 'Durango'),
            ('Chevrolet', 'Cobalt'), ('Ford', 'F150'),
            ('Dodge', 'Charger'), ('Ford', 'GT')]
 
sorted_vehicles = sorted(vehicles)
 
for key, group in groupby(sorted_vehicles, lambda x: x[0]):
    for maker, model in group:
        print('{model} is made by {maker}'.format(model=model, maker=maker))
    
    print ("**** END OF THE GROUP ***\n")<jupyter_output>Cobalt is made by Chevrolet
**** END OF THE GROUP ***

Charger is made by Dodge
Durango is made by Dodge
**** END OF THE GROUP ***

F150 is made by Ford
GT is made by Ford
Taurus is made by Ford
**** END OF THE GROUP ***
{'fraction_non_alphanumeric': 0.12146843390303454, 'fraction_numerical': 0.019009417509591908, 'mean_word_length': 4.292570373788648, 'pattern_counts': {'":': 0, '<': 97, '<?xml version=': 0, '>': 97, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '28', 'dir': 'jupyter-structured-clean-dedup', 'id': '526', 'license': 'MIT', 'n_tokens_mistral': 4745, 'n_tokens_neox': 4993, 'n_words': 1246, 'path': 'lectures/02-functions.ipynb', 'repo_name': 'sir-rois/mipt-python', 'size': '11485'}