'''Тема урока: необязательные и именованные аргументы
Позиционные аргументы
Необязательные аргументы
Именованные аргументы
Аннотация. Урок посвящен необязательным и именованным аргументам.

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

Рассмотрим следующий код:

def diff(x, y):
    return x - y


res = diff(10, 3)    # используем позиционные аргументы
print(res)
Такой код выведет число 77. При вызове функции diff() первому параметру x будет соответствовать первый переданный
аргумент, 1010, а второму параметру y  — второй аргумент, 33.

В Python можно использовать не только позиционные, но и именованные аргументы.

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

Рассмотрим следующий код:

def diff(x, y):
    return x - y


res = diff(x=10, y=3)   # используем именованные аргументы
print(res)
Такой код по-прежнему выведет число 77. При вызове функции diff() мы явно указываем, что параметру x соответствует
аргумент 1010, а параметру y— аргумент 33.

Использование именованных аргументов позволяет нарушать их позиционный порядок при вызове функции. Порядок упоминания
именованных аргументов не имеет значения!

Мы можем вызвать функцию diff() так:

res = diff(y=3, x=10)
и получить тот же результат 77.

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

Когда стоит применять именованные аргументы
Каких-то строгих правил на этот счёт не существует. Однако широко практикуется такой подход: если функция принимает
больше трёх аргументов, нужно хотя бы часть из них указать по имени. Особенно важно именовать значения аргументов,
когда они относятся к одному типу, ведь без имен очень трудно понять, что делает функция с подобным вызовом.

Рассмотрим определение функции make_circle() для рисования круга:

def make_circle(x, y, radius, line_width, fill):
    # тело функции
Вызвать такую функцию можно так:

make_circle(200, 300, 17, 2.5, True)
Тут непросто понять, какие параметры круга задают числа 200200, 300300 или 1717.

Сравните:

make_circle(x=200, y=300, radius=17, line_width=2.5, fill=True)
Такой код читать значительно проще!

В соответствии с PEP 8 при указании значений именованных аргументов при вызове функции знак равенства не окружается
 пробелами.

Когда значение аргументов очевидно, можно их не именовать. Да, очевидность относительна, но обычно легко понять, что
 скрывается за значениями при вызове функции point3d(7, 50, 13) или rgb(7, 255, 45). В первом случае переданные
 аргументы – координаты точки в трехмерном пространстве, во втором – значения компонент red, green, blue некоторого
  цвета. Тут можно ориентироваться только на здравый смысл, жестких правил нет.

Мы уже сталкивались с именованными аргументами, когда вызывали функцию print().

Приведенный ниже код:

print('aaaa', 'bbbbb', sep='*', end='##')
print('cccc', 'dddd', sep='()')
print('eeee', 'ffff', sep='123', end='python')
использует именованные аргументы sep и end и выводит:

aaaa*bbbbb##cccc()dddd
eeee123ffffpython
Используя именованные аргументы sep и end можно не беспокоиться, какой из них указать первым.

   Напомним, что значение по умолчанию для sep=' ' (символ пробела),  а для end='\n' (символ перевода строки).

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

Для функции diff() код:

res = diff(10, y=3)   # используем позиционный и именованный аргумент
работает как полагается, при этом параметру x соответствует значение 1010.

Приведенный ниже код:

res = diff(x=10, 3)   # используем позиционный и именованный аргумент
приводит к возникновению ошибки SyntaxError: positional argument follows keyword argument.

Необязательные аргументы
Бывает, что какой-то параметр функции часто принимает одно и то же значение. Например, для функции print() создатели
языка Python установили значения параметров sep и end равными символу пробела и символу перевода строки, поскольку
эти значения используют наиболее часто.

Другим примером служит функция int() , преобразующая строку в число. Она принимает два аргумента:

первый аргумент: строка, которую нужно преобразовать в число;
второй аргумент: основание системы счисления.
Это позволяет ей считывать числа в различных системах счисления.

Приведенный ниже код, преобразует двоичное число 101101:

num = int('101', 2)     # аргумент 2 указывает на то, что число 101 записано в двоичной системе
В переменной num будет храниться число 55, так как 101_{2} = 5_{10}101
2
​
 =5
10
​
 .

Но чаще всего эта функция используется для считывания из строки чисел, записанных в десятичной системе счисления.
Утомительно каждый раз писать 1010 вторым аргументом. В таких ситуациях Python позволяет задавать некоторым параметрам
значения по умолчанию. У функции int() второй параметр по умолчанию равен 1010, и потому можно вызывать эту функцию с
 одним аргументом. Значение второго подставится автоматически.

Чтобы задать значение параметра по умолчанию, в списке параметров функции достаточно после имени переменной написать
 знак равенства и нужное значение.

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

Рассмотрим все тоже определение функции make_circle(), которая рисует круг:

def make_circle(x, y, radius, line_width, fill):
    # тело функции
Поскольку обычно нам нужно рисовать круг с шириной линии, равной 11 с заливкой, то логично установить данные значения
 в качестве значений по умолчанию:

def make_circle(x, y, radius, line_width=1, fill=True):
    # тело функции
Теперь для того, чтобы нарисовать стандартный круг, то есть круг имеющий ширину линии, равную 11 с заливкой, мы
вызываем функцию так:

make_circle(100, 50, 20)
или так:

make_circle(x=100, y=50, radius=20)
Если вам хочется поменять ширину линии и заливку, то вы легко можете это сделать:

make_circle(x=100, y=50, radius=20, fill=False)                   # line_width=1, fill=False
make_circle(x=100, y=50, radius=20, line_width=3)                 # fill=True, line_width=3
make_circle(x=100, y=50, radius=20, line_width=5, fill=False)     # line_width=5, fill=False
   В соответствии с стандартом PEP 8 и при объявлении аргументов по умолчанию пробел вокруг знака равенства не ставят.

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

Рассмотрим определение функции append(), где в качестве значения по умолчанию используется изменяемый тип данных
(список, тип list):

def append(element, seq=[]):
    seq.append(element)
    return seq
Вызывая функцию append() следующим образом:

print(append(10, [1, 2, 3]))
print(append(5, [1]))
print(append(1, []))
print(append(3, [4, 5]))
получим ожидаемый вывод:

[1, 2, 3, 10]
[1, 5]
[1]
[4, 5, 3]
А если вызовем функцию append() так:

print(append(10))
print(append(5))
print(append(1))
 получим не совсем ожидаемый вывод:

[10]
[10, 5]
[10, 5, 1]
Что происходит? Значение по умолчанию для параметра создается единожды при определении функции (обычно при загрузке
 модуля) и становится атрибутом (свойством) функции. Поэтому, если значение по умолчанию изменяемый объект, то его
 изменение повлияет на каждый следующий вызов функции.

Чтобы посмотреть значения по умолчанию, можно использовать атрибут __defaults__.

Приведенный ниже код:

def append(element, seq=[]):
    seq.append(element)
    return seq

print('Значение по умолчанию', append.__defaults__)
выводит:

Значение по умолчанию ([],)
Приведенный ниже код:

def append(element, seq=[]):
    seq.append(element)
    return seq

print('Значение по умолчанию', append.__defaults__)
print(append(10))
print('Значение по умолчанию', append.__defaults__)
print(append(5))
print('Значение по умолчанию', append.__defaults__)
print(append(1))
print('Значение по умолчанию', append.__defaults__)
выводит:

Значение по умолчанию ([],)
[10]
Значение по умолчанию ([10],)
[10, 5]
Значение по умолчанию ([10, 5],)
[10, 5, 1]
Значение по умолчанию ([10, 5, 1],)
Для решения проблемы можно использовать константу None в качестве значения параметра по умолчанию, а в теле функции
устанавливать нужное значение:

def append(element, seq=None):
    if seq is None:
        seq = []
    seq.append(element)
    return seq
Вызывая функцию append() следующий образом:

print(append(10))
print(append(5))
print(append(1))
 получим ожидаемый вывод:

[10]
[5]
[1]
   Подход, основанный на значении None, общепринятый в Python.

Примечания
Примечание 1. При написании функций стоит указывать более важные параметры первыми.

Примечание 2. Именованные аргументы часто используют вместе со значениями по умолчанию.

Примечание 3. Именованные и позиционные аргументы не всегда хорошо ладят друг с другом. При вызове функции позиционные
 аргументы должны обязательно идти перед именованными аргументами.

Примечание 4. Параметр в программировании — принятый функцией аргумент. Термин «аргумент» подразумевает, что конкретно
и какой конкретной функции было передано, а параметр — в каком качестве функция применила это принятое. То есть
вызывающий код передает аргумент в параметр, который определен в описании (заголовке) функции.

Parameter → Placeholder (заполнитель принадлежит имени функции и используется в теле функции);
Argument → Actual value (фактическое значение, которое передается при вызове функции).
Подробнее можно почитать тут.

Примечание 5. Отличная статья на хабре про аргументы функций в Python.'''


