前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python工程结构

python工程结构

作者头像
神秘的寇先森
发布2020-02-19 11:02:57
8560
发布2020-02-19 11:02:57
举报

在一个健康的开发周期中,代码风格,API设计和自动化是非常关键的。同样的,对于工程的架构 ,仓库的结构也是关键的一部分。 当一个潜在的用户和贡献者登录到您的仓库页面时,他们会看到这些:

  • 工程的名字
  • 工程的描述
  • 一系列的文件

如果您的仓库的目录是一团糟,没有清晰的结构,他们可能要到处寻找才能找到您写的漂亮的文档。

仓库样例
README.rst
LICENSE
setup.py
requirements.txt
sample/__init__.py
sample/core.py
sample/helpers.py
docs/conf.py
docs/index.rst
tests/test_basic.py
tests/test_advanced.py

以下是一些细节介绍:

  • 核心代码 布局:./sample/ or ./sample.py 您的模块包是这个仓库的核心,它不应该隐藏起来: ./sample/ 如果您的模块只有一个文件,那么您可以直接将这个文件放在仓库的根目录下: ./sample.py 这个模块文件不应该属于任何一个模棱两可的src或者python子目录。
  • License 作用:许可证,在这个文件中要有完整的许可说明和授权。
  • Setup.py 作用:打包和发布管理
  • requirements.txt 作用:开发依赖 说明: requirements.txt应该放在仓库的根目录。它应该指明完整工程的所有依赖包: 测试, 编译和文档生成。 如果您的工程没有任何开发依赖,或者您喜欢通过 setup.py 来设置,那么这个文件不是必须的。
  • Documentsation 作用:包的参考文档
  • Test Suite 作用:包的集合和单元测试 最开始,一组测试例子只是放在一个文件当中: ./test_sample.py 当测试例子逐步增加时,您会把它放到一个目录里面,像下面这样:
tests/test_basic.py
tests/test_advanced.py

当然,这些测试例子需要导入我们的包来进行测试,有几种方式来处理: 1.将我们的包安装到site-packages中。 2.通过简单直接的路径设置来解决导入的问题。 推荐后者。如果使用 setup.py develop 来测试一个持续更新的代码库,需要为每一个版本的代码库设置一个独立的测试环境.太麻烦了。 可以先创建一个包含上下文环境的文件 tests/context.py。 file:

import os
import sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

import sample

然后,在每一个测试文件中,导入: from .context import sample 这样就能够像期待的那样工作,而不用采用安装的方式。

  • Makefile 作用:常规的管理任务 ** 样例 Makefile:**
init:
    pip install -r requirements.txt
test:
    py.test tests
PHONY: init test

一些其他的常规管理脚本(比如 manage.py 或者 fabfile.py),也放在仓库的根目录下.

结构是一把钥匙

得益于Python提供的导入与管理模块的方式,结构化Python项目变得相对简单。 这里说的简单,指的是结构化过程没有太多约束限制而且模块导入功能容易掌握。 因而您只剩下架构性的工作,包括设计、实现项目各个模块,并整理清他们之间 的交互关系。

模块

Python模块是最主要的抽象层之一,并且很可能是最自然的一个。抽象层允许将代码分为 不同部分,每个部分包含相关的数据与功能。请尽量保持模块名称简单,以无需分开单词。 最重要的是,不要使用下划线命名空间,而是使用子模块。

# ok
import library.plugin.foo
# not OK
import library.foo_plugin
  • 理解import的原理机制 具体来说,import modu 语句将 寻找合适的文件,即调用目录下的 modu.py 文件(如果该文件存在)。如果没有 找到这份文件,Python解释器递归地在 "PYTHONPATH" 环境变量中查找该文件,如果仍没 有找到,将抛出ImportError异常。 一旦找到 modu.py,Python解释器将在隔离的作用域内执行这个模块。所有顶层 语句都会被执行,包括其他的引用。方法与类的定义将会存储到模块的字典中。然后,这个 模块的变量、方法和类通过命名空间暴露给调用方,这是Python中特别有用和强大的核心概念。 在很多其他语言中,include file 指令被预处理器用来获取文件里的所有代码并‘复制’ 到调用方的代码中。Python则不一样:include代码被独立放在模块命名空间里,这意味着您 一般不需要担心include的代码可能造成不好的影响,例如重载同名方法。 也可以使用import语句的特殊形式 from modu import *模拟更标准的行为。但 import * 通常 被认为是不好的做法。使用 from modu import * 的代码较难阅读而且依赖独立性不足。 使用 from modu import func能精确定位您想导入的方法并将其放到全局命名空间中。 比 from modu import * 要好些,因为它明确地指明往全局命名空间中导入了什么方法,它和 import modu 相比唯一的优点是之后使用方法时可以少打点儿字。

from modu import *
x = sqrt(4)  # sqrt是模块modu的一部分么?或是内建函数么?上文定义了么?

稍好

from modu import sqrt
x = sqrt(4)  # 如果在import语句与这条语句之间,sqrt没有被重复定义,它也许是模块modu的一部分。

最好

import modu
[...]
x = modu.sqrt(4)  # sqrt显然是属于模块modu的。

除了简单的单文件项目外,其他项目需要能够明确指出类和方法 的出处,例如使用 modu.func 语句,这将显著提升代码的可读性和易理解性。

Python提供非常简单的包管理系统,即简单地将模块管理机制扩展到一个目录上(目录扩展为包)。 任意包含 init.py 文件的目录都被认为是一个Python包。导入一个包里不同模块的方式和普通的导入模块方式相似,特别的地方是 init.py 文件将集合所有包范围内的定义。

