Python 中的 Null 模式未得到充分利用?

发布于 2024-08-16 23:41:16 字数 413 浏览 4 评论 0原文

我时不时地会遇到这样的代码:

foo = Foo()
...
if foo.bar is not None and foo.bar.baz == 42:
   shiny_happy(...)

在我看来,这似乎不太像Python。

在 Objective-C 中,你可以向 nil 发送消息并得到 nil 作为答案。我一直认为这非常方便。当然,可以在 Python 中实现 Null 模式,但是从 Google 给出的结果来看我,看来这不是很广泛使用。这是为什么呢?

或者甚至更好 - 让 None.whatever 返回 None 而不是引发异常是一个坏主意吗?

Every now and then I come across code like this:

foo = Foo()
...
if foo.bar is not None and foo.bar.baz == 42:
   shiny_happy(...)

Which seems, well, unpythonic, to me.

In Objective-C, you can send messages to nil and get nil as the answer. I've always thought that's quite handy. Of course it is possible to implement a Null pattern in Python, however judging from the results Google's given me, it seems this is not very widely used. Why's that?

Or even better—would it be a bad idea to let None.whatever return None instead of raising an exception?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(12

偏闹i 2024-08-23 23:41:16

PEP 336 - Make None Callable 提出了类似的功能:

None 不应该是使用任何参数调用时的可调用对象
没有副作用并且返回 None。

被拒绝的原因很简单,“它被认为是一个在调用时 None 不会引发错误的功能”。

PEP 336 - Make None Callable proposed a similar feature:

None should be a callable object that when called with any arguments
has no side effect and returns None.

The reason for why it was rejected was simply "It is considered a feature that None raises an error when called."

手心的海 2024-08-23 23:41:16

抱歉,该代码是 pythonic 的。我认为大多数人都会同意 Python 中“显式优于隐式”。与大多数语言相比,Python 是一种易于阅读的语言,人们不应该通过编写神秘的代码来打败它。把意思说得很清楚。

foo = Foo()
...
if foo.bar is not None and foo.bar.baz == 42:
    shiny_happy(...)

在此代码示例中,很明显 foo.bar 在此代码路径上有时为 None,并且我们仅在它不是 None 且 .baz == 42 时才运行shiny_happy()。任何人都非常清楚这里发生了什么以及为什么。对于空模式或 try ... except 此处发布的答案之一中的代码来说,情况并非如此。如果您的语言(例如 Objective-C 或 javascript)强制执行 null 模式,这是一回事,但在根本不使用它的语言中,它只会造成混乱和难以阅读的代码。使用 Python 编程时,请像 Python 专家那样做。

I'm sorry, but that code is pythonic. I think most would agree that "explicit is better than implicit" in Python. Python is a language that is easy to read compared to most, and people should not defeat that by writing cryptic code. Make the meaning very clear.

foo = Foo()
...
if foo.bar is not None and foo.bar.baz == 42:
    shiny_happy(...)

In this code sample, it is clear that foo.bar is sometimes None on this code path, and that we only run shiny_happy() if it is not None, and .baz == 42. Very clear to anyone what is going on here and why. The same can not be said for the null pattern, or the try ... except code in one of the answers posted here. It's one thing if your language, like Objective-C or javascript enforces a null pattern, but in a language where it is not used at all, it will just create confusion and code that is difficult to read. When programming in python, do as the pythonistas do.

冰之心 2024-08-23 23:41:16

你就不能尝试一下吗? Pythonic 方式说请求宽恕比请求许可更容易

所以:

try:
    if foo.bar.baz == 42:
        shiny_happy(...)
except AttributeError:
    pass #or whatever

或者在不可能消除比预期更多的异常的情况下执行此操作:

try:
    baz = foo.bar.baz
except AttributeError:
    pass # handle error as desired
else:
    if baz == 42:
        shiny_happy(...)

Couldn't you do a try except? The Pythonic way says It is Easier to Ask for Forgiveness than Permission.

So:

try:
    if foo.bar.baz == 42:
        shiny_happy(...)
except AttributeError:
    pass #or whatever

Or do it without possibly silencing more exceptions than desired:

try:
    baz = foo.bar.baz
except AttributeError:
    pass # handle error as desired
else:
    if baz == 42:
        shiny_happy(...)
一场信仰旅途 2024-08-23 23:41:16

这种便利性的代价是无法尽早检测到愚蠢的错误,尽可能接近有缺陷的代码行。

我认为这个特殊功能的便利性充其量只是偶尔出现,而愚蠢的错误总是会发生。


当然,类似 SQL 的 NULL 不利于测试,因为测试实际上依赖于命题的真假。考虑来自 unittest.py 的这段代码:

class TestCase:
    ...
    def failUnless(self, expr, msg=None):
        """Fail the test unless the expression is true."""
        if not expr: raise self.failureException, msg

现在假设我有一个执行此操作的测试:

conn = connect(addr)
self.failUnless(conn.isOpen())

假设 connect 错误地返回 null。如果我使用“空模式”,或者语言内置了它,并且 conn 为 null,则 conn.isOpen() 为 null,并且 < code>not conn.isOpen() 也为 null,因此断言通过,即使连接显然未打开。

我倾向于认为 NULL 是 SQL 最糟糕的功能之一。事实上,null 在其他语言中默默地传递任何类型的对象也好不了多少。 (托尼·霍尔(Tony Hoare)称空引用“我的十亿美元错误”。)我们需要更少的诸如此类,仅此而已。

The handiness comes at the expense of dumb mistakes not being detected at the earliest possible time, as close to the buggy line of code as possible.

I think the handiness of this particular feature would be occasional at best, whereas dumb mistakes happen all the time.


Certainly a SQL-like NULL would be bad for testing, which really banks on propositions being either true or false. Consider this code, from unittest.py:

class TestCase:
    ...
    def failUnless(self, expr, msg=None):
        """Fail the test unless the expression is true."""
        if not expr: raise self.failureException, msg

Now suppose I have a test that does this:

conn = connect(addr)
self.failUnless(conn.isOpen())

Suppose connect erroneously returns null. If I'm using the "null pattern", or the language has it built-in, and conn is null, then conn.isOpen() is null, and not conn.isOpen() is null too, so the assertion passes, even though the connection clearly is not open.

I tend to think NULL is one of SQL's worst features. And the fact that null silently passes for an object of any type in other languages is not much better. (Tony Hoare called null references “my billion-dollar mistake”.) We need less of that sort of thing, not more.

且行且努力 2024-08-23 23:41:16

正如其他人所说,PEP 336 描述了为什么会出现这种行为。

添加类似 Groovy 的“安全导航操作符”(< code>?.) 在某些情况下也许可以使事情变得更加优雅:

foo = Foo()

if foo.bar?.baz == 42:
    ...

As others have said, PEP 336 describes why this is the behavior.

Adding something like Groovy's "safe navigation operator" (?.) could perhaps make things more elegant in some cases:

foo = Foo()

if foo.bar?.baz == 42:
    ...
假装爱人 2024-08-23 23:41:16

我认为这不是一个好主意。原因如下。假设您

foo.getValue()

假设 getValue() 返回一个数字,如果未找到,则返回 None
现在假设 foo 不是偶然的,也不是错误。因此,即使存在错误,它也会返回 None 并继续。

换句话说,您无法再区分是否没有值(返回 None)或是否存在错误(foo was None开始)。您正在使用不受例程本身控制的事实来更改例程的返回契约,最终覆盖其语义。

I don't think it's a good idea. Here's why. suppose you have

foo.getValue()

Suppose getValue() returns a number, or None if not found.
Now suppose foo was none by accident or a bug. As a result, it would return None, and continue, even if there's a bug.

In other words, you are no longer able to distinguish if there's no value (returns None) or if there's an error (foo was None to begin with). You are altering the return contract of a routine with a fact that is not under control of the routine itself, eventually overwriting its semantics.

挽袖吟 2024-08-23 23:41:16

这就是为什么我认为这不是一个好主意:

foo = Foo() // now foo is None

// if foo is None, I want to raise an exception because that is an error condition.
// The normal case is that I expect a foo back whose bar property may or may not be filled in.
// If it's not filled in, I want to set it myself.

if not foo.bar // Evaluates to true because None.bar is None under your paradigm, I think
  foo.bar = 42 // Now what?

你会如何处理这个案子?

Here's why I don't think that's a good idea:

foo = Foo() // now foo is None

// if foo is None, I want to raise an exception because that is an error condition.
// The normal case is that I expect a foo back whose bar property may or may not be filled in.
// If it's not filled in, I want to set it myself.

if not foo.bar // Evaluates to true because None.bar is None under your paradigm, I think
  foo.bar = 42 // Now what?

How would you handle this case?

走走停停 2024-08-23 23:41:16

虽然我完全同意这里的其他答案,即在请求成员时 None 引发异常是一件好事,但这是我有时使用的一个小模式:

getattr(foo.bar, 'baz', default_value)

While I wholeheartedly agree with other answers here that say that it's a good thing that None raises an exception when asked for a member, this is a little pattern I sometimes use:

getattr(foo.bar, 'baz', default_value)
耳钉梦 2024-08-23 23:41:16

我个人认为抛出异常是应该发生的。假设由于某种原因你正在用 Python 编写导弹软件。想象一下原子弹,假设有一个名为explode(timeToExplode)的方法,并且timeToExplode作为None传入。我想当你输掉战争的时候你会不高兴,因为你在测试中没有发现这一点。

Personally I believe throwing an exception is what should happen. Lets say for some reason you are writing software for missiles in Python. Imagine the atomic bomb and lets say there was a method called explode(timeToExplode) and timeToExplode got passed in as None. I think you would be unhappy at the end of the day when you lost the war, because you didn't find this in testing.

罗罗贝儿 2024-08-23 23:41:16
foo = Foo()
...
if foo.bar is not None and foo.bar.baz == 42:
   shiny_happy(...)

可以使用 None 解析为 False 的事实来清理上面的内容:

if foo.bar and foo.bar.baz == 42:
    shiny_happy(...)
else:
    not_happy(...)
foo = Foo()
...
if foo.bar is not None and foo.bar.baz == 42:
   shiny_happy(...)

The above can be cleaned up using the fact that None resolves to False:

if foo.bar and foo.bar.baz == 42:
    shiny_happy(...)
else:
    not_happy(...)
调妓 2024-08-23 23:41:16

我不是一个Python程序员(刚刚开始学习这门语言),但这似乎是关于何时返回错误或抛出异常的永无休止的讨论,事实是(当然这只是一个意见)这取决于。

异常应该用于特殊情况,因此将对罕见情况的检查移到主代码块之外。当未找到值是常见情况时,不应使用它们(想象一下请求树的左子节点,在许多情况下——所有叶子——它将为空)。对于返回值集的函数,还有第三种情况,您可能只想返回一个有效的空集以简化代码。

我相信遵循上述建议,代码会更加清晰。当这种情况很少见时,您无需担心 null(将调用异常),因此较少使用的代码会移出主块。

当 null 是有效返回值时,处理它是在主控制流内,但这很好,因为它是常见情况,并且是主算法的一部分(不要遵循图中的 null 边)。

在第三种情况下,当从可能不返回值的函数请求值时,返回空集可以简化代码:您可以假设返回的容器存在并处理所有找到的元素,而无需在代码中添加额外的检查。

再说一次,这只是一个观点,但作为我的观点,我倾向于遵循它:)

