Зачем хранить функцию внутри словаря Python?

68

Я новичок в питоне, и я только что изучил технику, включающую словари и функции. Синтаксис прост, и это кажется тривиальной вещью, но мои чувства питона покалывания. Что-то подсказывает мне, что это глубокая и очень питоническая концепция, и я не совсем понимаю ее важность. Может кто-нибудь дать название этой технике и объяснить, как / почему она полезна?


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

Пример, из которого я работаю, - «Изучите Python: трудный путь», 2-е изд. (Эта версия доступна, когда вы регистрируетесь через Udemy.com ; к сожалению, в настоящее время бесплатной бесплатной HTML-версией является версия Ed 3, которая больше не включает этот пример).

Перефразировать:

# make a dictionary of US states and major cities
cities = {'San Diego':'CA', 'New York':'NY', 'Detroit':'MI'}

# define a function to use on such a dictionary
def find_city (map, city):
    # does something, returns some value
    if city in map:
        return map[city]
    else:
        return "Not found"

# then add a final dict element that refers to the function
cities['_found'] = find_city

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

>>> find_city (cities, 'New York')
NY

>>> cities['_found'](cities, 'New York')
NY

Может кто-нибудь объяснить, что это за языковая функция, и, возможно, где она играет в "реальном" программировании? Этого игрушечного упражнения было достаточно, чтобы научить меня синтаксису, но оно меня не охватило.

mdeutschmtl
источник
13
Почему этот пост не по теме? Это отличный вопрос об алгоритме и концепции структуры данных!
Мартин Питерс
Я видел (и делал) кое-что подобное на других языках. Вы можете рассматривать это как оператор switch, но хорошо обернутый в проходимый объект со временем поиска O (1).
KChaloux
1
У меня было предчувствие, что есть что-то важное и самостоятельное в том, чтобы включить функцию в свой собственный диктат ... см. Ответ @ dietbuddha ... но, возможно, нет?
mdeutschmtl

Ответы:

83

Используя dict, давайте переведем ключ в вызываемый. Ключ не должен быть жестко закодирован, как в вашем примере.

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

def do_ping(self, arg):
    return 'Pong, {0}!'.format(arg)

def do_ls(self, arg):
    return '\n'.join(os.listdir(arg))

dispatch = {
    'ping': do_ping,
    'ls': do_ls,
}

def process_network_command(command, arg):
    send(dispatch[command](arg))

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

Использование метода диспетчеризации более безопасно, чем другие методы, например eval(), поскольку он ограничивает допустимые команды тем, что вы определили заранее. ls)"; DROP TABLE Students; --Например, ни один злоумышленник не сможет пробраться через инъекцию мимо таблицы диспетчеризации.

Мартейн Питерс
источник
5
@Martjin - Разве это не может быть названо реализацией «Командного шаблона» в этом случае? Похоже, это концепция, которую ОП пытается понять?
PhD
3
@PhD: Да, пример, который я построил, - это реализация Command Pattern; то dictдействует как диспетчер (менеджер команды, инициатор вызова, и т.д.).
Мартин Питерс
Отличное объяснение на высшем уровне, @Martijn, спасибо. Я думаю, что у меня есть идея «отправки».
mdeutschmtl
28

@Martijn Pieters хорошо объяснил методику, но я хотел кое-что прояснить из вашего вопроса.

Важно знать, что вы НЕ сохраняете «имя функции» в словаре. Вы храните ссылку на саму функцию. Вы можете увидеть это с помощью printфункции.

>>> def f():
...   print 1
... 
>>> print f
<function f at 0xb721c1b4>

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

>>> a = f
>>> a
<function f at 0xb721c3ac>
>>> a()
1

Точно так же вы можете передать функцию в качестве аргумента.

>>> def c(func):
...   func()
... 
>>> c(f)
1
unholysampler
источник
5
Упоминание функции первого класса определенно поможет :-)
Florian Margaine
7

