Python "空合并" / 连续属性访问
免责声明:我对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 个回答
你的解决方案是对空对象模式的一种变体。只要你明确地使用它,我觉得这样做是完全可以的。特别是当它能提高你代码的可读性时。
你还可以在你的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)
至于成员测试中的in
和not in
:
def __contains__(self, i):
try:
return i in self.inst
except (TypeError, IndexError, KeyError):
return False