在 Hy 中使用 let 进行动态绑定?

Dynamic bindings with let in Hy?

来自 Common Lisp,我正在尝试使用 let 动态隐藏全局变量的值。

(setv glob 18)

(defn callee []
  (print glob))

(defn nonl [x]
  (callee)
  (let [glob x]
    (callee))
  (callee))

(nonl 39)

=>
18
18
18

有没有办法让这个工作正常进行,以便第二次调用 callee 时返回 39?

[编辑]

根据 gilch 的回复,我使用 contextvars 编写了以下草稿:

(import contextvars :as cv)

(defmacro defparam [symbol value]
  (let [command (. f"{symbol} = cv.ContextVar('{symbol}')")]
    `(do (exec ~command)
         (.set ~symbol ~value))))

(defmacro parameterize [symbol value #* body]
  `(. (cv.copy-context)
      (run (fn [] (do (.set ~symbol ~value)
                      ~@body)))))

(defn callee []
  (glob.get))

;;;;;;;;;

(defparam glob 18)

(callee)                  => 18
(parameterize glob 39
  (callee))               => 39
(callee)                  => 18

感谢您的回答!

在 Python 中没有 built-in 方法可以给全局变量一个动态范围的临时值(而且 Hy 没有为它添加宏或任何东西),所以你必须这样做你自己:

(setv glob 18)

(defn callee []
  (print glob))

(defn nonl [x]
  (callee)
  (global glob)
  (setv old-glob glob)
  (try
    (setv glob x)
    (callee)
    (finally
      (setv glob old-glob)))
  (callee))

(nonl 39)

这个宏可能如下所示:

(defmacro dyn-rebind-global [symbol new-value #* body]
  (setv temp (hy.gensym))
  `(do
    (global ~symbol)
    (setv ~temp ~symbol)
    (try
      (setv ~symbol ~new-value)
      ~@body
      (finally
        (setv ~symbol ~temp)))))

那么你可以这样使用它:

(setv glob 18)

(defn callee []
  (print glob))

(defn nonl [x]
  (callee)
  (dyn-rebind-global glob x
    (callee))
  (callee))

(nonl 39)

最接近的 Python 等价动态绑定是 contextvars and unittest.mock.patch

patch 主要用于单元测试,几乎适用于任何东西,但不是线程安全的。如果你需要在库代码中动态重新绑定某些东西,patch 可以做到。用作上下文管理器或装饰器。

>>> from unittest.mock import patch
>>> name = 'Alice'
>>> def greet(): print("Hi", name)
...
>>> greet()
Hi Alice
>>> with patch('__main__.name', 'Bob'):
...     greet()
...
Hi Bob
>>> greet()
Hi Alice

使用 contextvars 它必须首先在模块顶层设置为 ContextVar,并且必须使用 .get() 调用显式取消引用,但它在线程和异步代码中正常工作。

如果你想在 Python 中声明你自己的动态变量,最好在可能使用异步或线程时将其设为上下文变量。 asyncio 任务管理器将自动为每个任务交换新的上下文。您也可以使用 copy_context 手动执行此操作。 run() 方法接受一个可调用对象,因此它可以用作装饰器:

>>> import contextvars as cv
>>> name = cv.ContextVar('name', default='Alice')
>>> def greet(): print('Hi', name.get())
...
>>> greet()
Hi Alice
>>> @cv.copy_context().run
... def _():
...   name.set('Bob')
...   greet()
...
Hi Bob
>>> greet()
Hi Alice

这在 Hy 中可以以完全相同的方式工作,但宏可以使它更好。