Является ли Pythonic для функции, возвращающей итеративное или не итерируемое в зависимости от ее ввода? - PullRequest
7 голосов
/ 22 сентября 2009

(заголовок и содержание обновляются после прочтения ответа Алекса)

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

Например, struct.unpack всегда возвращает кортеж, даже если он содержит только один элемент.

Я пытаюсь завершить API для модуля, и у меня есть несколько функций, которые могут принимать один или несколько параметров (через *args), например:

a = s.read(10)        # reads 10 bits and returns a single item
b, c = s.read(5, 5)   # reads 5 bits twice and returns a list of two items.

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

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

a, = s.read(10)       # Prone to bugs when people forget to unpack the object
a = s.read(10)[0]     # Ugly and it's not clear only one item is being returned

Другой вариант - две функции:

a = s.read(10)
b, c = s.read_list(5, 5)

это нормально, но это загромождает API и требует от пользователя запоминания в два раза больше функций без добавления какого-либо значения.

Итак, мой вопрос: Иногда возвращает итеративный, а иногда один элемент, сбивающий с толку и непифитический? Если да, то какой вариант лучше?


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

Сказав, что для моего конкретного случая, я думаю, что я пойду на разделение на две функции (read(item) / readlist(*items)), причина в том, что я думаю, что случай с одним элементом будет происходить гораздо чаще, чем случай нескольких элементов, что упрощает использование, а изменение API менее проблематично для пользователей.

Спасибо всем.

Ответы [ 7 ]

12 голосов
/ 22 сентября 2009

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

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

Если вам нужно сделать что-то другое, если вам возвращается один элемент, просто используйте if len(var):.

Помните, что последовательность является ценным благом.

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

2 голосов
/ 23 сентября 2009

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

Функции copy.copy и copy.deepcopy возвращают тот же тип, что и их аргумент, поэтому, конечно, они "возвращают разные типы в зависимости от" аргумента. «Возвращать тот же тип, что и для ввода» на самом деле ОЧЕНЬ распространено - здесь можно также указать «получить объект обратно из контейнера, в который он был помещен», хотя обычно это делается с помощью метода, а не функции ;-) , Но также, в том же духе, рассмотрим itertools.repeat (после итерации по возвращенному итератору) или, скажем, filter ...:

>>> filter(lambda x: x>'f', 'zaplepidop')
'zplpiop'
>>> filter(lambda x: x>'f', list('zaplepidop'))
['z', 'p', 'l', 'p', 'i', 'o', 'p']

фильтрация строки возвращает строку, фильтрация списка возвращает список.

Но подождите, это еще не все! -) Функции pickle.loads и его друзья (например, в модуле marshal & c) возвращают объекты типов полностью в зависимости от значения, которое вы передаете в качестве аргумента. Так же как и встроенная функция eval (и аналогично input в Python 2. *). Это второй общий шаблон: создайте или реконструируйте объект в соответствии со значением аргумента (ов) из широкого (или даже неограниченного) множества возможных типов и возвращайте его.

Я не знаю ни одного хорошего примера конкретного анти-паттерна, который вы наблюдали (и я действительно считаю, что это анти-паттерн, мягко говоря, не по какой-либо причине высокого фалутина, просто потому, что с ним противно и неудобно иметь дело ;-). Обратите внимание, что эти примеры, которые я привел в качестве примера, являются удобными и удобными - это настоящий дискриминант дизайна в большинстве стандартных библиотечных выпусков! -)

2 голосов
/ 22 сентября 2009

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

Представьте себе следующего разработчика, который будет читать и поддерживать ваш код. Сначала он / она прочитает метод, используя вашу функцию, и подумает: «Ах, read () возвращает один элемент».

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

Наконец, как только они поймут, что read () возвращает два возможных типа, они должны будут спросить себя: «Возможно, есть третий тип возврата, к которому я должен быть готов?»

Это напоминает мне высказывание: «Кодируй, как будто следующий парень, который поддержит твой код, - маньяк-убийца, который знает, где ты живешь»

1 голос
/ 23 сентября 2009

Я бы прочитал ( целое число ) и read_list ( повторяем ).

Таким образом, вы можете прочитать (10) и получить один результат и read_list ([5, 5, 10, 5]) и получить список результатов. Это более гибко и явно.

1 голос
/ 22 сентября 2009

Это может быть не вопрос "питона", а скорее вопрос "хорошего дизайна". Если вы возвращаетесь к разным вещам И , никто не должен проверять их, тогда, вероятно, все в порядке. Это полиморфизм для вас. OTOH, если вызывающий должен «пробить завесу», то у вас есть проблема с дизайном, известная как нарушение принципа подстановки Лискова. Pythonic или нет, это явно не ОО дизайн, что означает, что он будет подвержен ошибкам и неудобствам программирования.

1 голос
/ 22 сентября 2009

Единственная ситуация, в которой я бы сделал это, - это параметризованная функция или метод, где один или несколько параметров, которые дает вызывающая сторона, определяет возвращаемый тип; например, функция «фабрика», которая возвращает одно из логически похожих семейств объектов:

newCharacter = characterFactory("human", "male", "warrior")

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

0 голосов
/ 22 сентября 2009

В списках python есть объекты :) Так что никакого несоответствия типов

...