1、高阶函数(函数即变量)
''' 一个函数add()接收另一个函数abs作为参数,add称为高阶函数 ''' def add(a, b, f): return f(a) + f(b) res = add(-1,-3,abs) print(res)
2、嵌套函数-->闭包
def outer(): x = 1 def inner(): print(x) inner() outer() # 1 ''' inner做为嵌套函数,它可以访问外部函数的变量,调用 outer 函数时,发生了3件事: 1、给 变量 x 赋值为1 2、定义嵌套函数 inner,此时并不会执行 inner 中的代码,因为该函数还没被调用,直到第3步 3、调用 inner 函数,执行 inner 中的代码逻辑。 ''' def outer(x): def inner(): print(x) return inner closure = outer(2) closure() # 2 ''' 同样是嵌套函数,只是稍改动一下,把局部变量 x 作为参数了传递进来, 嵌套函数不再直接在函数里被调用,而是作为返回值返回, 这里的 closure就是一个闭包,本质上它还是函数,闭包是引用了自由变量(x)的函数(inner)。 '''
3、装饰器
装饰器:本质是函数,功能是用来装饰其他函数
为其他函数添加附加功能
原则:1、不修改被装饰函数的源代码
2、不修改被装饰函数的调用方式
即:装饰器对于被装饰函数来说是透明的
普通装饰器
''' 普通的装饰器 ''' def decorator(func): def wrapper(*args, **kwargs): print("前面加的代码") old_result = func(*args, **kwargs) print("后面加的代码") return old_result + "+后面加的返回值" return wrapper @decorator # 等价于 foo = decorator(foo) def foo(name, age): print("{0}'s age is {1}".format(name, age)) return "OK" result = foo("Lee", 23) print(result) # 前面加的代码 # Lee's age is 23 # 后面加的代码 # OK+后面加的返回值
装饰器叠加
''' 装饰器叠加, 类似包装套包装,距离函数最近的为最内层包装, 运行时解包装,从外到内再到外 ''' def decorator1(func): def wrapper(*args, **kwargs): print("decorator1.......begin") func() print("decorator1.......end") return wrapper def decorator2(func): def wrapper(*args, **kwargs): print("decorator2........begin") func() print("decorator2........end") return wrapper @decorator1 @decorator2 def foo(): print("in the foo") foo() # decorator1.......begin # decorator2........begin # in the foo # decorator2........end # decorator1.......end
装饰器工厂
''' 装饰器工厂 输入1返回decorator1 输入2返回decorator2 其他返回decorator3 ''' def closure(flag): def decorator1(func): def wrapper(*args, **kwargs): print("decorator1..begin") func(*args, **kwargs) print("decorator1..end") return wrapper def decorator2(func): def wrapper(*args, **kwargs): print("decorator2..begin") func(*args, **kwargs) print("decorator2..end") return wrapper def decorator3(func): def wrapper(*args, **kwargs): func(*args, **kwargs) return wrapper if flag == 1: return decorator1 elif flag == 2: return decorator2 else: return decorator3 @closure(2) # 等价于 foo = closure(2)(foo) def foo(): print("in the foo") foo() # decorator2..begin # in the foo # decorator2..end
给装饰器传参
''' 1、给index页面和page页面添加认证方式 2、index的认证方式为本地,page的认证方式为ldap 3、此处用获取的不同用户名和密码代替 ''' def auth(auth_type): def decorator(func): def wrapper(*args, **kwargs): if auth_type == "local": #本地验证方式的用户名密码 username = "superadmin" password = "123456" elif auth_type == "ldap": #ldap验证方式的用户名和密码 username = "admin" password = "123" input_username = input("输入用户名:").strip() input_password = input("输入密码:").strip() if username == input_username and input_password == password: print("Success Login!") func(*args, **kwargs) else: print("Invalid username or password!") return wrapper return decorator @auth(auth_type="local") def index(): print("Welcom to index page.") @auth(auth_type="ldap") def home(): print("Welcom to home page.") index() # superadmin 123456 home() # admin 123
4、生成器&迭代器
生成器的特性
1、生成器是一个有yield关键字的函数对象,yield暂停并保存并返回调用结果
2、第一次通过next开始运行这个函数,以后每次next就从yield开始继续运行函数
3、可以用send给生成器的yield关键字传值并激活函数
''' 生成器:循环n次找出第n个元素 可以由上一个数据推算出下一个数据。 一边循环一边计算的机制叫做生成器 ''' # 列表生成式,列表的初始化[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] l = [abs(i) for i in range(10)] # 变成生成器,相当于准备了一个算法, # 不访问不会生成数据,取数据只能通过for循环一个一个取 # 生成器有g.__next__()方法, # 取出当前存储值的下一个值,不能往后退, # 一次只能迈一步,不能迈大步子 g = (i*2 for i in range(10)) ''' 自定义生成器 用函数实现斐波那契数列长度10 1,1,2,3,5,8,13,21,34,55.. ''' def fib(length): n, a, b = 0, 0, 1 while(n < length): print(b) a, b = b, a+b n += 1 # 把fib变为生成器 def fib(length): n, a, b = 0, 0, 1 while(n < length): yield b a, b = b, a+b n += 1 return "生成器结束" fib_generator = fib(10) print(fib_generator.__next__()) print(fib_generator.__next__()) # 有了生成器后可以让函数停在某个地方, # 做点其他事之后回来继续运行 # 让主线程可以来回调用不同的函数, # 在各个工作之间不停的切换 print("做点其他的事情.........") for i in fib_generator: print(i) # 让生成器运行到结束(如果能结束的话) while True: try: print("斐波那契:",next(fib_generator)) except StopIteration as e: print("生成器函数结束后的返回值:",e.value) break # 生成器第一次调用next方法就开始执行生成器函数,执行到yield返回给next方法。 # 第二次执行next就接着上一次的yield后执行,执行到yield又返回,依次类推 # yield的作用是暂停并记住当前状态,并返回 # 生成器中return的作用是异常的时候打印消息
通过生成器并行计算实现单线程下的异步=单线程下的并行效果=协程
协程是比线程更小的单位
因为CPU效率高,可以很快的运行和在不同的任务中来回切换
生产者消费者模式:
''' 生产者消费者模型:一个负责生产producer包子,一个负责吃consumer包子 __next__()只是启动yield,send(value)即启动yield又给yield传值value 消费者不停的吃,生产者不停的喂, 生产者可以有很多,消费者也可以有很多,喂的方式也是灵活的 ''' def consumer(name):#消费者 while True: print("[%s]准备吃包子了" %name) baozi = yield # print(baozi,name) print("包子[%s]来了,被[%s]吃了" %(baozi,name)) def producer(con): for i in range(4): print("开始做包子喂给消费者了----------------") con.send(i) c = consumer("张三") c.__next__() producer(c)
迭代器的概念:
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator
所以生成器一定是迭代器,可迭代对象不一定是迭代器
''' 铺垫: 一类是集合数据类型:如list、tuple、dict、set、str等 一类是生成器generator,包括带yield的生成器方法 这些可以直接作用于for循环的对象称为Iterable可迭代对象 可以用isinstance('abc',Iterable)判断一个对象是否是Iterable对象 迭代器的概念: 可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator 所以生成器一定是迭代器,可迭代对象不一定是迭代器 通过iter("adcde")可以吧"abcde"转换成迭代器 可以用isinstance('abc',Iterator)判断一个对象是否是迭代器 '''
5、内置函数
# 求绝对值
abs(-1)
# 等价于x**y(带两个参数)或 x**y % z(带三个参数)有些类型
# 如整数,能够用一种更高效的算法使用三个参数的形式调用时。
pow()
# round(number[, ndigits]) -> number
# 把小数四舍五入到给定的位数(默认是0位)
round()
# Iterable中所有值x,bool(x)都为True则返回True,
# 可迭代对象为null返回True
all([1, 2, 3]) # True
# Iterable中所有值x,bool(x)任何一个为True则返回True,
# 可迭代对象为null返回False
any([0, 0, 0]) # False
# 返回int对象的二进制表示。
bin(2796202) # 0b1010101010101010101010
# 返回int对象的八进制表示。
oct(342391) # 0o1234567
# 返回int对象的十六进制表示。
hex(12648430) # 0xc0ffee
# 返回一个对象是否能被调用。类可以被调用
# 注意,假设有实例f,那么f()调用__call__()方法,
# 可以判断是否有__call__()方法
callable(f) # f对象如果重写__call__()方法返回True,因为可以f()
# 返回元祖(x//y, x%y). 不变式: div*y + mod == x.
divmod(7,3) # (2,1)
# dir() -> 不给参数,返回当前名字空间列表
# 其他情况,返回一个字母顺序的列表,
# 名字包括(一些)给定对象的属性,和从它可获得的属性
# 如果某对象x支持__dir__(self)方法,该方法将被调用得出dir(x)结果。# 否则默认dir()逻辑被使用并且返回:
# 模块对象:模块的属性
# 类对象:它的属性,和它递归所有基类的属性
# 任何其他对象:它的属性,它的类的属性,以及它递归所有的基类的属性
# dir()在方法中:['self']
''' dir()在模块中:['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__',
'__name__', '__package__', '__spec__'(还有类名方法名,变量名等)]
dir(f)f是对象:['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
'__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', (还有f对象的class自带属性和方法)]
# dir(f)f是模块:['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__',
'__spec__', (还有模块中的类名方法名,变量名等)]'''
dir()
# 对于Unicode编码i范围(0 <= i <= 0x10ffff)的字符
# 返回该Unicode字符串
chr(97) # a
# 对单个字符,返回Unicode码
ord("我") # 25105
# 编译源代码到可以被exec()和eval()执行的对象
# compile(source,filename,mode,flags,dont_inherit)
# source可以代表一个Python模块,语句或表达式
# filename将用于运行时错误消息。
# mode必须是"exec"编译一个模块,"single"编译一个交互式语句
# 或者"eval"编译一个表达式,
# flags参数(如果存在)控制哪一个future语句影响代码编译,
# 如果dont_inherit=True停止这个编译继承任何future语句的影响代码调# 用编译中的结果
# 如果缺失或者为False那些语句影响编译,除了显示指定的特性
# compile("for i in range(10): print(i)", '', 'exec') # at 0x10141e0b0, file "", line 1>
# exec(object, globals, locals)
# 对给的source在全局字典或局部字典环境下执行,source可以是一个字# 符串代表一个或多个Python语句,也可以是compile()
# 函数返回的代码对象
# globals必须是字典locals可以是任意映射,默认值为当前的全局或局部,# 如果只给了全局,那局部默认也是它
exec()
# eval(object, globals, locals)
# 对给的源在全局字典或局部字典环境下求值,源可以是Python表达式或
# 者compile()函数返回的代码对象
# 全局必须是字典局部可以是任意映射,默认值为当前的全局或局部,如果# 只给了全局,那局部默认也是它
eval()
'''返回一个包含当前名字空间全局变量的字典,类似`dir()`在模块中执行:['__annotations__', '__builtins__',
'__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__'(还有类名方法名,变量名等)]
# 注:本词典的更新会影响在当前全局范围的名称查找,反之亦然。'''
globals()
# 返回一个包含当前名字空间局部变量的字典
# 注:是否更新词典都会影响本地范围的名字查找,反之亦然是*实现依赖*
#而不是包含任何向后兼容性保证。
locals()
# 返回给定对象的hash值,两个比较相等的对象必须具有相同的散列值,
# 但反过来不一定成立。MD5是hash的一种算法
hash()
# help() -> 启动交互式会话
# help(thing) -> 查看thing的帮助
help()
# 返回对象的标识,保证在同时存在的对象之间是唯一的。
#(CPython用内存地址做标识)
id()
# 从标准输入读取字符串,忽略换行。如果给定的提示符字符串被打印到标# 准输出中,则在不使用换行之前阅读输入
# 如果用户点击EOF (*nix: Ctrl-D, Windows: Ctrl-Z+Return)抛出
# EOFError在*nix系统,readline可被使用
input()
# 返回一个对象是一个类的实例还是它的一个子类
# 一个元组,在``isinstance(x,(A,B,…)``可作为目标对照。
# 这相当于``isinstance(X,A)or isinstance(x,b)or ...``
isinstance()
# 返回“cls”是否来自另一个类或是同一个类。
# 一个元组,在``issubclass(x,(A,B,…)``可作为目标对照。
# 这相当于``issubclass(X,A)or issubclass(x,b)or ...``
issubclass()
# iter(iterable) -> iterator
# iter(callable, sentinel) -> iterator
# 从对象获取迭代器。在第一种形式中,参数必须提供自己的迭代器,
# 或者是一个序列。
# 在第二种形式中,调用可调用的方法,直到它返回标记为止
iter()
# max(iterable, *[, default=obj, key=func]) -> value
# max(arg1, arg2, *args, *[, key=func]) -> value
# 单一可迭代对象参数,返回它最大的项,有两个以上参数,
# 返回最大的参数,default参数用来返回迭代对象为空时的返回值
max()
# 和max一样
min()
# next(iterator[, default])
# 从迭代器返回下一项,如果给定default参数,在迭代器耗尽的时候会
# 返回default参数而不抛出StopIteration异常
next()
# 略
open()
# print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
# 打印输出到流,或者默认的sys.stdout
# 关键字选项参数:
# file: 一个类文件对象(stream); 默认是当前的sys.stdout.
# sep: 几个值中间的字符串默认为空格
# end: 追加在最后一个值后面的字符串默认为换行
# flush: 是否强制刷到流中
print()
# 返回一个从iterable创建的包含所有项的升序排列的list,
# 可设置flag进行降序排列,用key去定制排序规则
sorted()
# 返回start参数(默认为0)和一系列可迭代数字之和。
# 当迭代对象为空,返回start的值
# 这个方法是专门为使用数字值和可以阻止非数字型值
sum()
# vars([object]) -> dictionary
# 没有参数, 相当于locals() 有一个参数, 相当于 object.__dict__.vars()
# __import__(name, globals=None, locals=None,
# fromlist=(),level=0) -> module
# 导入一个模块。因为这个方法意味着被Python解释器使用而不是一般的
# 使用最好使用importlib
# import_module()以编程方式导入模块。
# globals参数仅仅用于确定上下文;
# 它们没有被修改,locals参数没有用,from list应该是一个list of
# names类似"from name import..."或者一个
# 空list类似"import name"
# 当从包导入一个模块,注意__import__('A.B',...)
# 返回包A当fromlist为空,返回它的子模块B当fromlist不为空
# level用来决定是否执行绝对或者相对的imports. 0是绝对,正数是相对
# 于当前模块搜索的父目录的数目
__import__()
#-------------类---------------- # bool(x)当x为True返回True否则返回False。内置的True或者False仅仅# 是bool类的两个实例,bool是int类的子类并且不能被继承 bool(0) # False # 构建可变字节数组对象从: # 1-range(256)范围内的可迭代对象 # 2-使用指定编码的文本字符串 # 3-一个bytes或者一个buffer对象 # 4-任意实现了bufferAPI的对象 # 5-一个integer对象 # bytearray(iterable_of_ints) -> bytearray # bytearray(string, encoding[, errors]) -> bytearray # bytearray(bytes_or_buffer) -> mutable copy of bytes_or_buffer # bytearray(int) -> bytes array of size given by the parameter # initialized with null bytes # bytearray() -> empty bytes array # bytearray("你好", encoding="utf8") bytearray(b'\xe4\xbd\xa0\xe5\xa5\xbd') # 构建不可改变的字节数组对象从: # 1-range(256)范围内的可迭代对象 # 2-使用指定编码的文本字符串 # 3-任意实现了bufferAPI的对象 # 4-一个integer对象 # bytes(iterable_of_ints) -> bytes # bytes(string, encoding[, errors]) -> bytes # bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer # bytes(int) -> bytes object of size given by the parameter # initialized with null bytes # bytes() -> empty bytes object bytes("你好", encoding="utf8") # b'\xe4\xbd\xa0\xe5\xa5\xbd' # 创建一个复数从一个实部和一个可选的虚部,相当于(real + imag*1j) # 其中image默认为0 complex(5, 2) # 5+2j # dict() -> {} # dict(mapping映射对象) -> 从一个映射对象的(key,value)初始化字典, # 一个mapping对象将可哈希的值映射为任意对象 # python中标准映射类型是字典 # dict(iterable) -> d = {}; for k, v in iterable: d[k] = v # dict(**kwargs) -> 初始化字典对象使用name=value键值对例 # 如:dict(one=1, two=2) dict() # 返回一个enumerate对象(有索引的序列),对象必须支持可迭代 # enumerate对象的一对yield包括一个下标(默认从0开始)和一个值(被自# 己的迭代参数中断) # 获取一个有下标的列表时enumerate是有用的 # (0, seq[0]), (1, seq[1]), (2, seq[2]), ... enumerate("abcdef") # (0, 'a'), (1, 'b'), (2, 'c'), ... # filter(function or None, iterable) --> filter object # 过滤器,返回一个迭代器保留那些func(item)之后为True的项, # 如果function为none,返回为True的项 # 例如:filter(lambda n:n>5,range(10)) # 派生:map(lambda n:n*2,range(10)) # 派生:[abs(i) : for i in range(10)] # 派生:functools.reduce(lambda x,y:x*y,range(10)) filter() # map(func, *iterables) --> map object # 制作一个迭代器,从每个可迭代对象获取参数用来计算函数。 # 当短迭代耗尽的时候停止 # 把一个或多个迭代器当成参数喂给函数,返回一个函数运行后的迭代器。 map() # 字符串或数字转换为浮点数 float() # frozenset() -> empty frozenset object # frozenset(iterable) -> frozenset object # 创建一个不可变的无序的唯一元素集合,对可迭代对象去重,并且不可变 frozenset() # 将数字或字符串转换为整数,如果没有参数,则返回0。如果x是一个数,# 返回x.__int__()。对于浮点数,向零截断。 # 如果x不是一个数或如果给定base,那么x必须是一个字符串或者字节, # 或ByteArray实例表示字面整数的给出的base。 # 字面上之前可以是“+”或“-”,然后被包围。base0意味着将字符串从基部# 解释为整数文字。>>> int('0b100', base=0) int() # list() -> [] 空list # list(iterable) -> 从可迭代对象初始化的list list() # 创建一个新的参考给定对象的memoryview内存查看对象 memoryview() # 最古老的基类,object是一个类 object() # range(stop) -> range object # range(start, stop[, step]) -> range object # 返回一个Integer序列(顾首不顾尾)range(start,stop,step) start默 # 认为0,step默认为1(步长可增可减) range() # 返回一个倒序的迭代器 reversed() # set() -> new empty set object # set(iterable) -> new set object # 构建一个无序的唯一元素集合 set() # 创建切片对象。这是用于扩展的切片(例如 a[0:10:2]) # slice(stop) # slice(start, stop[, step]) slice() # tuple() -> 空元祖 # tuple(iterable) -> 从可迭代对象的项去初始化元祖 # 如果参数是元祖,返回值是同样的对象 tuple() # type(object_or_name, bases, dict) # type(object) -> 对象的类型 # type(name, bases, dict) -> 新类型 type() # zip(iter1 [,iter2 [...]]) --> zip object # 返回一个拉链对象,它的__next__()方法返回一个元祖第i个元素来自第i # 个迭代对象参数。 # __next__()方法继续直到最短的可迭代对象在参数序列中被耗尽,抛出# StopIteration异常 zip()
#-----------面向对象----------- # classmethod(function) -> method把一个方法转换成类方法 # 类方法将类本身接收为隐式第一个参数,就像实例方法接收self一样。要声明类方法,请使用这个成语: # class C: # @classmethod # def f(cls, arg1, arg2, ...): # ... # 类方法可以被类调用(C.f())或被实例调用(C().f())除它的类之外实例被忽略。派生类调用类方法的时候会隐式的把派 # 生类作为第一个参数传递 classmethod() # 类 # property(fget=None, fset=None, fdel=None, doc=None) -> property attribute # fget是一个方法用作得到一个属性值,同样的,fset是一个方法用来设置, # fdel用来删除,一个属性。 典型的用法是定义一个使用过的属性: ''' class C(object): def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.") Decorators make defining new properties or modifying existing ones easy: class C(object): @property def x(self): "I am the 'x' property." return self._x @x.setter def x(self, value): self._x = value @x.deleter def x(self): del self._x ''' property() # 类 # staticmethod(function) -> method # 转换一个方法到静态方法 # 静态方法不会隐式接收第一个参数,用下面的语法声明一个静态方法: # class C: # @staticmethod # def f(arg1, arg2, ...): # 静态方法可以被类或者实例调用,除了类之外,该实例将被忽略 staticmethod() # 类 # super() -> 和super(__class__,)一样 # super(type) -> 不绑定父类object # super(type, obj) -> 绑定父类object; 要求 isinstance(obj, type) # super(type, type2) -> 绑定父类object; 要求 issubclass(type2, type) # 典型调用父类的方法: """ class C(B): def meth(self, arg): super().meth(arg) This works for class methods too: class C(B): @classmethod def cmeth(cls, arg): super().cmeth(arg) """ super() # 类 # 从给定对象中删除命名的属性delattr(x, 'y')相当于 ``del x.y'' delattr() # 从对象获取一个已命名的属性getattr(x, 'y')相当于x.y # 当给定一个默认参数,如果属性不存在会返回,没有它将会在那种情况抛出一个异常 getattr() # 返回是否对象有给定名字的属性,这是通过调用getattr(obj, name)和捕获AttributeError异常 hasattr() # 将给定对象上的命名属性设置为指定的值。 # setattr(x, 'y', v)相当于 ``x.y = v`` setattr()
#-----------有对应关系------------------------ # 返回对象的标准字符串表示形式。 # 对于许多类型的对象,包括大多数内置对象,eval(repr(obj))== obj。 # 重写__repr__()方法可以改变结果 repr("你好") # '你好' # 返回对象的ASCII表示,对象变为可打印的字符串,非ASCII字符用\\x, \\u或 \\U转义。repr()返回对象的可打印表示的字 符串。Python2中,和repr()返回的字符串相似 # 重写__repr__()方法可以改变结果 ascii("你好") # '\u4f60\u597d' # str(object='') -> str # str(bytes_or_buffer[, encoding[, errors]]) -> str # 从给定对象创建一个新字符串对象,如果指定了encoding或errors参数 # 那么对象必须公开数据缓冲区(将被使用给定的编码和error handler解码) # 否则,返回obj.__str__()的结果(如果有__str__()方法)或者repr(obj)(重写了__repr__()方法没有重写__str__()方法) # 编码格式默认为系统编码,错误默认为"strict" str("你好") # 你好, 注意,str是一个class # 返回容器中的项目数目,重写__len__()方法将被调用 len() # 返回value.__format__(format_spec),类重写__format__方法可以修改该方法返回值 # format_spec默认值为"",help('FORMATTING')查看细节 format()
6、模块导入的本质
导包原则:
一、导完能直接使用import后的方法或变量
(import func 使用时:func(xxx)而不要func.func1(xxx))
二、测试单独写,运行是单独入口互不干扰
''' 一、模块:本质是.py结尾的Python文件,模块是一个对象 二、包:是一个带__init__.py的目录,包里放模块,可以看出包也是一个对象, __init__.py是包初始化(即导入)的时候执行的 三、导入模块的方法: 1、import module_name(通过module_name.func调用) 2、from module_name import * (直接func调用,这样可能引起重名) 3、from module_name import logger as logger_alex(取个别名避免重名) 四、import的本质: 例如[import Alex]导入模块本质就是在sys.path里面找到Alex.py的路径并把模块Alex.py解释一遍, 可以把import后的模块看做一个对象,要通过这个对象去调用里面的方法 导入包只解释执行包下的__init__.py文件 五、导入优化:第二种方法好一点,不用再重复找模块 ########第一种方法########### import module module.func(xx) module.func(yy) ############################# ##########第二种方法######### from module import func func(xx) func(yy) ############################# ''' import sys,os search_path = os.path.dirname(os.path.abspath(__file__)) sys.path.append(search_path)
7、软件目录结构规范
软件目录结构规范
为什么要设计好目录结构?
"设计项目目录结构",就和"代码编码风格"一样,属于个人风格问题。对于这种风格上的规范,一直都存在两种态度:
- 一类同学认为,这种个人风格问题"无关紧要"。理由是能让程序work就好,风格问题根本不是问题。
- 另一类同学认为,规范化能更好的控制程序结构,让程序具有更高的可读性。
我是比较偏向于后者的,因为我是前一类同学思想行为下的直接受害者。我曾经维护过一个非常不好读的项目,其实现的逻辑并不复杂,但是却耗费了我非常长的时间去理解它想表达的意思。从此我个人对于提高项目可读性、可维护性的要求就很高了。"项目目录结构"其实也是属于"可读性和可维护性"的范畴,我们设计一个层次清晰的目录结构,就是为了达到以下两点:
- 可读性高: 不熟悉这个项目的代码的人,一眼就能看懂目录结构,知道程序启动脚本是哪个,测试目录在哪儿,配置文件在哪儿等等。从而非常快速的了解这个项目。
- 可维护性高: 定义好组织规则后,维护者就能很明确地知道,新增的哪个文件和代码应该放在什么目录之下。这个好处是,随着时间的推移,代码/配置的规模增加,项目结构不会混乱,仍然能够组织良好。
所以,我认为,保持一个层次清晰的目录结构是有必要的。更何况组织一个良好的工程目录,其实是一件很简单的事儿。
目录组织方式
关于如何组织一个较好的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
简要解释一下:
bin/
: 存放项目的一些可执行文件,当然你可以起名script/
之类的也行。foo/
: 存放项目的所有源代码。(1) 源代码中的所有模块、包都应该放在此目录。不要置于顶层目录。(2) 其子目录tests/
存放单元测试代码; (3) 程序的入口最好命名为main.py
。docs/
: 存放一些文档。setup.py
: 安装、部署、打包的脚本。requirements.txt
: 存放软件依赖的外部Python包列表。README
: 项目说明文件。
除此之外,有一些方案给出了更加多的内容。比如LICENSE.txt
,ChangeLog.txt
文件等,我没有列在这里,因为这些东西主要是项目开源的时候需要用到。如果你想写一个开源软件,目录该如何组织,可以参考这篇文章。
下面,再简单讲一下我对这些目录的理解和个人要求吧。
关于README的内容
这个我觉得是每个项目都应该有的一个文件,目的是能简要描述该项目的信息,让读者快速了解这个项目。
它需要说明以下几个事项:
- 软件定位,软件的基本功能。
- 运行代码的方法: 安装环境、启动命令等。
- 简要的使用说明。
- 代码目录结构说明,更详细点可以说明软件的基本原理。
- 常见问题说明。
我觉得有以上几点是比较好的一个README
。在软件开发初期,由于开发过程中以上内容可能不明确或者发生变化,并不是一定要在一开始就将所有信息都补全。但是在项目完结的时候,是需要撰写这样的一个文档的。
可以参考Redis源码中Readme的写法,这里面简洁但是清晰的描述了Redis功能和源码结构。
关于requirements.txt和setup.py
setup.py
一般来说,用setup.py
来管理代码的打包、安装、部署问题。业界标准的写法是用Python流行的打包工具setuptools来管理这些事情。这种方式普遍应用于开源项目中。不过这里的核心思想不是用标准化的工具来解决这些问题,而是说,一个项目一定要有一个安装部署工具,能快速便捷的在一台新机器上将环境装好、代码部署好和将程序运行起来。
这个我是踩过坑的。
我刚开始接触Python写项目的时候,安装环境、部署代码、运行程序这个过程全是手动完成,遇到过以下问题:
- 安装环境时经常忘了最近又添加了一个新的Python包,结果一到线上运行,程序就出错了。
- Python包的版本依赖问题,有时候我们程序中使用的是一个版本的Python包,但是官方的已经是最新的包了,通过手动安装就可能装错了。
- 如果依赖的包很多的话,一个一个安装这些依赖是很费时的事情。
- 新同学开始写项目的时候,将程序跑起来非常麻烦,因为可能经常忘了要怎么安装各种依赖。
setup.py
可以将这些事情自动化起来,提高效率、减少出错的概率。"复杂的东西自动化,能自动化的东西一定要自动化。"是一个非常好的习惯。
setuptools的文档比较庞大,刚接触的话,可能不太好找到切入点。学习技术的方式就是看他人是怎么用的,可以参考一下Python的一个Web框架,flask是如何写的: setup.py
当然,简单点自己写个安装脚本(deploy.sh
)替代setup.py
也未尝不可。
requirements.txt
这个文件存在的目的是:
- 方便开发者维护软件的包依赖。将开发过程中新增的包添加进这个列表中,避免在
setup.py
安装依赖时漏掉软件包。 - 方便读者明确项目使用了哪些Python包。
这个文件的格式是每一行包含一个包依赖的说明,通常是flask>=0.10
这种格式,要求是这个格式能被pip
识别,这样就可以简单的通过 pip install -r requirements.txt
来把所有Python包依赖都装好了。具体格式说明: 点这里。
关于配置文件的使用方法
注意,在上面的目录结构中,没有将conf.py
放在源码目录下,而是放在docs/
目录下。
很多项目对配置文件的使用做法是:
- 配置文件写在一个或多个python文件中,比如此处的conf.py。
- 项目中哪个模块用到这个配置文件就直接通过
import conf
这种形式来在代码中使用配置。
这种做法我不太赞同:
- 这让单元测试变得困难(因为模块内部依赖了外部配置)
- 另一方面配置文件作为用户控制程序的接口,应当可以由用户自由指定该文件的路径。
- 程序组件可复用性太差,因为这种贯穿所有模块的代码硬编码方式,使得大部分模块都依赖
conf.py
这个文件。
所以,我认为配置的使用,更好的方式是,
- 模块的配置都是可以灵活配置的,不受外部配置文件的影响。
- 程序的配置也是可以灵活控制的。
能够佐证这个思想的是,用过nginx和mysql的同学都知道,nginx、mysql这些程序都可以自由的指定用户配置。
所以,不应当在代码中直接import conf
来使用配置文件。上面目录结构中的conf.py
,是给出的一个配置样例,不是在写死在程序中直接引用的配置文件。可以通过给main.py
启动参数指定配置路径的方式来让程序读取配置内容。当然,这里的conf.py
你可以换个类似的名字,比如settings.py
。或者你也可以使用其他格式的内容来编写配置文件,比如settings.yaml
之类的。