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

В комментариях упоминалась возможность умножения коллекций на скаляр:

a = [0] * 3
s = 'a' * 2
print(a, s)  # -> [0, 0, 0], 'aa'

Более-менее опытный разработчик на языке python знает, что в нём отсутствует механизм копирования при записи

a = [0]
b = a
b[0] = 1
print(a, b)  # -> [1], [1]

Что же тогда выведет следующий код?

b = a * 2
b[0] = 2
print(a, b)

Python в данном случае работает по принципу наименьшего удивления: в переменной a у нас хранится одна единица, то есть b можно было объявить и как

b = [1] * 2

Поведение в данном случае будет такое же:

b = a * 2
b[0] = 2
print(a, b)  # -> [1], [2, 1]

Но не всё так просто, python копирует содержимое списка столько раз, на сколько вы его домножили и конструирует новый список. А в списках хранятся ссылки на значения, и если, при копировании таким образом ссылок на неизменяемые типы всё хорошо, то вот с изменяемыми вылезает эффект отсутствия копирования при записи.

row = [0] * 2
matrix = [row] * 2
print(matrix)        # -> [[0, 0], [0, 0]]
matrix[0][0] = 1
print(matrix)        # -> [[1, 0], [1, 0]]

Генераторы списков и numpy вам в помощь в данном случае.

Списки можно складывать и даже инкрементировать, при этом справа может находиться любой итератор:

a = [0]
a += (1,)
a += {2}
a += "ab"
a += {1: 2}
print(a)  # -> [0, 1, 2, 'a', 'b', 1] Заметьте, что строка вставилась посимвольно
# ведь именно так работает строковый итератор

Вопрос с подвохом (для собеседования): в python параметры передаются по ссылке или по значению?

def inc(a):
    a += 1
    return a

a = 5
print(inc(a))
print(a)         # -> 5

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

def appended(a):
    a += [1]
    return a

a = [5]
print(appended(a))  # -> [5, 1]
print(a)            # -> [5, 1]


В таких языках как C++ есть переменные, хранящиеся на стеке и в динамической памяти. При вызове ф-ции мы помещаем все аргументы на стек, после чего передаём управление функции. Она знает размеры и смещения переменных на стеке, соответственно может их правильно интерпретировать.
При этом у нас есть два варианта: скопировать на стек память переменной или положить ссылку на объект в динамической памяти (или на более высоких уровнях стека).
Очевидно, что при изменении значений на стеке функции, значения в динамической памяти не поменяются, а при изменении области памяти по ссылке, мы модифицируем общую память, соответственно все ссылки на эту же область памяти «увидят» новое значение.

В python отказались от подобного механизма, заменой служит механизм связывания(assignment) имени переменной с объектом, например при создании переменной:
var = "john"


Интерпретатор создаёт объект «john» и «имя» var, а затем связывает объект с данным именем.
При вызове функции, новых объектов не создаётся, вместо этого в её области видимости создаётся имя, которое связывается с существующим объектом.
Но в python есть изменяемые и неизменяемые типы. К первым, например, относятся числа: при арифметических операциях существующие объекты не меняются, а создаётся новый объект, с которым потом связывается существующее имя. Если же со старым объектом после этого не связано ни одного имени, оно будет удалено с помощью механизма подсчёта ссылок.
Если же имя связано с переменной изменяемого типа, то при операциях с ней изменяется память объекта, соответственно все имена, связанные с данной областью памяти «увидят» изменения.

Можно почитать про это в документации, более подробно изложено здесь.

Ещё один пример:

a = [1, 2, 3, 4, 5]

def rev(l):
    l.reverse()
    return l

l = a
print(a, l) # -> [1, 2, 3, 4, 5], [1, 2, 3, 4, 5]
l = rev(l)
print(a, l) # -> [5, 4, 3, 2, 1], [5, 4, 3, 2, 1]

Но что, если мы решили поменять переменную вне функции? В данном случае нам поможет модификатор global:

def change():
    global a
    a += 1


a = 5
change()
print(a)

Замечание: не надо так делать (нет, серьёзно, не используйте глобальные переменные в своих программах, и тем более не в своих). Лучше просто вернуть несколько значений из функции:
def func(a, b):
    return a + 1, b + 1


