All streams
Search
Write a publication
Pull to refresh
44
0

Программист

Send message
В питоне все значения имеют ссылочный характер

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


>>> a = 1
>>> b = a
>>> b += 1
>>> print(a, b)
1 2

Это поведение передачи по значению, а не передачи по ссылке, пусть и реализовано внутри оно как ссылка на что-то там. Особенно если учесть, что в структурах CPython много самых разных ссылок на разные структуры, и эти ссылки неизвестны большинству сидящих здесь, даже если они много пишут на питоне.
В случае copy-on-write операций со значеним программист, опять же, видит только передачу по значению, и его не должен особо волнует сложный механизм, который отслеживает ссылки и копирует значения при изменениях.


Именно поэтому x[0] += 1 с tuple не работает

Но работает x += (1, ). Что есть нецелостность языка. Как мне создать новый кортеж, в котором нулевое значение увеличено на единицу?


Ответ

https://stackoverflow.com/questions/11458239/python-changing-value-in-a-tuple


t = ('275', '54000', '0.0', '5000.0', '0.0')
lst = list(t)
lst[0] = '300'
t = tuple(lst)
Ну так подобное можно про большинство механизмов сказать, только потом окажется, что дело то не в них. В принципе можно сказать «Пишите код хорошо, а плохо не пишите».

Дай угадаю: ты на ассемблере программы пишешь. И написанные программы почти всегда работают как надо, не преподнося неожиданных проблем.

Генераторы и корутины являются сопрограммами. Я не представляю, откуда вы берете все эти идеи.

Являются. Зеленые потоки — это разновидность выполнения сопрограмм. Но yield и next — это не вызовы сопрограммы, а команды переключения контекста, которые могут переключать его на весьма неожиданные команды — отсюда и аналогия с goto. Моя претензия состояла в нарушении структуры программы генераторами, а не в необходимости обговорить определения терминов.

Приведите примеры. Лично я не наблюдал указанных вами явлений.

Проблема наблюдается при усложнении кода, когда в коде кроме генераторов есть состояния и контексты. Простой код можно написать и без генераторов, и без классов-итераторов — там уже нет разницы. По вопросам менеджеров контекстов в генераторах было уже пара утвержденных PEP, а вот эти ребята, например, хотят ввести возможность блокировать генераторы внутри менеджеров контекста (там же и примеры):
https://discuss.python.org/t/preventing-yield-inside-certain-context-managers/1091


Если переставить две строки кода местами, то порядок их выполнения поменяется

Ничего не переставлялось местами, порядок выполнения команд в обоих примерах из статьи одинаков: «gen_in_manager(); next(g1); print('Конец')» — разница только в том, что во втором примере «gen_in_manager(); next(g1)» вызваны в виде функции. Мне кажется, что ты привираешь про «я решил оба примера правильно».


Давайте я перепишу ваш пример без генераторов и менеджеров контекста, а вы скажете стало ли понятнее?
class Foo:
  def __init__(self):

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


А если не писать print в генераторах, то и предсказывать ничего не придётся.

Да, если не обращать внимания на проблему, то она исчезнет.


Между прочим, ваш пример с "вход-выходом" можно запросто переписать хоть на Го, хоть на Хаскель. Сможете ли вы угадать, в каком порядке будут выводиться строки у "независимо" обрабатываемых потоков?

Смогу: в произвольном. Но там будут настоящие потоки, которые можно выполнить на нескольких процессорах.


Ну а зеленый поток Го хранит не просто неявный фрейм стека, а аж целый "неявный" стек

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


Наверное, они просто не принимают в локальные переменные случайные ссылки.

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

Попробуй сделать int.__hash__() вместо hash(int) и многое прояснится само собой.

Я не совсем понимаю, при чем тут хэш от класса к вопросам диспетчеризации. Описанное по ссылке — это некоторые детали костыльности реализации слотов в CPython, а именно: в слоты класса не попадают атрибуты экземпляра объекта, и встроенные функции используют именно слоты класса. hash(int) делает то же, что и type.__hash__(int), поскольку именно type является классом для int.

Это проблема модульного тестирования, которая решается интеграционным тестированием системы в целом. Причем тут питон?

При том, что много языков проблемы систематической поломки тщательно оттестированного модуля не имеют. Да, есть проблемы с повреждением памяти у C/C++, но для того и придумали Java/C#.


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

Утиная типизация есть у интерфейсов Go: если у структуры имя метода совпадает с именем метода в интерфейсе, то считается, что для структуры реализован метод для этого интерфейса, даже несмотря на то, что интерфейс не упомянут в структуре и вообще мог быть написан после написания структуры. Например:
https://putridparrot.com/blog/interfaces-and-duck-typing-in-go/
Что не помешало Go быть статически типизированным и статически компилированным.


