python的模块与包进阶篇,自定义导入语句的秘籍
将代码组织成很多分层模块构成的包
folder/
__init__.py
sub_folder/
__init__.py
a.py
b.py
封装成包只用确保每个目录都定义了一个__init__.py
文件,该文件的主要目的是要包含不同运行级别的包的可选初始化代码,绝大多数时候都让该文件空着就好。
但是我们还是会发现即使没有__init__.py
文件,依然可以导入包,实际上是创建了一个“命名空间包”
控制模块被全部导入的内容
当希望模块内有些内部函数不要被导出,并且需要使用
from module import *
的时候,可以使用__all__
变量在模块中列出需要导出的内容
def A():
pass
def B():
pass
__all__ = ['A'] # 该变量需要定义一个list,如果是空列表,那么将没有东西导出
使用相对路径名导入模块
example:
package/
__init__.py
A/
__init__.py
xxx.py
zzz.py
B/
__init__.py
yyy.py
如果需要导入同目录的模块,比如xxx需要导入zzz中的模块:
from . import zzz
如果需要导入不同目录的模块,比如xxx需要导入yyy中的模块:
from ..B import yyy
导入模块可以使用绝对路径也可以使用相对路径,但是绝对路径的不好的地方是这将顶层包名硬编码到源码中,如果我们重新组织代码,代码将很难继续工作。
这里有一个很常见的问题:相对导入只适用于在合适的包中的模块。在顶层的脚本的简单模块内,他们将不起作用。所以如果包的部分被作为脚本直接执行,那么会报no module name ...
的错误
在工作目录里面执行:
python modules/database/function.py
的时候就会报错,因为function.py里面使用的是相对路径来导入模块,我们可以使用-m
的方式来解决:
python -m modules.database.function
就可以成功运行
-m
的作用就是将python中的模块当做脚本来执行
直接运行目录而不是单独文件
如果应用已经不是一个简单的脚本而是一个包含多个文件的应用,可以将应用程序放进它自己的目录并且添加一个__main__.py
文件,那么就可以直接在顶级目录里面运行Python解释器,比如顶级目录的名字为folder:
运行python folder
不使用open()函数来读取位于包中的数据
如果要读取数据文件,可能会直接使用内置的I/O功能代码,比方说open(),后面再说这种方法的缺点,这里推荐使用的是:
import pkgutil
data = pkgutil.get_data(__package__, 'metadata.txt')
# 这个生成的变量是包含该文件原始内容的字节字符串
使用内置open()方法的缺点:
- 包对解释器的当前目录几乎没有控制权,所以每次都需要使用绝对文件名,虽然可以弄清文件路径,但是很杂乱
- 如果用open()对一个包含数据文件的归档文件进行操作,根本不会工作
通过字符串导入模块
如果想导入一个模块,但是模块名需要手动输入,然后模块的名字在字符串内
import importlib
math = importlib.import_module('math')
math.sin(x)
string类型倒序:[::-1]
算法问题:根据对象特定的属性进行排序(比如对字典排序)
基本思想:配合使用sorted()
里面的key关键字,以及operator.itemgetter()
方法
以列表的某个值为key进行排序:
alist = [[1,2], [2,3], [3,4], [2,6], [4,7]]
sorted_list = sorted(alist, key=operator.itemgetter(1))
这样的话就是对大列表中的每个小列表的第二个值进行排序
以字典的value进行排序:
adict = {'1': 10, '2': 22....}
sorted_list = sorted(adict.items(), key=operator.itemgetter(1))
这里我们对字典使用了items()方法,这样就将字典转换成一个类列表的对象,然后遍历列表内的每一对键值对,以位置1的值为关键字进行排序。如果不使用items()方法进行转换,那么直接遍历字典得到是字典的key,一个key是没有itemgetter属性的,所以会报错。
算法问题:从数组中找出数字之和等于特定值
Partition Equal Subset Sum
这个问题是是否能把一个列表划分为两个所有数之和相等的小列表?
基本思路: 每个小列表的数字之和等于大列表数字之和的二分之一,设为target_value
方法1:
比较简单的思路就是建立一个集合,将列表内所有可能的总和值都放到这个集合内,如果target_value在这个集合中,那就表明列表内部有一堆数字总和是整体列表总和的一半,那么剩余的那堆数字的总和就是列表总和的另一半。所以就可以划分,返回True。
class Solution(object):
def canPartition(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
target = sum(nums)/2
if sum(nums) % 2 != 0:
return False
all_sum = {0}
for i in nums:
all_sum.update({(x + i) for x in all_sum})
return target in all_sum
上面代码的{0}不是一个字典,python的集合是一个无序不重复元素序列,可以使用大括号{}
或者set()
函数创建集合。但是如果要创建一个空集合一定要使用set()而不是{},因为空的{}是用来创建一个空字典的。集合添加元素的方法用set.update()
Python数据结构与算法 – 进阶篇
同时将序列中所有值赋值给多个变量:
假设现在有一个包含n个元素的元组或者是序列, 我们需要将里面的n个元素赋值给n个变量:
alist = [1, 2, 3]
a, b, c = alist
这样a,b,c就同时赋值了,唯一要求是变量数量需要和序列中元素数量一致。
那么如果变量的数量不一致,或者不知道序列中的元素数量怎么办?:
可以使用python的星号表达式
alist = [1,2,3,4,5]
a, b, *c = alist
c
>>> [3,4,5]
上面的加了星号的c,不管解压的数量是多少,就算是0,返回的也一定是列表!!!
从集合中抽取最大或者最小的n个元素:
可以使用heapq模块的nlargest()和nsmallest()函数获取:
import heapq
alist = [1,2,3,4,5,6]
print(heapq.nlargest(3, alist))
>>> [4,5,6]
这两个函数还可以接受一个关键字参数用于更复杂的数据结构。
adic = [{...}, {...},...]
result = heapq.nsmallest(3, adic, key=lambda s: s['price']
上面会对列表中的每一个字典进行比较,但是如何比较字典呢?我们上面指定了关键字为price,所以会对每一个字典的price进行比较。
将list进行排序并且针对特定算法进行排序
我们使用sorted()方法进行排序,该方法有三个属性:
sotred(iterable, reverse = True, key = function)
该方法第一个iterable是待排序的可迭代对象,reverse属性指明是否需要反向排序,最后一个key属性指定特定算法进行排序,下面例子针对特定算法排序:
def f(x):
return x-10
alist = [1,2,3,4,5,6]
sortedList = sorted(alist, key=f)
或者
sortedList = sorted(alist, key=lambda x: x-10)
找出序列中出现次数最多的元素
我们使用collections.Counter
类,这个类专门用于解决这个问题,它还有一个most_common()
方法直接能得到我们想要的答案,所以遇到这类问题,优先使用这个类,而不是手动建立字典。
word = ['a', 'a', 'b']
word_counts = Counter(word)
# 生成一个counter对象
top_three = word_counts.most_common(3)
# 找到频率最高的三个,返回成[('a',2), ('b', 1)]
对于一个字典列表,通过某字段排序或分组
通过使用operator
的itemgetter
函数可以获取字段
通过字段排序:
rows = [{}, {}, ...]
rows_by_id = sorted(rows, key=itemgetter('id'))
通过字段分组:
首先需要对字段排序,然后需要用到itertools.groupby()
函数对数据分组
rows.sort(key=itemgetter('date')
groupby(rows, key=itemgetter('date')
这里需要注意的是groupby只检查连续的元素,所以如果没有先对字段进行排序,那么就返回不了我们想要的结果。
过滤列表元素和字典
过滤列表元素除了可以使用filter以外,还可以使用最基本的列表推导方法:
new_list = [n for n in mylist if n>0]
这样当输入很大的时候,就会输出一个非常大的结果集
从字典中提取符合条件的子集:使用字典推导:
new_dict = {key:value for key, value in old_dict.items() if value>100}
映射名称到序列元素上(namedtuple):
我们需要使用collection.namedtuple()
函数来解决,这个函数需要传递一个类型名和需要的字段,然后就会返回一个类,可以初始化这个类为定义的字段传值。
from collections import namedtuple
builder = namedtuple('builder', ['name', 'age'])
# 这里创建一个类,规定了namedtuple的字段名,按顺序排序
# 下面开始初始化这个类,给定义的字段传递值
build = builder('Rob', 16)
# 然后就可以通过字段名获取值了
build.name
>> Rob
Python元编程:Python装饰器
如果代码中只要存在高度重复的东西,都应该想想是否有更好的解决方案,在python中通常都可以使用元编程来解决这类问题,元编程就是关于创建操作源代码的函数和类, 主要技术有装饰器,类装饰器,元编程等等。
函数的装饰器:
import time
from functools import wraps
def timethis(func):
# 这个装饰器用于计算函数运行的时间
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time
print(func.__name__, end-start)
return result
return wrapper
# 使用这个装饰器
@timethis
def countdown(n):
while n > 0:
n -= 1
上面的使用装饰器方法其实等同于countdown = timethis(countdown)
, 所以装饰器的原理是将一个函数作为参数传给另一个函数。
装饰器并不会修改原始函数的参数签名以及返回值,使用*args和**kwargs的目的是确保任何参数都能试用,而返回结果值基本都是调用原始函数的返回结果。
-
任何时候我们定义装饰器,我们都需要使用@wrap来注解底层包装函数,因为它可以保留这个函数的重要元信息,比如
__name__
,@wrap还有一个重要的作用就是可以通过属性__wrapped__
直接访问被装饰器包装的函数。 -
解除一个函数的装饰器:
如果一个函数已经被装饰器包装了,但是我们想要访问未包装的原始函数,上一条已经说了@wrap的__wrapped__
属性可以直接访问被包装的函数,所以我们也可以使用这个属性来获取未包装的函数
original_add = add.__wrapped__
但是如果函数被多个装饰器包装,这个属性的行为是不可预知的。如果使用的内置装饰器@staticmethod和@classmethod,这个属性是不适用的。 -
构建带有参数的装饰器:
如果我们想构建一个装饰器,给函数添加日志功能,同时允许用户指定日志的级别和其他选项:
def logged(level, name=None, message=None):
def decorate(func):
logname = name if name else func.__module__
log = logging,getLogger(logname)
logmsg = message if message else func.__name__
@wrap(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
# 使用装饰器
@logged(logging.DEBUG)
def add(x, y):
return x+y
这样写的原理就是最外层的logged函数接受参数并将它们作用在内部的装饰器上,内层的decorate()函数接受一个函数作为参数,然后在接受的函数上包装一个包装器,该包装器可以使用传递给logged的参数。
@decorator(x, y, z)
def func(a, b):
pass
等价于
def func(a, b):
pass
func = decorator(x,y,z)(func)
这里decorator()函数返回的结果应该是一个可调用对象,并且接受一个函数作为参数。
介绍python中的@staticmethod@classmethod方法
class A(object):
def m1(self, n): # 普通的实例方法
print("self:", self)
@classmethod
def m2(cls, n): # 类方法,第一个参数必须是cls
print("cls:", cls)
@staticmethod
def m3(n): # 静态方法
pass
a = A()
- 实例方法:
A.m1
是一个没有绑定对象的方法,所以调用时必须显式地传入一个实例对象,但是a.m1
已经绑定了实例的方法,所以隐式地把对象a传递给了self参数。所以A.m1(a, 1)
等同于a.m1(1)
- 类方法:如果要在方法里面调用静态类,那么定义成类方法合适,因为如果要是定义成静态方法,那么就要显式地引用类A,不利于继承
A.m2
和a.m2
都已经自动绑定了类对象A的方法,对于a.m2
,python可以通过实例对象a找到它的类对象是A,然后把A传递给了cls。所以A.m2(1)
等同于a.m2(1)
- 静态方法:如果方法中不需要访问任何实例方法和属性,纯粹通过传入参数的功能性方法。
静态方法跟普通函数没有区别,跟类和实例都没有绑定关系,所以不管通过类还是实例都可以使用该方法
装饰器实例:
装饰器一般用于插入日志,事务处理,缓存,权限效验等场景
1. 简单的不带参数的装饰器:
其实就是定义一个函数,参数为func
,然后里面再定义一个wrapper()
函数,然后最后这整个定义的函数需要return这个wrapper
def add_log(func):
def wrapper():
logger.info('func {} is running'.format(func.__name__))
return func()
return wrapper
@add_log
def a():
print('aaa')
a()
2. 被装饰的函数需要带有参数:
def add_log(func):
def wrapper(*args, **kwargs):
logger.info('func {} is running'.format(func.__name__))
return func(*args, **kwargs)
return wrapper
3. 带参数的装饰器:
上面的装饰器接受的唯一参数就是被装饰的函数,但是在使用装饰器的时候,我们可能会需要给这个装饰器传递参数,比如给装饰器指定日志等级:
def add_log(level):
def decorator(func):
def wrapper(*args, **kwargs):
logger.info('func {} is running'.format(func.__name__))
return func(*args, **kwargs)
return wrapper
return decorator
其实可以发现,就是将之前的装饰器函数名改为了decorator,然后在这个装饰器的外面再进行一层函数封装,然后这个封装函数返回这个装饰器。
Python 类与对象的高级用法
让对象以字符串显示:
平时我们想打印一个python对象,只会显示一个object对象,但是现在我们需要直接打印出这个对象里面的内容,让其更具有可读性。
Class Pair:
def __init__(self, x ,y):
self.x = x
self.y = y
def __repr__(self):
# 这个方法返回一个实例的代码,跟我们使用交互式解释器显示的值一样
return 'Pair({0.x!r}, {o, y!r})'.format(self)
def __str__(self):
# 将实例转换为一个字符串,使用str(), print()会输出这个字符串
return '({0.x!s}, {o, y!s})'.format(self)
这个format不是很容易看懂,其实{0.x}的意思是对应format里面第一个参数的x属性,所有在上述代码里面,0就是self, 也就是返回本身对象的x属性。
如果__str__()属性没有被定义的话,就会使用__repr__()属性来代替,所以我们可以简单的只定义repr属性
我们也可以使用一种看起来更直观的方式:
def __repr__(self):
return 'Pair('%r, %r)' % (self.x, self.y)
对象支持上下文
如果我们想要我们的对象支持上下文管理协议,with语句,怎么办?
为了让对象支持with,我们需要实现两个方法,__enter__()
和__exit__()
:
class Connection:
def __init__(self, address,....):
self.address = address
...
def __enter__(self):
...
self.sock.connect(self.address)
return self.sock
def __exit__(self, exc_ty, exc_val, tb):
self.sock.close()
这个对象用于创建网络连接,但是初始化这个对象的时候,它并没有创建一个网络连接,因为需要使用with在上下文环境里面才会触发enter和exit:
conn = Connection(...)
with conn as s: # 这样才会真正创建一个连接
s.send('safa')
....
with中发生了异常也不用担心,因为exit的第三个参数会自动控制异常。
类中封装私有属性
- 任何以单一下划线开头的属性或者方法都应该是内部实现,但是这不意味着我们从外面不能访问这些内部属性和方法,只是一种约束,这样做肯定不好
- 任何以两个下划线为开头的属性和方法会导致访问名称变成其他形式,比如在一个class A中有一个
__data
的属性,它会被直接重命名为_A__data
属性。这样的目的就是保证在继承的时候永远不会被覆盖
为类中属性增加附加条件@property
(如:规定属性类型)
class Person:
def __init__(self, name):
self.name = name
# 这里不使用self._name的原因是当初始化对象的时候,也能直接调用下面的setter
@property # 这里也相当于一个getter
# 将下面的方法定义为一个属性。
# 只有这里使之成为了一个属性,后面的两个方法才可以实现
def name(self):
return self._name
@name.setter
def name(self, value):
if 规定的条件:
error
self._name = value
@name.deleter
def name(self):
error # 表示不允许删除这个属性
使用实例:
a = Person('jaden')
a.name # 调用了getter,返回正确的值
a.name = 1 # 调用了setter,报错,因为setter里面规定了不是字符串就报错
del a.name # 调用了deleter,报错,因为deleter里面规定了不能删除
使用延迟计算属性功能
如果想将只读属性定义为一个property,只在访问的时候才会计算结果,但是一旦被访问过后,又希望下次不用再次计算,希望结果能被缓存起来。
先使用一个描述器定义它,然后在类中使用。。。
定义借口或者抽象基类: ABC和collections等模块
from abc import ABCMeta, abstractmethod
class IStream(metaclass=ABCMeta):
@abstractmethod
def read(self,...):
pass
@abstractmethod
def write(self, ....):
pass
抽象基类的特点是它不能直接被实例化,所以使用a = IStream()
会报错。抽象类的目的就是让别的类集成它的特定的方法。
标准库里面有很多用到抽象基类的地方,比如:
- collections模块里面有很多实现容器和迭代器,序列,映射,集合之类的抽象基类
- numbers库里面有很多数据对象的抽象基类
- io库里面有很多跟I/O操作相关的抽象基类
抽象基类还有一个很常用的用法是对类进行类型检查:
虽然ABCs模块可以很方便的做类型检查,但是尽量不要使用它,因为强制类型检查只会让代码变得更加复杂。
import collections
if isinstance(x, collections.Iterable): # 检查该类是不是一个迭代器
pass
使用collections模块里面的抽象基类,实现自定义类支持迭代等功能:
比如我们要使我们的自定义抽象基类能支持迭代,我们需要让该自定义类继承 collections.Iterable
import collections
class A(collections.Iterable):
pass
a = A() # 报错:无法实例化,因为我们需要继承collections.Iterable的所有抽象方法
# 但是对于迭代的功能,我们知道,只需要实现__iter__方法即可
=======================================================================
a = collections.Sequence() # 同上面,报错,也会显示所有的需要实现的抽象方法
# 我们可以看到需要实现__getitem__和__len__方法
# 所以下面我们在自定义类中实现这些抽象方法
class AList(collections.Sequence):
def __init__(self):
pass
def __getitem__(self, index):
return self._items[index]
def __len__(self):
return len(self._items)
def additem(self ,item):
# 这是一个在排序列表中插入元素的高效方法,保证插入后的列表顺序
bisect.insort(self._items, item)
现在这个自定义类跟列表没有什么区别,支持普通列表的几乎所有操作
使用Mixins拓展类的功能
加入我们需要一组类都使用一种你定义的方法,但是这些类都是一些完全不同的类,之间没有任何的继承关系,所以并不能将你定义的方法放入一个基类,然后让这一组类都继承这个基类。
如果我们想让自己定义的类具有添加日志,唯一性设置,类型检查等功能的话,我们可以继承一些混入类,于此同时我们仍然可以继承其他的类:
class SetOnceMappingMixin:
# this class allow key only be setted once
__slots__ = ()
def __setitem__(self, key, value):
if key in self:
raise KeyError
return super().__setitenm__(key, value)
这样一个单独的混入类没有任何的作用,而且当我们实例化它的话会报错,但是我们结合其他的类一起使用:
from collections import defaultdict
class SetOnceDefaultDict(SetOnceMappingMixin, defaultdict):
pass
上面这个类同时继承了混入类和其他类,该类拥有两者的功能。
混入类没有自己的状态信息,因为没有__init__
方法,没有实力属性,所以混入类会明确定义__slots__ = ()
。这里需要记住slots是用来对类限制属性的,只有在tuple里面的属性才能被添加,这里为空,表示不能添加任何实例属性。
状态机
如果不想在代码中出现很多次重复的条件判断,可以创建一个状态机。
待完成
让类支持比较操作
如果我们想让类的实例能支持标准的运算符 !=, >, <…,但是我们又不想操作一系列复杂的方法,我们就可以使用装饰器functools.total_ordering
来实现。
我们首先需要使用这个装饰器装饰一个类,然后我们需要在类中定义一个__eq__()
方法,外加lt,le,gt,ge中的任意一个,这个装饰器就会自动为类填充其他的比较方法了
@total_ordering
class House:
def __init__(self):
...
@property
# 这个是属性装饰器,将下面的方法定义为一个属性,只有被调用的时候才会计算
# 所以下面调用的时候都不需要括号
def living_space(self):
return sum(r.square_feet for r in self.rooms)
def __eq__(self, other):
return self.living_space == other.living_space
def __lt__(self, other):
return return self.living_space < other.living_space
使用这个类实例化出来的实例都可以进行运算符比较了。
未完待续。。。