因为基础的太基础了所以不多说明了,直接从第二章开始。
对象
python的对象理论上可以直接通过class定义,但是由于不同类别的功能不同,可能需要一些装饰器等用于更加方便地定义对象。
数据类型结构
虽说ppt里面英文为data structures,但是这里指的应该是专门表示数据的对象结构。
数据的存储通常可以使用元组、字典或者类实例来保存。前两种自然不用多说明,比较少见的是最后一种——类实例。在定义类别时一般class+__init__方法来定义一个基本的类和对应的属性,但是如果该类别用于存储数据,则有更好的定义方式,这里列举出了三种:
- slots 可以节省空间
- dataclasses 可以减少编码
- named tuples 不可变性
slots的使用非常简单:
class Stock:
__slots__ = ('name', 'shares', 'price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
只需要在slots魔法属性定义变量名的元组即可,这种方式可以减少内存占用(常规状态下class会为所有的属性分配一个字典来存储数据,当类实例较多的时候就会大大增加内存占用,而使用slots可以使用较为紧凑的方式——类似列表,对数据进行组织)
dataclass的使用主要用于节省编码:
from dataclasses import dataclass
@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)
class Stock:
name : str
shares : int
price: float
上述代码可以直接得到一个类,且类实例可以直接访问name、shares、price属性。详细的可以参考:https://docs.python.org/3/library/dataclasses.html
named tuples则有两种使用(定义)方法:
import typing
class Stock(typing.NamedTuple):
name: str
shares: int
price: float
from collections import namedtuple
Stock = namedtuple('Stock',['name', 'shares', 'price'])
通过这种方式定义的类与tuple属性相同,同时可以像类一样直接调用使用,唯一的缺点在于属性不能再变化。
(感觉用的会比较少,但是也挺好用)
容器
常见的容器有列表、集合、字典。其实三个都已经是非常常见的东西了,但是字典还有个非常巧妙的用法:
prices = {
('ACME','2017-01-01') : 513.25,
('ACME','2017-01-02') : 512.10,
('ACME','2017-01-03') : 512.85,
('SPAM','2017-01-01') : 42.1,
('SPAM','2017-01-02') : 42.34,
('SPAM','2017-01-03') : 42.87,
}
p = prices['ACME', '2017-01-01']
prices['ACME','2017-01-04'] = 515.20
当使用元组作为字典键时,可以分开查找对应的值,从而不需要嵌套字典。
这三种容器均可以使用表达式来进行快速的创建容器对象。(列表表达式、集合表达式、字典表达式)
Collections module
这个库在做算法题时非常常见。可以逐个说明:
- defaultdict
from collections import defaultdict
d = defaultdict(list)
d['x']
#output : []
通过定义defaultdict中的类别,在实例化defaultdict类别并填充对象时,会为其中不存在的键使用默认类别。
- Counter
from collections import Counter
totals = Counter()
totals.most_common(2)
Counter对象顾名思义就是用于计数或者统计的,其可以很方便的进行数据的存储和一些方便的数据操作,相较于自己的实现可以提高性能。常见的方法有most_common, elements, total。详见:https://docs.python.org/3/library/collections.html#collections.Counter
- deque (double-ended queue)
from collections import deque
q = deque()
q.append()
q.appendleft()
q.pop()
q.popleft()
这个东西主要应用于队列问题的解决中
Collections中有很多相当实用的类,可以方便的用于解决很多的问题,正如作者所说的,没有也没事,但是有的话会非常方便。
iteration
iteration 一般使用在for循环中。对于python3,for循环不仅可以简单的提取其中的内容,还可以使用解包操作等:
for x in xs:
pass
for x,y in points:
pass
for x,*other in points:
pass
在遍历时有时候需要对多个数组同时遍历,除了使用索引计数以外的另一种方法是使用zip()函数:
for colname, val in zip(columns, values):
pass
enumerate经常被我所使用,其返回一个索引和序列中的内容
序列还可以使用一些方便的规约手段进行处理,包括但不限于sum、max、min、any、all
前面知道了*可以用于解包列表,而对应的**则是用于解包字典。所以会有一些比较离谱的传参方法:
a = (1, 2, 3)
b = (4, 5)
func(*a, *b)
c = {'x': 1, 'y': 2 }
func(**c) func(x=1, y=2)
func(*a, **c)
func(*a, *b, **c)
func(0, *a, *b, 6, spam=37, **c)
builtin
builtin是python解释器中的内容,使用C实现,其中无法更加详细的自定义了。
所有的object都有一个id(内存中的地址),引用计数,类别内容。builtin有很多我们经常见到而不以为然的类别:None, int, float, str等
各种类别在内存中组织方式都是不尽相同的。举个例子None的大小为16字节,float为24字节。由于python中的int是无限精度的,所以其组织方式相当特殊:
另一个比较特殊的是str类。这里就不详细说明其内存组织方式。如果想要便利的获取某一变量的内存占用量,可以使用sys.getsizeof()
函数进行。
python所有的东西都是对象,通过定义对应的类别的魔法方法使其拥有对应的操作功能(也可以使用这种方法定义类似的builtin类)。如果想要从字节码了解一段代码的内容,可以使用dis.dis()
方法。
要想实现一个简单的类别,需要实现诸如:__str__
__repr__
__format__
__add__
__radd__
__iadd__
条件运算符等等等等(其中的条件运算符可以使用装饰器来简化:
from functools import total_ordering
@total_ordering
class MutInt:
__slots__ = ['value']
def __init__(self, value):
self.value = value
...
def __eq__(self, other):
if isinstance(other, MutInt):
return self.value == other.value
elif isinstance(other, int):
return self.value == other
else:
return NotImplemented
def __lt__(self, other):
if isinstance(other, MutInt):
return self.value < other.value
elif isinstance(other, int):
return self.value < other
else:
return NotImplemented
还可以实现诸如__index__
等
容器的表示
容器对象,诸如列表,其中的值容器只拥有其引用(指针),所以修改的时候修改的实际是对应引用的值,也因此会有一些奇怪的现象。
另一方面,python的不可变容器类和可变容器类的内存结构不同,其中不可变容器的内存空间会保留一部分用于append操作等。
集合、字典是基于hash的,不难理解,通过定义特殊方法__hash__
来控制值不同或者字典键不同,所以字典的索引必须是hashable的。其中,字典的内存结构也是相当特殊的:
对键进行hash之后对长度取余数,之后根据索引进行查找,要注意的是列表中的数据顺序是保持的。
虽然使用索引,键1进行容器的查询使用的是[],但是实际上都是对应了类的方法,对于列表就是:__getitem__
,对于字典就是__contains__
如果想要自己定义一个容器而又不知道需要实现哪些方法时,就可以使用:
from collections.abc import Mapping, MutableMapping, Sequence, MutableSequence, Set, MutableSet
以此为基类进行新的类定义时,如果对应的方法没有实现好,会有报错:
c = MyContainer()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyContainer
with abstract methods __delitem__, __getitem__, __iter__,
__len__, __setitem__
了解了上面的容器数据组织方式,可以通过一些手段减少代码对内存的占用。相较于把每个数据保存为字典之后放到列表里,先把数据按顺序放到列表里面之后在放到字典里面,可以节省极大的内存占用。
python大坑 —— 分配
要注意的一点是,python的=不是赋值或者说是拷贝,实际上是引用:
上面代码在内存中的结果如图。可以使用is操作(检查内存id)来判断是否为同一个东西。如果需要进行拷贝的话,浅拷贝可以通过如下方式进行:
a = [2, 3, [100, 101], 4]
b = list(a)
a is b
# False
如果需要深拷贝的话还是得使用
import copy
b = copy.deepcopy(a)
一切皆对象
基于python一切皆对象,所以可以使用函数字典的方式进行条件判断:
ops = {
'+' : add,
'-' : sub,
'*' : mul,
'/' : div
}
r = ops[op](x,y)
同理可以使用类型进行数据的格式化操作:
coltypes = [str, int, float]
r = list(zip(coltypes, row))
record = [func(val) for func, val in zip(coltypes, row)]
类和对象
除了一些常见的基础操作,还可以使用属性访问函数操作属性:
getattr(obj, 'name')
setattr(obj, 'name', value)
delattr(obj, 'name')
hasattr(obj, 'name')
可以使用getattr的默认参数来方式报错(方法类似dict().get())
相较于对象的方法,类方法使用的比较少,更多的时候用于定义一个稍作修改之后的类
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def today(cls):
tm = time.localtime()
return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)
d = Date.today()
静态方法用于不改变类别属性时的操作:
class SomeClass:
@staticmethod
def yow():
print('SomeClass.yow')
虽然我们常用的方式是在init方法里面修改类的构造方式,但是通过类方法可以方便的继承并对原类别的类属性进行修改。
关于类的封装,python使用_来表示私有属性:
class Base:
def __init__(self, name):
self._name = name
class Child(Base):
def spam(self):
print('Spam', self._name)
但是实际上无论是子类或是访问都是可以直接访问到的。单下划线表示私有属性,双下划线表示类属性,在该情况下该属性不能被子类所访问:
class Base:
def __init__(self, name):
self.__name = name
class Child(Base):
def spam(self):
print('Spam', self.__name) # AttributeError
在进行属性赋值时,有时候会需要防止类属性类别不同的结果,为了更好的进行错误处理。除了定义一个复杂的set_XXX()类方法进行处理以外,另一种方式是使用装饰器:
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('Expected int')
self._shares = value
property
装饰器是一个非常舒服的装饰器,在定义一些属性时可以使用该方法进行:
@property
def cost(self):
return self.shares * self.price
前面提到了__slots__方法,通过定义类属性来限制类的属性,并且能在内存和速度上产生提升。但是要注意的是该属性定义最好在比较基础的类上进行定义,否则其实并不能怎样提高速度和内存。
使用上面的property和setter有时候会增加工作量,另一种类别的限制方式是限制类别:
class DStock(Stock):
_types = (str, int, Decimal)
类的继承可以是程序设计的一个很好的特性,但是python的多重继承有非常多混沌的细节需要考虑。暂且不考虑。
在类定义的时候往往会遇到__str__
.__repr__
特殊方法,虽然两个都是输出,但是输出形式有所不同,前者以用户可见的形式进行输出,而后者以程序员可见的方式进行输出,同时可以很方便的被eval()函数所接收。
除非对内存管理非常了解否则尽量少用
在创建类的实例时,我们只需要使用构造函数即可,但是对于python,其构造经过了两个阶段:
d = Date.__new__(Date, 2012, 12, 21)
d.__init__(2012, 12, 21)
但是__new__
方法并不常用,一般用于一些特殊的类定义。
__del__
方法虽然是del,但是和del
关键字无关,如下:
c = Connection() # refcnt = 1
d = c # refcnt = 2
del d # Doesn't call d.__del__() (refcnt = 1)
c = None # Calls c.__del__() (refcnt = 0)
使用场景也比较固定,正确的释放资源或者锁。
还有一种东西是弱引用:
import weakref
f = Foo()
fref = weakref.ref(f)
g = fref() # Dereference
print(g)
除了使用__del__
进行对象的删除处理。还有一种经常在文件读写、http连接中使用的上下文管理,对应的函数为:__enter__
和__exit__
with obj as val:
xxx
特殊的类
使用接口极大的增加代码的复用性,除了自己定义接口类进行复用以外,还可以使用abc(abstruct base class)
from abc import ABC, abstractmethod
class IStream(ABC):
@abstractmethod
def read(self, maxbytes=None):
pass
@abstractmethod
def write(self, data):
pass
上述代码只有所有抽象方法均被重写后才能正常运行。
另一种特殊的类是处理者类,作为参数传入函数中,只定义一定的方法而不具有属性,前面的TableFormatter类就是一种handler类。
class TextTableFormatter(TableFormatter):
def headings(self, headers):
xxx
def row(self, rowdata):
xxx
这种handler类是python最流行的设计模式之一。
混沌的多重继承
python的多重继承使用的是MRO机制,具体的可以参考高天的视频。由此主要引出的一个概念是mixin class。这同样是python较常使用的一种设计方式,主要用于给类别增加可选的属性。(例如定义一些四则运算类、逻辑运算类,之后再定义一种运算类时可以直接继承上述类)
深入理解类
类的实例本质其实是一个字典,通过使用__dict__
方法可以使用查询其中的属性和方法,想要理解类和实例的__dict__
方法和__class__
方法比较复杂,可以直接参考下图:
在获取数据时,首先从实例的__dict__
中获取数据,之后去类的__dict__
中获取数据
多重继承
python的多重继承从__mro__
属性中获取顺序:
>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)
python的多重继承基于cooperative multiple inheritance
当重载方法时尽量使用super,super方法虽然是使用父类的方法,但是在多重继承上时使用的是mro顺序
在设计多重继承时可以遵循以下原则:
- 兼容的方法参数
在基于mro顺序进行方法继承时,最后控制参数是兼容的
- 方法链必须有终止
super不能被永久递归下去,最好的方法是使用一个抽象类来进行打底
- 最好使用super
由于多重继承时,如果不使用super方法,可能会导致父类方法定义的混乱,因此在设计多重继承时最好的方法还是使用super
描述符协议
对于类属性的修改涉及到了描述符协议,虽然很少听说,但是几乎存在于整个对象系统。
对于类对象的访问、修改和删除,实际上会用到函数,由这三个函数组成的类就是一个描述符
d.__get__(obj, cls)
d.__set__(obj, value)
d.__delete__(obj)
虽然不常用,但是存在于各种地方,从实例方法、静态方法、properties等等中都有所存在。
在进行代码编写的时候,有时会忘掉给类方法添加参数括号:
>>> s = Stock('GOOG',100,490.10)
>>> s.cost
<bound method Stock.cost of <__main__.Stock object at
0x37e250>>
>>> s.cost()
49010.0
之所以两者有着较大的差别,其中一个原因就是描述符的存在。相较于属性,方法实现了__get__
方法
属性访问控制
可以使用给__setattr__
,__getattr__
,__delattr__
进行属性访问控制,其中一种用法是代理:
class Proxy:
def __init__(self,obj):
self._obj = obj
def __getattr__(self,name):
print('getattr:', name)
return getattr(self._obj, name)
# 天才般的设计
class Readonly:
def __init__(self, obj):
self.__dict__['_obj'] = obj
def __setattr__(self, name, value):
raise AttributeError("Can't set attribute")
def __getattr__(self, name):
return getattr(self._obj, name)
其实基本上算是继承了。是一种继承的替代方案
但是与继承不同的是,__getattr__
不支持魔法方法(诸如len、getitem等)
函数
python的函数一般使用小写字母表示,私有函数要加下划线。传参时尽可能指明参数。
注意:不要使用可变值作为默认参数,经典错误:
def func(a, items=[]):
items.append(a)
return items
>>> func(1)
[1]
>>> func(2)
[1, 2]
>>> func(3)
[1, 2, 3]
函数中可以使用文档字符串:
def xxx():
test
```
return None
此外,在PEP484中支持使用类别标注来使得代码更容易看懂,同时也可以提供语法提示(但是并不会强制类型)。
### future
是一个平常情况下不会使用到的函数:
```python
from concurrent.futures import Future
def func(x, y, fut):
time.sleep(20)
fut.set_result(x+y)
def caller():
fut = Future()
threading.Thread(target=func, args=(2, 3, fut).start()
result = fut.result()
print('Got:', result)
其功能也不难理解,就是延迟获得结果,通常用在各种线程、异步、多进程中。可以通过判断fut的属性来判断该线程是否运行结束。
函数式编程
python支持高阶函式:
- 接受函数作为输入
- 函数可以返回函数
当函数作为输入时,通常也可以叫做回调函数。python支持lambda函数,其具有以下特点
- 支持匿名函数
- 只能包含一个表达式
- 没有控制流和exceptions
lambda除了用于省略部分简单函数,另一种常见的用法是更换函数参数:
def distance(x, y):
return abs(x - y)
dist_from10 = lambda y: distance(10, y)
dist_from(3)
# 7
from functools import partial
dist_from10 = partial(distance, 10)
还有就是mapreduce(这个概念不仅仅可以应用与分布式系统,很多地方都可以见到mapreduce的身影)
当函数作为输出时,有些非常有趣的现象:
def add(x, y):
def do_add():
print(f'{x} + {y} -> {x+y}')
return do_add
>>> a = add(3,4)
>>> a()
3 + 4 -> 7
上述的输出倒是不难理解,但是一个要考虑的细节是,x,y被存储在了哪里?
这种情况下的函数构成了一个闭包:如果一个内部的函数作为一个结果被返回,那么这个内部的函数为闭包。一个重要的特性是:一个闭包包含所有需要正确运行的变量值(所以上面的a仍然能够正常运行,或者说这并不是一件难理解的事情),python支持通过方法访问闭包的相关内容
>>> a.__closure__
(<cell at 0x54f30: int object at 0x54fe0>,
<cell at 0x54fd0: int object at 0x54f60>)
>>> a.__closure__[0].cell_contents
3
>>> a.__closure__[1].cell_contents
4
如果理解上面的内容,那么下面的结果应该也不会很奇怪:
def add(x, y):
result = x + y
def get_result():
return result
return get_result
>>> a = add(3, 4)
>>> a.__closure__
(<cell at 0x10bb52708: int object at 0x10b5d3610>,)
>>> a.__closure__[0].cell_contents
7
>>>
闭包的变量是可变的:
def counter(n):
def incr():
nonlocal n
n += 1
return n
return incr
>>> c = counter(10)
>>> c()
11
>>> c()
12
>>>
nonlocal是一个罕见的关键字,因为其不能在外界被定义,只能在闭包内被定义表示可以使用外部的变量。
虽说闭包有点奇怪,但是其有很多现实应用,包括惰性求值、回调函数、创建宏。
可以好好参考一下5.4关于类中类型的Exercise,非常舒服而且包含了很多非常重要的python高阶编程。
异常检测
异常检测通常是在程序流程中介绍的,这里放到了函数的章节。
首先要注意的一点是,除非为了确定错误类型,否则不要捕捉所有异常。其次,不要无视异常,可能会带来非常恐怖的错误:
try:
# Some complicated operation
...
except Exception:
pass
虽然不建议捕捉所有异常,但是可以通过这种方式来重新发起异常:
try:
# Some complicated operation
...
except Exception as e:
print("Sorry, it didn't work.")
print("Reason:", e)
raise
对于一整套异常,处理流程应该如下:
try:
...
except Exception as e:
raise TaskError('It failed') from e
try:
...
except TaskError as e:
print("It didn't seem to work.")
print("Reason:", e.__cause__)
另一种类似的东西叫做上下文管理:
def read_data(filename):
f = open(filename)
try:
... do whatever ...
finally:
f.close()
但是不难理解,上述代码实际上可以直接用with open()代替。
为了更好的捕捉异常,相较于使用print操作,可以使用logging进行打日志从而更好的发现问题:
import logging
log = logging.getLogger(__name__)
def read_data(filename):
...
try:
name = row[0]
shares = int(row[1])
price = float(row[2])
except ValueError as e:
log.warning("Bad row: %s", row)
log.debug("Reason : %s", e)
断言
并不难使用
def add(x, y):
'''
Adds x and y
'''
assert isinstance(x, int)
assert isinstance(y, int)
return x + y
但是可以在运行时屏蔽断言错误:
python3 -O prog.py
与代码共舞
定义在module的变量全局变量,在local进行修改时需要使用global拉出来:
import math
x = 42
def t():
global x
x = 37
为了确认清楚全局和局部的内容,可以使用globals()
,locals()
另一个特殊的模块是builtins
模块,该模块用起来也挺舒服的:
>>> abs(-45)
45
>>> import builtins
>>> builtins.abs(-45)
45
>>> builtins.pi = 3.1415926
>>> pi
3.1415926
>>>
上面的代码体现了builtins的两种使用方式,前者是调用内置函数,后者则可以进行修改以方便更多操作
观察函数和包
函数作为对象,同样有一些对应的属性方法:
def f(a,b,c):
```
return a,b,c
f.doc f.annotations f.name f.defaults f.code.co_argcount f.code.co_varname
太细了,一辈子用不到
### eval和exec
前者主要用于计算,后者则用于运算抽象代码。但是两者有一个比较大的坑,其作用域可能会有一定的问题,所以一般通过如下方式进行使用:
```python
def func():
x = 10
exec('x = 15; print(x)') # ---> 15
print(x) # ---> 10 ?????
def func():
x = 10
loc = locals()
exec('x = 15; print(x)', globals(), loc) # ---> 15
x = loc['x']
print(x) # ---> 15
虽然但是exec和eval有时候很容易带来很大的问题
callable
拥有callable的累具有与函数类似的性质
元编程
元编程主要涉及到宏、封装器等功能,其目的在于为代码提供更方便维护的策略。
封装器倒是不难理解,接收函数并将其修改之后返回(其实类似装饰器)所以这里直接从装饰器开始说明:
def logged(func):
# Define a wrapper function around func
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
@logged
def f(a, b):
return a + b
此外,装饰器可以叠加,最终的效果:
@foo
@bar
@spam
def add(x, y):
return x + y
前面提到函数保留有一些特性,例如:__doc__
,__name__
,但是装饰器不保存元数据,因此,需要一些操作来将函数的元数据转移到装饰器处理之后的元数据上:
def logged(func):
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
return wrapper
from functools import wraps
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
上面提供了两种复制元数据的方法,显然后者要更简单一些。此外装饰器作为函数也是可以接收参数的:
def logmsg(message):
def logged(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(message.format(name=func.__name__))
return func(*args, **kwargs)
return wrapper
return logged
装饰器是个用起来很舒服的东西,可以学习并搓一些自己需要的功能附加到函数上:
import time
def timethis(func):
def wrapper(*args, **kwargs):
start = time.time()
r = func(*args, **kwargs)
end = time.time()
print(func.__name__, end - start)
return r
return wrapper
除了函数装饰器,还有类装饰器,只不过相较函数装饰器更加复杂和少见。
类型解包
令人头大,如果不注意的话,不会发现type其实本身是一个类(不是方法)
python中的类的构造可以看作:名称、基类、函数的组合,只要以合理的方式和将这些东西组合起来就能构造出一个类,所以还有这样的类构造方式:
# Define some method functions
def __init__(self,name):
self.name = name
def yow(self):
print("Yow!", self.name)
# Make a method table
methods = {'__init__': __init__,
'yow': yow }
# Make a new type (Spam)
Spam = type('Spam', (object,), methods)
当一个类被定义的时候,发生了如下过程:
- 首先,类的主体内容被获取:
body = '''
def __init__(self, name):
self.name = name
def yow(self):
print("Yow!", self.name)
'''
- 其次,创建一个字典,同时会有一些元数据插入其中
__dict__ = type.__prepare__('Spam', (object,))
>>> type.__prepare__('Spam', (object,))
{}
>>>
__dict__['__qualname__'] = 'Spam'
__dict__['__module__'] = 'modulename'
- 之后,类的主体内容在字典内被执行
exec(body, globals(), __dict__)
- 最后,根据名字、基类和字典构造类
>>> Spam = type('Spam', (object,), __dict__)
>>> Spam
<class '__main__.Spam'>
>>> s = Spam('Guido')
>>> s.yow()
Yow! Guido
>>>
在理解了一个类的构造过程(核心其实还是通过type进行构造),我们可以通过如下方式进行类的构造:
class Typed(Validator):
expected_type = object
@classmethod
def check(cls, value):
if not isinstance(value, cls.expected_type):
raise TypeError(f'expected {cls.expected_type}')
super().check(value)
_typed_classes = [
('Integer', int),
('Float', float),
('String', str) ]
globals().update((name, type(name, (Typed,), {'expected_type':ty}))
for name, ty in _typed_classes)
上述的代码根据类的构造方式,可以迅速的构造一系列类别检查
元类
用于构造类的类称为元类, 上面提到的type就是一种元类。通常情况下,如果不特定说明,类的构造使用的元类都是type,此外,继承的元类与基类相同(所以很少见这个东西):
class Spam(metaclass=type):
def __init__(self, name):
self.name = name
def yow(self):
print("Yow!", self.name)
通过继承type并重写其中的__new__
和__prepare__
等方法来实现创建一个新的元类。
之所以使用元类,就是因为可以更好的监视一个类创建和操作的过程(但是实际上没人怎么用,太底层了):
这里列举几种使用方式见见世面:
class dupedict(dict):
def __setitem__(self, key, value):
assert key not in self, '%s duplicated' % key
super().__setitem__(key, value)
class dupemeta(type):
@classmethod
def __prepare__(cls, name, bases):
return dupedict()
class A(metaclass=dupemeta):
def bar(self):
pass
def bar(self):
pass
def decorator(func):
...
# Decorator
...
class meta(type):
@staticmethod
def __new__(meta, clsname, bases, dict):
for key, val in dict.items():
if callable(val):
dict[key] = decorator(val)
return super().__new__(meta, clsname, bases, dict)
class meta(type):
def __call__(cls, *args, **kwargs):
print('Creating instance of', cls)
return super().__call__(*args, **kwargs)
>>> class A(metaclass=meta):
pass
>>> a = A()
Creating instance of <class '__main__.A'>
>>>
要注意的是,由于元类具有非常恐怖的继承遗传,所以随意使用并不是一个好的主要,一般优先使用类装饰器。元类的目标群体主要是:搭建框架和库开发者。
迭代器、生成器和协程
迭代器
迭代器都给我们用烂了。但是迭代器主要要知道的是其协议,迭代器协议,迭代器的底层实现:
_iter = obj.__iter__() # Get iterator object
while True:
try:
x = _iter.__next__() # Get next item
except StopIteration: # No more items
break
# statements
...
从上面可以看到,能够被for loop迭代的,主要实现了__iter__
和__next__
方法
生成器
生成器简化了自定义化迭代(可以以函数的形式直接定义)
def countdown(n):
print('Counting down from', n)
while n > 0:
yield n
n -= 1
>>> x = countdown(10)
>>> x
<generator object at 0x58490>
>>>
与普通的函数不同,生成器在调用时不直接运行,只有在调用next()时才运行下一次,每次next时,通过yield生成一个值,之后暂挂函数。当遇到return时,迭代会被停止。当然,也是可以通过for调用的
>>> c = countdown(5)
>>> for x in c:
... print('T-minus', x)
但是在上面用完之后,需要重新为c赋值来调用。这种方式是生成器的复用。
当然,也可以把其封装到一个类中实现自动复用:
class Countdown:
def __init__(self, n):
self.n = n
def __iter__(self):
n = self.n
while n > 0:
yield n
n -= 1
从某个角度可以看得出来,这种迭代和获取,其实很像生产者、消费者模型。
协程
当将yield视为表达式时,他就变成一个协程:
def match(pattern):
print('Looking for %s' % pattern)
while True:
line = yield
if pattern in line:
print(line)
协程的执行类似生成器,但是在调用协程时,是不会发生任何事情的,只有调用send()方法才行:
>>> g = match('python')
>>> g.send(None) # Prime it (explained shortly)
Looking for python
>>> g.send('Yeah, but no, but yeah, but no')
>>> g.send('A series of tubes')
>>> g.send('python generators rock!')
python generators rock!
>>>
但是在使用协程之前,第一次需要先调用send(None),通过send(None)使得运行到yield的地方,在这种情况下可以使用一个装饰器来解决忘记的问题:
def consumer(func):
def start(*args,**kwargs):
cr = func(*args,**kwargs)
cr.send(None)
return cr
return start
@consumer
def match(pattern):
...
管理生成器
可以使用.close()
方法关闭,也可以自己.raise()
主动
def genfunc():
...
try:
yield item
except GeneratorExit:
# .close() was invoked
# perform cleanup (if any)
...
return
g = genfunc() # A generator
...
g.close()
def genfunc():
...
try:
yield item
except RuntimeError as e:
# Handle the exception
...
g = genfunc() # A generator
...
g.throw(RuntimeError, "You're dead")
详细的可以参考:
-
See: “Fear and Awaiting in Async” https://www.youtube.com/watch?v=E-1Y4kSsAFc
-
“Generator Tricks for Systems Programmers” tutorial from PyCon'08
-
“A Curious Course on Coroutines and Concurrency” tutorial from PyCon'09 http://www.dabeaz.com/coroutines
-
“Generators: The Final Frontier” tutorial from PyCon'14 http://www.dabeaz.com/finalgenerator
库和包
库定义中有一些特定的变量:
__file__ # Name of the source file
__name__ # Name of the module
__doc__ # Module documentation string
通过使用sys.modules
来查看加载的module
在调用库时,可以使用*
来调用文件中的所有内容,但是该调用不适用与_name
格式命名的变量。
如果需要重载一些module,可以通过importlib库进行(注意:直接使用import不能重载):
import foo
import importlib
importlib.reload(foo)
d
# pseudocode
def reload(mod):
code = open(mod.__file__, 'r').read()
exec(code, mod.__dict__, mod.__dict__)
return mod
虽然可以重载,但是已有的实例会继续使用老的代码,而且可能会导致代码类别检查或者继承的一系列问题。
在进行module的路径查找时,通过在sys.path
中添加路径来获得正确的调用
import sys
sys.path.append('/project/foo/pyfiles')
包的调用可以使用相对调用或者绝对调用,这种不需要多说。包内部有一些有用的变量:
__package__ # Name of the enclosing package
__path__ # Search path for subcomponents
>>> import xml
>>> xml.__package__
'xml'
>>> xml.__path__
['/usr/local/lib/python3.5/xml']
>>>
当进行如下调用时
from xxx import *
实际上是会调用子包中的__all__
属性,所以可以在__init__.py
中添加如下内容:
# foo.py
__all__ = ['Foo']
class Foo(object):
pass
# __init__.py
from .foo import *
from .bar import *
__all__ = [ *foo.__all__, *bar.__all__ ]
可以使用
python -m xxx.xxx
进行单元测试,此外,可以通过在包内定义__main__.py
作为入口,使得包目录可以运行:
spam/
__init__.py
__main__.py # Starting module
foo.py
bar.py
bash % python3 -m spam