Разумеется, что класс (как и почти любая вещь в питоне) может быть изменен во время выполнения

Много чего в CPython нельзя изменить во время выполнения: огромные слои стаднартной реализации неподвластны динамичности, я упомянул лишь один пример в статье — невозможность переопределять стандартные списки. Как там было… «вы можете выбрать автомобиль любого цвета, если этот цвет — чёрный».


Потому что multiprocessing работает с разными процессами, у которых внезапно разные адресные пространства. Причем тут питон?

Pickle же ж. Схоронили один класс, а достали уже в другой. Изменяемые определения классов — это почти всегда зло. Они не зло только при написании и отладке. Конкретно в случае параллельного выполнения задач изменяемые определения классов становятся злом в квадрате.


переопределение доступа к атрибутам через getattribute, getattr и прочие

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



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


Исключения, continue, break, with, return тоже можно рассматривать как частный случай goto.

По порядку:


  • исключения, действительно, проблематичны, особенно на фоне того, что нынче в CPython принято использовать их штатно, например, для остановки итераций, а не только лишь в исключительных случаях, как можно предположить из названия;
  • continue, break, return — это справедливые переходы для программы в форме иерархии Косарайю, которая считается модификацией оригинального структурного программирования Дейкстры. Эти конструкции не нарушают иерархию кода, давая выходы по конкретным логическим блокам, в отличие от исключений, обработка которых прыгает как попало. К слову, в Go исключения убрали, а continue, break, return — оставили;
  • with — не понимаю претензии, где тут goto? Вызов функции, как и вызов вложенного блока в нотации with, не нарушает структурированности кода.

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

Приведение типов считается некой разновидностью полиморфизма, и лишь от определений и конкретных критериев зависит, будем ли мы называть приведение «настоящим полиморфизмом» или же плюшевым.


В статье много рациональных зёрен, но они размазаны по стене текста с неясными претензиями

К сожалению, у меня нет конкретных конечных целей, и я не хотел ограничивать возможность действий чем-то вроде «завтра же начинаем делать транслятор питона с такими и такими функциями».

Разница в том, что реализация method у разных object разная

Тебя покалечили ООП. Функция — это функция, данные — это данные. Разные функции работают с разными данными, они сами по себе не принадлежат каким-то объектам. В питоне можно вполне законно вызвать метод объекта с произвольным значением вместо self:


class Test():
    value = "hello"
    def test(self):
        print(self.value)

class A():
    pass

a = A()
a.value = "world"
Test.test(a)

Механизм принадлежности к объекту или классу — это лишь некоторые варианты установления соответствия. Один из, но далеко не единственный.

Почему вы решили, что все мечтают о статической типизации?

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

Что-то не понял из этого потока сознания, зачем автор программирует на Питоне, если ему не нравятся основы языка?
Может, вам просто взять другой язык, C++ или Go например?

Очень мало пишу на питоне. Я написал в статье, что питон плохо ложится в привычные мне сферы. И даже Go плохо ложится. Очень хорошо ложится C++, но как с молодости не подружились.


Меня устраивают основы питона как языка — я не согласен с нынешней популярной реализацией. А она таки много кого не устраивает — просто, мало кто об этом говорит вслух, мало кто идет дальше «да нужно просто GIL убрать и все проблемы решатся» или «вот, аннотации расставим, и тогда заживем». Не просто так было создано столько альтернативных проектов.


Ну так транслируйте программу(или сразу пишите) в C/C++.

Nuitka и Cython уже есть, но они не особо помогают.


В Питоне самый смак в том, что можно запустить консоль и в реальном времени переопределять классы и переменные. Смысл это в нем убирать? Не нужно это?

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


Нужны проверки типов? Используйте сеттеры с проверкой типов, делов то

Да, вопрос применения дескрипторов атрибутов я затронул в статье. В нынешней реализации они решают лишь одну проблему из дюжины. Что делать с локальными переменными и аргументами функций? Что делать со вложенными структурами?


Нужен запрет переопределения классов? Используйте функцию проверки в конструкторе.

Вот эта магия мне еще не подвластна. Каким образом проверка в конструкторе поможет избежать переопределения класса?


Искать ошибку в коде на 1000строк? Так множественное наследование и динамическое выполнение на то и направлено, чтоб локализировать эту ошибку в 30 строках.

В последнее время не работаю с проектами меньше 500 тыс строк. Самый отвратительный код — это когда единый функционал разбит на много-много файлов и классов, по которым приходится прыгать, чтобы собрать картину воедино. Это не проблема на масштабе в тысячу строк, это проблема на масштабе 100+ тысяч. Именно по этой причине столько людей используют прием "божественный объект", который считается антипаттерном, однако же, при всех его ограничениях и проблемах, он дает замечательную локализацию кода и возможность прочитать единый функционал в одном месте.

