Домашнее задание №2#

Это домашнее задание нацеленно не только на закрепление материала второй лекции, но и на закрепление ряда хороших python практик. Всего от вас требуется реализовать и протестировать 6 функций:

  1. фильтрация пропущенных значений;

  2. арифметическое среднее;

  3. геометрическое среднее;

  4. гармоническое среднее;

  5. медиана;

  6. расстояние между объектами.

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

Проверка типа объекта#

Иногда необходимо проверять тип объекта, например, для того чтобы в зависимости от типа совершить те или иные действия. В C/C++ для этого нередко применяют ad hoc полиморфизм, т.е. пишут несколько реализаций одной и той же функции с разной сигнатурой, каждая из которых может по своему обрабатывать набор аргументов соответствующих типов.

В python достичь схожего эффекта просто так невозможно, т.к. а) тип параметров не указывается при объявлении функции, б) перегружать функции в python нельзя: повторное объявление функции с таким же именем затрет первичное объявление, даже если количество формальных параметров отлично. Хотя в python и принято писать такой код, который универсально обрабатывает объекты разных типов, иногда может возникнуть необходимость в модификации поведения функции в зависимости от типа аргумента. Особенно часто такое будет возникать на этапах освоения python, в период пока вы все ещё думаете в терминах C/C++.

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

isinstance(объект, класс)

Например, проверим, является ли объект "Hello, world!" целым числом или строкой?

is_integer = isinstance("Hello, world!", int)
is_string = isinstance("Hello, world!", str)

print(f'{is_integer=}, {is_string=}')
is_integer=False, is_string=True

Начинающие иногда используют вместо isinstance(объект, класс) конструкцию вида type(объект) == класс. Эти выражения во многих случаях эквиваленты, но считается хорошей практикой всегда прибегать к первому варианту. Разница между этими двумя выражениями наблюдается тогда, когда объект не является экземпляром непосредственно указанного класса, а является экземпляром производного класса.

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

from numbers import Number

x = 1

print(f"{type(x) == Number = }")
print(f"{isinstance(x, Number) = }")
type(x) == Number = False
isinstance(x, Number) = True

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

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

from numbers import Number


def check_types(x):
    t = type(x)
    is_number = isinstance(x, Number)
    is_float = isinstance(x, float)
    is_integer = isinstance(x, int)
    print(f"type={str(t):17}: {is_number=}, {is_float=}, {is_integer=}")


check_types(42)
check_types(3.14)
check_types(1 + 1j)
type=<class 'int'>    : is_number=True, is_float=False, is_integer=True
type=<class 'float'>  : is_number=True, is_float=True, is_integer=False
type=<class 'complex'>: is_number=True, is_float=False, is_integer=False

Сравнение с None#

Значение None в python имеет множество применений. Например, если функция завершается без ключевого слова return, то эта функция все равно вернет значение None.

def hello():
    print("Hello")

x = hello()
print(f"{x=}")
Hello
x=None

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

def hello():
    print("Hello")
    return

x = hello()
print(f"{x=}")
Hello
x=None

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

Повсеместное употребление None обуславливается не только договором сообщества программистов, но и тем, как это само значение устроенно. Дело в том, что None реализован по шаблону программирования одиночка: на самом деле есть класс NoneType у которого гарантировано всегда есть единственный экземпляр None. Иными словами в исходном коде программы может многократно встречаться None, но в момент исполнения программы гарантируется, что всего будет создан единственный объект None, а при попытке создания нового будет всегда возвращаться ссылка на уже существующий.

x = None
y = None
print(x is y)
True

В связи с этим в python принято сравнивать с None, используя именно ключевое слово is, а не оператор ==, т.е. принято делать так

if x is None:
    ...

if y is not None:
    ...

а не так

if x == None:
    ...

if y != None:
    ...

Note

Напомним, что оператор is проверяет, указывают ли два имени на один и тот же объект, а оператор == сравнивает два объекта на равенство. Оператор is всегда работает быстрее, т.к. сравнивает всего-навсего значения ссылок, а оператор == кроме того, что всегда медленнее, но ещё и может быть перегружен не очевидным образом.

Проверка на пустоту коллекции#

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

def tail(collection):
    return collection[-1]

print(f'{tail("xyz")=}')        # строка
print(f'{tail([1, 2, 3])=}')    # список
print(f'{tail((42, 43, 44))=}') # кортеж
print(f'{tail(range(15))=}')    # диапазон
tail("xyz")='z'
tail([1, 2, 3])=3
tail((42, 43, 44))=44
tail(range(15))=14

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

tail("")
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Input In [8], in <cell line: 1>()
----> 1 tail("")

Input In [7], in tail(collection)
      1 def tail(collection):
----> 2     return collection[-1]

IndexError: string index out of range

Модифицируем код этой функции таким образом, чтобы она возвращала None, если коллекция пустая (а значит нет ни первого ни последнего элемента). Начинающему программисту на ум может прийти следующая конструкция.

if len(collection) == 0: 
    return None

Т.е. непосредственное сравнение количества элементов с 0. Такой код будет работать, но в сообществе python считается хорошей практикой более элегантная конструкция.

  • Если требуется проверить, содержит коллекция collection хотя бы один элемент, то применяется конструкция вида

