Подтвердить что ты не робот

Как Python увеличивает элементы списка элементов?

Может кто-нибудь объяснить мне, почему первый блок кода не меняет список, но второй делает.

a = [1,2,3]
for el in a:
    el += 5

Это оставляет a как [1,2,3]. Тем не менее, если я запустил

a = [1,2,3]
for i in range(len(a)):
    a[i] += 5

затем a = [6,7,8]. Я предполагаю, что в первом случае при циклировании элементов el есть временная переменная, а не на самом деле вещь, которая ссылается на этот элемент в списке. Не уверен, почему приращение этого не влияет на список.

4b9b3361

Ответ 1

Целочисленные числа на Python не изменяются, но есть списки.

В первом случае el ссылается на неизменяемые целые числа, поэтому += создает новое целое число, к которому относится только el.

Во втором случае список a напрямую мутируется, напрямую изменяя его элементы. a[0] по-прежнему ссылается на неизменяемое целое число, поэтому += создает новое целое число, но его ссылка назначается непосредственно элементу изменяемого списка.

Примеры

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

a = [1,2,3]
print [id(x) for x in a]
print a

for el in a:
    el += 5   # creates new integer, but only temp name references it

print [id(x) for x in a] # references are not changed.
print a

Выход

[36615248, 36615236, 36615224]
[1, 2, 3]
[36615248, 36615236, 36615224]
[1, 2, 3]

Во втором случае обновляются ссылки на список:

a = [1,2,3]
print [id(x) for x in a]
print a

for i in range(len(a)):
    a[i] += 5      # creates new integer, but updates mutable list

print [id(x) for x in a] # references are changed.
print a

Выход

[36615248, 36615236, 36615224]
[1, 2, 3]
[36615188, 36615176, 36615164]
[6, 7, 8]

Ответ 2

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

Все другие назначения являются краткими для разных вызовов методов.

  • a[i] = 3 не подходит для a.__setitem__(i, 3)
  • a += 3 сокращен для a = a.__iadd__(3)
  • a[i] += 3 не подходит для a.__setitem__(i, a[i]+3)

Конечный результат каждого вызова метода зависит от того, как type(a) реализует вызываемый метод. Этот метод может изменить его вызывающий объект, или он может вернуть новый объект.

Ответ 3

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

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

  • Объекты привязаны к переменным.

    Обычное присваивание переменной, например x = 3, просто связывает объект справа, который может быть сконструирован на месте, если необходимо, - с именем слева.

  • Операторы "на месте", такие как +=, пытаются вызвать функции-модификаторы, которые позволяют изменять объекты, которые могут быть захвачены. Например, если x привязано к экземпляру класса, запись x += 3 будет фактически выполнять x.__iadd__(3), если x имеет __iadd__. 1 Если нет, он запускает x = x + 3, который вызывает оператор __add__: x = x.__add__(3). См. документацию оператора для всех подробностей. В этом случае задействованные объекты - обычные целые числа - не имеют модификационных функций:

    >>> (3).__add__
    <method-wrapper '__add__' of int object at 0x801c07f08>
    >>> (3).__iadd__
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'int' object has no attribute '__iadd__'
    

    поэтому этот конкретный поворот не имеет значения для int, но его стоит запомнить.

  • Индексированное назначение x[i] = expression вызывает метод __setitem__. Вот как мутируемые (модифицируемые) объекты мутируют сами. Объект списка реализует __setitem__:

    >>> [].__setitem__
    <method-wrapper '__setitem__' of list object at 0x8007767e8>
    

    Для полноты я отмечу, что он также реализует __getitem__ для извлечения x[i].

    Следовательно, когда вы пишете a[i] += 5, вы вызываете вызов:

    a.__setitem__(i, a.__getitem__(i) + 5)
    

    каким образом Python удается добавить 5 в i-й элемент списка, связанный с a.

Вот немного страшный пример. Объект кортежа не модифицируется, но объект списка является. Если мы вставляем список в кортеж:

>>> l = [0]
>>> t = (l,)

мы можем использовать t[0] для вызова t.__getitem__ и t.__setitem__. Между тем t[0] привязан к тому же объекту списка, что и l. Эта часть достаточно очевидна:

>>> t
([0],)
>>> l.append(1)
>>> t
([0, 1],)

Мы изменили l на место, поэтому t[0], который называет тот же список, что и l, был изменен. Но теперь:

>>> t[0] += [2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
([0, 1, 2],)

Что?! Как изменилось t, когда мы получили сообщение об ошибке, указывающее, что t не может измениться?

Ответ: t не изменился, но список (который мы также можем получить через l) изменился. Списки реализуют __iadd__, поэтому назначение:

t[0] += [2]

"означает":

t.__setitem__(0, t.__getitem__(0).__iadd__([2]))

__getitem__ просмотрел список и __iadd__ добавил список:

>>> l
[0, 1, 2]

а затем t.__setitem__(0, ...) поднял TypeError, но к этому списку уже был добавлен.

Обратите внимание, кстати, что настройка объекта списка, связанного с l, влияет на объект кортежа, связанный с t, потому что t[0] - это объект списка. Это - тот факт, что переменные привязаны к объектам, а элементы в структурах данных, таких как кортежи, списки и словари, могут ссылаться на другие объекты - имеет решающее значение при чтении и написании кода Python. Понимание как правил привязки, так и когда объекты создаются, является ключом к пониманию того, почему это обычно плохой идеей:

def f(a=[]):

В частности, объект списка здесь создается в def времени, то есть только один раз. Каждый раз, когда кто-то добавляет в список внутри f, например, a.append, он продолжает добавлять в этот исходный список!


См. также python: как работает привязка (слишком много) о дополнительных правилах привязки.: -)