I am not a python programmer (just starting to learn the language) but this seems like the never-ending discussion on when to return error or throw exception, and the fact is that (of course this is only an opinion) it depends.

Exceptions should be used for exceptional conditions, and as such move the checks for rare situations outside of the main block of code. They should not be used when a not-found value is a common condition (think of requesting the left child of a tree, in many cases --all leaves-- it will be null). There is yet again a third situation with functions that return sets of values, where you might just want to return a valid empty set to ease the code.

I believe that following the above advice, code is much more legible. When the situation is rare you do not need to worry about null (an exception will be called) so less used code is moved out of the main block.

When null is a valid return value, processing it is within the main flow of control, but that is good, as it is a common situation and as such part of the main algorithm (do not follow a null edge in a graph).

In the third case, when requesting values from a function that can possibly return no values, returning an empty set simplifies the code: you can assume that the returned container exists and process all found elements without adding the extra checks in the code.

Then again, that is just an opinion, but being mine I tend to follow it :)

好多鱼好多余 2024-08-23 23:41:16

免责声明:这可能不是一个好主意,因为它不太可读,但我只是将其添加到此处以保持完整性。

利用 and 的工作原理

如果您想将任意函数 transform 应用于可为 null 的变量 var:Optional[T],这里有一个表达式。 ,如果变量不是 None,则返回该计算的结果,否则返回 None(本着 Java 的 Optional 的精神):

