• 进击的Python【第四章】:Python的高级应用(一)


    Python的高级应用(一)

    本章内容:

    1. 内置函数
    2. 生成器
    3. 迭代器
    4. 装饰器
    5. JSON和PICKLE的简单用法
    6. 软件目录结构规范

    一、内置函数

    1、数学运算类

    abs(x) 求绝对值
    1、参数可以是整型,也可以是复数
    2、若参数是复数,则返回复数的模
    complex([real[, imag]]) 创建一个复数
    divmod(a, b) 分别取商和余数
    注意:整型、浮点型都可以
    float([x]) 将一个字符串或数转换为浮点数。如果无参数将返回0.0
    int([x[, base]])  将一个字符转换为int类型,base表示进制
    long([x[, base]])  将一个字符转换为long类型
    pow(x, y[, z])  返回x的y次幂
    range([start], stop[, step])  产生一个序列,默认从0开始
    round(x[, n])  四舍五入
    sum(iterable[, start])  对集合求和
    oct(x) 将一个数字转化为8进制
    hex(x) 将整数x转换为16进制字符串
    chr(i) 返回整数i对应的ASCII字符
    bin(x) 将整数x转换为二进制字符串
    bool([x]) 将x转换为Boolean类型

    2、集合类操作

    basestring() str和unicode的超类
    不能直接调用,可以用作isinstance判断
    format(value [, format_spec]) 格式化输出字符串
    格式化的参数顺序从0开始,如“I am {0},I like {1}”
    unichr(i) 返回给定int类型的unicode
    enumerate(sequence [, start = 0]) 返回一个可枚举的对象,该对象的next()方法将返回一个tuple
    iter(o[, sentinel]) 生成一个对象的迭代器,第二个参数表示分隔符
    max(iterable[, args...][key])  返回集合中的最大值
    min(iterable[, args...][key]) 返回集合中的最小值
    dict([arg]) 创建数据字典
    list([iterable])  将一个集合类转换为另外一个集合类
    set() set对象实例化
    frozenset([iterable]) 产生一个不可变的set
    str([object])  转换为string类型
    sorted(iterable[, cmp[, key[, reverse]]])  队集合排序
    tuple([iterable])  生成一个tuple类型
    xrange([start], stop[, step])  xrange()函数与range()类似,但xrnage()并不创建列表,而是返回一个xrange对象,它的行为与列表相似,但是只在需要时才计算列表值,当列表很大时,这个特性能为我们节省内存

    3、逻辑判断

    all(iterable) 1、集合中的元素都为真的时候为真
    2、特别的,若为空串返回为True
    any(iterable) 1、集合中的元素有一个为真的时候为真
    2、特别的,若为空串返回为False
    cmp(x, y) 如果x < y ,返回负数;x == y, 返回0;x > y,返回正数

    4、反射

    callable(object) 检查对象object是否可调用
    1、类是可以被调用的
    2、实例是不可以被调用的,除非类中声明了__call__方法
    classmethod() 1、注解,用来说明这个方式是个类方法
    2、类方法即可被类调用,也可以被实例调用
    3、类方法类似于Java中的static方法
    4、类方法中不需要有self参数
    compile(source, filename, mode[, flags[, dont_inherit]]) 将source编译为代码或者AST对象。代码对象能够通过exec语句来执行或者eval()进行求值。
    1、参数source:字符串或者AST(Abstract Syntax Trees)对象。
    2、参数 filename:代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。
    3、参数model:指定编译代码的种类。可以指定为 ‘exec’,’eval’,’single’。
    4、参数flag和dont_inherit:这两个参数暂不介绍
    dir([object]) 1、不带参数时,返回当前范围内的变量、方法和定义的类型列表;
    2、带参数时,返回参数的属性、方法列表。
    3、如果参数包含方法__dir__(),该方法将被调用。当参数为实例时。
    4、如果参数不包含__dir__(),该方法将最大限度地收集参数信息
    delattr(object, name) 删除object对象名为name的属性
    eval(expression [, globals [, locals]]) 计算表达式expression的值
    execfile(filename [, globals [, locals]]) 用法类似exec(),不同的是execfile的参数filename为文件名,而exec的参数为字符串。
    filter(function, iterable) 构造一个序列,等价于[ item for item in iterable if function(item)]
    1、参数function:返回值为True或False的函数,可以为None
    2、参数iterable:序列或可迭代对象
    getattr(object, name [, defalut]) 获取一个类的属性
    globals() 返回一个描述当前全局符号表的字典
    hasattr(object, name) 判断对象object是否包含名为name的特性
    hash(object) 如果对象object为哈希表类型,返回对象object的哈希值
    id(object) 返回对象的唯一标识
    isinstance(object, classinfo) 判断object是否是class的实例
    issubclass(class, classinfo) 判断是否是子类
    len(s)  返回集合长度
    locals()  返回当前的变量列表
    map(function, iterable, ...)  遍历每个元素,执行function操作
    memoryview(obj)  返回一个内存镜像类型的对象
    next(iterator[, default])  类似于iterator.next()
    object()  基类
    property([fget[, fset[, fdel[, doc]]]])  属性访问的包装类,设置后可以通过c.x=value等来访问setter和getter
    reduce(function, iterable[, initializer])  合并操作,从第一个开始是前两个参数,然后是前两个的结果与第三个合并进行处理,以此类推
    reload(module)  重新加载模块
    setattr(object, name, value) 设置属性值
    repr(object)  将一个对象变幻为可打印的格式
    slice()  
    staticmethod 声明静态方法,是个注解
    super(type[, object-or-type])  引用父类
    type(object) 返回该object的类型
    vars([object])  返回对象的变量,若无参数与dict()方法类似
    bytearray([source [, encoding [, errors]]]) 返回一个byte数组
    1、如果source为整数,则返回一个长度为source的初始化数组;
    2、如果source为字符串,则按照指定的encoding将字符串转换为字节序列;
    3、如果source为可迭代类型,则元素必须为[0 ,255]中的整数;
    4、如果source为与buffer接口一致的对象,则此对象也可以被用于初始化bytearray.
    zip([iterable, ...])  实在是没有看懂,只是看到了矩阵的变幻方面

    5、IO操作

    file(filename [, mode [, bufsize]]) file类型的构造函数,作用为打开一个文件,如果文件不存在且mode为写或追加时,文件将被创建。添加‘b’到mode参数中,将对文件以二进制形式操作。添加‘+’到mode参数中,将允许对文件同时进行读写操作
    1、参数filename:文件名称。
    2、参数mode:'r'(读)、'w'(写)、'a'(追加)。
    3、参数bufsize:如果为0表示不进行缓冲,如果为1表示进行行缓冲,如果是一个大于1的数表示缓冲区的大小 。
    input([prompt])  获取用户输入
    推荐使用raw_input,因为该函数将不会捕获用户的错误输入
    open(name[, mode[, buffering]])  打开文件
    与file有什么不同?推荐使用open
    print 打印函数
    raw_input([prompt])  设置输入,输入都是作为字符串处理

    二、生成器

      通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器(Generator)。

    创建一个简单生成器

    >>> L = [x * x for x in range(10)]
    >>> L
    [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
    >>> g = (x * x for x in range(10))
    >>> g
    <generator object <genexpr> at 0x104feab40>
    

    创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。
    我们可以直接打印出list的每一个元素,但是如果我们要一个一个打印出生成器的内容,就需要通过generator的next()方法:

    >>> g.next()
    
    0
    
    >>> g.next()
    
    1
    
    >>> g.next()
    
    4
    
    >>> g.next()
    
    9
    
    >>> g.next()
    
    16
    
    >>> g.next()
    
    25
    
    >>> g.next()
    
    36
    
    >>> g.next()
    
    49
    
    >>> g.next()
    
    64
    
    >>> g.next()
    
    81
    
    >>> g.next()
    
    Traceback (most recent call last):
    
      File "<stdin>", line 1, in <module>
    
    StopIteration
    

    通过上面的用法我们看到虽然用next方法可以一个一个输出生成器内容,但是非常笨拙,所以我们一般用for循环来输出生成器。

    >>> g = (x * x for x in range(10))
    
    >>> for n in g:
    
    ...     print(n)
    
    ...
    
    0
    
    1
    
    4
    
    9
    
    16
    
    25
    
    36
    
    49
    
    64
    
    81
    

    上面的例子只是一个实现简单功能的生成器,如果我们实现一个复杂功能的生成器的话,就要用到函数了,我们如果把一个函数编程生成器呢,这里我们就引入了yield语句。

    我们都知道著名的斐波那契数列(1,1,2,3,5,8......),形成原理就是从1,1开始,第三个数就是前两个数的和,一次类推。

    我们如何用生成器来实现斐波那契数列呢?

    def fib(max):
         n, a, b = 0, 0, 1
         while n < max:
             yield b
             a, b = b, a + b
             n = n + 1
       return "done"

    这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

    >>> fib(6)
    
    <generator object fib at 0x104feaaa0>
    

    这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

    执行结果:

    D:python3.5python.exe E:/learn_python/day04/gen.py
    1
    1
    2
    3
    5
    8
    13
    21
    

    我们发现最后会生成一个异常,而不返回正常的返回值,这里我需要用异常捕获指令,try...except

    >>> fib(5)
    <generator object fib at 0x02F8E180>
    >>> f = fib(5)
    >>> while True:
         try:
             x = next(f)
             print('g:', x)
         except StopIteration as e:
             print('Generator return value:', e.value)
             break
    g: 1
    g: 1
    g: 2
    g: 3
    g: 5
    Generator return value: done
    

    生成器的高级应用,单线程并发小程序:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    
    #_*_coding:utf-8_*_
    __author__ = 'Alex Li'
    
    import time
    def consumer(name):
        print("%s 准备吃包子啦!" %name)
        while True:
           baozi = yield
    
           print("包子[%s]来了,被[%s]吃了!" %(baozi,name))
    
    
    def producer(name):
        c = consumer('A')
        c2 = consumer('B')
        c.__next__()
        c2.__next__()
        print("老子开始准备做包子啦!")
        for i in range(10):
            time.sleep(1)
            print("做了2个包子!")
            c.send(i)
            c2.send(i)
    
    producer("alex")
    

    其中的send方法会把参数传回生成器,并从生成器暂停的地方继续执行。结果如下:

    A 准备吃包子啦!
    B 准备吃包子啦!
    老子开始准备做包子啦!
    做了2个包子!
    包子[0]来了,被[A]吃了!
    包子[0]来了,被[B]吃了!
    做了2个包子!
    包子[1]来了,被[A]吃了!
    包子[1]来了,被[B]吃了!
    做了2个包子!
    包子[2]来了,被[A]吃了!
    包子[2]来了,被[B]吃了!
    做了2个包子!
    包子[3]来了,被[A]吃了!
    包子[3]来了,被[B]吃了!
    做了2个包子!
    包子[4]来了,被[A]吃了!
    包子[4]来了,被[B]吃了!
    做了2个包子!
    包子[5]来了,被[A]吃了!
    包子[5]来了,被[B]吃了!
    做了2个包子!
    包子[6]来了,被[A]吃了!
    包子[6]来了,被[B]吃了!
    做了2个包子!
    包子[7]来了,被[A]吃了!
    包子[7]来了,被[B]吃了!
    做了2个包子!
    包子[8]来了,被[A]吃了!
    包子[8]来了,被[B]吃了!
    做了2个包子!
    包子[9]来了,被[A]吃了!
    包子[9]来了,被[B]吃了!
    
    Process finished with exit code 0
    

    三、迭代器

      迭代器是访问集合内元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素都被访问一遍后结束。

      迭代器不能回退,只能往前进行迭代。这并不是什么很大的缺点,因为人们几乎不需要在迭代途中进行回退操作。

      迭代器也不是线程安全的,在多线程环境中对可变集合使用迭代器是一个危险的操作。但如果小心谨慎,或者干脆贯彻函数式思想坚持使用不可变的集合,那这也不是什么大问题。

      对于原生支持随机访问的数据结构(如tuple、list),迭代器和经典for循环的索引访问相比并无优势,反而丢失了索引值(可以使用内建函数enumerate()找回这个索引值,这是后话)。但对于无法随机访问的数据结构(比如set)而言,迭代器是唯一的访问元素的方式。

      迭代器的另一个优点就是它不要求你事先准备好整个迭代过程中所有的元素。迭代器仅仅在迭代至某个元素时才计算该元素,而在这之前或之后,元素可以不存在或者被销毁。这个特点使得它特别适合用于遍历一些巨大的或是无限的集合,比如几个G的文件,或是斐波那契数列等等。这个特点被称为延迟计算或惰性求值(Lazy evaluation)。

      迭代器更大的功劳是提供了一个统一的访问集合的接口。只要是实现了__iter__()方法的对象,就可以使用迭代器进行访问。

      一个对象,物理或者虚拟存储的序列。list,tuple,strins,dicttionary,set以及生成器对象都是可迭代的,整型数是不可迭代的。

      我们可以使用isinstance()判断一个对象是否是Iterable对象:

    >>> from collections import Iterable
    >>> isinstance([], Iterable)
    True
    >>> isinstance({}, Iterable)
    True
    >>> isinstance('abc', Iterable)
    True
    >>> isinstance((x for x in range(10)), Iterable)
    True
    >>> isinstance(100, Iterable)
    False
    

      Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。所以虽然string,list,dic等可以被for循环是可迭代的,但是他们并不能称作迭代器。

    实际上,string,list,dic等类型数据被for循环时,在底层也就是一个不断调用next方法的过程,比如

    for x in [1, 2, 3, 4, 5]:
        pass
    

      等价于

    # 首先获得Iterator对象:
    it = iter([1, 2, 3, 4, 5])
    # 循环:
    while True:
        try:
            # 获得下一个值:
            x = next(it)
        except StopIteration:
            # 遇到StopIteration就退出循环
            break
    

    四、装饰器

      装饰器简单来讲就是在不改变原函数的情况下为该函数新增功能,具体怎么实现的呢?

    前置知识一:高阶函数

    我们知道一个函数都有一个函数名,在Python中这个函数名是可以当做一个参数被另一个函数调用的,而这个调用别的函数名做参数的函数就称作高阶函数。

    举个高阶函数最经典的栗子:

    def add(x,y,f):
        return f(x) + f(y)
     
     
    res = add(3,-6,abs)
    print(res)
    

    在上面的例子中,函数f()就被函数add()当做参数调用了。

    前置知识二:函数的嵌套

    Python语言允许在定义函数的时候,其函数体内又包含另外一个函数的完整定义,这就是我们通常所说的嵌套定义,如下:

    def foo():         #定义函数foo(),
        m=3            #定义变量m=3;
        def bar():     #在foo内定义函数bar()
            n=4        #定义局部变量n=4
            print m+n  #m相当于函数bar()的全局变量
         bar()
    

    在我们了解了上面两个函数类型后就可以说说这个装饰(逼)器是个什么鬼。。。

    现在我们有一个现成的函数:

    def f():
        print '来装饰我吧!!'
    

    如果我们想在不改变函数f()的本体结构的情况下给他增加功能,我们怎么做?

    我们先做一个装饰器函数

    def deco(func):
          def inner():
                print("我是装饰器!!")
                return func()
          return inner
    

    我们可以看到,这个装饰器函数deco(func)的参数就是给要被调用的函数名准备的参数,里面同时又内嵌了函数。那么这个装饰器怎么用呢?

    @deco
    def f():
          print("来装饰我吧!!")
    

    这样一用就把f()函数给装饰了,结果是什么样的呢?

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    
    
    def deco(func):
          def inner():
                print("我是装饰器!!")
                return func()
          return inner
    
    @deco
    def f():
          print("来装饰我吧!!")
    
    f()
    
    ######################################
    
    D:python3.5python.exe E:/learn_python/day04/deco.py
    我是装饰器!!
    来装饰我吧!!
    

    我们发现经过装饰器装饰后,直接执行f()出的结果包含了装饰器里的语句,是不是很神奇?那这里面的具体流程是怎么样的呢,f()的功能是怎么被改变的呢?

    1、首先会执行载入装饰器函数,同时把原函数作为参数调入装饰器中

    2、将执行完的装饰器deco(func)的返回值inner()赋给@deco下面的函数,这样就给f()函数赋予了新的功能。之后直接调用f()就可以运行装饰后的新的功能。

    被装饰的函数带参数的情况下,怎么写装饰器?

    def deco(func):
        def inner(arg1,arg2,arg3):
            #新增功能
            return func(arg1,arg2,arg3)
        return inner
    
    @deco
    def f(arg1,arg2,arg3):
        print 'f1'
    

    处理n个参数的函数,并被多个装饰器装饰:

    def deco1(func):
        def inner(*args,**kwargs):
            # 新增功能
            return func(*args,**kwargs)
        return inner
     
    def deco2(func):
        def inner(*args,**kwargs):
            # 新增功能
            return func(*args,**kwargs)
        return inner
     
     
    @deco1
    @deco2
    def f(arg1,arg2,arg3):
        print 'f1'
    

    五、JSON和PICKLE的简单用法

    用于序列化的两个模块

    • json,用于字符串 和 python数据类型间进行转换
    • pickle,用于python特有的类型 和 python的数据类型间进行转换

    Json模块提供了四个功能:dumps、dump(序列化,存)、loads(反序列化,读)、load

    pickle模块提供了四个功能:dumps、dump(序列化,存)、loads(反序列化,读)、load  (不仅可以序列化字典,列表...还可以把一个程序,一个类给序列化掉)

    import pickle
    data = {'k1':123, 'k2':123}
    #dumps可以将数据类型转换成只有python才认识的字符串
    p_str = pickle.dumps(data)
    print(p_str)
    

      结果

    b'x80x03}qx00(Xx02x00x00x00k1qx01K{Xx02x00x00x00k2qx02K{u.'
    

      将数据转换成只有Python认识的字符串,并写入文件:

    import pickle
    data = {'k1':123, 'k2':123}
    #打开文件,然后将data写入
    with open('data.pkl', 'wb') as f:
        pickle.dump(data, f)
    #同样读取的时候也需要打开文件
    with open('data.pkl', 'rb') as f:
        data_1 = pickle.load(f)
    print(data_1)
    

      结果

    {'k1': 123, 'k2': 123}
    

    json的用法和pickle是一样的

    import json
    data = {'k1':123, 'k2':123}
    p_str = json.dumps(data)
    print(p_str, type(p_str))
    

      结果

    {"k1": 123, "k2": 123} <class 'str'>
    

      我们看到结果看起来是个字典,其实是字符串型的,因为json只能是字符串型。

    import json
    data = {'k1':123, 'k2':123}
    
    #打开文件,然后将data写入
    with open('data.pkl', 'w') as f:
        json.dump(data, f)
    
    #同样读取的时候也需要打开文件
    with open('data.pkl', 'r') as f:
        data_1 = json.load(f)
    print(data_1, type(data_1))
    

      上面这段代码,是写入文件又读取出来。看看执行结果

    {'k2': 123, 'k1': 123} <class 'dict'>

    那pickle和json有什么区别呢?

    在上面两段代码中,pickle写入和读取文件时,用的是 ‘b’模式,而json没有。

    json是可以在不同语言之间交换数据的,而pickle只在python之间使用。

    json只能序列化最基本的数据类型,而pickle可以序列化所有的数据类型,包括类,函数都可以序列化。

    六、软件目录结构规范

    为什么要设计好目录结构?

    "设计项目目录结构",就和"代码编码风格"一样,属于个人风格问题。对于这种风格上的规范,一直都存在两种态度:

    1. 一类同学认为,这种个人风格问题"无关紧要"。理由是能让程序work就好,风格问题根本不是问题。
    2. 另一类同学认为,规范化能更好的控制程序结构,让程序具有更高的可读性。

    我是比较偏向于后者的,因为我是前一类同学思想行为下的直接受害者。我曾经维护过一个非常不好读的项目,其实现的逻辑并不复杂,但是却耗费了我非常长的时间去理解它想表达的意思。从此我个人对于提高项目可读性、可维护性的要求就很高了。"项目目录结构"其实也是属于"可读性和可维护性"的范畴,我们设计一个层次清晰的目录结构,就是为了达到以下两点:

    1. 可读性高: 不熟悉这个项目的代码的人,一眼就能看懂目录结构,知道程序启动脚本是哪个,测试目录在哪儿,配置文件在哪儿等等。从而非常快速的了解这个项目。
    2. 可维护性高: 定义好组织规则后,维护者就能很明确地知道,新增的哪个文件和代码应该放在什么目录之下。这个好处是,随着时间的推移,代码/配置的规模增加,项目结构不会混乱,仍然能够组织良好。

    所以,我认为,保持一个层次清晰的目录结构是有必要的。更何况组织一个良好的工程目录,其实是一件很简单的事儿。

    目录组织方式

    关于如何组织一个较好的Python工程目录结构,已经有一些得到了共识的目录结构。在Stackoverflow的这个问题上,能看到大家对Python目录结构的讨论。

    这里面说的已经很好了,我也不打算重新造轮子列举各种不同的方式,这里面我说一下我的理解和体会。

    假设你的项目名为foo, 我比较建议的最方便快捷目录结构这样就足够了:

    Foo/
    |-- bin/
    |   |-- foo
    |
    |-- foo/
    |   |-- tests/
    |   |   |-- __init__.py
    |   |   |-- test_main.py
    |   |
    |   |-- __init__.py
    |   |-- main.py
    |
    |-- docs/
    |   |-- conf.py
    |   |-- abc.rst
    |
    |-- setup.py
    |-- requirements.txt
    |-- README
    

    简要解释一下:

    1. bin/: 存放项目的一些可执行文件,当然你可以起名script/之类的也行。
    2. foo/: 存放项目的所有源代码。(1) 源代码中的所有模块、包都应该放在此目录。不要置于顶层目录。(2) 其子目录tests/存放单元测试代码; (3) 程序的入口最好命名为main.py
    3. docs/: 存放一些文档。
    4. setup.py: 安装、部署、打包的脚本。
    5. requirements.txt: 存放软件依赖的外部Python包列表。
    6. README: 项目说明文件。

    除此之外,有一些方案给出了更加多的内容。比如LICENSE.txt,ChangeLog.txt文件等,我没有列在这里,因为这些东西主要是项目开源的时候需要用到。如果你想写一个开源软件,目录该如何组织,可以参考这篇文章

    下面,再简单讲一下我对这些目录的理解和个人要求吧。

    关于README的内容

    这个我觉得是每个项目都应该有的一个文件,目的是能简要描述该项目的信息,让读者快速了解这个项目。

    它需要说明以下几个事项:

    1. 软件定位,软件的基本功能。
    2. 运行代码的方法: 安装环境、启动命令等。
    3. 简要的使用说明。
    4. 代码目录结构说明,更详细点可以说明软件的基本原理。
    5. 常见问题说明。

    我觉得有以上几点是比较好的一个README。在软件开发初期,由于开发过程中以上内容可能不明确或者发生变化,并不是一定要在一开始就将所有信息都补全。但是在项目完结的时候,是需要撰写这样的一个文档的。

    可以参考Redis源码中Readme的写法,这里面简洁但是清晰的描述了Redis功能和源码结构。

    关于requirements.txt和setup.py

    setup.py

    一般来说,用setup.py来管理代码的打包、安装、部署问题。业界标准的写法是用Python流行的打包工具setuptools来管理这些事情。这种方式普遍应用于开源项目中。不过这里的核心思想不是用标准化的工具来解决这些问题,而是说,一个项目一定要有一个安装部署工具,能快速便捷的在一台新机器上将环境装好、代码部署好和将程序运行起来。

    这个我是踩过坑的。

    我刚开始接触Python写项目的时候,安装环境、部署代码、运行程序这个过程全是手动完成,遇到过以下问题:

    1. 安装环境时经常忘了最近又添加了一个新的Python包,结果一到线上运行,程序就出错了。
    2. Python包的版本依赖问题,有时候我们程序中使用的是一个版本的Python包,但是官方的已经是最新的包了,通过手动安装就可能装错了。
    3. 如果依赖的包很多的话,一个一个安装这些依赖是很费时的事情。
    4. 新同学开始写项目的时候,将程序跑起来非常麻烦,因为可能经常忘了要怎么安装各种依赖。

    setup.py可以将这些事情自动化起来,提高效率、减少出错的概率。"复杂的东西自动化,能自动化的东西一定要自动化。"是一个非常好的习惯。

    setuptools的文档比较庞大,刚接触的话,可能不太好找到切入点。学习技术的方式就是看他人是怎么用的,可以参考一下Python的一个Web框架,flask是如何写的: setup.py

    当然,简单点自己写个安装脚本(deploy.sh)替代setup.py也未尝不可。

    requirements.txt

    这个文件存在的目的是:

    1. 方便开发者维护软件的包依赖。将开发过程中新增的包添加进这个列表中,避免在setup.py安装依赖时漏掉软件包。
    2. 方便读者明确项目使用了哪些Python包。

    这个文件的格式是每一行包含一个包依赖的说明,通常是flask>=0.10这种格式,要求是这个格式能被pip识别,这样就可以简单的通过 pip install -r requirements.txt来把所有Python包依赖都装好了。具体格式说明: 点这里

    关于配置文件的使用方法

    注意,在上面的目录结构中,没有将conf.py放在源码目录下,而是放在docs/目录下。

    很多项目对配置文件的使用做法是:

    1. 配置文件写在一个或多个python文件中,比如此处的conf.py。
    2. 项目中哪个模块用到这个配置文件就直接通过import conf这种形式来在代码中使用配置。

    这种做法我不太赞同:

    1. 这让单元测试变得困难(因为模块内部依赖了外部配置)
    2. 另一方面配置文件作为用户控制程序的接口,应当可以由用户自由指定该文件的路径。
    3. 程序组件可复用性太差,因为这种贯穿所有模块的代码硬编码方式,使得大部分模块都依赖conf.py这个文件。

    所以,我认为配置的使用,更好的方式是,

    1. 模块的配置都是可以灵活配置的,不受外部配置文件的影响。
    2. 程序的配置也是可以灵活控制的。

    能够佐证这个思想的是,用过nginx和mysql的同学都知道,nginx、mysql这些程序都可以自由的指定用户配置。

    所以,不应当在代码中直接import conf来使用配置文件。上面目录结构中的conf.py,是给出的一个配置样例,不是在写死在程序中直接引用的配置文件。可以通过给main.py启动参数指定配置路径的方式来让程序读取配置内容。当然,这里的conf.py你可以换个类似的名字,比如settings.py。或者你也可以使用其他格式的内容来编写配置文件,比如settings.yaml之类的。

    参考文献:http://www.cnblogs.com/alex3714/articles/5765046.html

  • 相关阅读:
    Octave Tutorial(《Machine Learning》)之第二课《数据移动》
    Octave Tutorial(《Machine Learning》)之第一课《数据表示和存储》
    《Machine Learning》系列学习笔记之第三周
    《Machine Learning》系列学习笔记之第二周
    《Machine Learning》系列学习笔记之第一周
    如何下载图片新闻并将其写入文件
    openmv之ApriTag
    opencv学习记录之阈值处理
    opencv学习记录之几何变换
    opencv学习记录之alpha通道
  • 原文地址:https://www.cnblogs.com/yunweiqiang/p/5770602.html
Copyright © 2020-2023  润新知