Python 中的非严格别名参数?
Non-strict by-name arguments in Python?
问题
有没有办法将函数参数声明为非严格的(已通过 by-name)?
如果这不能直接实现:是否有任何辅助函数或装饰器可以帮助我实现类似的目标?
具体例子
这里有一个小玩具示例可以用来试验。
假设我想构建一个小型解析器组合器库,它可以处理以下 classic 语法,用于带括号的算术表达式(为简单起见,数字由单个文字值 1
替换):
num = "1"
factor = num
| "(" + expr + ")"
term = factor + "*" + term
| factor
expr = term + "+" + expr
| term
假设我将一个 解析器组合器 定义为一个对象,该对象具有一个方法 parse
,该方法可以获取标记列表、当前位置,或者抛出一个解析错误, 或 return 一个结果和一个新位置。我可以很好地定义一个 ParserCombinator
基础 class,它提供 +
(串联)和 |
(替代)。然后我可以定义接受常量字符串的解析器组合器,并实现 +
和 |
:
# Two kinds of errors that can be thrown by a parser combinator
class UnexpectedEndOfInput(Exception): pass
class ParseError(Exception): pass
# Base class that provides methods for `+` and `|` syntax
class ParserCombinator:
def __add__(self, next):
return AddCombinator(self, next)
def __or__(self, other):
return OrCombinator(self, other)
# Literally taken string constants
class Lit(ParserCombinator):
def __init__(self, string):
self.string = string
def parse(self, tokens, pos):
if pos < len(tokens):
t = tokens[pos]
if t == self.string:
return t, (pos + 1)
else:
raise ParseError
else:
raise UnexpectedEndOfInput
def lit(str):
return Lit(str)
# Concatenation
class AddCombinator(ParserCombinator):
def __init__(self, first, second):
self.first = first
self.second = second
def parse(self, tokens, pos):
x, p1 = self.first.parse(tokens, pos)
y, p2 = self.second.parse(tokens, p1)
return (x, y), p2
# Alternative
class OrCombinator(ParserCombinator):
def __init__(self, first, second):
self.first = first
self.second = second
def parse(self, tokens, pos):
try:
return self.first.parse(tokens, pos)
except:
return self.second.parse(tokens, pos)
到目前为止,一切都很好。然而,由于文法的非终结符号是以相互递归的方式定义的,我不能急切地展开所有可能的解析器组合树,我必须使用解析器的 factories组合器,并将它们包装成这样的东西:
# Wrapper that prevents immediate stack overflow
class LazyParserCombinator(ParserCombinator):
def __init__(self, parserFactory):
self.parserFactory = parserFactory
def parse(self, tokens, pos):
return self.parserFactory().parse(tokens, pos)
def p(parserFactory):
return LazyParserCombinator(parserFactory)
这确实让我能够以非常接近 EBNF 的方式写下语法:
num = p(lambda: lit("1"))
factor = p(lambda: num | (lit("(") + expr + lit(")")))
term = p(lambda: (factor + lit("*") + term) | factor)
expr = p(lambda: (term + lit("+") + expr) | term)
它确实有效:
tokens = [str(x) for x in "1+(1+1)*(1+1+1)+1*(1+1)"]
print(expr.parse(tokens, 0))
然而,每一行的p(lambda: ...)
有点烦人。有没有一些惯用的方法来摆脱它?如果能以某种方式通过规则的整个 RHS 就好了 "by-name",而不会触发对无限相互递归的热切求值。
我试过的
我查看了核心语言中的可用内容:似乎只有if
、and
和or
可以"short-circuit",请指正'我错了。
我试过看看其他非玩具示例库是如何做到这一点的。
例如,
funcparserlib
使用明确的前向声明来避免相互递归
(看看 forward_decl
和 value.define
github README.md 示例代码中的一部分)。
parsec.py
使用一些特殊的 @generate
装饰器
并且似乎使用协程做一些类似单子解析的事情。
这一切都很好,但我的目标是了解哪些选项
我有关于可用的基本评估策略
在 Python.
我也发现了类似 lazy_object_proxy.Proxy
的东西,但它似乎无助于以更简洁的方式实例化此类对象。
那么,有没有更好的方法来按名称传递参数并避免相互递归定义的值爆炸?
这是个好主意,但 Python 的语法不允许这样做:Python 表达式总是严格求值(if
块和 and
和 or
short-circuiting 表达式)。
特别是,问题是在这样的表达式中:
num = p(lit("1"))
p
函数参数总是以绑定到同一对象的新名称接收。评估 lit("1")
得到的对象不是 named 任何东西(直到通过 p
的形式参数创建名称),所以没有名称可以绑定到。反之,那里必须有一个对象,否则 p
根本无法接收到值。
您可以做的是添加一个新对象来代替 lambda 来延迟对名称的评估。例如,类似于:
class DeferredNamespace(object):
def __init__(self, namespace):
self.__namespace = namespace
def __getattr__(self, name):
return DeferredLookup(self.__namespace, name)
class DeferredLookup(object):
def __init__(self, namespace, name):
self.__namespace = namespace
self.__name = name
def __getattr__(self, name):
return getattr(getattr(self.__namespace, self.__name), name)
d = DeferredNamespace(locals())
num = p(d.lit("1"))
在这种情况下,d.lit
实际上不是 return lit
,它 return 是一个将使用 getattr(locals(), 'lit')
的 DeferredLookup
对象在实际使用时解析其成员。请注意,这会急切地捕获 locals()
,这可能是您不想要的;您可以调整它以使用 lambda,或者更好的是,无论如何只需在其他名称空间中创建所有实体。
您仍然会在语法中遇到 d.
的缺点,它可能是也可能不是 deal-breaker,具体取决于您使用此 API 的目标。
必须完全接受 one by-name 参数的函数的特殊解决方案
If you want to define a function f
that has to take one single argument by-name, consider making f
into a @decorator
. Instead of an argument littered with lambdas
, the decorator can then directly receive the function definition.
出现问题中的lambdas
是因为我们需要一种方法使右侧的执行变得懒惰。但是,如果我们将 non-terminal 符号的定义更改为 def
而不是局部变量,则 RHS 也不会立即执行。那么我们要做的就是通过某种方式将这些def
转换成ParserCombinator
。为此,我们可以使用装饰器。
我们可以定义一个装饰器,将一个函数包装成一个LazyParserCombinator
,如下所示:
def rule(f):
return LazyParserCombinator(f)
然后将其应用于保存每个语法规则定义的函数:
@rule
def num(): return lit("1")
@rule
def factor(): return num | (lit("(") + expr + lit(")"))
@rule
def term(): return factor + lit("*") + term | factor
@rule
def expr(): return (term + lit("+") + expr) | term
规则右侧的句法开销很小(没有引用其他规则的开销,不需要 p(...)
-wrappers 或 ruleName()
-parentheses),并且没有 counter-intuitive 带有 lambda 的样板。
解释:
给定一个高阶函数h
,我们可以用它来修饰其他函数f
,如下所示:
@h
def f():
<body>
这基本上是:
def f():
<body>
f = h(f)
和 h
不限于 returning 函数,它还可以 return 其他对象,如上面的 ParserCombinator
s。
问题
有没有办法将函数参数声明为非严格的(已通过 by-name)?
如果这不能直接实现:是否有任何辅助函数或装饰器可以帮助我实现类似的目标?
具体例子
这里有一个小玩具示例可以用来试验。
假设我想构建一个小型解析器组合器库,它可以处理以下 classic 语法,用于带括号的算术表达式(为简单起见,数字由单个文字值 1
替换):
num = "1"
factor = num
| "(" + expr + ")"
term = factor + "*" + term
| factor
expr = term + "+" + expr
| term
假设我将一个 解析器组合器 定义为一个对象,该对象具有一个方法 parse
,该方法可以获取标记列表、当前位置,或者抛出一个解析错误, 或 return 一个结果和一个新位置。我可以很好地定义一个 ParserCombinator
基础 class,它提供 +
(串联)和 |
(替代)。然后我可以定义接受常量字符串的解析器组合器,并实现 +
和 |
:
# Two kinds of errors that can be thrown by a parser combinator
class UnexpectedEndOfInput(Exception): pass
class ParseError(Exception): pass
# Base class that provides methods for `+` and `|` syntax
class ParserCombinator:
def __add__(self, next):
return AddCombinator(self, next)
def __or__(self, other):
return OrCombinator(self, other)
# Literally taken string constants
class Lit(ParserCombinator):
def __init__(self, string):
self.string = string
def parse(self, tokens, pos):
if pos < len(tokens):
t = tokens[pos]
if t == self.string:
return t, (pos + 1)
else:
raise ParseError
else:
raise UnexpectedEndOfInput
def lit(str):
return Lit(str)
# Concatenation
class AddCombinator(ParserCombinator):
def __init__(self, first, second):
self.first = first
self.second = second
def parse(self, tokens, pos):
x, p1 = self.first.parse(tokens, pos)
y, p2 = self.second.parse(tokens, p1)
return (x, y), p2
# Alternative
class OrCombinator(ParserCombinator):
def __init__(self, first, second):
self.first = first
self.second = second
def parse(self, tokens, pos):
try:
return self.first.parse(tokens, pos)
except:
return self.second.parse(tokens, pos)
到目前为止,一切都很好。然而,由于文法的非终结符号是以相互递归的方式定义的,我不能急切地展开所有可能的解析器组合树,我必须使用解析器的 factories组合器,并将它们包装成这样的东西:
# Wrapper that prevents immediate stack overflow
class LazyParserCombinator(ParserCombinator):
def __init__(self, parserFactory):
self.parserFactory = parserFactory
def parse(self, tokens, pos):
return self.parserFactory().parse(tokens, pos)
def p(parserFactory):
return LazyParserCombinator(parserFactory)
这确实让我能够以非常接近 EBNF 的方式写下语法:
num = p(lambda: lit("1"))
factor = p(lambda: num | (lit("(") + expr + lit(")")))
term = p(lambda: (factor + lit("*") + term) | factor)
expr = p(lambda: (term + lit("+") + expr) | term)
它确实有效:
tokens = [str(x) for x in "1+(1+1)*(1+1+1)+1*(1+1)"]
print(expr.parse(tokens, 0))
然而,每一行的p(lambda: ...)
有点烦人。有没有一些惯用的方法来摆脱它?如果能以某种方式通过规则的整个 RHS 就好了 "by-name",而不会触发对无限相互递归的热切求值。
我试过的
我查看了核心语言中的可用内容:似乎只有if
、and
和or
可以"short-circuit",请指正'我错了。
我试过看看其他非玩具示例库是如何做到这一点的。
例如, funcparserlib 使用明确的前向声明来避免相互递归 (看看
forward_decl
和value.define
github README.md 示例代码中的一部分)。parsec.py
使用一些特殊的@generate
装饰器 并且似乎使用协程做一些类似单子解析的事情。 这一切都很好,但我的目标是了解哪些选项 我有关于可用的基本评估策略 在 Python.
我也发现了类似 lazy_object_proxy.Proxy
的东西,但它似乎无助于以更简洁的方式实例化此类对象。
那么,有没有更好的方法来按名称传递参数并避免相互递归定义的值爆炸?
这是个好主意,但 Python 的语法不允许这样做:Python 表达式总是严格求值(if
块和 and
和 or
short-circuiting 表达式)。
特别是,问题是在这样的表达式中:
num = p(lit("1"))
p
函数参数总是以绑定到同一对象的新名称接收。评估 lit("1")
得到的对象不是 named 任何东西(直到通过 p
的形式参数创建名称),所以没有名称可以绑定到。反之,那里必须有一个对象,否则 p
根本无法接收到值。
您可以做的是添加一个新对象来代替 lambda 来延迟对名称的评估。例如,类似于:
class DeferredNamespace(object):
def __init__(self, namespace):
self.__namespace = namespace
def __getattr__(self, name):
return DeferredLookup(self.__namespace, name)
class DeferredLookup(object):
def __init__(self, namespace, name):
self.__namespace = namespace
self.__name = name
def __getattr__(self, name):
return getattr(getattr(self.__namespace, self.__name), name)
d = DeferredNamespace(locals())
num = p(d.lit("1"))
在这种情况下,d.lit
实际上不是 return lit
,它 return 是一个将使用 getattr(locals(), 'lit')
的 DeferredLookup
对象在实际使用时解析其成员。请注意,这会急切地捕获 locals()
,这可能是您不想要的;您可以调整它以使用 lambda,或者更好的是,无论如何只需在其他名称空间中创建所有实体。
您仍然会在语法中遇到 d.
的缺点,它可能是也可能不是 deal-breaker,具体取决于您使用此 API 的目标。
必须完全接受 one by-name 参数的函数的特殊解决方案
If you want to define a function
f
that has to take one single argument by-name, consider makingf
into a@decorator
. Instead of an argument littered withlambdas
, the decorator can then directly receive the function definition.
出现问题中的lambdas
是因为我们需要一种方法使右侧的执行变得懒惰。但是,如果我们将 non-terminal 符号的定义更改为 def
而不是局部变量,则 RHS 也不会立即执行。那么我们要做的就是通过某种方式将这些def
转换成ParserCombinator
。为此,我们可以使用装饰器。
我们可以定义一个装饰器,将一个函数包装成一个LazyParserCombinator
,如下所示:
def rule(f):
return LazyParserCombinator(f)
然后将其应用于保存每个语法规则定义的函数:
@rule
def num(): return lit("1")
@rule
def factor(): return num | (lit("(") + expr + lit(")"))
@rule
def term(): return factor + lit("*") + term | factor
@rule
def expr(): return (term + lit("+") + expr) | term
规则右侧的句法开销很小(没有引用其他规则的开销,不需要 p(...)
-wrappers 或 ruleName()
-parentheses),并且没有 counter-intuitive 带有 lambda 的样板。
解释:
给定一个高阶函数h
,我们可以用它来修饰其他函数f
,如下所示:
@h
def f():
<body>
这基本上是:
def f():
<body>
f = h(f)
和 h
不限于 returning 函数,它还可以 return 其他对象,如上面的 ParserCombinator
s。