zoukankan      html  css  js  c++  java
  • 《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初始化父类

  • 相关阅读:
    kuangbin 专题一:G题,POJ3087:Shuffle'm Up
    kuangbin专题一:F题,POJ3126:Prime Path
    /*分治典型应用 快速排序*/
    kuangbin专题一 简单搜索 E,POJ 1426 Find The Multiple
    kuangbin专题一:C题,POJ3278:Catch That Cow
    kuangbin专题一B题:POJ2251:Dungeon Master
    kuangbin专题一A题 :POJ1321 :棋盘问题
    1282: ykc想吃好吃的
    2017年ACM第八届山东省赛I题: Parity check(判断 第n项斐波那契数列奇偶性)
    2017年ACM第八届山东省赛J题:company
  • 原文地址:https://www.cnblogs.com/sidianok/p/12803731.html
Copyright © 2011-2022 走看看