电子商务网站设计实践报告,网站建设与管理案例教程在线阅读,天门市网站建设seo,网上国网推广经验前言
编程#xff0c;其实和玩电子游戏有一些相似之处。你在玩不同游戏前#xff0c;需要先学习每个游戏的不同规则#xff0c;只有熟悉和灵活运用游戏规则#xff0c;才更有可能在游戏中获胜。
而编程也是一样#xff0c;不同编程语言同样有着不一样的“规则”。大到是…前言
编程其实和玩电子游戏有一些相似之处。你在玩不同游戏前需要先学习每个游戏的不同规则只有熟悉和灵活运用游戏规则才更有可能在游戏中获胜。
而编程也是一样不同编程语言同样有着不一样的“规则”。大到是否支持面向对象小到是否可以定义常量编程语言的规则比绝大多数电子游戏要复杂的多。
当我们编程时如果直接拿一种语言的经验套用到另外一种语言上很多时候并不能取得最佳结果。这就好像一个 CS反恐精英 高手在不了解规则的情况下去玩 PUBG绝地求生虽然他的枪法可能万中无一但是极有可能在发现第一个敌人前他就会倒在某个窝在草丛里的敌人的伏击下。
Python 里的规则
Python 是一门初见简单、深入后愈觉复杂的语言。拿 Python 里最重要的“对象”概念来说Python 为其定义了多到让你记不全的规则比如 定义了 __str__ 方法的对象就可以使用 str() 函数来返回可读名称 定义了 __next__ 和 __iter__ 方法的对象就可以被循环迭代 定义了 __bool__ 方法的对象在进行布尔判断时就会使用自定义的逻辑 ... ...
熟悉规则并让自己的代码适应这些规则可以帮助我们写出更地道的代码事半功倍的完成工作。下面让我们来看一个有关适应规则的故事。
案例从两份旅游数据中获取人员名单
某日在一个主打新西兰出境游的旅游公司里商务同事突然兴冲冲的跑过来找到我说他从某合作伙伴那里要到了两份重要的数据 所有去过“泰国普吉岛”的人员及联系方式 所有去过“新西兰”的人员及联系方式
数据采用了 JSON 格式如下所示
# 去过普吉岛的人员数据users_visited_phuket [{first_name: Sirena, last_name: Gross, phone_number: 650-568-0388, date_visited: 2018-03-14},{first_name: James, last_name: Ashcraft, phone_number: 412-334-4380, date_visited: 2014-09-16},... ...]# 去过新西兰的人员数据users_visited_nz [{first_name: Justin, last_name: Malcom, phone_number: 267-282-1964, date_visited: 2011-03-13},{first_name: Albert, last_name: Potter, phone_number: 702-249-3714, date_visited: 2013-09-11},... ...] 每份数据里面都有着 姓、 名、 手机号码、 旅游时间 四个字段。基于这份数据商务同学提出了一个听上去毫无道理的假设“去过普吉岛的人应该对去新西兰旅游也很有兴趣。我们需要从这份数据里找出那些去过普吉岛但没有去过新西兰的人针对性的卖产品给他们。
第一次蛮力尝试
有了原始数据和明确的需求接下来的问题就是如何写代码了。依靠蛮力我很快就写出了第一个方案
def find_potential_customers_v1():找到去过普吉岛但是没去过新西兰的人for phuket_record in users_visited_phuket:is_potential Truefor nz_record in users_visited_nz:if phuket_record[first_name] nz_record[first_name] and \phuket_record[last_name] nz_record[last_name] and \phuket_record[phone_number] nz_record[phone_number]:is_potential Falsebreakif is_potential:yield phuket_record 因为原始数据里没有“用户 ID”之类的唯一标示所以我们只能把“姓名和电话号码完全相同”作为判断是不是同一个人的标准。
find_potential_customers_v1 函数通过循环的方式先遍历所有去过普吉岛的人然后再遍历新西兰的人如果在新西兰的记录中找不到完全匹配的记录就把它当做“潜在客户”返回。
这个函数虽然可以完成任务但是相信不用我说你也能发现。它有着非常严重的性能问题。对于每一条去过普吉岛的记录我们都需要遍历所有新西兰访问记录尝试找到匹配。整个算法的时间复杂度是可怕的 O(n*m)如果新西兰的访问条目数很多的话那么执行它将耗费非常长的时间。
为了优化内层循环性能我们需要减少线性查找匹配部分的开销。
尝试使用集合优化函数
如果你对 Python 有所了解的话那么你肯定知道Python 里的字典和集合对象都是基于 哈希表Hash Table 实现的。判断一个东西是不是在集合里的平均时间复杂度是 O(1)非常快。
所以对于上面的函数我们可以先尝试针对新西兰访问记录初始化一个集合之后的查找匹配部分就可以变得很快函数整体时间复杂度就能变为 O(nm)。
让我们看看新的函数
def find_potential_customers_v2():找到去过普吉岛但是没去过新西兰的人性能改进版# 首先遍历所有新西兰访问记录创建查找索引nz_records_idx {(rec[first_name], rec[last_name], rec[phone_number])for rec in users_visited_nz}for rec in users_visited_phuket:key (rec[first_name], rec[last_name], rec[phone_number])if key not in nz_records_idx:yield rec
使用了集合对象后新函数在速度上相比旧版本有了飞跃性的突破。但是对这个问题的优化并不是到此为止不然文章标题就应该改成“如何使用集合提高程序性能” 了。
对问题的重新思考
让我们来尝试重新抽象思考一下问题的本质。首先我们有一份装了很多东西的容器 A普吉岛访问记录然后给我们另一个装了很多东西的容器 B新西兰访问记录之后定义相等规则“姓名与电话一致”。最后基于这个相等规则求 A 和 B 之间的“差集”。
如果你对 Python 里的集合不是特别熟悉我就稍微多介绍一点。假如我们拥有两个集合 A 和 B那么我们可以直接使用 A-B 这样的数学运算表达式来计算二者之间的 差集。 a {1, 3, 5, 7} b {3, 5, 8}# 产生新集合所有在 a 但是不在 b 里的元素 a - b{1, 7}
所以计算“所有去过普吉岛但没去过新西兰的人”其实就是一次集合的求差值操作。那么要怎么做才能把我们的问题套入到集合的游戏规则里去呢?
利用集合的游戏规则
在 Python 中如果要把某个东西装到集合或字典里一定要满足一个基本条件“这个东西必须是可以被哈希Hashable的” 。什么是 “Hashable”
举个例子Python 里面的所有可变对象比如字典就 不是 Hashable 的。当你尝试把字典放入集合中时会发生这样的错误 s set() s.add({foo: bar})Traceback (most recent call last):File stdin, line 1, in moduleTypeError: unhashable type: dict
所以如果要利用集合解决我们的问题就首先得定义我们自己的 “Hashable” 对象VisitRecord。而要让一个自定义对象变得 Hashable唯一要做的事情就是定义对象的 __hash__ 方法。
class VisitRecord:旅游记录def __init__(self, first_name, last_name, phone_number, date_visited):self.first_name first_nameself.last_name last_nameself.phone_number phone_numberself.date_visited date_visited
一个好的哈希算法应该让不同对象之间的值尽可能的唯一这样可以最大程度减少“哈希碰撞”发生的概率默认情况下所有 Python 对象的哈希值来自它的内存地址。
在这个问题里我们需要自定义对象的 __hash__ 方法让它利用 姓名电话元组作为 VisitRecord 类的哈希值来源。
def __hash__(self):return hash((self.first_name, self.last_name, self.phone_number))
自定义完 __hash__ 方法后 VisitRecord 实例就可以正常的被放入集合中了。但这还不够为了让前面提到的求差值算法正常工作我们还需要实现 __eq__ 特殊方法。
__eq__ 是 Python 在判断两个对象是否相等时调用的特殊方法。默认情况下它只有在自己和另一个对象的内存地址完全一致时才会返回 True。但是在这里我们复用了 VisitRecord 对象的哈希值当二者相等时就认为它们一样。
def __eq__(self, other):# 当两条访问记录的名字与电话号相等时判定二者相等。if isinstance(other, VisitRecord) and hash(other) hash(self):return Truereturn False
完成了恰当的数据建模后之后的求差值运算便算是水到渠成了。新版本的函数只需要一行代码就能完成操作
def find_potential_customers_v3():return set(VisitRecord(**r) for r in users_visited_phuket) - \set(VisitRecord(**r) for r in users_visited_nz) Hint如果你使用的是 Python 2那么除了 __eq__ 方法外你还需要自定义类的 __ne__判断不相等时使用 方法。 使用 dataclass 简化代码
故事到这里并没有结束。在上面的代码里我们手动定义了自己的 数据类 VisitRecord实现了 __init__、 __eq__ 等初始化方法。但其实还有更简单的做法。
因为定义数据类这种需求在 Python 中实在太常见了所以在 3.7 版本中标准库中新增了 dataclasses 模块专门帮你简化这类工作。
如果使用 dataclasses 提供的特性我们的代码可以最终简化成下面这样
dataclass(unsafe_hashTrue)class VisitRecordDC:first_name: strlast_name: strphone_number: str# 跳过“访问时间”字段不作为任何对比条件date_visited: str field(hashFalse, compareFalse)def find_potential_customers_v4():return set(VisitRecordDC(**r) for r in users_visited_phuket) - \set(VisitRecordDC(**r) for r in users_visited_nz)
不用干任何脏活累活只要不到十行代码就完成了工作。
案例总结
问题解决以后让我们再做一点小小的总结。在处理这个问题时我们一共使用了三种方案 使用普通的两层循环筛选符合规则的结果集 利用哈希表结构set 对象创建索引提升处理效率 将数据转换为自定义对象利用规则直接使用集合运算
为什么第三种方式会比前面两种好呢
首先第一个方案的性能问题过于明显所以很快就会被放弃。那么第二个方案呢仔细想想看方案二其实并没有什么明显的缺点。甚至和第三个方案相比因为少了自定义对象的过程它在性能与内存占用上甚至有可能会微微强于后者。
但请再思考一下如果你把方案二的代码换成另外一种语言比如 Java它是不是基本可以做到 1:1 的完全翻译换句话说它虽然效率高、代码直接但是它没有完全利用好 Python 世界提供的规则最大化的从中受益。
如果要具体化这个问题里的“规则”那就是 “Python 拥有内置结构集合集合之间可以进行差值等四则运算” 这个事实本身。匹配规则后编写的方案三代码拥有下面这些优势 为数据建模后可以更方便的定义其他方法 如果需求变更做反向差值运算、求交集运算都很简单 理解集合与 dataclasses 逻辑后代码远比其他版本更简洁清晰 如果要修改相等规则比如“只拥有相同姓的记录就算作一样”只需要继承 VisitRecord 覆盖 __eq__ 方法即可
其他规则如何影响我们
在前面我们花了很大的篇幅讲了如何利用“集合的规则”来编写事半功倍的代码。除此之外Python 世界中还有着很多其他规则。如果能熟练掌握这些规则就可以设计出符合 Python 惯例的 API让代码更简洁精炼。
下面是两个具体的例子。
使用 __format__ 做对象字符串格式化
如果你的自定义对象需要定义多种字符串表示方式就像下面这样
class Student:def __init__(self, name, age):self.name nameself.age agedef get_simple_display(self):return f{self.name}({self.age})def get_long_display(self):return f{self.name} is {self.age} years old.piglei Student(piglei, 18)# OUTPUT: piglei(18)print(piglei.get_simple_display())# OUTPUT: piglei is 18 years old.print(piglei.get_long_display())
那么除了增加这种 get_xxx_display() 额外方法外你还可以尝试自定义 Student 类的 __format__ 方法因为那才是将对象变为字符串的标准规则。
class Student:def __init__(self, name, age):self.name nameself.age agedef __format__(self, format_spec):if format_spec long:return f{self.name} is {self.age} years old.elif format_spec simple:return f{self.name}({self.age})raise ValueError(invalid format spec)piglei Student(piglei, 18)print({0:simple}.format(piglei))print({0:long}.format(piglei))
使用 __getitem__ 定义对象切片操作
如果你要设计某个可以装东西的容器类型那么你很可能会为它定义“是否为空”、“获取第 N 个对象”等方法
class Events:def __init__(self, events):self.events eventsdef is_empty(self):return not bool(self.events)def list_events_by_range(self, start, end):return self.events[start:end]events Events([computer started,os launched,docker started,os stopped,])# 判断是否有内容打印第二个和第三个对象if not events.is_empty():print(events.list_events_by_range(1, 3))
但是这样并非最好的做法。因为 Python 已经为我们提供了一套对象规则所以我们不需要像写其他语言的 OO面向对象 代码那样去自己定义额外方法。我们有更好的选择
class Events:def __init__(self, events):self.events eventsdef __len__(self):自定义长度将会被用来做布尔判断return len(self.events)def __getitem__(self, index):自定义切片方法# 直接将 slice 切片对象透传给 events 处理return self.events[index]# 判断是否有内容打印第二个和第三个对象if events:print(events[1:3])
新的写法相比旧代码更能适配进 Python 世界的规则API 也更为简洁。
关于如何适配规则、写出更好的 Python 代码。Raymond Hettinger 在 PyCon 2015 上有过一次非常精彩的演讲 “Beyond PEP8 - Best practices for beautiful intelligible code”。这次演讲长期排在我个人的 “PyCon 视频 TOP5” 名单上如果你还没有看过我强烈建议你现在就去看一遍 :) Hint更全面的 Python 对象模型规则可以在 官方文档 找到有点难读但值得一读。 总结
Python 世界有着一套非常复杂的规则这些规则的涵盖范围包括“对象与对象是否相等“、”对象与对象谁大谁小”等等。它们大部分都需要通过重新定义“双下划线方法 __xxx__” 去实现。
如果熟悉这些规则并在日常编码中活用它们有助于我们更高效的解决问题、设计出更符合 Python 哲学的 API。下面是本文的一些要点总结 永远记得对原始需求做抽象分析比如问题是否能用集合求差集解决 如果要把对象放入集合需要自定义对象的 __hash__ 与 __eq__ 方法 __hash__ 方法决定性能碰撞出现概率 __eq__ 决定对象间相等逻辑 使用 dataclasses 模块可以让你少写很多代码 使用 __format__ 方法替代自己定义的字符串格式化方法 在容器类对象上使用 __len__、 __getitem__ 方法而不是自己实现