Comments 97
# coding: utf-8
def positive_answer(response):
print("Положительный ответ: {}".format(response))
def negative_answer(response):
print("Отрицательный ответ: {}".format(response))
def incorrect_answer(response):
print("Введено некорректное значение: {}".format(response))
actions_map = {
("y", "Y", "д", "Д"): positive_answer,
("n", "N", "н", "Н"): negative_answer,
}
def are_you_sure4():
while True:
print("Вы уверены? [Д/н (Y/n)]: ")
response = raw_input()
for options, action in actions_map.iteritems():
if response in options:
action(response)
break
else:
incorrect_answer(response)
are_you_sure4()
Если словарь всегда используется как список пар, то нафига словарь, а не список или кортеж? И у вас будет NameError (raw_input) (и AttributeError (iteritems) если вы исправите): приведённый код запускается на Python-3*: иначе ни приведённые регулярки, ни .upper() не работали бы с русским текстом.
P.S. Код работает для python 2.7
Если нужно итерировать парами значений, то я стараюсь на всякий случай использовать словарь, потому что так проще красивее получить доступ к списку только первых или к списку только вторых значений.
Ага. А читающий ваш код будет думать, что вы откуда‐то будете брать ("y", "Y", "д", "Д")
, а не то, что вам нужен список пар. Словарь предполагает, что основной или один из основных методов доступа — __getitem__
, а вы его не собираетесь использовать.
P.S. Код работает для python 2.7
А приведённый в статье там не работает.
А читающий ваш код будет думать, что вы откуда‐то будете брать («y», «Y», «д», «Д»), а не то, что вам нужен список пар.
Сомневаюсь что читающий код начнет пристально вглядываться в actions_map вне контекста его использования.
Словарь предполагает, что основной или один из основных методов доступа — __getitem__, а вы его не
собираетесь использовать.
Словарь предполагает, а программист располагает. В оригинале звучит как, программировать нужно не на языке, а с использованием языка.
Вообще, свое решение привел для того что бы:
- показать пример легко расширяемого кода — легко добавлять новые варианты и увеличивать размер кода для условий
- структурированный код (перекликается с расширяемостью) — легко можно найти какие варианты есть вообще и куда вносить изменения
- использование функций как объектов
- как бонус — отсутсвие повторяющихся if-ов
А приведённый в статье там не работает.
Я не ставил целью кого-либо запутать. Я работаю с 2.7.
Я не ставил целью кого-либо запутать. Я работаю с 2.7.
Ещё одна причина не использовать ваш вариант со словарём: в случае со списком пар вы будете использовать итератор что в Python-3*, что в 2.6+. А в случае со словарём вам нужен либо dict.items()
, который в 2* генерирует список, а не итератор, либо 2to3 (что не удобно при разработке и вообще, по слухам там много ошибок; если нужно писать для обоих Python, лучше писать сразу на совместимом «диалекте» Python-23), либо какой‐то wrapper (класс‐наследник от dict, у которого items == iteritems; внешняя функция вместо метода; …).
Вообще, свое решение привел для того что бы:
Ничего из списка не является основанием для предпочтения словаря перед списком пар.
структурированный код (перекликается с расширяемостью) — легко можно найти какие варианты есть вообще и куда вносить изменения
Более структурированный вариант был бы
RETRY = ()
class Questioner:
def __init__(self, question, answers, default_action=None):
self.prompt = question
self.answers = answers
self.default_action = default
def ask(self, *args, **kwargs):
while True:
response = input(self.prompt)
action = self.answers.get(response.lower(), self.default_action)
if action is None:
raise ValueError('Invalid response')
if action(response, *args, **kwargs) is not RETRY:
break
def positive_answer(response):
print("Положительный ответ: {}".format(response))
def negative_answer(response):
print("Отрицательный ответ: {}".format(response))
def incorrect_answer(response):
print("Введено некорректное значение: {}".format(response))
return RETRY
qner = Questioner('Вы уверены? [Д/н (Y/n)]: ', {
'y': positive_answer,
'д': positive_answer,
'n': negative_answer,
'н': negative_answer,
}, default_action=incorrect_answer)
are_you_sure5 = qner.ask
are_you_sure5()
Основное отличие — не введение класса, а то, что incorrect_answer отделён от основных ответов, но определяется рядом, а не где‐то далеко в функции. Это и без класса можно было сделать, просто я обычно предпочитаю так.
Кстати, а чего это habrahabr превращает двойные новые строки в одинарные? По PEP8 в ряде случаев предполагаются двойные.
Зачем делать ключами actions_map кортежи и потом еще и итерироваться по ним, когда уже есть мап?
Не понимаю о чем речь.
Чтобы сэкономить пару строчек когда?
Если получается без особых усилий сэкономить хотя бы одну строчку (без ущерба читаемости конечно), то я обычно экономлю.
Второй момент: слишком много функций, которые выводят одинаковый шаблон. Предлагаю сделать вот такой простенький вариант:
def are_you_sure():
print("Вы уверены? [Д/н (Y/n)]: ")
response = input()
answer_templates = {
True: lambda r: "Положительный ответ: {}".format(r),
False: lambda r: "Отрицательный ответ: {}".format(r),
None: lambda r: "Введено некорректное значение: {}".format(r)
}
answer_types = {"Д": True, "Y": True, "Н": False, "N": False}
answer_key = answer_types.get(response.upper(), None)
print(answer_templates[answer_key](response))
are_you_sure()
def are_you_sure():
print("Вы уверены? [Д/н (Y/n)]: ")
response = input()
out_result = {True: "Положительный ответ", False: "Отрицательный ответ", None: "Введено некорректное значение"};
answer_types = {"Д": True, "Y": True, "Н": False, "N": False}
answer_key = answer_types.get(response.upper(), None)
print(out_result[answer_key]+": {}".format(answer_key))
are_you_sure()
А в остальном самый лучший вариант.
>>> import datetime
>>> get_message = lambda answer_type, dtm, answer: '{0}: Your answer is {1}. You said "{2}"'.format(dtm, answer_type, answer)
>>> answers = {True: 'positive', False: 'negative', None: 'bullshit'}
>>> print(get_message(answers[None], datetime.datetime.now(), 'Use While loop or regexp in this task'))
2016-05-24 03:49:20.928235: Your answer is bullshit. You said "Use While loop or regexp in this task"
Не итерации по псевдо-таблице, а честную таблицу:
def positive() :
print "принято!"
return True
def negative() :
print "отказано."
return False
def mistake() :
print "ошибка, попробуйте ещё"
actions_map_source = {
( "y","Y","д","Д" ) : positive,
( "n", "N", "н", "Н" ) : negative,
}
# вот этот шаг
action_map = { k:v for (ks,v) in actions_map_source.iteritems() for k in ks }
def are_you_sure(what):
answers = '/'.join(sorted(k for k in action_map.keys()))
while True:
response = raw_input("вы уверены, что %s? [%s] " % (what, answers))
action = action_map.get(response, None)
if action:
return action()
else:
mistake()
print are_you_sure("надо думать")
А еще у вас при запуске будет
«надо думать YNynДдНн»
что не очень показательно, что это выбор ответов, а не пожелание к размышлению
Так-то можно
answers = '/'.join( ''.join(ks) for ks in actions_map_source.keys() ) # "yYдД/nNнН"
А что до фреймворка, то всё зависит от количества и способа повторений в коде.
Для однократного использования можно и самый тупой вариант наколбасить.
while True:
response = raw_input("вы уверены, что пора валить? [Y/н]")
if action == 'Y' or action == '':
print "Time to go!"
exit()
elif action == 'н':
print "Было бы предложено."
break
else:
print "Определитесь, товарищ!"
continue
То есть стандартные ошибки программиста, который не может написать fizzbuzz? ;)
{"Y","y","Д","д"}
), а не list. Всё дело в том, что проверка на вхождение у set имеет сложность О(1), а у list — O(N) (линейно растёт с увеличением количества элементов). На четырёх элементах конечно не заметно, но чем больше элементов — тем больше будет отрыв.Способ №4, кстати, работает некорректно, так как ответ «деревня», например, воспримется как положительный ответ, хотя должен быть некорректный. Всё дело в том, что re.match пытается сматчить начало строки, и не обязательно, если вся остальная часть строки под регулярку не подходит, re.match вернёт объект матча, который приводится к True
На четырёх элементах можно увидеть и обратное: проверка наличия в списке будет быстрее, чем проверка наличия в множестве. В pypy-2.6.0-r1, например, timeit.timeit('"д" in {"Y","y","Д","д"}')
в 4—5 раз медленнее timeit.timeit('"д" in ("Y","y","Д","д")')
и timeit.timeit('"д" in ["Y","y","Д","д"]')
, в jython-2.7.0 — в 1,5 (список)—2,5 (кортеж) медленнее, в python-2.7.10-r1 — в 2 раза медленнее. В остальных случаях (python-3.4.3-r1, pypy3-2.4.0) медленнее (…)
/[…]
(где‐то в два раза). Короче, не стоит заниматься фигнёй на пустом месте, сама проверка на наличие может и медленнее, но этот список/множество создаётся при каждом вызове функции, а создание множества затратнее создания списка. Ну и коэффициент при единице у множества и при N у списка совершенно разный.
Спекуляции вроде «у этого O(N), у того O(1)» без собственно тестов (причём, желательно, на реальном коде, а не как я проверял выше) легко могут привести к деоптимизации. Слишком много факторов: может твой список процессор нормально пихает в кэш, а множество нет. Может при 1 у множества огромный коэффициент. Может PyPy оптимизирует x in […]
в несколько десятков тактов, а x in {…}
(из‐за того, что так реже пишут) — в несколько сотен. И т.д.
Пока что по моим тестам получается, что лучше не заморачиваться и писать кортеж.
Дело в том, что при указании сложности мы забываем про коэффициенты и «незначительные» члены, а на практике получается, что сортировка вставками работает быстрее быстрой сортировки на малых наборах данных.
Ну и про преждевременную оптимизацию, как корень всех бед тоже не стоит забывать.
Написал комментарий скорей всего про то, чтобы мышление программиста не забывало про set, чтобы использовать его для проверок на вхождение. Ведь завтра может придётся написать программу, где придётся искать элемент среди сотни или тысячи элементов.
В любом случае спасибо за комментарии. Внесу дополнения в статью.
from distutils.util import strtobool
if response in 'yYдД':
# ...
elif response in 'nNнН':
# ...
if response[:1] in 'yYдД':
#…
elif response[:1] in 'nNнН':
#…
>>> list('1234')
['1', '2', '3', '4']
>>> '1234'.split()
['1234']
while True:
response = input('Вы уверены? [Д/н (Y/n)]: ')
msgs = {
'yYдД': 'Положительный ответ',
'nNнН': 'Отрицательный ответ',
}
defalut = 'Введено некорректное значение'
msg = next((v for k, v in msgs.items() if response in k), defalut)
print('{}: {}'.format(msg, response))
Ваш вариант принимает ответы 'Yд'
, 'Nн'
и т.д. К тому же, несмотря на кажущуюся изящность предложенного решения, идет разбор словаря (окей, это py3, список ключей достается бесплатно), а сам поиск происходит в строках 'yYдД'
, т.е. вроде как используется самый быстрый инструмент, но по факту ради красоты.
Чтобы два раза не вставать:
В этом способе тоже можно использовать принцип отсечения лишнего и сократить регулярные выражения до вида [YД] и [NН] с помощью метода upper().
Достаточно добавить флаг игнорирования регистра:
import re
positive = ('y', 'yes', 'д', 'да', 'si', 'ja')
positive_check = re.compile(r'^({})$'.format('|'.join(positive)), re.I) # вот это re.I
messages = {
0: 'Отрицательный ответ',
1: 'Положительный ответ',
}
while True:
response = input('Вы уверены? [Д/н (Y/n)]: ')
choice = response or '' # Оно может быть None?
match = positive_check.match(choice)
msg = messages[bool(match)]
print(msg)
# Для любителей экономия строк и дебаггеров-экстрасенсов
response = input('Вы уверены? [Д/н (Y/n)]: ')
print(messages[bool(positive_check.match(response))])
Если нужно добавлять новые значения будет другой разработчик — оно читается и разбирается хуже, чем решение из предыдущего комментария, где позитивный и негативный ответ явно разделены.
while True: там зачем? Оно же всегда верно и всегда будет выполняться. По идее можно выбросить и функция все равно будет работать.
Также стоит отметить, что обычно написание одного из вариантов с заглавной буквы (Yes
/Да
в данном случае) обычно означает, что это вариант по умолчанию и может быть выбран вводом пустой строки. Примеры в статье это не обрабатывают в текущий момент.
response = input('Вы уверены? [Д/н (Y/n)]: ').lower()
print(True if response in ['y', 'д', ''] else False)
Вопрос не в упрощении кода (проверка response == ''
слегка усложнит его), а в уменьшении ментальной нагрузки на пользователя программы.
Т. е. если я вижу Upgrade packages? [y/N]
, то нажав просто энтер ожидаю, что установка будет отменена; набрав t
(промазав по y
) ожидаю, что меня спросят повторно.
Если это не так (нестандартное поведение), то мне придётся специально помнить о том, что эта программа ведёт себя не по-человечески и надо вводить y
и только y
. Примерно, как если бы принималась только заглавная n
и только прописная y
.
def check_answer():
response = input('Вы уверены? [Д/н (Y/n)]: ').lower()
if response in {'y', 'д', ''}:
return True
elif response in {'n', 'н'}:
return False
else:
print('Ответом должен быть один из символов из набора [Д, д, Y, y, Н, н, N, n]')
return check_answer()
if check_answer():
print('Действие подтверждено')
else:
print('Действие отменено')
все-таки должно быть две проверки, на наличие и корректность ввода, и уже потом на значение.
то есть по-хорошему это нечто вроде
if ( input =~ /^[YДNН]$/i ) {
if ( input =~ /YД/i ) {
# yes
}
else {
# no
}
}
else {
raise ArgumentError.new( "input" );
}
Искренне недоумеваю, а какая разница? Ловите на Perl:
if ( $input =~ m/^[YДNН]$/i ) {
if ( $input =~ m/YД/i ) {
# yes
}
else {
# no
}
}
else {
die "input";
}
Вот на C++, навскидку (могут быть мелкие ошибки, не писал лет двадцать):
#include <boost/regex.hpp>
using namespace boost;
smatch m;
if ( regex_match( input, m, "^[YyДдNnНн]$" ) {
if ( regex_match( input, m, "[YyДд]" ) {
// yes
}
else {
// no
}
}
else {
throw new ArgumentError( "input" );
}
Странные фанатики языка мне минус поставили. Знал бы Python, написал бы на нем. Но какая разница, если речь о концепциях, как обрабатывать входные данные. Язык вторичен в данном случае.
Ну и, конечно, наверняка в Python есть аналог вот этому:
case input
when /YД/i
# Yes
when /NН/i
# No
else
throw ArgumentError.new "input"
end
и вправду, ребячество. я уже посмотрел, встроенного switch в python нет, но очень легко реализуется.
Ничуть не спорю. В том же Perl использование given/Switch сейчас не рекомендовано (experimental smartmatch feature).
Я хотел в первую очередь сместить акцент на то, чтобы разнести проверку на корректность значения и само значение. А оператор выбора — это по настроению.
Я добавлял в лисп и ребол то, что мне было нужно. И важна именно возможность добавления, а не то, что добавлял конкретный программист, ведь судить о языке по опыту одного человека не очень умно, согласитесь?
Switch в языке высокого уровня, где всё есть объекты — излишек. Нужен либо паттерн-матчинг, как в скале, например, либо ничего.
Всё потому что switch хорошо работает только с примитивами. Свич в подавляющем количестве языков сравнивает через ==. Для чисел и строк это нормально. Для объектов он бесполезен:
class Foo:
def __init__(self, bar, baz):
self.bar = bar
self.baz = baz
a = Foo(1, 2)
b = Foo(1, 2)
a == b # => False
Сейчас я в основном пишу на java и js, тут есть свич. Но в коде его надобность практически отсутствует.
Лямбды не дают спать? Ну язык не поощряет длинные анонимные функции.
И как показала практика, все эти маленькие, но разумные и логически обоснованные ограничения и сделали язык таким популярным: код на питоне почти всегда легко читать и писать. В отличие от тех же лиспов, где ты хз, функция ли это, или же макрос, который изменяет синтаксис. Возможностей в лиспах много, но почему-то популярными они не становятся.
Switch на объектах все еще полезен, ведь есть возможность перегружать __eq__ (или как оно там правильно).
То, что в твоем коде не используется switch — не показатель, в примере в этом посте он, например. может быть использован.
Ну язык не поощряет длинные анонимные функции
Это откровенная неправда, Гвидо был не прочь сделать нормальные лямбды, да вот только череда провальных решений в дизайне языка не позволила сделать ничего лучше, чем то что мы получили в итоге. Как минимум многострочные лямбды не парсятся из-за того, что отступы являются частью семантики. Но теперь принято называть откровенно плохой дизайн «не поощрением».
В отличие от тех же лиспов, где ты хз, функция ли это, или же макрос, который изменяет синтаксис.
Аргумент уровня — в динамическом языке ты хз, X — это число, объект, функция или строка.
Возможностей в лиспах много, но почему-то популярными они не становятся.
Не почему-то, а именно поэтому. Возможности всегда ведут к усложнению, многие крутейшие вещи в CL существуют только в нем самом, поэтому его изучение это не просто новый синтаксис и парочка фич, а целые новые концепции и системы (например я долго ломал голову над тем как же работает condition-restart system). Поэтому программистов на лиспе в условный промежуток времени появляется меньше, чем на более простых языках, что ведет к уменьшению шансов что он будет взят на очередной проект, что уменьшает стимул его изучения и это замкнутый круг.
Простите за некропостинг, но все же.
В Ruby оператор switch (который case) работает по-особенному.
На объектах есть перегружаемый оператор ===, который в книге Мацумото называется case-equality operator, созданный специально для него.
Собственно:
case(object)
when cond1 then 1
when cond2 then 2
end
эквивалентно:
if cond1 === object
1
elsif cond2 === object
2
end
Эта часть языка мне не нравится, поскольку может ввести в заблуждение (и вводит!).
Поэтому лично я предпочитаю использовать его форму без параметра (где не используется ===) ради простых веток:
case
when then something1
when cond2 then something2
when cond3 then something3
end
vs
if cond1
something1
elsif cond2
something2
elsif cond3
something3
end
def check_answer():
response = input('Вы уверены? [Д/н (Y/n)]: ').lower()
if response in ['y', 'д']:
return "True"
elif response in ['n', 'н']:
return "False"
else:
return 'Ругань'
кортежи неизменяемы. Строка тоже кортеж, но при этом еще и типизированный.
2) Как связано то, что кортежи неизменяемы с тем, что итерация или проверка на вхождение по ним якобы шустрее аналогичной для списка?
3) Нет, строка в Python совсем не тоже самое, что кортеж.
4) А теперь самое главное. Возьмём кусок вышеприведенного кода с заменой списка на кортеж и без замены, вот так:
>>> test1 = lambda response: response in ('y', 'д')
>>> test2 = lambda response: response in ['y', 'д']
>>>
затем дизасемблируем обе функции, сначала ту, что с кортежем:
>>> from dis import dis
>>> dis(test1)
1 0 LOAD_FAST 0 (response)
3 LOAD_CONST 3 (('y', 'д'))
6 COMPARE_OP 6 (in)
9 RETURN_VALUE
>>>
всё логично и предсказуемо, а теперь ту, что со списком:
>>> dis(test2)
1 0 LOAD_FAST 0 (response)
3 LOAD_CONST 3 (('y', 'д'))
6 COMPARE_OP 6 (in)
9 RETURN_VALUE
>>>
неожиданно? как видите, листинг вообще ничем не отличается.
Знакомьтесь, Peephole Optimizer, встроенный в CPython.
В последнем варианте надо писать len("5") == 1 and "5" in "123456789"
, а не то, что вы написали: первый и второй не эквивалентны третьему. И на это обращали внимание здесь уже много раз. И ещё везде нужно написать переменную, а не "5"
: загрузка значения переменной занимает большее время, чем загрузка константы, а в последнем варианте это делается два раза. Может оптимизатор поможет, может нет, но в любом случае реальный код работает с переменной.
На самом деле, не увидел в статье разного мышления. По-моему, это одно и то же решение, которое улучшается со временем. Единственное, что выделяется — решение с помощью регулярок и то… В общем, имхо способы одинаковые, никакого разного мышления здесь нет, просто лаконичность меняется.
— почему не используется кортеж вместо списка?
— почему нельзя скомбинировать .upper с кортежем и тем самым сократить длину вдвое?
— почему в примере с регекспами не используется re.IGNORECASE?
Использую вариант:
answer = (((answer[0].lower()).replace('y', 'д')).replace('n', 'н'))
which(response)
(
oneof('Y', 'y', 'Д', 'д'), [&]
{
cout << "True";
},
oneof('N', 'n', 'Н', 'н'), [&]
{
cout << "False";
},
always<true>(), [&]
{
cout << "Error";
}
);
Python: Мышление программиста