Однако, в python присутствует и другая область видимости и соответствующее ключевое слово:

def private(value=None):
    def getter():
        return value

    def setter(v):
        nonlocal value
        value = v

    return getter, setter


vget, vset = private(42)
print(vget())    # -> 42
vset(0)
print(vget())    # ->  0

В данном примере, мы создали переменную, которую можно изменить (и чьё значение получить) только через методы, можно использовать подобный механизм и в классах:

def private(value=None):
    def getter():
        return value

    def setter(v):
        nonlocal value
        value = v
    return getter, setter


class Person:
    def __init__(self, name):
        self.getid, self.setid = private(name)


adam = Person("adam")
print(adam.getid())
print(adam.setid("john"))
print(adam.getid())
print(dir(adam))

Но, пожалуй, лучше будет ограничиться свойствами или определением __getattr__, __setattr__.

Можете даже определить __delattr__.

Ещё одной особенностью python является наличие двух методов для получения атрибута: __getattr__ и __getattribute__.

В чём между ними разница? Первый вызывается лишь, если атрибут в классе не был найден, а второй безусловно. Если в классе объявлены оба, то __getattr__ вызовется, лишь, если явно его вызвать в __getattribute__ или, если __getattribute__ сгенерировал AttributeError.

class Person():
    def __getattr__(self, item):
        print("__getattr__")
        if item == "name":
            return "john"
        raise AttributeError

    def __getattribute__(self, item):
        print("__getattribute__")
        raise AttributeError


person = Person()
print(person.name)
# -> __getattribute__
# -> __getattr__
# -> john

И на последок пример того, как python вольно обращается с переменными и областями видимости:

    e = 42
     
    try:
        1 / 0
    except Exception as e:
        pass
     
    print(e)  # -> NameError: name 'e' is not defined

Это, кстати, пожалуй единственный пример, когда второй python лучше третьего, потому что он выводит:

    ...
    print(e)  # -> float division by zero

