Найдите и объясните ошибку в Python-функции, предназначенной для подсчёта среднего: def mean(values=[]): return sum(values)/len(values) — опишите тесты, которые покажут проблему, и перепишите функцию правильно, обсудив побочные эффекты и контракт функции
Код def meanvalues=[]values=[]values=[]: return sumvaluesvaluesvalues/lenvaluesvaluesvalues
содержит как минимум две проблемные вещи.
1) Пустой аргумент приводит к делению на ноль Если вызвать mean или mean[][][], то lenvaluesvaluesvalues == 0 и вы получите ZeroDivisionError. Это плохая и неинформативная ошибка для клиента функции — лучше явно проверять и выбрасывать ValueError с объяснением.
2) Использование изменяемого значения по умолчанию values=[]values=[]values=[]
Хотя текущая реализация сама по себе не модифицирует список, использование как значения по умолчанию опасно: одно и то же объект-список будет разделяться между вызовами функции и может привести к неожиданному поведению, если кто-то его модифицирует. Пример классической ошибки иллюстрацияпроблемыmutabledefaultиллюстрация проблемы mutable defaultиллюстрацияпроблемыmutabledefault:
append_and_return111 -> 111
append_and_return222 -> 1,21, 21,2 # тот же список, накопление состояния между вызовами
Даже если в mean вы сейчас ничего не меняете, такой дефолт делает API хрупким и вводит неожиданность.
Тесты, показывающие проблему встилеpytestв стиле pytestвстилеpytest
1) Деление на ноль при пустом входе def test_mean_empty_list_raises_zero_division: with pytest.raisesZeroDivisionErrorZeroDivisionErrorZeroDivisionError: mean[][][]
def test_mean_no_args_raises_zero_division: with pytest.raisesZeroDivisionErrorZeroDivisionErrorZeroDivisionError: mean
2) Иллюстрация опасности mutable default пояснительныйпримерпояснительный примерпояснительныйпример
def test_mutable_default_is_shared:
если реализация бы модифицировала default, это бы проявилось# (пример демонстрационный, для функции mean это не выполняется, # но показывает риск такого подхода) pass
Правильная реализация и контракт Рекомендации по контракту функции:
Принимает итерируемый объект чисел list,tuple,generatorит.п.list, tuple, generator и т.п.list,tuple,generatorит.п..Требует хотя бы одного числа — при пустом входе должна возникать осмысленная ошибка ValueErrorилиспециальноеисключениеValueError или специальное исключениеValueErrorилиспециальноеисключение.Не должна модифицировать переданный объект.Не использовать изменяемые значения по умолчанию.
Два варианта реализации.
A) Простой, понятный конвертируетитераторвсписок,удобендлятестовконвертирует итератор в список, удобен для тестовконвертируетитераторвсписок,удобендлятестов: def meanvaluesvaluesvalues: values = listvaluesvaluesvalues # поддержка любых итерируемых if not values: raise ValueError("mean() requires at least one value") return sumvaluesvaluesvalues / lenvaluesvaluesvalues
Плюсы: простая логика; минусы: требует хранения всех элементов в памяти копируетгенераторыкопирует генераторыкопируетгенераторы.
B) Использование single-pass, без копирования всех элементов работаетспотоком/генераторомработает с потоком/генераторомработаетспотоком/генератором: def meanvaluesvaluesvalues: it = itervaluesvaluesvalues
try: first = nextititit
except StopIteration: raise ValueError("mean() requires at least one value") total = first count = 1 for x in it: total += x count += 1 return total / count
Плюсы: не копирует весь вход в память; работает и для ленивых итераторов. Минусы: немного сложнее.
Замечания
Подчеркну: не следует писать values= в сигнатуре. Лучше явно требовать аргумент: def meanvaluesvaluesvalues: ... — это ясно и безопасно.Если хочется синтаксиса mean1,2,31,2,31,2,3, можно сделать def mean∗values*values∗values: и в этом случае нужно проверить if not values: raise ValueError.Встроенный модуль statistics предоставляет statistics.meandatadatadata и делает грамотную проверку пустого входа — можно использовать его.
Итого: основной баг — использование mutable default и отсутствие проверки на пустоту делениенанольделение на нольделениенаноль. Исправление: удалить значение по умолчанию, явно проверять пустой ввод и документировать контракт итерабельность,отсутствиемодификации,поведениенапустомвводеитерабельность, отсутствие модификации, поведение на пустом вводеитерабельность,отсутствиемодификации,поведениенапустомвводе.
Код
def meanvalues=[]values=[]values=[]:
return sumvaluesvaluesvalues/lenvaluesvaluesvalues содержит как минимум две проблемные вещи.
1) Пустой аргумент приводит к делению на ноль
Если вызвать mean или mean[][][], то lenvaluesvaluesvalues == 0 и вы получите ZeroDivisionError. Это плохая и неинформативная ошибка для клиента функции — лучше явно проверять и выбрасывать ValueError с объяснением.
2) Использование изменяемого значения по умолчанию values=[]values=[]values=[] Хотя текущая реализация сама по себе не модифицирует список, использование как значения по умолчанию опасно: одно и то же объект-список будет разделяться между вызовами функции и может привести к неожиданному поведению, если кто-то его модифицирует. Пример классической ошибки иллюстрацияпроблемыmutabledefaultиллюстрация проблемы mutable defaultиллюстрацияпроблемыmutabledefault:
def append_and_returnx,lst=[]x, lst=[]x,lst=[]:
lst.appendxxx return lst
append_and_return111 -> 111 append_and_return222 -> 1,21, 21,2 # тот же список, накопление состояния между вызовами
Даже если в mean вы сейчас ничего не меняете, такой дефолт делает API хрупким и вводит неожиданность.
Тесты, показывающие проблему
встилеpytestв стиле pytestвстилеpytest
1) Деление на ноль при пустом входе
def test_mean_empty_list_raises_zero_division:
with pytest.raisesZeroDivisionErrorZeroDivisionErrorZeroDivisionError:
mean[][][]
def test_mean_no_args_raises_zero_division:
with pytest.raisesZeroDivisionErrorZeroDivisionErrorZeroDivisionError:
mean
2) Иллюстрация опасности mutable default пояснительныйпримерпояснительный примерпояснительныйпример def test_mutable_default_is_shared:
если реализация бы модифицировала default, это бы проявилось# (пример демонстрационный, для функции mean это не выполняется,# но показывает риск такого подхода)
pass
Правильная реализация и контракт
Принимает итерируемый объект чисел list,tuple,generatorит.п.list, tuple, generator и т.п.list,tuple,generatorит.п..Требует хотя бы одного числа — при пустом входе должна возникать осмысленная ошибка ValueErrorилиспециальноеисключениеValueError или специальное исключениеValueErrorилиспециальноеисключение.Не должна модифицировать переданный объект.Не использовать изменяемые значения по умолчанию.Рекомендации по контракту функции:
Два варианта реализации.
A) Простой, понятный конвертируетитераторвсписок,удобендлятестовконвертирует итератор в список, удобен для тестовконвертируетитераторвсписок,удобендлятестов:
def meanvaluesvaluesvalues:
values = listvaluesvaluesvalues # поддержка любых итерируемых
if not values:
raise ValueError("mean() requires at least one value")
return sumvaluesvaluesvalues / lenvaluesvaluesvalues
Плюсы: простая логика; минусы: требует хранения всех элементов в памяти копируетгенераторыкопирует генераторыкопируетгенераторы.
B) Использование single-pass, без копирования всех элементов работаетспотоком/генераторомработает с потоком/генераторомработаетспотоком/генератором:
def meanvaluesvaluesvalues:
it = itervaluesvaluesvalues try:
first = nextititit except StopIteration:
raise ValueError("mean() requires at least one value")
total = first
count = 1
for x in it:
total += x
count += 1
return total / count
Плюсы: не копирует весь вход в память; работает и для ленивых итераторов. Минусы: немного сложнее.
Замечания
Подчеркну: не следует писать values= в сигнатуре. Лучше явно требовать аргумент: def meanvaluesvaluesvalues: ... — это ясно и безопасно.Если хочется синтаксиса mean1,2,31,2,31,2,3, можно сделать def mean∗values*values∗values: и в этом случае нужно проверить if not values: raise ValueError.Встроенный модуль statistics предоставляет statistics.meandatadatadata и делает грамотную проверку пустого входа — можно использовать его.Итого: основной баг — использование mutable default и отсутствие проверки на пустоту делениенанольделение на нольделениенаноль. Исправление: удалить значение по умолчанию, явно проверять пустой ввод и документировать контракт итерабельность,отсутствиемодификации,поведениенапустомвводеитерабельность, отсутствие модификации, поведение на пустом вводеитерабельность,отсутствиемодификации,поведениенапустомвводе.