Pull to refresh

Comments 69

Хорошо, спасибо за информацию, единственное что не совсем очевидно это последний пример.
То есть когда мы объявляем список lst=[], он создаётся один раз и при каждом вызове функции он обращается к нему, а если мы будем создавать его внутри функции, то он будет каждый раз его создавать? Хотелось бы понять его видимость)
Речь идет только про параметры по умолчанию. Если внутри функции создается локальный объект, то при следующем вызове он создастся вновь. Вот пример:
def get_lst(a):
    lst = []
    lst.append(a)
    return lst
get_lst(1)
[1]
get_lst(2)
[2]

Вы же про это спрашивали, верно?
Следовало бы добавить, как правильно использовать аргументы по умолчанию (если нужно передавать что-то, изменяемое, например список, экземпляр класса или словарь)

def func(a, lst=None):
    if lst is None: 
        lst = []
    lst.append(a)
    return lst
print(func(1))
print(func(2))
print(func(3, [1]))
...
[1]
[2]
[1, 3]
Также можно добавить, что иногда None может быть валидным значением параметра, тогда такой вариант не позволит отличить получение значения по умолчанию или явное указание None для параметра при вызове функции. Тогда можно использовать специальное дефолтное значение для параметра:
DEFAULT_VAL = object()
def func(a, lst=DEFAULT_VAL):
    if lst is DEFAULT_VAL: 
        lst = []
    elif lst is None:
        # some None handling
        pass
    lst.append(a)
    return lst
спасибо за подробный разбор. Мне кажется это тот случай когда багу назвали фичей.

Следовало бы добавить первопричину такого поведения: Питон неотложно исполняет инструкции, соответственно, когда он натыкается на


def f(val = []):
    print(id(val))

то создаётся объект f типа function и записываются вычисленные значения аргументов в аттрибуты f.__defaults__ и f.__kwdefaults__.


Это легко проверить:


>>> f()
140694137408008

>>> print(id(f.__defaults__[0]))
140694137408008
Теперь понятнее, спасибо
Я немного идиот во внутреннем устройстве питона, но как я понимаю неочевидное поведение с параметром по умолчанию возникает потому что muttable объект создаётся на этапе импорта модуля, а не вызова функции.
> 1. Копирование словарей или списков
> 3. Обновление списков или словарей
> 4. Интернированные (Interned) строки

Это же объясняется у Лутца сразу же в первой паре сотен страниц. Поэтому первая ошибка — не читать учебник, а думать, что по статьям в интернете всему научишься.
Тут не нужна статья, чтобы догадаться, что процедура вставки элемента в список новый список не вернет, почему она вообще должна это сделать?
И списки можно копировать через срезы:
b = a[:]

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

В некоторых языках коллекции по-умолчанию иммутабельны

>>list_a = list_a.append(6)

Присваивание лишнее. С update у dict тоже самое.
То, что статья про распространенные ошибки, вас не смутило?
Да я потом уже увидел, поплавило меня что-то, голова уже несколько дней болит, извиняйте
Так как довольно часто (если не везде) используется NumPy, то в третий пункт для списков я бы добавил, что при случае с np.array() фокус с методом np.append() происходит как раз в обратную сторону, т.е. для добавления элемента в список нужно присваивать переменной именно возвращаемое значение метода, т.к. простой вызов метода np.append() не добавит значение в существующую переменную, а создаст новый объект:
>>> a = np.array([1,2,3])
>>> a
array([1,2,3])
>>> np.append(a, 4)
array([1,2,3,4])
>>> a
array([1,2,3])
>>> a = np.append(a, 4)
>>> a
array([1,2,3,4])

Бывает, что на автомате пишешь и забываешь про это, а потом ищешь, где же ты потерял значения.
TL/DR: проблема не в вас, а в NumPy.

Стандартные библиотеки Python обычно построены с учётом принципа CQS. Python-программисты обычно привыкают к нему. Это позволяет создавать простые и интуитивно понятные API.

NumPy нарушает этот принцип без видимых на то причин, он непитоничен, поэтому им трудно пользоваться.

А зачем давать ссылку на английском, если есть на русском?

Интересно, а хоть один из минусующих нормально ответить на мой вопрос сможет, или все, на что они способны, — по тихому анонимно сливать мой комментарий по делу? Если вы отвечаете за себя, конечно.

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

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


numpy вполне себе питоничен и им легко пользоваться когда вы работаете с матрицами или многомерными массивами и выполняете над ними векторизованные операции.

