Python "空合并" / 连续属性访问

1 投票
1 回答
1333 浏览
提问于 2025-04-17 20:56

免责声明:我对Python和Django还比较陌生。

虽然我的问题并不特定于Django,但我在使用Django时经常遇到这个问题。有时候我会给我的网站添加新功能,这些功能需要用户登录才能使用,并且只能访问登录用户才能看到的内容。然后,当我在另一个没有登录的浏览器中访问这个页面时,就会出现“AttributeNotFound”的错误,因为此时用户实际上是一个匿名用户。

这没什么大不了的。只需要检查用户是否已登录:

def my_view(request):
    if request.user.is_authenticated():
        posts = user.post_set.filter(flag=True).all()
    else:
        posts = None

    return render(request, 'template.html', {posts: posts})

然后在模板中:

<h1>Your Posts</h1>
<ul>
{% for post in posts %}
    <li>{{ post.name }}</li>
{%else%}
    <p>No posts found.</p>
{%endfor%}
</ul>

但我发现我的代码中经常出现这种模式:我想在某个条件满足时做某件事(也就是说,属性不为None),否则就返回None、一个空集合或类似的东西。

所以我立刻想到了Scala中的一些选项,但由于Python中lambda表达式的语法比较繁琐,我对结果不是很满意:

# create a user option somewhere in a RequestContext
request.user_option = Some(user) if user.is_authenticated() else Empty()

# access it like this in view
posts = request.user_option.map(lambda u: u.post_set.filter(flag=True).all()).get_or_else(None)

代码被选项语法弄得有些复杂,几乎掩盖了实际的意图。而且我们还得知道,用户必须登录才能拥有post_set这个属性。

我想要的是一个类似“空合并”的操作符,这样我就可以像这样写代码:

def my_view(request):
    user_ = Coalesce(user)
    posts = user_.post_set.filter(flag=True).all() # -> QuerySet | None-like
    return render(request, 'template.html', {posts: posts})

接着我写了一个包装类,实际上让我可以做到这一点:

class Coalesce:
    def __init__(self, inst):
        self.inst = inst

    def __call__(self, *args, **kwargs):
        return Coalesce(None)

    def __iter__(self):
        while False:
            yield None

    def __str__(self):
        return ''

    def __getattr__(self, name):
        if hasattr(self.inst, name):
            return getattr(self.inst, name)
        else:
            return Coalesce(None)

    def __len__(self):
        return 0

用Coalesce包装用户对象让我可以按我想要的方式写代码。如果用户没有定义post_set这个属性,它就会像一个空集合/列表那样工作(在可迭代对象方面),并且会被评估为False。所以这在Django模板中是可行的。

假设这些合并后的对象通过命名约定或显式转换进行了标记,这样做可以吗?还是说应该避免这样?如果要避免,处理可选属性的最佳方法是什么,而不需要写大量的if-else语句?

1 个回答

1

你的解决方案是对空对象模式的一种变体。只要你明确地使用它,我觉得这样做是完全可以的。特别是当它能提高你代码的可读性时。

你还可以在你的Coalesce类中添加以下方法,这样就可以像这样访问索引:first_post = user_.post_set.filter(flag=True)[0]

def __getitem__(self, i):
    try:
        return self.inst[i]
    except (TypeError, IndexError, KeyError):
        return Coalesce(None)

至于成员测试中的innot in

def __contains__(self, i):
    try:
        return i in self.inst
    except (TypeError, IndexError, KeyError):
        return False

撰写回答