返回
Featured image of post python mastery

python mastery

暂时继续推python

因为基础的太基础了所以不多说明了,直接从第二章开始。

对象

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是无限精度的,所以其组织方式相当特殊:

image-20230824003809945
image-20230824003809945

另一个比较特殊的是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的。其中,字典的内存结构也是相当特殊的:

image-20230824151225211
image-20230824151225211

对键进行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的=不是赋值或者说是拷贝,实际上是引用:

image-20230904180432978
image-20230904180432978

上面代码在内存中的结果如图。可以使用is操作(检查内存id)来判断是否为同一个东西。如果需要进行拷贝的话,浅拷贝可以通过如下方式进行:

a = [2, 3, [100, 101], 4]
b = list(a)
a is b
# False

image-20230904180838103
image-20230904180838103

如果需要深拷贝的话还是得使用

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__方法比较复杂,可以直接参考下图:

image-20230908091816305
image-20230908091816305

在获取数据时,首先从实例的__dict__中获取数据,之后去类的__dict__中获取数据

多重继承

python的多重继承从__mro__属性中获取顺序:

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
<class '__main__.B'>, <class '__main__.A'>,
<type 'object'>)

python的多重继承基于cooperative multiple inheritance

image-20230908111935345
image-20230908111935345

当重载方法时尽量使用super,super方法虽然是使用父类的方法,但是在多重继承上时使用的是mro顺序

image-20230908112235711
image-20230908112235711

在设计多重继承时可以遵循以下原则:

  • 兼容的方法参数

在基于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__等方法来实现创建一个新的元类。

之所以使用元类,就是因为可以更好的监视一个类创建和操作的过程(但是实际上没人怎么用,太底层了):

image-20230922211100923
image-20230922211100923

这里列举几种使用方式见见世面:

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")        

详细的可以参考:

库和包

库定义中有一些特定的变量:

__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
Licensed under CC BY-NC-SA 4.0