《Effective Python》59个有效方法(今日到25)
chapter1:用Pythonic方式来思考
第1条:确认自己所用的Python
shijianzhongdeMacBook-Pro:~ shijianzhong$ python -V Python 2.7.16 shijianzhongdeMacBook-Pro:~ shijianzhong$ python --version Python 2.7.16 shijianzhongdeMacBook-Pro:~ shijianzhong$ python3 --version Python 3.7.4 shijianzhongdeMacBook-Pro:~ shijianzhong$ python3 -V Python 3.7.4 shijianzhongdeMacBook-Pro:~ shijianzhong$
In [48]: import sys In [49]: sys.version_info Out[49]: sys.version_info(major=3, minor=7, micro=4, releaselevel='final', serial=0) In [50]: sys.version Out[50]: '3.7.4 (default, Jul 9 2019, 18:13:23) \n[Clang 10.0.1 (clang-1001.0.46.4)]' In [51]:
以后重点使用py3,2已经冻结,只会bug修复
第2条:遵循PEP8风格指南
《Python Enhancement Proposal#8》又叫PEP8,它是针对Python代码格式而编订的风格指南。
空白:
使用space(空格)来表示缩进,而不要用tab(制表符)。
和语法相关的每一层缩进都用4个空格来表示
每行的字符数不应超过79,学习笔记说,现在可以100了
对于占据多行的长表达式来说,除了首行之外的其余各行都应该在通常的缩进级别之上再加4个空格(pycharm是6个空格)
文件中函数跟类应该用两个空行分开
在同一个类中,各方法之间应该用一个空行隔开
在使用下标来获取列表元素、调用函数或给关键字参数赋值的时候,不要在两旁添加空格
为变量赋值的时候,赋值符号的左侧和右侧应该各自写上一个空格,而且只写一个就好。
命名:
函数、变量和属性应该用小写字母来拼写,各单词之间以下划线相连。
受保护的实例属性,应该以单个下划线开头,例如_xxx_xx
私有的实例属性,应该以两个下划线开头,例如__double_xxx
类与异常,应该以每个单词首字母大写的形式来命名
模块级别的常量,应该全部采用大写字母来拼写,个单词之间以下划线相连,如ALL_CAPS
实例方法首字母(self)
类方法首字母(cls)
表达式语句
采用内联形式的否定词,而不是把否定词放在整个表达式的前面,列如 if a is not b 而不是 if not a is b
不要通过len一个序列去判断空,直接用对象就可以了。
文件中的那些import语句应该按照顺序划分三个部分,分别表示标准版模块、第三方模块以及自用模块。在每一个部分之中,各import语句应该按模块的字母顺序来排序.
第3条:了解bytes、str与unicode的区别
Python3有两种表示字符序列的类型:bytes和str。前者实例包含原始的8位值;或者的实例包含Unicode字符
Python2也有两种表示字符序列的类型,分别叫做str和unicode。与Python3不同的是,str的实例包含原始的8位值;而unicode的实例,则包含Unicode字符。
也就是说,py3的bytes与py2中的str数据格式相等
另外写的比较简单,py2默认的打开方式是2进制模式打开,py3是以文件的形式打开,如果向文件对象进行二进制操作,可以用'rb'与'wb'。
第4条:用赋值函数来取代复杂的表达式。
书中主要用到了or操作符左侧的子表达式估值为False,那么整个表达式的值就是or操作符右侧那个子表达式的值,和三元操作符。
最后告诉我们要适当使用Python语法特性。
In [58]: my_values = parse_qs('red=5&blue=0&green=',keep_blank_values=True) In [59]: my_values Out[59]: {'red': ['5'], 'blue': ['0'], 'green': ['']} In [60]: red = my_values.get('red',[''])[0] or 0 In [61]: blue = my_values.get('blue',[''])[0] or 0 In [62]: green = my_values.get('green',[''])[0] or 0 In [64]: red;blue;green Out[64]: 0 In [65]: red Out[65]: '5' In [66]: blue Out[66]: '0' In [67]: green Out[67]: 0 In [68]:
上面就用到了or操作符的使用,但如果还要对参数进行数字加减,还要使用Int
blue = int(my_values.get('blue',[''])[0] or 0 )
这样有点麻烦,就写成三元表达式
In [68]: red = my_values.get('red',['']) In [69]: red = int(red[0]) if red[0] else 0 In [70]:
如果是一个参数还好,如果频发使用到的话,可以写一个辅助函数。
In [70]: def get_first_int(values,key,defalut=0): ...: found = values.get(key, ['']) ...: if found[0]: ...: found = int(found[0]) ...: else: ...: found = defalut ...: return found ...: In [71]: get_first_int(my_values,'red') Out[71]: 5 In [72]: get_first_int(my_values,'blue') Out[72]: 0 In [73]:
好无聊的一个章节,这个书有点买亏了
第5条:了解切割序列的方法。
切片赋值属于浅拷贝,切片数值[a:b]取头不取尾,-1取最后一个数值,a就是头的下标不能越界。
后面讲了对切片进行赋值
In [91]: a = list(range(9)) In [92]: a Out[92]: [0, 1, 2, 3, 4, 5, 6, 7, 8] In [93]: a[2:5] = ['a','b'] In [94]: a Out[94]: [0, 1, 'a', 'b', 5, 6, 7, 8] In [95]:
都是一个套路,很多书中以前就有过介绍。
第6条 在单词切片中,不要同时指定start、end、stride
In [95]: a = list(range(10)) In [96]: a Out[96]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] In [97]: a[::3] Out[97]: [0, 3, 6, 9] In [98]: b = 'hello' In [99]: b[::-1] Out[99]: 'olleh' In [100]:
书中主要说了,如果有切片了,就不要写步进,分开好比较好,我觉的都无所谓。
第7条:用列表推导取代map和filter
列表推导式的强大,确实可以完全取代map与filter,reduce还是有点意思的。
In [100]: from functools import reduce In [101]: reduce(lambda x,y:x+y, range(10)) Out[101]: 45 In [102]: map(lambda x: x**2, range(10)) Out[102]: <map at 0x1142e4e90> In [103]: list(map(lambda x: x**2, range(10))) Out[103]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] In [104]: [x**2 for x in range(10)] Out[104]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] In [105]: list(filter(lambda x: x%2, range(10))) Out[105]: [1, 3, 5, 7, 9] In [107]: [x for x in range(10) if x%2!=0] Out[107]: [1, 3, 5, 7, 9] In [108]:
字典,集合都支持列表推导式的结构。
第8条:不要使用含有两个以上表达式的列表推导
双for的列表推导式,可以将矩阵(即二维列表)简化成维列表,也可以改变二维列表内的元素
In [113]: matrix = [[1,2,3],[4,5,6],[7,8,9]] In [114]: flat = [x for row in matrix for x in row] In [115]: flat Out[115]: [1, 2, 3, 4, 5, 6, 7, 8, 9] In [116]:
跟for循环的顺序一样,第一次循环写前面,第二次循环写后面
下面是改变矩阵的数据
In [118]: squared = [[x**2 for x in row] for row in matrix] In [119]: squared Out[119]: [[1, 4, 9], [16, 25, 36], [49, 64, 81]] In [120]:
但如果层数太多的话,就不建议使用列表推导式,应该会让人看过去很头痛。
列表推导式还支持多个if条件。
In [120]: a = list(range(1,11)) In [121]: a Out[121]: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] In [122]: b = [x for x in a if x > 4 if x%2==0] In [123]: c = [x for x in a if x > 4 and x%2==0] In [124]: b Out[124]: [6, 8, 10] In [125]: b==c Out[125]: True In [126]:
可以看出两个答案是等价的。
每一层循环的for表达式后面都可以指定条件。
In [127]: filtered = [[x for x in row if x%3==0] for row in matrix if sum(row) >=10] In [128]: filtered Out[128]: [[6], [9]] In [129]:
这种写法还是很骚包的
第9条:用生成器表达式来改写数据量较大的列表推导
假如读取一份文件并返回每行的字符数,若采用列表推导来做,则需要文件每一行的长度度保存在内存中。
如果文件非常大,那可能会内存溢出或者运行很慢,可以采取生成器表达式,就是把[]换成()
it = (x for x in open('/tmp/file.txt'))
可以通过next函数对数据进行读取
使用生成器还有一个好处,就是可以互相组合。
In [129]: generatpr = (i for i in range(10)) In [130]: generatpr Out[130]: <generator object <genexpr> at 0x1144f0dd0> In [131]: roots = ((x,x**2) for x in generatpr) In [132]: next(roots) Out[132]: (0, 0) In [133]: next(roots) Out[133]: (1, 1) In [134]: next(roots) Out[134]: (2, 4) In [135]:
外围的迭代器每次前进时,都会推动内部那个迭代器,这就产生了连锁效应,使得执行循环、评估条件表达式、对接输入和输出等逻辑度组合在了一期。
第10条: 尽量用enumerate取代range
本章主要讲了enumerate可以将一个迭代器包装成一个生成器。而且包装的对象会自动加上索引。
In [7]: a = enumerate('0123') In [8]: next(a) Out[8]: (0, '0') In [9]: next(a) Out[9]: (1, '1') In [10]: next(a) Out[10]: (2, '2') In [11]: next(a) Out[11]: (3, '3') In [12]: next(a) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-12-15841f3f11d4> in <module> ----> 1 next(a) StopIteration: In [13]: b = enumerate('123',1) In [14]: next(b) Out[14]: (1, '1') In [15]: next(b) Out[15]: (2, '2') In [16]:
感觉还是挺不错的,第二个参数是开始计数使用的值(默认为0)
第11条:用zip函数同事遍历两个迭代器
zip可以平行的遍历多个可迭代对象。
In [16]: z = zip('abc',[1,2,3]) In [17]: for chart, index in z: ...: print(chart, index) ...: a 1 b 2 c 3 In [18]: for o in z: ...: print(o) ...: ...: In [19]: z = zip('abc',[1,2,3]) In [20]: for o in z: ...: print(o) ...: ...: ('a', 1) ('b', 2) ('c', 3) In [21]:
如果提供的可迭代对象长度不等,则以短的为准
In [21]: z = zip('a',range(3)) In [22]: next(z) Out[22]: ('a', 0) In [23]: next(z) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-23-cf9ac561a401> in <module> ----> 1 next(z) StopIteration: In [24]:
如果想不在长度是否相等,可以用itertools.zip_longest
第12条:不要在for和while循环后面写else块
这个我个人其实觉的蛮好用的,很装逼,但很多进阶的书都说这种功能不是太好。
书中主要通过return的方式打断返回,或者在循环外部定义一个变量,break之前修改变量。
我感觉其实都不是太好,还for或者while+else好。
主要记住,正常循环完毕会执行else条件,如果中间打断了,就不执行else条件。
第13条:合理利用try/except/else/finally结构中的每个代码块
try:编写可能会报错的逻辑
except:捕获错误的信息
esle:写需要返回的结果情况,与try中可能存在的错误分离
finaly:写肯定会执行的逻辑
try,可以配合finaly直接使用
使用了else必须带上except
Chapter2
函数
第14条:尽量用异常来表示特殊请, 而不要返回None
编写工具函数的时候,有些程序员喜欢将错误捕获,直接返货None
def divide(a, b): try: return a / b except ZeroDivisionError: return None
这样会存在一些问题,很多时候,我们拿到函数运行,会直接把返回值拿到if条件中。
None的效果与0,空字符串,等效果相等,所以当分子是的0,分母不为0的时候,就会出现问题。
解决的方法应该不反悔None,而是把异常抛给上一级,使得调用者必须应对它。下面就是把ZeroDivisionError转换成ValueError,表示输入无效
def divide(a, b): try: return a / b except ZeroDivisionError as e: raise ValueError('Invalid inputs') from e if __name__ == '__main__': x, y = 5, 2 try: res = divide(x, y) except ValueError: print('Invalid inputs') else: print('Result is %.1f' % res)
这个再写工具函数的时候确实一个不错的选择,错误的捕获可以上浮正确的错误形式,让上级去处理。
第15条: 了解如何在闭包里使用外围作用域中的变量
先来一个特定要求的排序,再特定组里面的数组排队优先,写的很棒
def sort_priority(values, group): def helper(x): if x in group: return (0, x) return (1, x) values.sort(key=helper)
这是我两个月来见过写的最有意思的函数。
使用了闭包,内部函数引用了外包函数的变量,而且排序的时候,使用的多元素比较,第一元素比好以后,比第二元素。
后面书中讲了闭包的作用域问题LEGB,已经内部作用域可以引用外部作用域变量,可以修改外部作用域的可变对象。
但内部作用域与外部作用域变量名同名时,想修改外部作用域变量不能操作。因为赋值操作,当前作用域没有这个变量,会新建这么一个变量,有的话就修改。
书中也介绍了nolocal语句,来表明该变量名,可以修改外部变量的指向。但这个只能用再较短的函数,对于一些比较长的函数。使用class,内部建议__call__,到时候实例一个可调用的对象,对象内保存状态
def sort_priority(values, group): found = False def helper(x): if x in group: nonlocal found found = True return (0, x) return (1, x) values.sort(key=helper) return found
nolocal的用法
def sort_priority(values, group): sort_priority.found = False def helper(x): if x in group: sort_priority.found = True return (0, x) return (1, x) values.sort(key=helper) return sort_priority.found
我自己想的通过函数对象赋值一个属性。
class Sorter: def __init__(self, group): self.group = group self.found = False def __call__(self, x): if x in self.group: self.found = True return (0, x) return (1, x)
上面时书上面特意写了一个用于被排序用的可调用对象。
在Python2中没有nolocal,可以在闭包的参数设置为可变参数,这样内部的函数就可以改变该参数的内部属性。
这个章节的一些内容以前看过,就当再理解一下,那个排序的写法,第一次看到,真的让我眼前一亮。
第16条: 考虑用生成器来改写直接返回列表的函数
def index_words(text): result = [] if text: result.append(0) for index, letter in enumerate(text, 1): if letter == ' ': result.append(index) return result
这个一个标准版的返回各个单词首字母索引的函数。书中说明,这个函数有两个不好的地方
1:函数内定义了一个result的列表,最后返回这么一个列表,显的很啰嗦
2:假如这个text很大,那这个列表也会输入量很大,那么程序就有可能耗尽内存并奔溃。
def index_words_iter(text): if text: yield 0 for index, letter in enumerate(text, 1): if letter == ' ': yield index
这种生成器的写法就可以避免这样的问题发生,而且用生成器表达更加清晰。
最后记录一下,书中一个处理文件的生成器函数
def index_file(handle): offset = 0 for line in handle: if line: yield offset # 这时真正函数运行消耗内存的地方,需要读取一行数据 for letter in line: offset += 1 if letter == ' ': yield offset
表名了处理文档,函数真正消耗内存的地方
第17条:在参数上面迭代时,要多加小心
一个函数如果对一个迭代器进行操作,务必要小心,因为迭代器保存一种状态,一但被读取完以后,就不能返回开始的状态。
def normalize(numbers): total = sum(numbers) result = [] for value in numbers: percent = 100 * value / total result.append(percent) return result visits = [15, 35, 80] percentages = normalize(visits) print(percentages)
这是一个函数,处理每个数据的百分比,当传入的是一个容器对象的时候就没事。
但传入一个迭代器的时候,就出问题了,sum函数会把迭代器里面的内容读取完毕,后面的for循环去读取numbers的时候,里面是没有数据的。
def normalize(numbers): numbers = list(numbers) total = sum(numbers) result = [] for value in numbers: percent = 100 * value / total result.append(percent) return result
这样是最简单偷懒的方式,但这样函数这样处理大数据的时候就会出问题,还有一个方式就是参数接收另外一个函数,那个函数调用完都会返回一个新的迭代器
def normalize_func(get_iter): total = sum(get_iter()) result = [] for value in get_iter(): percent = 100 * value / total result.append(percent) return result # 传入一个lambda函数,后面的是一个返回生成器的函数 percentahes = normalize_func(lambda : read_bisits(path))
这样看过去比较不舒服,那就自己定一个可迭代的容器。
class ReadVisits: def __init__(self, data_path): self.data_path = data_path def __iter__(self): with open(self.data_path) as f: for line in f: yield int(line) visits = ReadVisits('/user/haha.txt') percentages = normalize(visits)
通过自己定义的类,实例化以后的对象,当需要对这个容器进行操作的时候,都会调用__iter__方式返回的迭代器。比如sum,for,max,min等
迭代器协议有这样的约定:如果把迭代器对象传给内置的iter函数,那么此函数会把该迭代器返回,也就是返回自身,反之,如果传给iter函数的是个容器类型对象,那么iter函数则每次返回新的迭代器对象。
由于我们前面定义的函数需要处理容器对象,所以对进来的参数可以通过前面的方式进行过滤
def normalize_defensive(numbers): # 判断是否是容器对象 if iter(numbers) is iter(numbers): raise TypeError('Must suplly a container') total = sum(numbers) result = [] for value in numbers: percent = 100 * value / total result.append(percent) return result
本章节我主要很好的学到了自定义容器对象,以及容器对象每次iter返回的都是不同内存地址的迭代器,也就是不同的对象。但迭代器iter返回的是自身。
而且可以看出来容器对象与迭代器没有啥必然的关系,两种也是各自为政的对象.
第18条:用数量可变的未知参数减少视觉杂讯
本条主要讲了*args接收不定长参数的使用。
def log(message, *value): if not value: print(message) else: value_str = ', '.join(str(x) for x in value) print('%s: %s' % (message, value_str))
上面的例子中*value可以接收任意长度的参数,在函数内部value会转化成元祖
书中还有一个就是通过解包的方式传递参数
如果传递进去的是一个序列
l = [1,2,3,4]
可以在传递的时候通过*l的方式解包,里面的每一个元祖都会被视为位置参数,如果解包的是一个迭代器,如果数据过大就会消耗大量的内存。
第19条:用关键字参数来表达可选的行为。
def remainder(number, divisor): return number % divisor assert remainder(20, 7) == 6
上面的传参形式就是标准的位置传参。
可以通过下面的形式传参
remainder(20, divisor=7) remainder(numver=20, divisor=7) remainder(divisor=7, number=20)
Python中有规定,位置参数必须在关键字参数之前,每个参数只能被指定一次【也就是说,位置参数传参过以后,不能再通过关键字传参】
remainer(number=20, 7)
assert remainder(number=7, 20) == 6 ^ SyntaxError: positional argument follows keyword argument
语法错误
emainder(20, number=7) == 6
assert remainder(20, number=7) == 6 TypeError: remainder() got multiple values for argument 'number'
类型错误
书中简单说明了关键字传参的好处,能让人了解,提供默认值,扩展函数参数的有效性。
第20条:用None和文档字符串来秒速具有默认值的参数。
import time from datetime import datetime def log(message, when=datetime.now()): print('%s: %s' % (message, when)) log('Hi there !') time.sleep(0.1) log('Hi, again') print(log.__defaults__)
输出
Hi there !: 2020-09-13 13:06:55.770035
Hi, again: 2020-09-13 13:06:55.770035
(datetime.datetime(2020, 9, 13, 13, 6, 55, 770035),)
流畅的Python中介绍,以及本人的理解,默认参数会成为函数对象的属性。在函数进行初始化的时候进行运行一次。
后面就不会再重新运行加载,所以会出现前面的情况。
import time from datetime import datetime def log(message, when=None): when = datetime.now() if when is None else when print('%s: %s' % (message, when)) log('Hi there !') time.sleep(0.1) log('Hi, again') print(log.__defaults__)
改成上面这样
输出
Hi there !: 2020-09-13 13:10:11.295604 Hi, again: 2020-09-13 13:10:11.400005 (None,)
后面介绍了,传参给了一个可变对象,多人调用这个函数修改这个参数。
这个在流畅的Python中也有详细的介绍:
书中原文【在Python中,函数得到参数的副本,但是参数始终是引用。因此,如果参数引用的是可变对象,那么对象可能被修改,但是参数的表示不变】
第21条:用只能以关键字形式指定的参数来确保代码明晰
这里主要讲诉了如果传参必须要关键字传参的操作,
在Python3中可以通过*操作,
def demo(x,y,* must_show): ....
在Python2中没有*操作,只能通过**kwargs的方式接收关键字传参的不定长参数
然后在函数体内进行逻辑判断。
第三章 类与继承
第22条:尽量用辅助类来维护程序的状态,而不要用字典和元祖。
书中前面讲了,如果通过一个类保存一个复杂的数据结构状态。
如果保存程序的数据结构一旦变得过于复杂,就应该将其拆解为类,以便提供更为明确的接口。并更好的封装数据。这样做也能够在接口和具体实现之间创建抽象层。
后面都为本人的理解
最为对象层级最底层的grade(成绩),书中了用了nametuple进行了对象封装。
成绩对象的封装写法
import collections # 一个权重,一个成绩 Grade = collections.namedtuple('Grade', ('score', 'weight'))
然后每一次成绩称为科目的对象的元素之一,科目有一个属性_grades的列表保存每一次成绩
class Subject: def __init__(self): self._grades = [] # 在科目对象装入成绩对象 def report_grade(self, score, weight): self._grades.append(Grade(score, weight)) # 求出这门功课的平均分 def average_grade(self): total, total_weight = 0, 0 for grade in self._grades: total += grade.score * grade.weight total_weight += grade.weight return total / total_weight
科目可以称为学生的属性,一个学生会有很多科目。学生的科目用的是dictionaries保存
class Student: def __init__(self): self._subject = {} # 写入一门功课 def subject(self, name): return self._subject.setdefault(name, Subject()) # 求出一个学生所有功课的平均分 def average_grade(self): total, count = 0, 0 for subject in self._subject.values(): total += subject.average_grade() count += 1 return total / count
学生又是班级花名册里面的一个对象,所有最后定一个花名册
#!/usr/bin/env python3 # -*- coding: UTF-8 -*- import collections # 一个权重,一个成绩 Grade = collections.namedtuple('Grade', ('score', 'weight')) class Subject: def __init__(self): self._grades = [] # 在科目对象装入成绩对象 def report_grade(self, score, weight): self._grades.append(Grade(score, weight)) # 求出这门功课的平均分 def average_grade(self): total, total_weight = 0, 0 for grade in self._grades: total += grade.score * grade.weight total_weight += grade.weight return total / total_weight class Student: def __init__(self): self._subject = {} # 写入一门功课 def subject(self, name): return self._subject.setdefault(name, Subject()) # 求出一个学生所有功课的平均分 def average_grade(self): total, count = 0, 0 for subject in self._subject.values(): total += subject.average_grade() count += 1 return total / count class Gradebook: def __init__(self): self._student = {} def student(self, name): return self._student.setdefault(name, Student()) if __name__ == '__main__': book = Gradebook() # 学生 albert = book.student('Albert Einstein') # 学生添加功课 math = albert.subject('Math') # 功课添加成绩与权重 math.report_grade(90,0.3) math.report_grade(95, 0.5) print(book.student('Albert Einstein').average_grade())
上面直接上了,整体代码,首先定义一个成绩对象,需要几个属性就写几个,用了nametuple,然后成绩方位具体课程的对象中,由于一个课程可以有多门成绩,所以用列表来保存所有的成绩。
还在课程对象中直接定义了,该课程求出平均成绩的方法。
然后课程可以作为学生对象的元素,由于每个课程的都是一个独立的对象,考虑要后期需要取出该课程信息,所有用了dictionaries来保存,key是课程的名称,value是课程对象
后面同样的方式,把学生对象放入花名册。
这个其实主要考虑的是一个抽象的思考能力,在模型复杂的过程中,层层剥离对象,使一个对象称为另一个对象的属性。
书中的案例,是先写好最小的对象,层层方法,一个对象是另外一个的属性,这也是非常不错的思考方式,值的我学习,借鉴。
第23条:简单的接口应该接收函数,而不是类的实例。
def increment_with_report(current, increments): added_count = 0 # 定义一个回调函数 def missing(): nonlocal added_count added_count += 1 return 0 # 使用defaultdict调用missing函数 result = defaultdict(missing, current) for key, amount in increments: result[key] += amount return result, added_count result, count = increment_with_report(current, increments) print(result) print(count)
运行输出:
defaultdict(<function increment_with_report.<locals>.missing at 0x118403268>, {'green': 12, 'blue': 20, 'red': 5, 'orange': 9}) 2
上面通过了使用了闭包,内部函数调用了外部的非全局变量。内部定义一个函数给当做回调函数。
通过闭包来保存一种状态也可以,当当过辅助类是一种相对更加好理解的方式
class BetterCountMissing: def __init__(self): self.added = 0 # 实例化以后, 称为一个可调用对象。 def __call__(self, *args, **kwargs): self.added += 1 return 0 count = BetterCountMissing() result = defaultdict(count, current) for key, amount in increments: result[key] += amount print(count.added)
__call__方法强烈地暗示了该类的用途,它告诉我们,这个类的功能就相当与一个带有状态的闭包。
第24条:用@classmethod形式的多态去通用地构建对象。
在这一个章节,书中主要介绍了通过装饰器@classmethod,类方式去初始化一些特定的对象。还有就是介绍了一个有趣的模块 tempfile.TemporaryDirectory,建立一个临时目录,当对象删除的时候,目录也没了。
书中展示了一个输入处理类,和一个工作类的衔接。
#!/usr/bin/env python3 # -*- coding: UTF-8 -*- import abc import os from threading import Thread from tempfile import TemporaryDirectory class InputData: @abc.abstractmethod def read(self): raise NotImplementedError # 处理输入的 class PathInputData(InputData): def __init__(self, path): super(PathInputData, self).__init__() self.path = path def read(self): return open(self.path).read() # 工作线程基类 class Worker: def __init__(self, input_data): self.input_data = input_data self.result = None @abc.abstractmethod def map(self): raise NotImplementedError @abc.abstractmethod def reduce(self, other): raise NotImplementedError # 处理有几行的工作类 class LineCountWorker(Worker): def map(self): data = self.input_data.read() self.result = data.count('\n') def reduce(self, other): self.result += other.result # 返回文件夹下面所有文件的处理对象生成器 def generate_inputs(data_dir): for name in os.listdir(data_dir): yield PathInputData(os.path.join(data_dir, name)) # 返回的处理文档对象,建立工作实例对象列表 def create_workers(input_list): workers = [] for input_data in input_list: workers.append(LineCountWorker(input_data)) return workers # 最后面多线程执行工作对象 def execute(workers): threads = [Thread(target=w.map) for w in workers] for thread in threads: thread.start() for thread in threads: thread.join() first, rest = workers[0], workers[1:] for worker in rest: first.reduce(worker) return first.result # 然后把前面的打包成一个函数 def mapreduce(data_dir): inputs = generate_inputs(data_dir) workers = create_workers(inputs) return execute(workers) def write_test_files(tmpdir): for i in 'abcde': abs_path = os.path.join(tmpdir, i) with open(abs_path, 'w') as f: f.writelines(['2', '\n', '3','\n']) with TemporaryDirectory() as tmpdir: print(tmpdir) write_test_files(tmpdir) result = mapreduce(tmpdir) print('There are', result, 'lines')
书中通过对象的调用,函数式的方式完成了任务
但是这个写法有个大问题,那就是mapreduce函数不够通用。如果要编写其它的InputData或Woker子类,那就要重写generate_inpurs, create_workers, mapreduce函数,以便与之匹配。
书中不知道是翻译的僵硬,还是我的理解能力不够,主要介绍了,编写基类的方式,通过类方法的继承实现类方法的多态,返回所需要的要求
# 基类 class GenericInputData: def read(self): raise NotImplementedError @classmethod def generate_inputs(cls, config): raise NotImplementedError class PathInputData(GenericInputData): def __init__(self, path): self.path = path def read(self): return open(self.path).read() # 类方法直接返回一个生成器 @classmethod def generate_inputs(cls, config): data_dir = config['data_dir'] for name in os.listdir(data_dir): yield cls(os.path.join(data_dir, name)) # 工作的基类 class GenericWorker: def __init__(self, input_data): self.input_data = input_data self.result = None def map(self): raise NotImplementedError def reduce(self, other): raise NotImplementedError # 返回需要处理的的workers容器列表 @classmethod def create_workers(cls, input_class, config): workers = [] # 调用前面的类方法,产生的生成器 for input_data in input_class.generate_inputs(config): workers.append(cls(input_data)) return workers class LineCountWorker(GenericWorker): def map(self): data = self.input_data.read() self.result = data.count('\n') def reduce(self, other): self.result += other.result def mapreduce(worker_class, input_class, config): workers = worker_class.create_workers(input_class, config) return execute(workers) with TemporaryDirectory() as tmpdir: write_test_files(tmpdir) config = {'data_dir': tmpdir} result = mapreduce(LineCountWorker, PathInputData, config) print(result)
这是书中通过类方法的方式写的,通过定义通用接口。
在Python中,类只有一个构造器函数__init__,可以通过@classmethod的方式来丰富创建对象的形式。按照就可以直接通过类方法,返回列表,生成器等。__init__可是没有返回值的。
第25条 用super初始化父类