Изменить то, что операторы *splat и **splatty-splat делают с моим объектом

Как переопределить результат распаковки синтаксиса *obj и **obj?

Например, можно ли как-то создать объект thing, который ведет себя так:

>>> [*thing]
['a', 'b', 'c']
>>> [x for x in thing]
['d', 'e', 'f']
>>> {**thing}
{'hello world': 'I am a potato!!'}

Примечание. итерация через __iter__ ("для x в вещи") возвращает другие элементы из распаковки *splat.

Я посмотрел в operator.mul и operator.pow, но эти функции касаются только использования с двумя операндами, такими как a*b и a**b, и кажутся не связанными с операциями знака.


person wim    schedule 12.03.2014    source источник
comment
я на 99% уверен, что вы не можете... но хотелось бы, чтобы здесь ошиблись (см. stackoverflow.com/questions/9722272/)   -  person Joran Beasley    schedule 13.03.2014
comment
Вы должны быть в состоянии просто реализовать итерируемые или картографические протоколы. Однако у меня возникают странные проблемы с правильной работой сопоставления.   -  person user2357112 supports Monica    schedule 13.03.2014


Ответы (2)


* перебирает объект и использует его элементы в качестве аргументов. ** перебирает keys объекта и использует __getitem__ (эквивалент скобочной нотации) для выборки пар ключ-значение. Чтобы настроить *, просто сделайте свой объект итерируемым, а чтобы настроить **, сделайте свой объект сопоставлением:

class MyIterable(object):
    def __iter__(self):
        return iter([1, 2, 3])

class MyMapping(collections.Mapping):
    def __iter__(self):
        return iter('123')
    def __getitem__(self, item):
        return int(item)
    def __len__(self):
        return 3