В Python есть аннотация типов, а также механизмы приведения и проверки. Да, это не особо удобно или красиво, но почему «не даёт возможности» тестирования?

Конкретно в этом случае вопрос не в тестировании, а в статической проверке кода. Только в 3.8 родили наконец понятие структурного подтипа: PEP 544 — Protocols: Structural subtyping (static duck typing)
Почему так поздно? Мне кажется, что создатели сами понимают, что и этот механизм очень далек от идеала, и скорее удовлетворяет хотелки людей, которым нравится жить в иллюзии, будто в питон вводится статическая типизация. Стоит понимать, что статическая типизация нынче много кому нужна, но ее нет и в ближайшее время не будет.

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

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


Однако же, по логике вещей, x[0].append(0) должно было бы создать новый или взять уникальный список из нулевого элемента и добавить в него нуль

Откуда такая логика? Потому что каком-то другом языке так? Не стоит по умолчанию тянуть концепции из других языков в Python.


Наверное, это самое странное утверждение в статье. Смысл Python зафиксирован в The Zen of Python. Всё остальное — ваши домыслы.



Ну а как же "Explicit is better than implicit" и "practicality beats purity"? Ссылки на массивы формируют неявное поведение, которое непрактично. Я соглашусь разве что с тем, что нельзя менять поведение для старого кода — новые списки должны быть явно обозначены. Здесь фундаментальная проблема заключается даже не в списках, а в отсутствии явного разграничения значений и ссылок на значения.


С чего вы это решили? Если меняете значение переменной, то поменяется результат вычислений. Это работает во всех языках.

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

А что тут общего с goto? Конструкция goto передает управление навсегда, а тут идёт обычный вызов сопрограммы. С гарантией, что управление вернется обратно.

Конструкции goto использовались как для продолженния работы с обходом ненужных инструкций, так и для цикличного/подчиненного выполнения. Если придираться к деталям, то генераторы — это самые что ни на есть зеленые потоки, а вызов next/yield переключает контекст. Это не вызов сопрограммы, потому что точка входа неопределена — вызывающая функция не знает, что вызывает. Примерно так в древности вызывали функций при помощи goto/jmp, когда вызывающий код прыгает куда-то в середину блока кода, и последний должен по каким-то правилам вернуть управление/возобновить выполнение вышестоящего кода. Это рушит структурированность кода и создает проблемы как минимум в понимании алгоритма работы человеком. Кроме того, это создает трудности в понимании работы еще и для машины, читай "для тестов и оптимизаторов".


Но я решил оба примера правильно. Они ведут себя совершенно логично. Где тут плохая предсказуемость?

Поздравляю. Мое первое возражение описывается так: я жму 120 кг от груди; можно ли сказать, что это детский вес и любой ребенок с ним справится? Мое второе возражение описано в статье: минимальное изменение структуры кода меняет порядок выполнения. То есть, для предсказания поведения кода программиста досконально должен знать весь код в генераторов с контекстом, даже если этот код разбросан по нескольким модулям и часть из этих модулей хорошо оттестированы.


Генератор — это уже оптимизация

Да. Код на итераторах в CPython выполняется медленнее. "Сломал ногу — учись ходить на руках".


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

В статье я слабо прошелся по матчасти о параллелизации кода, о том, почему одни алгоритмы очень легко параллелизуются, а другие — нет. Один из ключевых факторов параллелизуемости — возможность изменить порядок выполнения команд. Те же Go и Haskell свои зеленые потоки могут спокойно разбрасывать по потокам ОС, потому что контексты, изменяемые состояния у потоков изолированы, их обработка независима, порядок выполняемых операций в разных потоках может меняться. Генераторы питона, как зеленые потоки, наоборот, отличаются тесной связанностью состояний потоков, недопустимостью изменения порядка команд, и повышением сложности контекста, который теперь хранит неявный объект фрейма стэка со всеми объектами в этом стэке, и эти объекты, по идее, не должны меняться во время замирания потока-генератора. Напоминаю, что это происходит на фоне отсутствия механизмов работы со значениями, из-за чего, даже при воображаемом наличии реализации многопоточности, нам пришлось бы прикладывать большие усилия для того, чтобы случайно не выстрелить себе в ногу, приняв локальной переменной в генераторе ссылку на изменяемый объект.


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

И где гарантия того, что протестированный отдельно генератор будет работать так же в составе целой системы? Её нет.

12 ...
8

Information

Rating
Does not participate
Location
Украина
Registered
Activity