var and transform(var)

这适用于任何函数,而不仅仅是布尔上下文中的函数(例如 if 子句的条件)。原因是, and 运算符定义如下(来自文档):

表达式x和y首先计算x;如果 x 为 false,则返回其值;否则,将计算 y 并返回结果值。

因此,如果 varNone,则 var 和 transform(var) 将计算为 None(不计算 >transform(var),因为 Python 中的布尔运算符是短路的),如果不是,它将计算出 transform(var) 的结果并返回。

不像正确的 null 模式那么简洁,但仍然比

transform(var) if var is not None else None

示例

import datetime


def null_pattern_demo(x: datetime.datetime):
    return x and x.timestamp()

>>> null_pattern_demo(None)
>>> null_pattern_demo(datetime.datetime(2020, 6, 29))
1593381600.0


链接

更简洁在 Python 3.8+ 中,借助赋值表达式,您甚至可以链接此模式,即您可以组成一系列函数并返回 None 如果任何计算返回 None,否则返回最终结果:

var and (r := f(var)) and (r := g(r)) and (r := h(r))

一旦任何中间步骤(或 >var 本身)返回 None。作为副作用,最终结果也将存储在 r 中。

示例

def f(x, y):
    return 2*x if x == y else None


def g(x, y):
    return x + y if y != 42 else None


