Как стать автором
Обновить

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

со значением по умолчанию в функции вообще внимательным нужно быть,

def f(x, d=datetime.now()):
    print(x, d)


скорее всего результат будет не тот которого ожидаешь

Поэтому правильный вариант, очевидно:


def f(x, val_getter=datetime.now):
    print(x, val_getter())

Хотя можно и чуточку усложнить, дабы предупредить более "плоское" использование, но тогда логике посыпится в редких случаях, но они специфичны и довольно легко обходятся:


def f(x, val_handler=datetime.now):
    if callable(val_handler):
        print(x, val_handler())
    else:
        print(x, val_handler)

Вообще, статья какая-то игрушечная, я бы сказал — кликбейторская. Обладая простейшим пониманием как работают ссылки (и немного про область видимости в третьей "задаче") — "решения" наиочевиднейшие.

вообще, если на вход ожидается datetime,
def f(x, d=None):
    print(x, d or datetime.now())

мне больше нравится вот такое решение

Это если ожидается datetime, здесь это никоим образом не продемонстрированно, а из контекста непонятно. Но и такой вариант жизнеспособен, конечно.

Только такой вариант я и видел в качестве «рекомендованного».

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

Тут вопрос в использовании данных. Если функция быстрая — то всё хорошо, но если медленная/асинхронная, а то и вовсе — переодическая — то у нас проблемы. Подобные подходы в ORM используются, например.

Мне кажется, что во второй задаче, вместо решения


if s is None:
    s = set()

будет более идиоматично использовать:


s = s or set()

Ну и это решение сразу показывает другую особенность Python, когда логический оператор возвращает значение.

Согласен, такое решение более элегантно. Но тут код взят из оригинальной статьи =) Не стал менять =)
Нет, как раз таки хорошее решение. Если в функцию передать пустой set(), в который мы хотим добавить значений, то они не добавятся в него
Нет, ибо если функции передать пустой set (именованный), в надежде, что она его поменяет также, как не пустой, то «более идиоматичный» способ поведёт себя по-другому и оставит его неизменным.

Да, вы правы но, если мы обсуждаем коня сферического в вакууме то:


  • идиоматичным (причем не только в Python), как раз, является возврат значения из функции, а не изменение входных аргументов и, таким образом, неявный возврат значения (да исключения бывают).
  • в данном примере ничего не планировалось возвращать из функции, это синтетический пример, суть которого в объяснении одной из особенностей языка Python, связанной с временем вычисления аргумента по умолчанию.

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

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

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

Ну или грузовик — прекрасно ездящий если не пытаться на нём грузы перевозить…

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

Совсем ненормально — объяснять, что код-то отличный, идеоматичный, красивый… а что он не работает как нужно — ну так то вообще дело десятое…

Нравятся функции с неявным возвратом значений через изменение аргументов (побочные эффекты)? Ну тогда не используйте предложенную конструкцию. На SO достаточно веток, обсуждающих "правила безопасности" функций с побочными эффектами, но все реально.


Хотите создавать функции без побочных эффектов с явным возвратом значений ("Explicit is better than implicit." — Zen Of Python)? Можете использовать как одну, так и другую конструкции. Оба варианта будут полностью работоспособными. На SO есть ветки обсуждающие что "правильнее".


На суть статьи ни один из вариантов никак не влияет, хотя-бы потому, что "как нужно" не указано. Мы же не будем, устраивать полноценный code review для синтаксического, учебного примера?


Ну и не надо забывать, что вариант с s is None не является эквивалентом s or set(), надо четко представлять в чем разница и плюсы и минусы обеих конструкций. В жизни приходится применять оба подхода. Всегда лучше иметь выбор и знать о том, что логические операторы в Python могут использоваться подобным образом.


Эпитеты, пожалуйста, оставьте при себе.

Хотите создавать функции без побочных эффектов с явным возвратом значений («Explicit is better than implicit.» — Zen Of Python)? Можете использовать как одну, так и другую конструкции. Оба варианта будут полностью работоспособными.
Ну что за детский сад, честное слово.

Ни одна из этих конструкций работать, очевидно, не будет, потому что ни в одной из них множество не копируется.

Если весь дальнейший код это множество, после этого, ещё и не меняет — то ни одна из этих конструкций не нужна. Если меняет — ни одна из них недостаточна, нужно либо copy(), либо, возможно, даже deepcopy() использовать.