У иммутабельных типов в Python нет метода 'append'. Он есть у мутабельных. Называть так метод, создающий новый объект, как минимум неконсистентно. А по-честному, так это просто сбивает с толку пользователей.

NumPy вообще-то непитоничен и в более простых вещах, например, не придерживается PEP-8 в именованиях методов и параметров, и насыщен сокращениями вплоть до полной нечитаемости. Откройте справочник и убедитесь.

Я не уверен, зачем это так сделано, но полагаю, что это должно было как-то помочь пользователям MatLab или Fortran перейти на Python. Если можете, просветите меня в этом вопросе. Но говорить, что NumPy питоничен − это просто отрицать очевидное.
не придерживается PEP-8 в именованиях методов и параметров, и насыщен сокращениями вплоть до полной нечитаемости. Откройте справочник и убедитесь.

Вот вообще не вижу, где numpy в именовании методов не придерживается PEP-8? Вот вижу, например, как стандартный logging и unittest не придерживаются :)


Сокращения, вы имеете в виду разные математические функции? Это общеупотребительные устоявшиеся названия. Вы же не ругаете Python за то, что там есть len, pow, min, max и модуль math, в котором есть sin, cos ну и т. д.


Я не вижу в чём numpy непитоничен, для меня это точно так же не очевидно. Непитоничность очевидна для logging и unittest, которые скопированы с Java библиотек, очевидна для обёрток, которые тупо сгенерированы каким-нибудь SWIG или скажем для PyQt/PySide, который копирует API Qt без всяких pythonic-фишек сверху.

То есть все эти asarray, infstr, nditer, ma − это всё общепринятые математические символы?

А то, что функции NumPy игнорируют возможности Python по обращению с аргументами и требуют всё приводить к единственному аргументу-последовательности вместо args − это тоже питонично? Вот это, например, как следствие: github.com/numpy/numpy/issues/6555?

Я соглашусь, что в numpy есть косяки с api и с поддержкой iterable. Ну вот, например, самый явный:


np.zeros(4, 5)  # TypeError: data type not understood
np.zeros((4, 5))  # ok

np.random.rand(4, 5)  # ok
np.random.rand((4, 5))  # TypeError: 'tuple' object cannot be interpreted as an integer

Неконсистентность есть, конечно.


Или вот, например:


a = np.array([1, 2, 3])
if a:
    pass
# ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

a = np.array([])
if a:
    pass
DeprecationWarning: The truth value of an empty array is ambiguous. Returning False, but in future this will result in an error. Use `array.size > 0` to check that an array is not empty.

Ну и iterable, конечно


g = (_ for _ in range(10))
np.array(g)
Out[62]: array(<generator object <genexpr> at 0x0000017750CCC048>, dtype=object)

То есть все эти asarray, infstr, nditer, ma − это всё общепринятые математические символы

Нет, конечно :)
Но в Python тоже есть str, iter, asyncio, io, sys, enum и т. д.


Смысл придираться к подобным сокращениям, если в рамках предметной области ясно о чём идёт речь.

logging и unittest действительно чужеродны, но они хотя бы понятны. Потому что они скопированы с Java.

То, что NumPy не просто чужероден, а выглядит как жертва обфускации, я могу объяснить только тем, что он, возможно, скопирован с Fortran.

logging и unittest монстроузны и избыточны именно потому что просто скопированы из Java. :)


Сравните их с пакетами loguru и pytest. При том, что pytest не только более простой и изящный в использовании (и плевать, что там под капотом дикая черная магия), но ещё и более мощный с точки зрения функциональности и рсширяемости.

Я сам вовсю пользуюсь pytest, you're preaching to the choir. Просто как по мне, так лучше многословность Джавы, чем вот эта ситуация, когда буквально каждый идентификатор в библиотеке − это самобытное сокращение.

На ваш взгляд, насколько готовая к проду библиотека loguru?

Я использую loguru в новых проектах и пока не сталкивался с какими-то проблемами. Для небольших и экспериментальных проектов её даже настраивать не надо, просто используешь и всё работает. Вполне себе production ready библиотека. Она очень простая в отличие от замороченного logging, но в то же время умеет всё или почти всё, что умеет logging. Также может конфигурироваться через конфиг.


Кстати, на счет конфигов, вот такие вещи меня просто ставят в тупик в logging:


Warning

The fileConfig() function takes a default parameter, disable_existing_loggers, which defaults to True for reasons of backward compatibility. This may or may not be what you want, since it will cause any non-root loggers existing before the fileConfig() call to be disabled unless they (or an ancestor) are explicitly named in the configuration. Please refer to the reference documentation for more information, and specify False for this parameter if you wish.

