Python, `let`,` with`, локальные области, отладочная печать и временные переменные - PullRequest
4 голосов
/ 22 апреля 2020

Я пытаюсь провести рефакторинг проекта, нацеленного на Python 3.6 и pytest. Набор тестов содержит много отладочных операторов, таких как:

print('This is how something looks right now', random_thing.foo.bar.start,
      random_thing.foo.bar.middle, random_thing.foo.bar.end)

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

Я бы хотел не повторять random_thing.foo.bar. столько раз. Я мог бы присвоить это временной переменной, но на самом деле код не нуждается в этой переменной, доступной когда-либо после. Я не очень беспокоюсь о производительности, но у меня есть сильное предпочтение держать код "чистым" - и "утечка" этих имен переменных меня теряет. Там есть такая функция на других языках, с которыми я знаком, поэтому мне интересно, как это сделать на Python.

Я свободно владею C ++, где Возможно, я бы просто поместил эту отладочную печать в дополнительную область:

{
  const auto& bar = random_thing.foo.bar;
  debug << "start: " << bar.start << ", middle: " << bar.middle << ", end: " << bar.end;
}

Учитывая, что в Python нет анонимных блоков, есть ли способ "Pythoni c" избежать этого беспорядка пространства имен? Я на самом деле не ищу мнения или конкурс популярности, но для обзора, основанного на том, как люди, которые делают Python дольше меня, воспринимают эти подходы, поэтому вот несколько вещей, которые я попробовал:

1. Просто добавьте эту чертову переменную и del потом

Ну, я не люблю многократно делать вещи, которые машина должна делать для меня.

2. with оператор и contextlib.nullcontext

В Python нет новой области видимости с оператором with, поэтому это оставляет переменную opj доступной через locals:

>>> import os
>>> import os.path
>>> import contextlib
>>> with contextlib.nullcontext(os.path.join) as opj:
...   print(type(opj))
... 
<class 'function'>
>>> print(type(opj))
<class 'function'>

3. with заявление и декоратор заявления Владимира Яковлева let

from contextlib import contextmanager
from inspect import currentframe, getouterframes

@contextmanager
def let(**bindings):
    frame = getouterframes(currentframe(), 2)[-1][0] # 2 because first frame in `contextmanager` is the decorator  
    locals_ = frame.f_locals
    original = {var: locals_.get(var) for var in bindings.keys()}
    locals_.update(bindings)
    yield
    locals_.update(original)

Код выглядит потрясающе для меня:

>>> a = 3
>>> b = 4
>>> with let(a=33, b=44):
...     print(a, b)
... 
(33, 44)
>>> print(a, b)
(3, 4)

Это не undef a переменная, которая не была определена ранее, но ее легко добавить. Является ли манипулирование стеком таким образом разумной идеей? Мой Python -фу ограничен, поэтому я разрываюсь между тем, чтобы видеть это как супер-крутой и супер-хаки sh. Является ли окончательный результат «разумно Pythoni c»?

4. Обертка вокруг print с **kwargs

Давайте использовать **kwargs:

def print_me(format, **kwargs):
    print(format.format(**kwargs))

print_me('This is it: {bar.start} {bar.middle} {bar.end}', bar=random_thing.foo.bar)

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

foo = 10
print(f'{foo + 1}')

Я хотел бы сохранить эту функциональность. Я понимаю, что str.format не может на самом деле поддерживать это из-за проблем безопасности передачи пользовательских входных данных.

Ответы [ 2 ]

1 голос
/ 23 апреля 2020

Ваш лучший вариант - просто создать переменную и оставить ее там, или del потом, если она действительно сильно вас беспокоит.


with - нереальный подход. В частности, это let вещь полностью сломана несколькими способами.

Самый важный путь, по которому это неправильно, это то, что изменение f_locals - это неопределенное поведение , но это не сразу видно в тестах из-за других ошибок. Две другие ошибки заключаются в том, что 2 управляет чем-то совершенно не связанным с тем, что думал автор, а [-1] индексирует не с того конца. Эти ошибки приводят к тому, что код обращается к фрейму стека "root", который находится в начале стека, вместо фрейма, который хотел автор. Наконец, у него нет обработки для фактической очистки переменных - он может только установить для них None.

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

from contextlib import contextmanager
from inspect import currentframe, getouterframes

@contextmanager
def let(**bindings):
    frame = getouterframes(currentframe(), 2)[-1][0] # 2 because first frame in `contextmanager` is the decorator  
    locals_ = frame.f_locals
    original = {var: locals_.get(var) for var in bindings.keys()}
    locals_.update(bindings)
    yield
    locals_.update(original)

def f():
    x = 1
    with let(x=3):
        print(x)

f()

print(x)

Вывод:

1
None

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 3 * * * впоследствии неправильная область действия.

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


Если вы действительно ненавидите эту переменную и не хотите использовать del, наиболее подходящим вариантом может быть использование лямбды с немедленным вызовом Javascript:

(lambda x: print(f'start: {x.start}, middle: {x.middle}, end: {x.end}'))(
    random_thing.foo.bar)

Я думаю эта опция намного хуже, чем просто назначить x обычным способом, но, возможно, вы думаете по-другому.

0 голосов
/ 23 апреля 2020

Вот немного веселья с ним.


#Fake object structure ?

class Bar:
    start="mystart"
    middle= "mymiddle"
    end="theend"

class Foo:
    bar = Bar

class Rando:
    foo = Foo


random_thing = Rando()

#Fake object structure ?

def printme(tmpl, di_g={}, di_l={}, **kwargs):
    """ use passed-in dictionaries, typically globals(), locals() then kwargs
        last-one wins.
    """

    di = di_g.copy()
    di.update(**di_l)
    di.update(**kwargs)
    print(tmpl.format(**di))


bar = random_thing.foo.bar

printme('This is it: {bar.start} {bar.middle} {bar.end}', globals())
printme('This is it: {bar.start} {bar.middle} {bar.end}', bar=Bar)

def letsdoit():
    "using locals and overriding bar"
    bar = Bar()
    bar.middle = "themiddle"
    printme('This is it: {bar.start} {bar.middle} {bar.end} {fooplus}', globals(), locals(), fooplus=(10+1))    


letsdoit()


вывод:

This is it: mystart mymiddle theend
This is it: mystart mymiddle theend
This is it: mystart themiddle theend 11
...