На суть статьи ни один из вариантов никак не влияет, хотя-бы потому, что «как нужно» не указано.
Не указано. А ещё там не указано, что данная функция не должна отсылать содержимое /etc/passwd на сервер ботнета. И ешё не указано, что она не должна ломаться в пятницу, 13го. Обозначает ли это что решение, содержащее «по приколу» что-нибудь «развлекательно-познавательное», ну, скажем… if random.randrange(1000000) < 7: sys.exit() будет хорошим решением?

А чё? В условиях же не сказано.

Мы же не будем, устраивать полноценный code review для синтаксического, учебного примера?
А почему нет, собственно? Как вообще на учебных примерах человек может чему-то научиться если они по идиотски написаны?

Ну и не надо забывать, что вариант с s is None не является эквивалентом s or set(), надо четко представлять в чем разница и плюсы и минусы обеих конструкций.
Но вы же забыли. Или специально решили «подловить новичка на подлости» (что ещё хуже).

Может быть вы хотели написать что-нибудь типа s = (s or set()).copy()?

Да, это, возможно, будет работать — но это, извините, тоже ребус. Уж в этом-то случае проще написать s = set() в параметрах и s = s.copy() в качестве первой строки…

На SO есть ветки обсуждающие что «правильнее».
Отлично — покажите, пожалуйста, где, кто и когда изобретённую вами семантику «чтобы получить результат работы через переданное вами множество нужно в него, предварительно, засунуть фиктивный элемент» считает правильной. Особенно интересно увидеть как они предлагают боротьсь с тем, что этот элемент может оказаться и в результатах работы тоже… а главное — зачем такое может потребоваться…

Эпитеты, пожалуйста, оставьте при себе.
Знаете — когда тебе, на полном серьёзе, предлагают что-то из книжки «вредные советы»… это не так-то просто сделать…
Если бы a был изменяемым объектом, то команда a.append(5) меняла бы исходный объект, и тогда список s «видел» бы изменение.
Но в данном случае же a — изменяемый объект, и его изменение влияет на s, нет?
Вы правы, ошибся немного в переводе. Скорректировал

"при первом вызове s будет инициализировано как пустое множество"


Пустое множество создаётся в момент определения функции, а не её вызова.

a = [1]
b = [2]
s = [a, b]
a.append(5)

>>print(s)
>>[[1, 5], [2]]


Если бы a был изменяемым объектом, то команда a.append(5) меняла бы исходный объект, и тогда список s «видел» бы изменение.

так вроде ж так и есть?
почему бы?

Скучно. Нате поинтереснее


def f():
    a = 1
    def g():
        nonlocal a
        print(a)
        a+=1
    return g

g1 = f()
g2= f()

g1()
g2()
g1()
А что тут интересного? Ещё если бы nonlocal вёл себя как global — я бы удивился… а так — всё работает самым очевидным образом, я бы сказал…

Ок, ок! Ну так то хоть повеселее?


def f():
    a = 1

    def g():
        nonlocal a
        print(a)
        a += 1

    def update(x):
        nonlocal a
        a=x
    g.update=update

    return g

g1 = f()
g2 = f()

g1()
g2()
g1()

g1.update(100)
g1()
Если помнить, что в Python всё на свете это просто dictionary, то такие примеры распутываются легко. Но когда у вас там начинает уже фигурировать десяток объектов, то тут уже нужно на бумажке рисовать куда, чего и когда вы двигаете. Но ничего принципиально не меняется.

Ну кроме того, что если функции ничего не возвращают, то результат — всегда None… но я надеюсь пример был не об этом…
По поводу 3-го случая объяснение в статье какое-то загадочное.
Однако попытка изменить неизменяемую переменную во второй функции y += x приводит к тому, что y начинает ссылаться на другой адрес в памяти: исходная y будет забыта, что приведёт к ошибке UnboundLocalError.

Здесь мы пытаемся использовать неинициализированную переменную. Потому как: y += x это y = y + x. Справа доступ к неинициализированной y.
Потому как: y += x это y = y + x. Справа доступ к неинициализированной y.

Не совсем.


Во-первых, y += x это не y = y + x, а вовсе даже y = operator.iadd(y, x).


Во-вторых, а с чего она там не инициализирована-то? — мы же вроде как захватили ее из родительского скоупа, см. пример 1. Более того, если вместо y += x вы напишете z = y + x все снова будет прекрасно работать с кложурно захваченным y. y становится неинициализированным внутри скоупа функции когда парсер натыкается на y =. Тут происходит неявный хойстинг и внутри всего скоупа — бамс! — y не инициализирован. Смотрите:


>>> def g():
...   y = 0
...   def inner():
...     print(y) # BOOM!
...     y = 42
...     return y
...   return inner
... 
>>> g()()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in inner
UnboundLocalError: local variable 'y' referenced before assignment

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

В контексте данной проблемы x += y и x = x + y одно и тоже (т.е. от деталей можно абстрагироваться). Если Питон видит assignment для переменной, то она автоматом помещается внутрь локального scope, если не указан nonlocal или global.

Понятие hoisting немного смежное, но к питону это не имеет отношения. При загрузке модуля (скрипта), пока мы не дойдем до объявления класса/функции (и не выполним этот код!) — обращаться к таким объектам нельзя. Переменные вообще невозможно объявить без инициализации.

Бывают специфические для языков штуки. Вот как оно в CS бывает.
Такие случаи или понятны интуитивно, или ты в них не уверен и проверяешь в интерпретаторе.
Зачем это на бумаге уметь выводить/помнить — хз.
Почему так происходит? Когда мы исполняем l.append(x), меняется изменяемый объект, созданный при определении функции. Но переменная l всё ещё ссылается на старый адрес в памяти. Однако попытка изменить неизменяемую переменную во второй функции y += x приводит к тому, что y начинает ссылаться на другой адрес в памяти: исходная y будет забыта, что приведёт к ошибке UnboundLocalError.

Полнейшая чепуха.

>>> def f():
...     l = [1]
...     def inner(x):
...         l += [x]
...         return l
...     return inner
...
>>> f_inner = f()
>>> print(f_inner(2))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in inner
UnboundLocalError: local variable 'l' referenced before assignment

Если следовать логике автора, то эта функция должна работать точно так же, как и её собственная f(): тут точно такой же изменяемый объект-список.
Берём пример и смотрим на байткод.

import dis

a = 10
b = 20
def foo():
    print(b)
    print(a)
    a += 10

dis.dis(foo)


6 0 LOAD_GLOBAL 0 (print)
2 LOAD_GLOBAL 1 (b)
4 CALL_FUNCTION 1
6 POP_TOP

7 8 LOAD_GLOBAL 0 (print)
10 LOAD_FAST 0 (a)
12 CALL_FUNCTION 1
14 POP_TOP

8 16 LOAD_FAST 0 (a)
18 LOAD_CONST 1 (10)
20 INPLACE_ADD
22 STORE_FAST 0 (a)
24 LOAD_CONST 0 (None)
26 RETURN_VALUE


У нас есть a и b. Для их чтения используются разные инструкции (LOAD_FAST, LOAD_GLOBAL).
При построении байткода если есть локальная переменная, все обращения к ней локальны (LOAD_FAST), вне зависимости от порядка их следования.
Для людей знакомых например с с++, но не знакомых с питон, знание о передаче аргументов по ссылке или по значению — лишь немного путают в этой ситуации )

На самом деле в питоне все есть объекты. Вот сколько например у int методов:

i = 1
print(type(i))
print(dir(i))

<class 'int'>
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

И все передается по ссылке, ниже id выводит адрес объекта в памяти:


def foo(value):
    print('2)', id(value))

i = 1
print('1)', id(i))
foo(i)

# 1) 93983712618240
# 2) 93983712618240


Здесь другая ситуация. При попытке сделать assignment для переменной, котороя ссылается на immutable объект, питон создает новый объект. Встроенный int это объект, но не изменяемый.

x = 1  # int is immutable
print(id(x))
x += 1
print(id(x))

# 94029430744832
# 94029430744864

x = (1, 2)  # tuple is immutable
print(id(x))
x += (3, 4)
print(id(x))

# 140065572803008
# 140065572117216

x = []  # list is mutable
print(id(x))
x += [1, 2]
print(id(x))

# 140381673357056
# 140381673357056
Еще для наглядности.
Все операции типа +, += это вызов специальных методов. x += y это x.__iadd__(y). В таком методе мы можем поменять состояние объекта и вернуть себя же (self), или же создать и вернуть новый объект.

class Immutable:
    def __iadd__(self, x):
        return self._value + x  #  ну или Immutable(self._value + x)

class Mutable:
    def __iadd__(self, x):
        self._value += x
        return self
Зарегистрируйтесь на Хабре, чтобы оставить комментарий