pack/目录下的modu.py文件通过 import pack.modu语句导入。 该语句会在 pack 目录下寻找 init.py 文件,并执行其中所有顶层语句。以上操作之后,modu.py 内定义的所有变量、方法和类在pack.modu命名空间中均可看到。

一个常见的问题是往 init.py 中加了过多代码,随着项目的复杂度增长, 目录结构越来越深,子包和更深嵌套的子包可能会出现。在这种情况下,导入多层嵌套 的子包中的某个部件需要执行所有通过路径里碰到的 init.py文件。如果包内的模块和子包没有代码共享的需求,使用空白的 init.py 文件是正常甚至好的做法。

最后,导入深层嵌套的包可用这个方便的语法:import very.deep.module as mod。 该语法允许使用 mod 替代冗长的 very.deep.module。

面向对象编程

在Python中一切都是对象,并且能按对象的方式处理。这么说的意思是,例如函数是一等对象。 函数、类、字符串乃至类型都是Python对象:与其他对象一样,他们有类型,能作为函数参数传递,并且还可能有自己的方法和属性。这样理解的话,Python是一种面向对象语言。 然而,与Java不同的是,Python并没有将面向对象编程作为最主要的编程范式。非面向对象的Python项目(比如,使用较少甚至不使用类定义,类继承,或其它面向对象编程的机制)也是完全可行的。 在一些情况下,需要避免不必要的面向对象。当我们想要将状态与功能结合起来,使用标准类定义是有效的。但正如函数式编程所讨论的那个问题,函数式的“变量”状态与类的状态并不相同。

动态类型

Python是动态类型语言,这意味着变量并没有固定的类型。实际上,Python中的变量和其他语言有很大的不同,特别是静态类型语言。变量并不是计算机内存中被写入的某个值,它们只是指向内存的 ‘标签’ 或 ‘名称’ 。因此可能存在这样的情况,变量 'a' 先代表值1,然后变成字符串'a string' , 然后又变为指向一个函数。

Python 的动态类型常被认为是它的缺点,的确这个特性会导致复杂度提升和难以调试的代码。 命名为 'a' 的变量可能是各种类型,开发人员或维护人员需要在代码中追踪命名,以保证它 没有被设置到毫不相关的对象上。

避免发生类似问题的参考方法:

  • 避免对不同类型的对象使用同一个变量名 差
a = 1
a = 'a string'
def a():
    pass  # 实现代码

count = 1
msg = 'a string'
def func():
    pass  # 实现代码

使用简短的函数或方法能降低对不相关对象使用同一个名称的风险。即使是相关的不同 类型的对象,也更建议使用不同命名

重复使用命名对效率并没有提升:赋值时无论如何都要创建新的对象。然而随着复杂度的 提升,赋值语句被其他代码包括 'if' 分支和循环分开,使得更难查明指定变量的类型。 在某些代码的做法中,例如函数编程,推荐的是从不重复对同一个变量命名赋值。Java 内的实现方式是使用 'final' 关键字。Python并没有 'final' 关键字。尽管如此,避免给同一个变量命名重复赋值仍是是个好的做法,并且有助于掌握 可变与不可变类型的概念。

可变和不可变类型

Python提供两种内置或用户定义的类型。可变类型允许内容的内部修改。典型的动态类型 包括列表与字典:列表都有可变方法,如 list.append() 和 list.pop(), 并且能就地修改。字典也是一样。不可变类型没有修改自身内容的方法。比如,赋值为整数 6的变量 x 并没有 "自增" 方法,如果需要计算 x + 1,必须创建另一个整数变量并给其命名。

my_list = [1, 2, 3]
my_list[0] = 4
print my_list  # [4, 2, 3] <- 原列表改变了

x = 6
x = x + 1  # x 变量是一个新的变量

这种差异导致的一个后果就是,可变类型是不 '稳定 '的,因而不能作为字典的键使用。合理地 使用可变类型与不可变类型有助于阐明代码的意图。例如与列表相似的不可变类型是元组, 创建方式为 (1, 2)。元组是不可修改的,并能作为字典的键使用。

Python 中一个可能会让初学者惊讶的特性是:字符串是不可变类型。这意味着当需要组合一个字符串时,将每一部分放到一个可变列表里,使用字符串时再组合 ('join') 起来的做法更高效。 而且,使用列表推导的构造方式比在循环中调用append()来构造列表更好也更快。 差

# 创建将0到19连接起来的字符串 (例 "012..1819")
nums = ""
for n in range(20):
    nums += str(n)   # 慢且低效
print nums

# 创建将0到19连接起来的字符串 (例 "012..1819")
nums = []
for n in range(20):
    nums.append(str(n))
print "".join(nums)  # 更高效

更好

# 创建将0到19连接起来的字符串 (例 "012..1819")
nums = [str(n) for n in range(20)]
print "".join(nums)

最好Best

# 创建将0到19连接起来的字符串 (例 "012..1819")
nums = map(str, range(20))
print "".join(nums)

除了 str.join() 和 +,也可以使用 % 格式运算符来连接确定数量的字符串,不过PEP 3101 建议使用 str.format() 替代 % 操作符。

foo = 'foo'
bar = 'bar'

foobar = '%s%s' % (foo, bar) # 可行
foobar = '{0}{1}'.format(foo, bar) # 更好
foobar = '{foo}{bar}'.format(foo=foo, bar=bar) # 最好
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 仓库样例
  • 结构是一把钥匙
  • 模块
  • 面向对象编程
  • 动态类型
  • 可变和不可变类型
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档