И ещё на счёт CQS в Python.


It states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both.

Привет, dict.setdefault :)

Если правильно понимаю, можно уточнить про ключи (пункт 2). «Эквивалентно» для True и 1 значит то, что методы __hash__ у них возвращают 1 (для предотвращения коллизий, насколько мне известно, в словаре ещё производится сравнение ключей по __eq__, что для 1 и True возвращает также True). Получается, можно написать класс вида

class Deceiver:
    def __hash__(self):
        return 1
    def __eq__(self, everything):
        return True


и он будет вытеснять ключ int(1), хоть класс и не наследуется от int:

a = {1: 1, 2: 2}  # a = {1: 1, 2: 2}
a[Deceiver()] = 100  # a = {1: 100, 2: 2}
Кто-нибудь на практике встречал использование a[True] и a[1] в одном месте? Боюсь, что начинающий в этом не допустит ошибки, т.к. просто не будет подходящей ситуации.
никогда не было необходимости написать a[True]
Я, конечно, фантазирую, но допускаю ситуации вида:
keys = read_keys_from_json_from_other_service() #[0, 1, True, 2]
res = dict()
for key in keys:
    value = str(key)
    # Some additional code
    res[key] = value

В результате получим, что в res будет {0: '0', 1: 'True', 2: '2'}

append, update это методы, который вызываются у инстанса объекта, о каком присваивании может вообще идти речь? тут дело не в новичке питона, а новичке в IT и программировании в частности.
Вы неправы. В других ЯП, например, JavaScript, есть много API, построенных на том, что методы всегда возвращают ссылку на объект, для которого были вызваны. Этот приём называется method chaining. Программисты, привыкшие к нему, пытаются применить его в Python − чаще всего безуспешно, в Python это не принято.

Гвидо не любит цепочки вызовов: I find the chaining form a threat to readability.
Был бы очень благодарен, если бы кто-нибудь рассказал как правильно работать с импортами. Делать модуль глобальным иногда плохая практика, а просто так импортировать модуль из родительской директории питон не даёт. Не нужно напоминать, что это тоже плохая практика, это всегда trade-off, просто дайте рецепт. Java, C++, Ruby, и практически все популярные языки позволяют это делать.
UFO just landed and posted this here

В свое время больно обжегся на таком:


correct = [ [1] * 3 for i in range(0, 3)]
correct[0][0] = 10
print(correct) # [ [10, 1, 1], [1, 1, 1], [1, 1, 1] ]

incorrect = [[1] * 3] * 3
incorrect[0][0] = 10
print(incorrect) # [ [10, 1, 1], [10, 1, 1], [10, 1, 1] ]

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

UFO just landed and posted this here
  1. Такая проблем может возникнуть и людей пришедших из древнючих языков (вроде Бейсика или Перла), где присваивание идет по значению.


  2. Имхо это легаси Перла и косяк дизайна Питона. Булевы значения никакого отношения к целочисленным значениям не имеют, и то что True == 1 — это нифига не очевидная особенность языка. Впрочем все равно лучше чем у прародителя — true/false вообще отсутствуют, а в качестве false используется 0, '' или undef, а в качестве true — все остальное.


  3. Лишь вопрос API. Например такой подход к построению API не позволяет делать чейнинг операций:


    a = []
    a.append('A').append('B').append('C') #=> ['A', 'B', 'C']

  4. А есть еще и языки где строчки вообще иммутабельные.


  5. Скорее косяк Питона — тут ничего удивительного что у людей возникают вопросы.


    Например в Ruby:


    def foo(a = [])
      a << "world"
    end
    
    foo()
    => ["world"]
    foo()
    => ["world"]

    JavaScript:


    > function foo(bar = []) { bar.push("world"); return bar; }
    > foo()
    ["world"]
    > foo()
    ["world"]

Насчет 4 — Строки и в python считаются иммутабельным типом.
В остальном же, ИМХО, приведенные ответы на пункты из статьи являются всего лишь субъективными (абсолютно валидными при этом) дополнениями, так что не вижу необходимости комментировать.

древнючих языков (вроде Бейсика или Перла)
К сведению: разработка перла началась в 1987-м, а питона в 1989-м
Спасибо, очень полезная статья. Прошёл в своё время через все эти ошибки. Исключая, конечно, True и 1. Это, действительно, маловероятно.
Питон прекрасный язык, но есть два класса людей, между которыми лежит пропасть: (грубо говоря) те, которые писали питон, и все остальные. Это очень эффективный инструмент, когда ты знаешь, что там под капотом. Проблема в том, что он всё чаще используется для обучения: ничего не надо знать, учись программированию на питоне! Я своими глазами видел тысячи человеко-часов, потраченных на дебаг именно подобных ошибок. Причём большинство из приведённых в статье ошибок основаны на том, что в питоне всё идёт через указатели (ссылки). Но есть и куча других проблем, например, scope:

for i in range(10):
  pass
print(i)


WTF?! Почему я могу достучаться до переменной i вне блока, её объявившего?

Ну или вот такое:

def foo():
    print(a)

a = 1
foo()


Этот код просто выведет 1 в консоль. А вот этот не скомпилируется:

def foo():
    a = a + 1
    print(a)

a = 1
foo()


А почему? А потому что по умолчанию scope в питоне глобальный, если переменная только на чтение, и локальный если на запись… Те, кому это не было очевидно с первого же взгляда, не забывайте отмечаться в комментариях :)

Ещё примеры подобных языков?

Про for: по той простой причине, что если нужно пользоваться значением переменной при выходе из цикла при break, вам не придётся заводить дополнительной переменной вне цикла.


Про "не скомпилируется" — у питона чётко прописан механизм доступа к переменным. Когда в функции foo() происходит чтение переменной a, то интерпретатору не надо задумываться откуда происходит чтение. Сначала идёт поиск в локальном контексте, потом в глобальном.


А вот при присваивании всё сложнее:


def foo():
    a = a + 1

С чего интерпретатор должен считать что при присваивании значения переменной a, её область видимости глобальна? Отстутствие a в локальном контексте — не причина так считать. Ведь есть ещё и вложенные функции! Например:


def foo():
    a = 20

    def foo_inside():
        a = a + 1

    foo_inside()

Здесь будет та же ошибка, хотя а не глобальна, а лежит вне локального контекста foo_inside().


Поэтому и существуют операторы global и nonlocal явным образом задающие контекст переменной.

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

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

Ну так по вашей логике, человек пришедший скажем из Питона в C, будет также недоумевать, а с чего это


#include "stdio.h"

int main() {
    for (int i = 0; i < 10; i++);
    printf("%d", i);        
    return 0;
}

не компилируется?


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


А учителям и ученикам можно порекомендовать соответствующий раздел туториала :)

Если что-то не компилируется, то это просто прекрасно — компилятор нашёл ошибку. Хуже, когда компилируется, а с питоном, к сожалению, это случается чаще.

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

А для программирования на плюсах корочки по устройству памяти, неявным преобразованиям всего подряд в целочисленный тип и по UB :)
>> WTF?! Почему я могу достучаться до переменной i вне блока, её объявившего?
100 лет не держал в руках VB, но там вроде то же самое было. VB6, VBA.
Иногда было удобно.
WTF?! Почему я могу достучаться до переменной i вне блока, её объявившего?

Ну потому что цикл не создает никакого «блока», где объявляются переменны (в отличие от def или class).


for i in range(10):
    a = 2
print(a)
Да, ответ таков, но он неочевиден, в чём и смысл моего сообщения.

А вот если мы возьмём не цикл, а list comprehensions, то поведение вообще будет разное между Py2 и Py3.


Py2:


>>> a = [i for i in range(10)]
>>> i
9

Py3:


>>> a = [i for i in range(10)]
>>> i
NameError: name 'i' is not defined

В Python 3 поправили область видимости для list comprehensions.


А как вам новый синтаксис из Py3.8 (PEP572)?


if (n := 15) > 10:
    n += 5
print(n)
20

И тоже n оказывается доступна вне if. :)

UFO just landed and posted this here

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

Просто все функции, которые ничего не возвращают, возвращают None. :)


Если у вас функция возвращает, например: typing.Optional[str] (None или str), то на вас лежит ответственность проверять результат на None. Если функция просто ничего не возвращает, то, естественно, не надо проверять её результат и удивляться, что там None, а не ошибка.

>>> dict_a[True] = "mango"


Неужели это — распространенная ошибка?
Могу только надеяться, что она может быть распространена в стране, которая начинается на «И» и заканчивается на «я».

Я вот тоже в замешательстве. Может Италия или Индонезия?

Но цепочки вызовов хотя бы отчасти позволяют обходить урезанные возможности лямбд. Ещё цепочечные функции удобны в векторных и матричных вычислениях. Там читаемость, ИМХО, напротив, повышается.
Я думал, что Питон из JS не брал ничего…
Sign up to leave a comment.

Articles