~={green}Определения:=~
- Функция - именованный блок кода который выполняет определенную задачу или возвращает значение.
* Функция - набор операторов, которые возвращают некоторое значение вызывающему объекту. В функцию также может быть передано ноль или более аргументов, которые могут использоваться при выполнении тела функции.
- Такое определение функций допустимо, но вторая функция перезатрет первую.
```python
def f():
    print("QWERTY")

def f():
    print("ABC")

f()
f()
```
- Любая функция в python возвращает значение. Значение возвращается при помощи оператора return. Если функция не содержит оператор return, то по умолчанию такая функция вернет значение None.
-  Функция должна выполнять только одну операцию. Она должна выполнять ее хорошо. И ничего другого она делать не должна. Если функция выполняет только те действия, которые находятся на одном уровне под объявленным именем функции, то эта функция выполняет одну операцию.
- Будьте последовательны в выражениях возврата: либо все операторы return в функции должны возвращать выражение, либо ни один из них не должен. Если какой-либо оператор return возвращает выражение, то оставшиеся операторы return тоже должны явно возвращать значение, не смотря на то, что python по умолчанию возвращает None.
Статья по оформлению [[https://pythonchik.ru/osnovy/imenovanie-v-python]]

~={green}Пример оформления простой функции:=~
```python
# is - в названии функциии
# нет лишних return
def is_even(x):
    return x%2 == 0

# пример использования без лишних сравнений
number = int(input("Введите число: "))
while is_even(number):
	print(f'{number} является четным')
	number = int(input("Введите число: "))
print(f"Вы ввели нечетное число {number}, программа завершилась")
```

~={green}Возврат кортежа=~
```python
# в return можно не ставить скобки, все равно будет возвращен кортеж
def calc_square_and_perimeter(a, b):
	retun a * b, 2 * (a + b)
```

~={green}Возврат списка=~
```python
# в данном случае нужны скобки [ ]
def calc_square_and_perimeter(a, b):
	return [a * b, 2 * (a + b)]
```

~={green}Передача аргументов=~
- При комбинированной передачи аргументов сначала должны быть указаны позиционные аргументы, а только потом именованные!
- Параметры являются локальными переменными и они определяются в момент вызова функции. В параметры присваиваются ссылки на объекты, переданные в аргументы.
- Параметры функции делятся на `_обязательные_` и `_необязательные_`.
- Никогда не используйте изменяемые объекты в качестве значений по умолчанию.
- Значение по умолчанию вычисляется только один раз при определении функции.

~={green}Изменяемые объекты в качестве параметров по умолчанию=~
* сперва присваивайте параметру значению _`None`_
* внутри функции проверяйте, если параметр принимает _`None`_, значит создаем пустой изменяемый объект
```python
def append_to_list_2(value, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(value)
    print(my_list, id(my_list))
    return my_list
```

~={green}Множественное присваивание=~
Остальные значения сохраняются в виде списка с переменную "с"
```python
a, b, *c = [1, True, 4, 6, 'hello ', 7, 9]
1, True, [4, 6, 'hello ', 7, 9]

a, *b, c = [1, True, 4, 6, 'hello ', 7, 9]
1, [True, 4, 6, 'hello ', 7], 9

a, b, *c = [1, True, 4, 6, 'hello ', 7, 9]
1, True, [4, 6, 'hello ', 7, 9]

a, *b, c = 'hello moto'
h ['e', 'l', 'l', 'o', ' ', 'm', 'o', 't'] o

a, *b, c = [1, 4]
1 [] 4
```

~={green}Передача переменного количество аргументов=~
```python
def my_func(*args)
def my_func(**kargs)

# Необязательный аргумент после *args
def my_func(*args, foo=True)

# Объединенный вариант передачи аргументов
def my_func(a, b, *args, c, d=4, **kwargs):

```
В args будет ~={red}кортеж=~.
В kwargs будет ~={red}словарь=~.

```python
# При распаковке словаря передаются значения
def print_args(a, b, c=15):
    print(a, b, c)

dct = {'a': 5, 'b': 10}
print_args(**dct)

>>> 5, 10 15

# Распаковка при передачи списка и словаря
my_list =[5, 19, 23, 88]
my_dict = {'a': 11, 'b': 23}
item_sum(*my_list, **my_dict)
```

~={green}Как нельзя передавать аргументы=~
~={red}Нельзя передавать позиционные аргументы после именованных!=~
```python
def my_func(a, b, *args):
    print(f'{a=}, {b=}, {args=}')

# Будет ошибка при передаче аргументов по ключам
my_func(a=20, b=20, 30, 40, 50)
```
~={green}Только ключевые аргументы=~
Все параметры, которые стоят справа от `*` должны принимать значения только по ключу.
Все параметры, которые стоят слева от `/` должны принимать значения только по позиции.
`*` - только ~={magenta}ключевые=~ аргументы
`/` - как ~={magenta}позиционные=~ так и ~={magenta}ключевые=~ аргументы
```python
def my_func(*, a, b):
    print(f'{a=}, {b=}')

# Так передать можно
my_func(b=30, a=40)

# А так нельзя
my_func(10, b=20)
```

~={green}Объект первого класса=~
- С ним можно работать как с переменными
- Он может быть передан в функцию как аргумент
- Он может быть возвращен из функции как результат
- Он может быть включен в другие структуры данных. Например, быть элементом словаря или списка

~={green}Пространство имен=~

**Встроенное пространство имен** (`build-in namespace`) представляет собой набор имен всех встроенных функций и объектов в Python. 
```python
# Выведет все объекты встроенного пространства имен
print(dir(__builtins__))
```

**Глобальное пространство имен** (`global namespace`) содержит имена, определенные на уровне основной программы, и создаётся сразу при запуске тела этой программы.

**Локальное пространство имен** (`local namespace`) содержит имена, которые доступны только внутри определенной функции.

Локальная область создается при вызове функций, причем каждый раз, когда вы вызываете функцию, будет создаваться новая локальная область видимости. Все параметры и имена, которые вы создаете внутри функции, образуют локальную область этой функции в пределах одного конкретного вызова. Когда функция завершает работу, локальная область видимости уничтожается, а имена забываются
```python
# Вывести локальное пространство имен
print(locals())
```

**Объемлющее пространство имен** возникает когда определение одной функции вкладывается в другую.
```python
def main_func():
    a = 1

    def inner_func():
        print('Печатаем a из inner_func', a)
        print('Печатаем b из inner_func', b) # Вот тут будет ошибка NameError

    inner_func()
    b = 2
    print('Печатаем a из main_func', a)
    print('Печатаем b из main_func', b)


main_func()
```

Для изменения переменной `a` в inner нужно использовать `nonlocal`.

~={green}Создание атрибутов функциям
=~
```python
def get_product(a: int, b: int) -> int:
    return a + b

get_product.category = 'math'
get_product.type_action = 'arithmetic operation'

# Или через setattr
setattr(obj, name_attr, value)


```

~={green}Вложенные функции (inner function)=~
Вложенные функции имеют доступ к параметрам и к переменным, определенным во внешней функции. Применяются в замыканиях.
Определяются в момент вызова родительской функции.

~={yellow}Функция высшего порядка=~ - это функция, которая может принимать другие функции в качестве аргументов и/или возвращать функции в качестве выходных данных.
```python
def get_math_func(operation='+'):
	def add(a, b):
		return a + b

# Вызов сразу с помощью дополнительного оператора вызова
print(get_math_func('+')(3, 4))
```

Аннотация вложенных функций:
```python
from typing import Callable

def get_math_func(operation: str) -> Callable[[int, int], int]:
    ...
```

~={green}Замыкания=~

Замыкание - функция, которая находится внутри другой функции и ссылается на переменные объявленные в теле объемлющей функции (нелокальные переменные).
Замыкание заставляет внутреннюю функцию сохранять состояние ее окружения при вызове. 
```python
def make_greeter(greeting):
	def greet(name):
		return f"{greenting}, {name}!"
	return greet

greeter = make_greeter("Hello")

print(greeter("World"))  # Выведет "Hello, World!"
print(greeter("Ivan"))  # Выведет "Hello, Ivan!"
```

~={green}Декораторы=~

Декоратор - это функция, которая принимает исходную функцию и возвращает другую функцию, как правило, с дополненным поведением.
```python
def decorator(func):
    def inner(*args, **kwargs):
        print('Стартуем декоратор')
        func(*args, **kwargs)
        print('Заканчиваем декоратор')
    return inner

my_fun = decorator(my_func)
```

```python
# Декоратор с параметром
def decorator_factory(a, b):
    print('Запуск функции создания декоратора')
    def decorator(fn):
        print('Запуск декоратора')
        def wrapper(*args, **kwargs):
            print('Запуск функции wrapper')
            print('Переданные аргументы: ', a, b)
            return fn(*args, **kwargs)
        return wrapper
    return decorator

@decorator_factory() # Обратите внимание на оператор вызова
def original_func(10, 20):
	print('Запуск оригинальной функции')
```

~={green}Итераторы=~
- ~={magenta}Итерация=~ - процесс перебора элементов объекта в цикле
- ~={magenta}Итерируемый объект=~ - это объект, который поддерживает итерацию.
- ~={magenta}Итератор=~ - это специальный объект-перечислитель, обязанность которого заключается в перемещении элементов коллекции. Итератор выдает по одному следующий элемент последовательности. Когда элементов больше не останется, он выбросит исключение StopIteration. Любой объект, который может взаимодействовать с функцией ~={magenta}next=~, считается итератором.
```python
# создание итератора
my_str = 'qwery'
iterator = iter(my_str)
```
next() - получает следующее значения итератора
```python
my_list = [1, 2, 3, 4, 5]
it = iter(my_list)
print(next(it)) # получаем первый элемент из итератора
print(next(it)) # получаем второй элемент из итератора
print(next(it))
print(next(it))
print(next(it)) # получаем пятый элемент из итератора

# следующий вызов next(it) выбросит исключение StopIteration
# print(next(it))
```
~={magenta}Цикл for =~при помощи ~={magenta}iter=~ запрашивает у объекта его ~={magenta}итератор=~.
```python
s = 'Python'
for letter in s:
    print(letter)

# for будет развернут в:
s = 'Python'

it = iter(s)
while True:
    try:
        print(next(it))
    except StopIteration:
        del it
        break
```
~={magenta}Итератор=~ - это не сама коллекция, а механизм обхода элементов или способ получения следующего элемента в коллекции. Поэтому итераторы намного эффективнее расходуют память в отличие от итерируемых объектов.
Функции ~={magenta}map, filter, zip, enumerate, open=~ возвращают ~={magenta}итератор=~.

~={green}Функции all и any=~
Любое непустое значение преобразуется к значению ~={magenta}True=~, пустые значения - к ~={magenta}False=~.
```python
print(bool(0))  # False
print(bool(''))  # False
print(bool([]))  # False
print(bool({}))  # False
print(bool(None))  # False
print(bool(False))  # False

print(bool(67))  # True
print(bool(-45))  # True
print(bool('.'))  # True
print(bool(' '))  # True
print(bool('qwerty'))  # True
print(bool([0]))  # True
print(bool(True))  # True
```
~={magenta}Функция all()=~ принимает итерируемую последовательность и возвращает `True` , когда все элементы в этой последовательности правдивы. Если хотя бы один элемент пустой, вернется `False`.
~={magenta}Функция any()=~ принимает на вход итерируемую последовательность и возвращает `True`, _если хотя бы один_ из элементов коллекции будет правдивым (непустым). Если все элементы последовательности ложные (пустые), вернется `False`.
```python
value = 100
condition_1 = value % 2 != 0
condition_2 = value > 50
condition_3 = value < 1000

print(all((condition_1, condition_2, condition_3)))  # False
print(any([condition_1, condition_2, condition_3]))  # True
```

```python
words = ['notice', 'deter', 'west', 'brush', 'stadium',
         'package', 'price', 'clothes', 'sword', 'apology']

check = [len(word) >= 4 for word in words]
print(all(check))  # Все ли слова больше или равны 4х символов

print(all([len(word) > 4 for word in words]))  # Все ли слова больше 4х символов

print(any([len(word) > 7 for word in words]))  # Хотя бы одно слово больше 7х символов
print(any([len(word) >= 7 for word in words]))  # Хотя бы одно слово больше или равно 7 символов
```

~={yellow}Функция map=~

Возвращает итератор
```python
# Преобразование к списку
a = [-1, 2, -3, 4, -5]
b = list(map(abs, a))
print(b)

# Можно передавать методы объктов
a = ['hello', 'hi', 'good morning']
b = list(map(str.upper, a))
print(b)

# Можно передвать lambda
list_strings = ['hello', 'hi', 'good morning']
b = list(map(lambda x: x[::-1], list_strings))
print(b)
```

~={yellow}Функция map и несколько последовательностей=~

```python
def power(base: int, exp: int) -> int:
    return base ** exp

bases = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
exponents = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

result = list(map(power, bases, exponents))
```

~={yellow}Функция filter=~

```python
def greater_10(x: int) -> bool:
	return x > 10

numbers = [14, 0, 5, 79, 645, 7952, 18, 0, 192, 471]
numbers_gt_10 = filter(greater_10, numbers)
```