if collection:
    return None
  • Если требуется проверить, пуста ли коллекция collection , то применяется конструкция вида

if not collection:
    return None

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

Модифицируем функцию tail используя одну из вышеприведенных конструкций.

def tail(collection):
    if not collection: # <----
        return None
    return collection[-1]


print(f'{tail("")=}')           # пустая строка
print(f'{tail([])=}')           # пустой список
print(f'{tail(())=}')           # пустой кортеж
print(f'{tail(range(0, 0))=}')  # пустой диапазон

print(f'{tail("xyz")=}')        # строка
print(f'{tail([1, 2, 3])=}')    # список
print(f'{tail((42, 43, 44))=}') # кортеж
print(f'{tail(range(15))=}')    # диапазон
tail("")=None
tail([])=None
tail(())=None
tail(range(0, 0))=None
tail("xyz")='z'
tail([1, 2, 3])=3
tail((42, 43, 44))=44
tail(range(15))=14

Задачи#

1. Фильтрация пропущенных значений. None#

Предположим некий физический измерительный прибор выдаёт результаты измерения в виде списка, при этом если при каком-то очередном измерении произошел сбой и результатам измерения доверять нельзя, то в качестве результата измерения записывается значение None. В качестве примера рассмотрим следующий список:

\[ [3.14, \mathrm{None}, \mathrm{None}, 2.71, 1.41] \]

Значение None на второй и третей позициях этого списка указывает на то, что эти значения были пропущены. Напишите функцию, которая принимает на вход список такого вида L, а возвращает список, состоящий только из непропущенных значений списка L.

def filter_missed_values(L):
    ...


L = [3.14, None, None, 2.71, 1.41]
print(filter_missed_values(L)) # [3.14, 2.71, 1.41]

Дополнительное необязательное задание

Предположим, что вместо пропущенных значений прибор записывает не значение None, а значение NaN (Not a Number), которое в python можно получить выражением float("nan") или в модуле math. Как изменится код функции filter_missed_values в таком случае?

2. Средние значения#

Ваша задача реализовать 3 функции, принимающих на вход последовательность чисел \(L=[x_1,\ldots,x_n]\) и возвращающих одно из средних. Случай пустой последовательности обработайте особым образом.

\[ A\bigl(\{x_1, \ldots, x_n\}\bigr) = \dfrac{1}{n}\sum_{i=1}^n x_i. \]
def arithmetic_mean(x):
    ...
\[ G\bigl(\{x_1, \ldots, x_n\}\bigr) = \sqrt[n]{x_1 x_2 \cdots x_n}. \]
def geometric_mean(x):
    ...
\[ H\bigl(\{x_1, \ldots, x_n\}\bigr) = \dfrac{n}{\sum\limits_{i=1}^n\dfrac{1}{x_i}}. \]
def harmonic_mean(x):
    ...
\[\begin{split} M\bigl(\{x_1, \ldots, x_n\}\bigr) = \begin{cases} X_{\frac{n+1}{2}}, & \text{если } n \text{ нечетно}, \\ \left(X_{\frac{n}{2}} + X_{\frac{n}{2} + 1}\right) \biggr/ 2, & \text{если } n \text{ четно}. \end{cases} \end{split}\]

Здесь \(X_1, \ldots, X_n\) — упорядоченный по возрастанию значений ряд чисел \(x_1, \dots, x_n\), от которого вычисляется медиана.

def median(x):
    ...

Note

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

Дополнительное задание

Протестируйте функцию arithmetic_mean на списках [1.e20, 0., 1., -1.e20] и [10**20, 0, 1, -10**20] и попытайтесь объяснить результат. Сравните результат с работой функции mean из модуля стандартной библиотеки statistics на тех же примерах.

3. Расстояние между объектами#

Предположим, что вы пишите программный комплекс для анализа данных как числовой природы, так и текстового характера. Вам известно, что вам придется вычислять расстояния между объектами обоих классов. В качестве расстояния между двумя числами \(x,\, y\in\mathbb{R}\) вы выбрали модуль разницы

\[ \rho(x, y) = |x-y|, \]

а в качестве расстояния между строками одинаковой длины \(s_1\) и \(s_2\) вы выбрали расстояние Хэмминга

\[ \rho(s_1, s_2) = |\{s_1^i \neq s_2^i\mid i=0,\ldots,n-1\}|, \]

где \(s_1^i\) и \(s_2^i\)\(i\)-е символы строк \(s_1\) и \(s_2\) соответственно, \(n = \mathrm{len}(s_1) = \mathrm{len}(s_2)\) — длина каждой строки, а \(|\cdot|\) обозначает мощность множества. Иными словами расстояние Хэмминга — число позиций, в которых соответствующие символы двух слов одинаковой длины различны.

Реализуйте функцию distance(x, y), которая будет возвращать расстояние Хэмминга, если x и y строкового типа, и модуль разницы, если x и y числового типа.

def distance(x, y):
    ...


print(distance(42, 13))                 # 29
print(distance(3.14, 2.71))             # 0.43000000000000016
print(distance(1 + 2j, 3 + 4j))         # 2.8284271247461903
print(distance("абв", "вба"))           # 2
print(distance("Течение", "Течении"))   # 1