Комментарии (27)


  1. Satim
    11.09.2018 15:15
    +2

    Разве механизм CoW имеет отношение к языку??
    То что вы описываете это просто работа ссылочного типа данных.


    1. LinearLeopard Автор
      11.09.2018 15:29
      -1

      Честно говоря, не знаю, введено ли подобное поведение в спецификацию языка или является особенностью реализации CPython. Но многие языки явно требуют наличие подобного механизма. Например.
      Вот здесь небольшой список, где ещё это поведение является стандартом.
      Если мне не изменяет память, в Delphi, Java и C# так же.


      1. LinearLeopard Автор
        12.09.2018 09:07

        Если мне не изменяет память, в Delphi, Java и C# так же.


        Изменила, в Java и C# коллекции, поддерживающие CoW, вынесены в отдельные пакеты.


        1. lagranzh
          12.09.2018 14:05

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


      1. masai
        12.09.2018 15:56

        Замечание не про это. CoW и ссылки на объекты — это вещи из разных областей.


        CoW означает, что при запросе на копирование (например, при присваивании в некоторых языках) объекта фактическое копирование данных происходит при попытке его изменить. Это способ оптимизировать работу с памятью отложив тяжёлую операцию на потом. Из того, что язык не поддерживает CoW вовсе не следует, что он не копирует объекты. Он вполне может копировать данные сразу при запросе на копирование.


        Просто немного сбивает с толку абзац, в котором вы говорите, что Python не поддерживает CoW и иллюстрируете это тем, что он копирует ссылку, а не сам объект.


    1. Zanak
      12.09.2018 09:07

      Скорее всего автор имел ввиду, что в python не использует CoW при управлении памятью, и ни чего больше. :)


  1. barker
    11.09.2018 20:20

    ну, здесь уже точно понятно стало, что автор плавает в основах, например в понимании что такое ссылка, передача по ссылке итд


    1. LinearLeopard Автор
      11.09.2018 20:25

      del


    1. LinearLeopard Автор
      11.09.2018 20:26

      Прошу прощения, можно поподробнее объяснить то, в чём, как вы думаете, я плаваю?


      1. kilgur
        12.09.2018 08:04

        Возможно, дело в термине «передача по ссылке» (в С++) — в этом случае вызываемая подпрограмма может изменить содержимое переменной, определённой в вызывающей программе.
        Python передаёт параметры всегда по значению, но все переменные хранят указатели на значения. Т.е. вызываемая подпрограмма получает копию указателя, следовательно, она может изменить (если это указатель на изменяемый объект) значение указателя. НО не может изменить содержимое переданной переменной (чтобы она указывала на другой объект).


        1. Zanak
          12.09.2018 10:20

          Берем:

          def abc(x):
              x.append(len(x))
          
          a=[]
          abc(a)
          print(a)        # [0]
          abc(a)
          print(a)        # [0, 1]
          abc(a)
          print(a)        # [0, 1, 2]
          

          Python передаёт параметры всегда по значению, но все переменные хранят указатели на значения.
          Не согласен:
          — все значения в python являются объектами, не зависимо от способа реализации на Си или python.
          — все переменные являются ссылками на значение.
          — тривиальные типы, числа и строки, при присваивании копируются. это касается и передачи в качестве параметра в функцию.
          — при присваивании значений более сложных по конструкции происходит присваивание целевой переменной ссылки на исходное значение.
          — для того, чтобы создать копию значения существуют функции copy/deepcopy, первая из которых выполняет поверхностное, а вторая глубокое копирование значения. кроме этого, можно перекрыть __copy__/__deepcopy__ функции, чтобы исключить копирование полей, специфичных для конкретного инстанса, например соединения с БД, или по сети.
          — как альтернатива copy/deepcopy можно посмотреть на теневое копирование:
          a=[1,2,3,4]
          b=a[:]        # можно явно b=list(a)
          a+=[5,6,7,8]
          легко убедится, что a изменит значение, b — нет.
          — а для особо экзотичных случаев есть модуль pickle, который позволяет сериализовать и десериализовать значения, со всем, что из этого следует.


          1. kilgur
            12.09.2018 11:47
            +1

            Мне несколько непонятно, к чему ваш пример? Учитывая, что все переменные в python являются указателями на значения, что вы хотели показать с помощью примера? Я не вижу противоречий с тем, что я сказал.
            Переменная a указывает куда-то в память на объект list, при вызове функции abc в локальную переменную x копируется переменная a (передача по значению), теперь x указывает на тот же объект list. Соответственно, все операции с x внутри abc меняют объект, на который также указывает и a.
            «Передача параметра по ссылке» позволила бы внутри функции abc изменить переменную a, например так, чтобы она указывала на другой list или вообще dict. Этого в python, разумеется, нет, о чём я и сказал в предыдущем комментарии.


            1. Zanak
              12.09.2018 11:58

              Мне несколько непонятно, к чему ваш пример? Учитывая, что все переменные в python являются указателями на значения, что вы хотели показать с помощью примера? Я не вижу противоречий с тем, что я сказал.

              При передаче в качестве параметра тривиального значения, например числа, копируется само значение, а при передаче, например, списка, передается ссылка на него. Организация связи имя_переменной->значение, в данном контексте, факт менее значимый.


              1. kilgur
                12.09.2018 12:34

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

                Откуда сведения? В python даже числа — объекты… указатель на которые хранится в переменной и копируется при передаче в функцию. Неважно, переменная указывает на объект 5 или на объект [1, 2, 3], в функцию «прилетит» копия указателя на этот объект. Поиграйтесь с функцией id() — в CPython она возвращает адрес объекта в памяти.
                Организация связи имя_переменной->значение, в данном контексте, факт менее значимый.

                Это как? Об этом же и рассуждаем, не?
                Вся «засада» в терминологии: передача параметра в функцию «по ссылке» или «по значению» не имеет никакого отношения к самому значению — указатель там на что-то в памяти или банальный uint8, неважно. Важно только то, что мы складываем на стек — значение, которое храниться в переменной или адрес этой самой переменной.


                1. Zanak
                  12.09.2018 13:07

                  Отчасти убедили, я упустил из виду, что питон хитро организует хранение переменных, и если вы раз написали a=1, а потом путем вычислений получили other_var=1, то id(a) и id(other_var) будут равны.

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


                  1. kilgur
                    12.09.2018 13:29

                    1 в python — это не байт со значением 0x01 и даже не 4 байта 00 00 00 01 (или 01 00 00 00), а объект класса int, хранящий значение 1 (сколько байт в памяти он будет занимать, если я правильно помню, зависит от платформы). В целях оптимизации интерпретатор заранее создает объекты-числа в диапазоне [-5, 256]. Поэтому, если a = 257, а потом путем вычислений получили other_var = 257, то id(a) != id(other_var). Это уже разные объекты, пусть и имеющие одно значение.
                    Выделение памяти и копирование указателя как раз-таки и происходит при передаче параметров в функцию. Т.е. в функции вы всегда имеете дело с копиями ссылок на объекты. Просто для мутабельных объектов (changeable in-place) нет разницы как вы с ним манипулируете: через оригинальную ссылку (переменную вызывающей функции) или её копию (аргумент в вызываемой функции).


                    1. Zanak
                      12.09.2018 14:20

                      Препарирование вот такого кролика:

                      def abc0():
                          print(id([1, 300]))
                      
                      def abc(x):
                          print(id(x))
                      
                      a=300
                      print(id(a))
                      abc(a)
                      print(id(300))
                      print('---')
                      a=[1, 300]
                      print(id(a))
                      abc(a)
                      print(id([1, 300]))
                      abc([1, 300])
                      abc0()
                      
                      дало вот такой результат:
                      140492550767728
                      140492550767728
                      140492550767728
                      ---
                      140492518644424
                      140492518644424
                      140492518700616
                      140492518700616
                      140492518700616
                      По моему, с вашей версией поведения не совсем совпадает?


                      1. kilgur
                        12.09.2018 15:15
                        +1

                        a=300

                        python создал объект класса int со значением 300 для литерала 300, в переменную a положил адрес этого объекта.
                        
                        def abc(x):
                            print(id(x))
                        

                        в локальную переменную x было скопировано значение переменной a, которая содержит адрес объекта 300, следовательно, x теперь тоже содержит адрес объекта 300
                        
                        print(id(300))
                        

                        поскольку значение переменной a ранее было задано литералом, который также используется и в данной инструкции, логично что инструкция выведет тот же адрес — зачем нам два литерала с одинаковым значением?
                        Если вы измените a=300 на a=299+1, то это не поможет, поскольку python достаточно умный, чтобы сразу создать литерал объекта-числа 300. А вот если сначала b=250, а потом a=b+50, то в коде программы уже литерал 300 не встречается и print(id(300)) придется создать новый объект числа 300.

                        Что касается списков, то тут я в затруднении — списковые литералы должны создаваться отдельными объектами, даже если они содержат одинаковые элементы. Возможно, интерпретатор достаточно умён, чтобы определить, что через переменную, хранящую адрес списка, сам список не изменяется… Т.е. тут у меня знаний не хватает, есть только догадки.


                        1. Zanak
                          12.09.2018 15:24

                          Ну, даже с целыми, удивили. А почему [-5, 256], а не [0, 255], что было бы ожидаемо и логично?


                          1. kilgur
                            12.09.2018 15:33

                            К октету привязываться смысла нет, всё равно оверхэд на объекты приличный, поэтому imho [0, 255] как раз-таки нелогично. Я думаю, что этот диапазон как-то статистически определили… надеюсь, во всяком случае, но точной информации не имею.


        1. LinearLeopard Автор
          12.09.2018 10:56

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


  1. LinearLeopard Автор
    11.09.2018 22:34

    Возможно, я не совсем понятно объяснил, что ж, попытаюсь быть понятнее.
    В таких языках как C++ есть переменные, хранящиеся на стеке и в динамической памяти. При вызове ф-ции мы помещаем все аргументы на стек, после чего передаём управление ф-ции. Ф-ция знает размеры и смещения переменных на стеке, соответственно может их правильно интерпретировать.
    При этом у нас есть два варианта: скопировать на стек память переменной или положить ссылку на объект в динамической памяти (или на более высоких уровнях стека).
    Очевидно, что при изменении значений на стеке ф-ции, значения в динамической памяти не поменяются, а при изменении области памяти по ссылке, мы модифицируем общую память, соответственно все ссылки на эту же область памяти «увидят» новое значение.

    В python отказались от подобного механизма, заменой служит механизм связывания(assignment) имени переменной с объектом, например при создании переменной:

    var = "john"
    


    Интерпретатор создаёт объект «john» и «имя» var, а потом связывает объект с данным именем.
    При вызове ф-ции, новых объектов не создаётся, вместо этого в области видимости ф-ции создаётся имя, которое связывается с существующим объектом.
    Но в python есть изменяемые и неизменяемые типы. К первым, например, относятся числа: при арифметических операциях существующие объекты не меняются, а создаётся новый объект с соответствующим значением, с которым потом связывается существующее имя. Если же со старым объектом после этого не связано ни одного имени, оно будет удалено с помощью механизма подсчёта ссылок.
    Если же имя связано с переменной изменяемого типа, то при операциях с ней изменяется память объекта, соответственно все имена, связанные с данной областью памяти «увидят» изменения.

    Можно почитать про это в документации, более подробно изложенно здесь.


    1. kilgur
      12.09.2018 12:08

      При этом у нас есть два варианта: скопировать на стек память переменной или положить ссылку на объект в динамической памяти (или на более высоких уровнях стека).

      ещё можно положить в стек ссылку на переменную, которую мы передаём в функцию

      Очевидно, что при изменении значений на стеке ф-ции, значения в динамической памяти не поменяются

      не понял, о чем речь — на стеке значение переменной? тогда о каком значении в динамической памяти идёт речь? или речь о том, что передаём указатель и если его поменять, то значение в динамической памяти не поменяется? так это, мягко говоря, очевидно

      В python отказались от подобного механизма

      уточните, пожалуйста, от какого именно механизма отказались, а то непонятно — сначала речь шла о передаче параметров и стеке функции, потом от какого-то механизма отказались… И, кстати, что за «механизм связывания(assignment)»? Не могу «нагуглить»… Любопытно, просто, чем он отличается от ссылок/указателей.


      1. LinearLeopard Автор
        12.09.2018 12:35

        ещё можно положить в стек ссылку на переменную, которую мы передаём в функцию.

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


        не понял, о чем речь — на стеке значение переменной?

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

        уточните, пожалуйста, от какого именно механизма отказались

        Весь комментарий про это, внизу есть ссылки, или вас интересует реализация?

        Если вкратце там словарь (имя переменной: ссылка на объект).
        ideone.com/LPrD5j


        1. kilgur
          12.09.2018 13:04

          «положить в стек ссылку на переменную, которую мы передаём в функцию» != «положить ссылку на объект в динамической памяти»

          Давайте внимательно почитаем, что пишут по вашей же ссылке.
          «call-by-reference» — это передача параметра по ссылке, если бы оно было в python, то могло бы выглядеть как-то так:

          def cbr(&x):
              x = {}
          a = [0, 1, 2]
          cbr(a)
          print(a) # {}
          

          «просекаете фишку»? вот такая хрень и называется «передача параметра по ссылке», чего в python нет. Так какой правильный ответ «на собеседованиях» на вопрос «как передаются в python параметры в функцию: по ссылке или по значению?»

          [зануда mode on]
          Весь комментарий про это, внизу есть ссылки, или вас интересует реализация?

          не, меня интересует ровно то, что я спросил: «от какого механизма отказались?»

          И по ссылке тоже не нашел, что за механизм связывания (assignment) — там есть про то, что «присваивание создаёт ссылку на объект» и «аргументы передаются присваиванием»
          [зануда mode off]


  1. random1st
    12.09.2018 13:18

    По-моему, ничерта это не полезности. Идем в консоль питона, вбиваем import this и читаем до просветления.


  1. Antervis
    12.09.2018 13:42

    это не «интересности и полезности», а «грабли и мины» — правильное использование всего этого разве что запутает читающего код, а вот наступить и заюзать неправильно можно.