Интересности и полезности python. Часть 3

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

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

    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
    
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 44

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


          Изменила, в Java и C# коллекции, поддерживающие CoW, вынесены в отдельные пакеты.
          • НЛО прилетело и опубликовало эту надпись здесь
            0

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


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


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

            0
            Скорее всего автор имел ввиду, что в python не использует CoW при управлении памятью, и ни чего больше. :)
            +1
            ну, здесь уже точно понятно стало, что автор плавает в основах, например в понимании что такое ссылка, передача по ссылке итд
              0
              del
                0
                Прошу прощения, можно поподробнее объяснить то, в чём, как вы думаете, я плаваю?
                  0
                  Возможно, дело в термине «передача по ссылке» (в С++) — в этом случае вызываемая подпрограмма может изменить содержимое переменной, определённой в вызывающей программе.
                  Python передаёт параметры всегда по значению, но все переменные хранят указатели на значения. Т.е. вызываемая подпрограмма получает копию указателя, следовательно, она может изменить (если это указатель на изменяемый объект) значение указателя. НО не может изменить содержимое переданной переменной (чтобы она указывала на другой объект).
                    0
                    Берем:
                    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
                      Мне несколько непонятно, к чему ваш пример? Учитывая, что все переменные в python являются указателями на значения, что вы хотели показать с помощью примера? Я не вижу противоречий с тем, что я сказал.
                      Переменная a указывает куда-то в память на объект list, при вызове функции abc в локальную переменную x копируется переменная a (передача по значению), теперь x указывает на тот же объект list. Соответственно, все операции с x внутри abc меняют объект, на который также указывает и a.
                      «Передача параметра по ссылке» позволила бы внутри функции abc изменить переменную a, например так, чтобы она указывала на другой list или вообще dict. Этого в python, разумеется, нет, о чём я и сказал в предыдущем комментарии.
                        0
                        Мне несколько непонятно, к чему ваш пример? Учитывая, что все переменные в python являются указателями на значения, что вы хотели показать с помощью примера? Я не вижу противоречий с тем, что я сказал.

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

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

                          Это как? Об этом же и рассуждаем, не?
                          Вся «засада» в терминологии: передача параметра в функцию «по ссылке» или «по значению» не имеет никакого отношения к самому значению — указатель там на что-то в памяти или банальный uint8, неважно. Важно только то, что мы складываем на стек — значение, которое храниться в переменной или адрес этой самой переменной.
                            0
                            Отчасти убедили, я упустил из виду, что питон хитро организует хранение переменных, и если вы раз написали a=1, а потом путем вычислений получили other_var=1, то id(a) и id(other_var) будут равны.
                            Это как? Об этом же и рассуждаем, не?
                            на сколько я знаю, при передаче в функцию, к примеру, списочной переменной выделения памяти и копирования ее значения не происходит, что как раз обычно и происходит при передаче параметров по значению. Разве нет?
                              +1
                              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
                                Препарирование вот такого кролика:
                                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
                                  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.

                                  Что касается списков, то тут я в затруднении — списковые литералы должны создаваться отдельными объектами, даже если они содержат одинаковые элементы. Возможно, интерпретатор достаточно умён, чтобы определить, что через переменную, хранящую адрес списка, сам список не изменяется… Т.е. тут у меня знаний не хватает, есть только догадки.
                                    0
                                    Ну, даже с целыми, удивили. А почему [-5, 256], а не [0, 255], что было бы ожидаемо и логично?
                                      0
                                      К октету привязываться смысла нет, всё равно оверхэд на объекты приличный, поэтому imho [0, 255] как раз-таки нелогично. Я думаю, что этот диапазон как-то статистически определили… надеюсь, во всяком случае, но точной информации не имею.
                      0
                      Вчера тупил интернет, не получилось вставить этот комментарий в тред, а сегодня все его игнорируют, может здесь он позаметнее будет. :)
                      Текст статьи тоже изменил.
                  0
                  Возможно, я не совсем понятно объяснил, что ж, попытаюсь быть понятнее.
                  В таких языках как C++ есть переменные, хранящиеся на стеке и в динамической памяти. При вызове ф-ции мы помещаем все аргументы на стек, после чего передаём управление ф-ции. Ф-ция знает размеры и смещения переменных на стеке, соответственно может их правильно интерпретировать.
                  При этом у нас есть два варианта: скопировать на стек память переменной или положить ссылку на объект в динамической памяти (или на более высоких уровнях стека).
                  Очевидно, что при изменении значений на стеке ф-ции, значения в динамической памяти не поменяются, а при изменении области памяти по ссылке, мы модифицируем общую память, соответственно все ссылки на эту же область памяти «увидят» новое значение.

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


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

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

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

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

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

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

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

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


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

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

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

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

                      Если вкратце там словарь (имя переменной: ссылка на объект).
                      ideone.com/LPrD5j
                        0
                        «положить в стек ссылку на переменную, которую мы передаём в функцию» != «положить ссылку на объект в динамической памяти»

                        Давайте внимательно почитаем, что пишут по вашей же ссылке.
                        «call-by-reference» — это передача параметра по ссылке, если бы оно было в python, то могло бы выглядеть как-то так:
                        def cbr(&x):
                            x = {}
                        a = [0, 1, 2]
                        cbr(a)
                        print(a) # {}
                        

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

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

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

                        И по ссылке тоже не нашел, что за механизм связывания (assignment) — там есть про то, что «присваивание создаёт ссылку на объект» и «аргументы передаются присваиванием»
                        [зануда mode off]
                    0
                    По-моему, ничерта это не полезности. Идем в консоль питона, вбиваем import this и читаем до просветления.
                      +1
                      это не «интересности и полезности», а «грабли и мины» — правильное использование всего этого разве что запутает читающего код, а вот наступить и заюзать неправильно можно.
                        0
                        С передачей параметров как раз всё понятно, в вот кто бы объяснил почему -22//10=-3, я читал ЧаВо, но не смог понять.
                          +1
                          А сколько должно быть?)
                          print(-22 // 10)    # -> -3
                          print(-22 % 10)   # -> 8
                          

                          В руби так же
                          ideone.com/eMm8Dh
                          Происходит это потому, что остаток от деления принято делать лежащим в интервале от 0 до делителя, то он отрицательным быть не может.
                            0
                            Вообще-то в числе 22 десятка умещается только два раза (именно поэтому 22//10=2), откуда три можно взять?
                              +2
                              В статье, ссылку на которую, я сбросил, сказано, что обычно в математике остаток считают положительным, число -22 представляется в виде:
                              -22 = -3 * 10 + 8
                                0
                                А, понял, благодарю, не сообразил что-то
                                0
                                А какую задачу вы решаете, что упираетесь в это?
                                  0
                                  Cлучайно наткнулся, точно не помню, возможно листая официальный faq.
                                    0
                                    А тут поясняют когда на это можно напороться.
                                      0
                                      Правда похоже питона это не касается
                                  0
                                  Тут пишут
                                  When either a or n is negative, the naive definition breaks down and programming languages differ in how these values are defined.

                                  хотя похоже что питон использует математически верную реализацию
                                    0
                                    Лучше не использовать целочисленное деление и floor/ceil для отрицательных чисел.
                                    import math
                                    
                                    e = 2.7
                                    
                                    print(math.floor(e), math.floor(-e)) # - > 2 -3
                                    print(math.ceil(e), math.ceil(-e))   # - > 3 -2
                                    


                                    А по мат. определению дробная часть {-2,7} = 0.3.

                                    А возвращаясь к остатку от деления, как правильно сказано в приведённой вами ссылке, обычный способ проверки на нечётность — это y % 2 == 1

                                    Будет обидно, если эта проверка сломается из-за того, что для некоторых чисел данный остаток будет равен -1. Так что лучше не вводить отрицательных остатков.
                                      0
                                      С округлениями как раз всё правильно -3 же меньше -2
                                        0
                                        Разумеется правильно, но не всегда интуитивно, если подойти к человеку и спросить, чему равна целая и вещественная часть -2,7, скорее всего он скажет -2 и 0,7, а не -3 и 0,3. :)
                                          0

                                          Это неинтуитивно только если не знать определения floor и ceil как наименьшего и наибольшего ближайшего целого. Очень быстро привыкаешь правильно их использвать, и всё становится интуитивно понятно.

                                    0
                                    Всё оказалось как-то мутнее.
                                  0
                                  По-моему, книга Бизли Python. Книга рецептов. будет как минимум полезней.
                                  И наверное, тоже интересной.
                                    0
                                    Прежде всего спасибо за наводку. (Скоро рекомендаций наберётся на отдельную статью).

                                    Мы уже обсуждали этот вопрос.

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

                                    У вас вот нет времени читать все статьи из цикла и комментарии к ним, у кого-то нет времени читать несколько книг. :) Так как перекинули на новый проект, который нужно сдавать вчера\вдруг что-то навернулось в работавшей несколько лет системе\через неделю сдавать курсач.

                                    Ну и кроме того, книг много, прочитав одну, не факт, что начнёшь другую, так как и рабочая и личная жизнь диктует своё, а в книге могли что-то не рассматривать, она могла уже устареть.

                                    Надеюсь, всё же в цикле было несколько фактов, которые вы не знали.

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

                                  Самое читаемое