1 Как Дункан указывает в комментарии на ответ chepner, после вызова __iadd__ возвращаемый результат снова привязывается к объекту. (Все функции возвращают результат, возвращаются без выражения или "отваливаются от конца" функции, определяются как возвращающие None.)

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

"""
demonstration of iadd behavior
"""
from __future__ import print_function

class Oddity(object):
    """
    A very odd class: like a singleton, but there can be
    more than one of them.  Each one is a list that just
    accumulates more items.  The __iadd___ (+=) operator
    augments the item, then advances to the next instance.

    Creating them is tricky as we want to create new ones
    up until we "freeze" the class, then start re-using
    the instances.  We use a __new__ operator a la immutable
    objects, plus a boolean in the class itself, even though
    each instance is mutable.
    """
    def __new__(cls):
        if not hasattr(cls, 'frozen'):
            cls.frozen = False
        if cls.frozen:
            whichone = cls.rotator
            cls.rotator = (whichone + 1) % len(cls.instances)
            return cls.instances[whichone]
        self = object.__new__(cls)
        if not hasattr(cls, 'instances'):
            cls.instances = []
        self.whichone = len(cls.instances)
        self.values = []
        cls.instances.append(self)
        print('created', self)
        return self

    def __str__(self):
        return '#{}, containing {}'.format(self.whichone, self.values)

    def __iadd__(self, value):
        print('iadd to', self)
        self.values.append(value)
        all_oddities = self.__class__.instances
        nextone = (self.whichone + 1) % len(all_oddities)
        return all_oddities[nextone]

    @classmethod
    def freeze(cls):
        if not hasattr(cls, 'frozen'):
            raise TypeError('need at least one instance to freeze')
        cls.frozen = True
        cls.rotator = 0

# Now make two instances, and freeze the rest so that
# we can cycle back and forth.
o0 = Oddity()
o1 = Oddity()
Oddity.freeze()

print('o0 is', o0)
o0 += 'first add to o0'
o0 += 'second add to o0'
o0 += 'third add to o0'
print('now o0 is', o0, 'and o1 is', o1)
print('the first object is', Oddity.instances[0])

Как только мы создадим два объекта и заморозим класс, мы вызываем __iadd__ три раза на o0, поэтому в конце o0 и o1 фактически привязаны ко второму объекту. Первый объект, который можно найти только через поле класса cls.instances, состоит из двух элементов в списке элементов.

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

(Экстра-продвинутое упражнение: превратите Oddity в метакласс, который может быть применен к классам, чтобы превратить их в замораживаемые мульти-синглтоны. [Есть термин для "вещи, которая похожа на одноэлементный, но допускает N их"?] См. также Почему синтоны противоречивы.)

[Edit: это меня прослушивало: оказалось, что есть имя для фиксированных наборов-одиночек. Когда набор имеет только два элемента, это "двойник". В Python True и False являются двунаправленными типами bool. Обобщение на n объектов - это multiton, часто созданный или используемый как фиксированный, по крайней мере, после "замораживания" хэш-таблицы времени, Обратите внимание, что булевы экземпляры Python doubleton неизменяемы, как и Python singleton None.]

Ответ 4

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

Когда вы выполняете e1 += 5 (или более четко в этом случае, e1 = e1 + 5), то, что вы делаете, переназначает e1 значение e1 + 5. После этой операции e1 больше не указывает на первый элемент списка, а созданную новую переменную (e1 + 5). Список остается неизменным.

Во втором кодовом блоке вы назначаете что-то непосредственно элементу списка, а не временную ссылку e1.

Ответ 5

В первом примере el является локальной переменной и полностью не осознает, что она является частью списка.

Во втором примере вы явно манипулируете элементами списка.

Урок здесь будет несколько более ясным, если мы раскроем стенограмму +=, и если мы будем использовать список изменяемых объектов вместо неизменяемых целых чисел:

Итератор:

a = [[1], [2]]
for el in a:
    el = [3]

После этого цикла a не изменяется:

>>> a
[[1], [2]]

Схема цикла подсчета:

for i in range(len(a)):
    a[i] = [3]

После этого цикла изменяется a:

>>> a
[[3], [3]]

С другой стороны, если вы сами мутируете el, а не переназначаете его:

a = [[1], [2]]
for el in a:
    el[0] = el[0] + 5

Обратите внимание, что здесь мы не переназначаем el, мы мутируем его без переопределения.

Изменение также будет видно из списка:

>>> a
[[6], [7]]

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