#
# def fancy(length, char1, char2):
#     return (char1 + char2) * length + char1
#
#
# print(fancy(5, '-', '*'))


# def fancy(length, char1='-', char2='*'):
#     return (char1 + char2) * length + char1
#
#
# print(fancy(3))


# def fancy(length, char1='-', char2='*'):
#     return (char1 + char2) * length + char1
#
#
# print(fancy(3, '.'))


# def fancy(length, char1='-', char2='*'):
#     return (char1 + char2) * length + char1
#
#
# print(fancy(2, ':', '|'))


# def fancy(length, char1='-', char2='*'):
#     return (char1 + char2) * length + char1
#
#
# print(fancy(4, char2='#'))


# def fancy(length, char1='-', char2='*'):
#     return (char1 + char2) * length + char1
#
#
# print(fancy(char2='$', length=3))


# def fancy(length, char1='-', char2='*'):
#     return (char1 + char2) * length + char1
#
#
# print(fancy(char2='!'))


'''Напишите функцию matrix(), которая создает, заполняет и возвращает матрицу заданного размера. При этом 
(в зависимости от переданных аргументов) она должна вести себя так:

matrix() — создает матрицу 1 \times 11× 1, в которой единственное число равно нулю;
matrix(n) — создает матрицу n \times nn× n, заполненную нулями;
matrix(n, m) — создает матрицу из nn строк и mm столбцов, заполненную нулями;
matrix(n, m, value) — создает матрицу из nn строк и mm столбцов, в которой каждый элемент равен value.
При создании функции пользуйтесь аргументами по умолчанию.

Примечание 1. Приведнный ниже код:

print(matrix())                   # матрица 1 × 1 из 0
print(matrix(3))                  # матрица 3 × 3 из 0
print(matrix(2, 5))               # матрица 2 × 5 из 0
print(matrix(3, 4, 9))            # матрица 3 × 4 из 9
должен выводить:

[[0]]
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
[[9, 9, 9, 9], [9, 9, 9, 9], [9, 9, 9, 9]]
Примечание 2. Вызывать функцию matrix() не нужно, требуется только реализовать ее.'''



# def matrix(n=None,m=None,a=0):
#     if n==None and m==None:
#         n=1
#         m=1
#     elif m==None:
#         m=n
#     return [[a for i in range(m)] for j in range(n)]


# def matrix(n=1, m=0, value=0):
#     return [[value for _ in range(m if m else n)] for _ in range(n)]


# def matrix(n=1, m=0, x=0):
#     matrix = [[x for i in range(m or n)] for j in range(n)]
#     return matrix

# matrix = lambda n=1, m=0, value=0: [[value] * (m or n)] * n



# def matrix(n=1, m=0, v=0):
#     if m == 0:
#         m = n
#     return [[v for j in range(m)] for i in range(n)]
#
#
# print(matrix(2, 5))
#