Если вы хотите, чтобы * и ** делали что-то помимо того, что описано выше, вы не можете этого сделать. У меня нет ссылки на документацию для этого утверждения (поскольку легче найти документацию о том, что вы можете это сделать, чем не можете), но у меня есть цитата из источника. Цикл интерпретатора байт-кода в PyEval_EvalFrameEx вызывает ext_do_call для реализации вызовов функций с * или ** аргументами. ext_do_call содержит следующий код:

        if (!PyDict_Check(kwdict)) {
            PyObject *d;
            d = PyDict_New();
            if (d == NULL)
                goto ext_call_fail;
            if (PyDict_Update(d, kwdict) != 0) {

который, если аргумент ** не является словарем, создает словарь и выполняет обычный update для его инициализации из аргументов ключевого слова (за исключением того, что PyDict_Update не принимает список пар ключ-значение). Таким образом, вы не можете настроить ** отдельно от реализации протокола сопоставления.

Точно так же для * аргументов ext_do_call выполняет

        if (!PyTuple_Check(stararg)) {
            PyObject *t = NULL;
            t = PySequence_Tuple(stararg);

что эквивалентно tuple(args). Таким образом, вы не можете настроить * отдельно от обычной итерации.

Было бы ужасно запутанно, если бы f(*thing) и f(*iter(thing)) делали разные вещи. В любом случае, * и ** являются частью синтаксиса вызова функции, а не отдельными операторами, поэтому их настройка (если возможно) будет задачей вызываемого объекта, а не аргумента. Я полагаю, что могут быть варианты использования для того, чтобы позволить вызываемым объектам настраивать их, возможно, для передачи dict подклассов, таких как defaultdict, через...

person user2357112 supports Monica    schedule 12.03.2014
comment
Да, я уже знаю это. Я говорю о настройке знака независимо от __iter__. Я добавил примечание в свой вопрос, чтобы попытаться уточнить, что я говорю об одном и том же объекте thing - person wim; 13.03.2014
comment
@wim: Тогда нет. Это было бы ужасно запутанно. - person user2357112 supports Monica; 13.03.2014
comment
Я не согласен. Но я хочу знать, как эти вещи вписываются в грамматику/язык, потому что они кажутся качественно отличными от других операторов. - person wim; 13.03.2014
comment
@wim: это не отдельные операторы. Они являются частью синтаксиса вызова функции. Вы не можете настроить их отдельно по той же причине, по которой вы не можете настроить то, что происходит, когда что-то передается в качестве обычного аргумента. - person user2357112 supports Monica; 13.03.2014
comment
Я верю вам в этом, но я думаю, что правильный ответ должен цитировать ссылку или предоставлять некоторые доказательства того, что это правда, а не просто утверждать, что это так, потому что в противном случае это было бы ужасно запутанным. Обратите внимание, что f(*thing) и f(*iter(thing)) дизассемблируются в разные байтовые коды. - person wim; 13.03.2014
comment
@wim: в документации просто сказано, что это то, что делают * и **, но вы не можете заставить их делать что-то еще. Думаю, я поищу, какая часть исходного кода реализует соответствующие коды операций. - person user2357112 supports Monica; 13.03.2014
comment
так что похоже, что, по крайней мере, в cpython, splat и splatsplat - это не то, что объект справа когда-либо видит или даже вообще не знает. вместо этого это часть грамматики. - person wim; 14.03.2014
comment
Я вижу PEP для двух новых магических методов, вытекающих из этого: __splat__ и __splatty_splat__, которые по умолчанию возвращаются к __iter__ и __iter__-с-__getitem__ при нормальных обстоятельствах. - person Mad Physicist; 18.01.2018
comment
@MadPhysicist splatty-splat — это .keys() с __getitem__. __iter__ не требуется. - person wim; 18.01.2018
comment
@Вим. Значит, он не будет пытаться использовать __iter__ в качестве резервной копии, так же как __iter__ переключается на __len__ и __getitem__? - person Mad Physicist; 18.01.2018
comment
Нет, не будет. - person wim; 18.01.2018
comment
@MadPhysicist: keys - это эвристика, которую он использует, чтобы решить, является ли объект отображением вообще, поэтому, если объект, который вы пытаетесь ** распаковать, не имеет keys, Python не думает, что он должен быть ** распаковываемым. - person user2357112 supports Monica; 18.01.2018
comment
@user2357112. ТИЛ. Это был очень продуктивный день для меня, за счет вас и Вима. Спасибо вам обоим. - person Mad Physicist; 19.01.2018
comment
@user2357112supportsMonica Я только что разместил в основном тот же вопрос по здесь, который был немедленно помечен как дубликат, и это справедливо. Однако еще до того, как я опубликовал этот вопрос, я опробовал первую версию, которую вы предлагаете реализовать __iter__, и она не удалась. Затем я увидел, что вы предлагаете то же самое, и я запустил ваш код, и он не удался. Что мне не хватает? Это уже не актуально? Вам нужно создать подкласс сопоставления? - person mapf; 21.06.2020
comment
@mapf: __iter__ предназначен для * распаковки, а не ** распаковки. - person user2357112 supports Monica; 21.06.2020
comment
@user2357112supportsМоника, спасибо! Я понял это и сейчас. Я говорил об этом с другим пользователем в моем посте, который указал мне на ваш ответ. Он предложил реализовать методы __getitem__ и keys для ** распаковки, чтобы она работала без необходимости создания подкласса Mapping, о чем изначально был мой вопрос. Может быть, вы могли бы включить это в свой ответ? - person mapf; 21.06.2020
comment
@mapf: технически он ищет только keys и __getitem__ в текущей реализации CPython, но ссылка на язык указывает, что распакованный аргумент должен быть сопоставлением: если синтаксис **expression появляется в вызове функции, expression должен оцениваться как сопоставление, содержимое которого обрабатывается как дополнительные аргументы ключевого слова. Реализация ровно столько, чтобы заставить ** взять ваш объект, - это рецепт ошибок и путаницы, поэтому я намеренно исключил его из своего ответа. - person user2357112 supports Monica; 21.06.2020
comment
@ user2357112supportsMonica Я вижу, поэтому реализация только keys и __getitem__ будет скорее хаком, я думаю. Однако это работает. Интересно, в каком контексте это потерпит неудачу. Большое спасибо за разработку! - person mapf; 21.06.2020

Мне удалось создать объект, который ведет себя так, как я описал в своем вопросе, но мне действительно пришлось схитрить. Так что просто размещаю это здесь для развлечения, на самом деле -

class Thing:
    def __init__(self):
        self.mode = 'abc'
    def __iter__(self):
        if self.mode == 'abc':
            yield 'a'
            yield 'b'
            yield 'c'
            self.mode = 'def'
        else:
            yield 'd'
            yield 'e'
            yield 'f'
            self.mode = 'abc'
    def __getitem__(self, item):
        return 'I am a potato!!'
    def keys(self):
        return ['hello world']

Протоколу итератора соответствует объект-генератор, возвращенный из __iter__ (обратите внимание, что экземпляр Thing() сам по себе не является итератором, хотя он итерируемый). Протокол отображения удовлетворяется наличием keys() и __getitem__. Тем не менее, если это еще не очевидно, вы не можете вызвать *thing дважды подряд и заставить его распаковать a,b,c дважды подряд - так что на самом деле это не переопределяет splat, как он притворяется.

person wim    schedule 13.03.2014
comment
Достаточно легко заставить *thing и **thing по крайней мере всегда действовать так, как вам нравится, независимо от порядка - просто определите def keys(self): return ('hello world',) - person Nick Matteo; 13.04.2017
comment
Есть ли какая-то конкретная причина, по которой __len__ не вернул 1? Кроме того, по какой причине вам нужно расширить Mapping? - person Mad Physicist; 18.01.2018
comment
@MadPhysicist Если вы не наследуете Mapping, вам нужно будет крякать, как сопоставление. В контексте Thing это означает, что мы должны определить метод keys. Если вы наследуете Mapping, вам необходимо определить абстрактный метод __len__, но меня не волнует, что он здесь возвращает — только то, что разрешается имя. - person wim; 18.01.2018
comment
@Вим. Я всегда думал, что можно просто __len__ вернуть 1. Интересно. - person Mad Physicist; 18.01.2018