《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) 
[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('
')

    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', '
', '3','
'])


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('
')

    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初始化父类

原文地址:https://www.cnblogs.com/sidianok/p/12803731.html