Обратите внимание, что класс Python - это просто синтаксический сахар для словаря. Когда вы делаете:

class Foo(object):
    def find_city(self, city):
        ...

когда ты звонишь

f = Foo()
f.find_city('bar')

действительно так же, как:

getattr(f, 'find_city')('bar')

который после разрешения имени точно такой же как:

f.__class__.__dict__['find_city'](f, 'bar')

Одним из полезных методов является сопоставление пользовательского ввода с обратными вызовами. Например:

def cb1(...): 
    ...
funcs = {
    'cb1': cb1,
    ...
}
while True:
    input = raw_input()
    funcs[input]()

Это может быть альтернативно написано в классе:

class Funcs(object):
    def cb1(self, a): 
        ...
funcs = Funcs()
while True:
    input = raw_input()
    getattr(funcs, input)()

Какой синтаксис обратного вызова лучше, зависит от конкретного приложения и вкуса программиста. Первый - более функциональный стиль, последний - более объектно-ориентированный. Первый может показаться более естественным, если вам нужно динамически изменять записи в словаре функций (возможно, на основе пользовательского ввода); последний может показаться более естественным, если у вас есть набор различных предустановок, которые можно выбирать динамически.

Ли Райан
источник
Я думаю, что эта взаимозаменяемость заставила меня думать «питон», тот факт, что то, что вы видите на поверхности, является просто традиционным способом представления чего-то гораздо более глубокого. Может быть, это не относится к python, хотя упражнения на python (и программисты на python?), Похоже, очень много говорят о языковых особенностях таким образом.
mdeutschmtl
Другая мысль: есть ли что-то специфическое для python в том, как он хочет оценить то, что выглядит как два «термина», просто соседствующих друг с другом, dict reference и список аргументов? Другие языки позволяют это? Это своего рода программный эквивалент скачка в алгебре от 5 * xк 5x(простите за простую аналогию).
mdeutschmtl
@mdeutschmtl: на самом деле он не уникален для Python, хотя в языках, в которых отсутствует функция или объект первого класса, никогда не может возникнуть ситуаций, когда возможен доступ к словарю, за которым сразу следует вызов функции.
Ли Райан
2
@mdeutschmtl «тот факт, что то, что вы видите на поверхности, является просто традиционным способом представления чего-то гораздо более глубокого». - это называется синтаксическим сахаром и существует повсюду
Изката
6

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

1. Техника сокрытия / инкапсуляции и сплоченности информации (обычно они идут рука об руку, поэтому я их объединяю).

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

2. Диспетчерские таблицы

Не классический случай, потому что есть только одна запись с функцией. Однако таблицы диспетчеризации используются для организации различных поведений по ключу, так что их можно искать и вызывать динамически. Я не уверен, что вы думаете об этом, поскольку вы не ссылаетесь на функцию динамически, но тем не менее вы все равно получаете эффективное позднее связывание («косвенный» вызов).

компромиссы

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

dietbuddha
источник
Инкапсуляция, потому что данные и функции, хранящиеся в dict (ионные), связаны между собой и, следовательно, имеют единство. Данные и функции взяты из двух совершенно разных доменных областей, поэтому на первый взгляд пример, похоже, объединяет разнородные сущности.
ChuckCottrill
0

Я публикую это решение, которое я считаю достаточно общим и может быть полезным, поскольку оно простое и легко адаптируется к конкретным случаям:

def p(what):
    print 'playing', cmpsr

def l(what):
    print 'listening', cmpsr

actions = {'Play' : p, 'Listen' : l}

act = 'Listen'
cmpsr = 'Vivaldi'

actions[act].__call__(cmpsr)

Можно также определить список, в котором каждый элемент является функциональным объектом, и использовать __call__встроенный метод. Кредиты всем за вдохновение и сотрудничество.

«Великий художник - упрощитель», Анри Фредерик Амиэль

Emanuele
источник