以下代码均可直接粘贴运行, 在Python3.7中测试通过
问题引入
先前遇到了一个排序问题, 具有不同属性的对象, 需要根据其中一些属性
进行排序, 比如对一系列的评论进行排序, 有评论者, 点赞数, 发布时间这些
特征, 这些特征可能所占的比重不同, 而后根据计算的结果进行排序.
假如有下面这样一些评论:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import datetime topics = [ { "content": "这是评论1", "likes": 10, "user_weight": 10, 'time': datetime.datetime(2019, 2, 20, 19, 31, 58, 412000), }, { "content": "这是评论2", "likes": 5, "user_weight": 20, 'time': datetime.datetime(2019, 2, 20, 19, 41, 58, 412000), }, { "content": "这是评论1", "likes": 20, "user_weight": 5, 'time': datetime.datetime(2019, 2, 20, 20, 1, 58, 412000), } ]
|
打算使用这样的表达式计算权值:
用户权重 * 5 + 点赞数 * 1 + 发布距今秒数 * -0.02
用户权值占的比值较大, 用户权值越高, 越靠前; 发布越早的帖子的权值越低, 越靠后
初级的解决思路
我们会写一个for循环, 遍历这个数组, 进行计算:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import time
cur_time = 1550664218.0
for topic in topics: topic['weight'] = 5 * topic['user_weight'] + topic['likes'] + \ (cur_time - time.mktime(topic['time'].timetuple())) * -0.02
[{'content': '这是评论1', 'likes': 10, 'user_weight': 10, 'time': datetime.datetime(2019, 2, 20, 19, 31, 58, 412000), 'weight': 22.0}, {'content': '这是评论2', 'likes': 5, 'user_weight': 20, 'time': datetime.datetime(2019, 2, 20, 19, 41, 58, 412000), 'weight': 79.0}, {'content': '这是评论1', 'likes': 20, 'user_weight': 5, 'time': datetime.datetime(2019, 2, 20, 20, 1, 58, 412000), 'weight': 43.0}]
|
这样写的一个好处就是快, 以及准确. 无论如何, 只要你把单元测试也写对,
上面的写法是不可能出错的.
进阶写法
想象一下, 如果你把上面的过程写完了, 发现运营给你提出了新的需求, 甚至加入了新的
字段. 那么你就要不停的维护上面这一段代码. 每次加上了新的字段, 你需要不断的与运营
的同学对接. 好吧, 我承认渲染的有些过分了, 毕竟产品的排序算法也不会很快的变来变去.
但是不可否认, 确实有一种可以简化操作的写法.
让我们回想上面的表达式:
1
| 用户权重 * 5 + 点赞数 * 1 + 发布距今秒数 * -0.02
|
这不就是一个表达式的求值问题吗, 只要程序帮我们把这个表达式解析了, 然后套入
数据就可以了.
这个过程需要分两步进行:
- 使程序理解表达式是什么, 该怎么计算
- 告诉程序, 表达式中的每个元素如何获取
如何让程序理解计算式
想到这个部分, 我不知道有多少人做过简单的计算器, 其中有什么中缀表达式转后缀.
其实想想转来转去还是挺难的. 自从学习了编译原理, 什么东西都想借助词法, 语法分析一波.
就像下面的表达式树, 就是语法分析之后的结果.
表示了一个(a+b)*(c-d))
这样的算式, 只要给出abcd的值, 就可以遍历得出.
如何告诉程序每个元素的值
我在下面直接贴出了代码, 请注意看data值如何被使用
下面就是一小段Python代码了, Python中有AST模块, 我们可以借助这个模块进行语法树
的构建, 而后自己去遍历:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import ast import operator
BINOPS = { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Mod: operator.mod, } _node = ast.parse('(a+b)*(c-d)', mode='eval')
_data = { 'a': 2, 'b': 3, 'c': 4, 'd': 5, }
def make_calculate(node, data): def _eval(node): if isinstance(node, ast.Expression): return _eval(node.body) elif isinstance(node, ast.Name): return data[node.id] elif isinstance(node, ast.Num): return node.n elif isinstance(node, ast.BinOp): return BINOPS[type(node.op)](_eval(node.left), _eval(node.right)) else: raise Exception('Unsupported type {}'.format(node)) return _eval(node)
make_calculate(_node, _data)
|
重新看看输入, 我们给定了表达式(a+b)*(c-d)
, 又给定了a,b,c,d的值,
就可以计算得到结果, 那么放在刚刚的评论系统中, 权值的求解要怎么进行呢?
正式的解决方案
我们可以列出这样的表达式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| weight_expr = """ 'user_weight' * 5 + 'likes' + (0 - 1) * 0.02 * 'time_offset' """
def get_user_weight(topic): return topic['user_weight']
def get_likes(topic): return topic['likes']
def get_time_offset(topic): cur_time = 1550664218.0 return cur_time - time.mktime(topic['time'].timetuple())
TOPIC_UNIT = { 'likes': get_likes, 'user_weight': get_user_weight, 'time_offset': get_time_offset, }
_node = ast.parse(weight_expr, mode='eval')
def make_calculate(node, topic):
def _eval(node): if isinstance(node, ast.Expression): return _eval(node.body) elif isinstance(node, ast.Str): return TOPIC_UNIT[node.s](topic) elif isinstance(node, ast.Num): return node.n elif isinstance(node, ast.BinOp): return BINOPS[type(node.op)](_eval(node.left), _eval(node.right)) else: raise Exception('Unsupported type {}'.format(node)) return _eval(node.body)
for _topic in topics: _topic['weight'] = make_calculate(_node, _topic)
|
本段代码将属性值的获取, 从用户手动调用变成了由程序自动调用.
未来, 假如我们想要增加属性(例如dislikes), 仅仅需要完成下面几件事:
- 创建get函数, 例如
get_dislikes
;
- 在TOPIC_UNIT中, 插入 ‘dislikes’: get_dislikes;
- 在表达式中增加对’dislikes’的使用;
总结
这次的问题是针对某个对象, 对它的不同属性给予不同的权值来考量其比重, 很适合
使用表达式进行计算, 正好Python自带有的ast库, 可以方便的构建表达式树.
但我还是希望大家能够再理解一下编译原理中的思想: 让程序去理解人类.
各位学弟学妹, 不论老师如何, 你还是可以认真学习一下编译原理的.