def h(x):
    return x**2

如果我们尝试一次性应用 fgh 组合,如果其中一个中间步骤返回 无:

>>> h(g(f(2, 2), 42))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in h
TypeError: unsupported operand type(s) for ** or pow(): 'NoneType' and 'int'

但是,我们可以使用上面的技巧:

>>> (x := None) and (x := f(x, 3)) and (x := g(x, 1)) and (x := h(x))
>>> (x := 2) and (x := f(x, 3)) and (x := g(x, 1)) and (x := h(x))
>>> (x := 2) and (x := f(x, 2)) and (x := g(x, 42)) and (x := h(x))
>>> (x := 2) and (x := f(x, 2)) and (x := g(x, 1)) and (x := h(x))
25

Disclaimer: this is probably not a very good idea, as it isn't very readable, but I'm just adding it here for completeness.

Exploit how and works

Here is an expression that works if you want to apply an arbitrary function transform to a nullable variable var: Optional[T], returning the result of that computation if the variable is not None, or else None (in the spirit of Java's Optional):

var and transform(var)

This works for any function, not just in a boolean context (e.g. the condition to an if clause). The reason is, the and operator is defined as follows (from the docs):

The expression x and y first evaluates x; if x is false, its value is returned; otherwise, y is evaluated and the resulting value is returned.

Thus, if var is None, var and transform(var) will evaluate to None (without evaluating transform(var), since boolean operators in Python are short-circuiting), and if it is not, it will evaluate to the result of transform(var) and return that.

Not quite as concise as a proper null pattern, but still less verbose than

transform(var) if var is not None else None

Example

import datetime


def null_pattern_demo(x: datetime.datetime):
    return x and x.timestamp()

>>> null_pattern_demo(None)
>>> null_pattern_demo(datetime.datetime(2020, 6, 29))
1593381600.0


Chaining

In Python 3.8+, with the help of assignment expressions, you can even chain this pattern, i.e. you can compose a sequence of functions and return None if any of the computations returned None, or return the final result otherwise:

var and (r := f(var)) and (r := g(r)) and (r := h(r))

This will evaluate to None as soon as any of the intermediate steps (or var itself) returns None. As a side effect, the final result will also be stored in r.

Example

def f(x, y):
    return 2*x if x == y else None


def g(x, y):
    return x + y if y != 42 else None


def h(x):
    return x**2

If we try apply f, g and h composed in one go, we will get an error if one of the intermediate steps returns None:

>>> h(g(f(2, 2), 42))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in h
TypeError: unsupported operand type(s) for ** or pow(): 'NoneType' and 'int'

But, instead, we can use the above trick:

>>> (x := None) and (x := f(x, 3)) and (x := g(x, 1)) and (x := h(x))
>>> (x := 2) and (x := f(x, 3)) and (x := g(x, 1)) and (x := h(x))
>>> (x := 2) and (x := f(x, 2)) and (x := g(x, 42)) and (x := h(x))
>>> (x := 2) and (x := f(x, 2)) and (x := g(x, 1)) and (x := h(x))
25
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文