something about python

基础知识

使用函数名称调用函数(方法)

eval

eval直接将函数字符串名称转换成函数对象

locals globals

locals和globals以字典形式分别返回当前位置的全部局部变量和全局变量,这就包括了定义的方法

1
2
>>> locals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'foo': <function foo at 0x7f4c71cfa5f0>, '__doc__': None, '__package__': None}
1
2
3
4
5
6
7
def foo():
  print "foo"

eval("foo")()

locals()["foo"]()
globals()["foo"]()
getattr

当通过对象调用函数的时候,可以使用getattr

methodcaller

也可以使用operator模块的methodcaller调用函数

1
2
3
4
5
6
7
8
9
class Foo():
  def foo(self):
    print "foo"

f = Foo()
getattr(f,"foo")()

from operator import methodcaller
methodcaller("foo")(f)

__init__ 和 __new__

__init__是初始化方法,创建对象后,就立刻被默认调用了,可接收参数。__init__中的self参数就是__new__返回的对象

__new__必须要有返回值,返回实例化出来的实例

is 和 ==

is是检查对象的标示符是否一致,==是检查俩个对象是否相等,a is b就是id(a) == id(b),a == b就是a.__eq__(b)

内存驻留

交互式命令行,对于只包含字母、数字、下划线组成的字符串,python会自动保留一个副本,当创建新的字符串的时候就直接指向该副本,此外的字符串不会驻留在内存中,

1
2
3
4
>>> a = "hello world"
>>> b = "hello world"
>>> a is b
False

可变对象和不可变对象

可变对象在创建后可改变但是地址不会改变,不可变对象创建后不能改变。字典和列表是可变对象,int、字符串、元组和float是不可变对象

join和+

在连接列表中的字符串的时候,join会优先计算需要多大的内存,一次性申请,使用+就会一步一步申请,性能比前者差。

finally

当try语句块中含有break、continue、return的时候,在离开try之前,finally的语句会被执行

enumerate

1
2
for index,value in enumerate(["a","b","c"]):
    print index,value

zip()

1
2
for item in zip([1,2,3],[5,6,7]):
    print item

collections

collections中包含除了集合、元素、列表、字典意外的数据类型

1
2
3
4
5
6
namedtuple
deque       双端队列
ChainMap   创建多个映射的单一视图,python2似乎没有
Counter 字典的子类,用于对可哈希对象进行计数
OrderedDict 字典子类,可以保留元素的添加顺序
defaultdict 字典子类,可以调用用户自定义的工厂函数设置缺失值
1
2
3
4
5
from collections import namedtuple
City = namedtuple('City','name country population coordinates') #创建了一个City类
tokyo = City('Tokyo','JP','36',(35,139))
print tokyo.population
print tokyo.coordinates

单例模式

使用模块

python的模块天然就是一个单例,在一个模块中生成一个对象,在其他模块中导入这个对象全部都是同一个对象。因为python在导入模块的时候会生成pyc文件,在导入的从这个pyc文件导入对象

使用装饰器
1
2
3
4
5
6
7
8
9
def Singleton(cls):
    _instance = {}

    def _singleton(*args, **kargs):
        if cls not in _instance:
            _instance[cls] = cls(*args, **kargs)
        return _instance[cls]

    return _singleton
使用类
在__new__方法中实现
1
2
3
4
5
6
7
8
9
10
11
12
13
import threading
class Singleton(object):
    _instance_lock = threading.Lock()

    def __init__(self):
        pass

    def __new__(cls, *args, **kwargs):
        if not hasattr(Singleton, "_instance"):
            with Singleton._instance_lock:
                if not hasattr(Singleton, "_instance"):
                    Singleton._instance = object.__new__(cls)
        return Singleton._instance
使用元类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading

class SingletonType(type):
    _instance_lock = threading.Lock()
    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, "_instance"):
            with SingletonType._instance_lock:
                if not hasattr(cls, "_instance"):
                    cls._instance = super(SingletonType,cls).__call__(*args, **kwargs)
        return cls._instance

class Foo(metaclass=SingletonType):
    def __init__(self,name):
        self.name = name

协程

协程通过中断实现,在执行一个协程的时候,可以随时中断,去执行另一个协程;协程是在一个线程中执行,没有线程开换的开销,不需要多线程的锁机制,不存在写变量冲突,控制共享资源不加锁。

python中使用生成器实现对协程的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        r = '200 OK'

def produce(c):
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

c = consumer()
produce(c)

常用库函数

os

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os

os.name
os.listdir(path)
os.getcwd()
os.path.abspath(path)
os.system(shell)
os.path.dirname(path)
os.path.basename(path)
os.path.join(path1,path2,...)
os.mkdir(path)
os.makedirs(path)
os.chdir(path)
os.path.isfile(path)
os.path.isdir(path)
os.path.exists(path)

装饰器

用类去做装饰器

可以用于非参数化装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DecoratorAsClass:
	def __init__(self,function):
		self.func = function

	def __call__(self,*args,**kwargs):
		result = self.func(*args,**kwargs)
		print "hehe"
		return result

def func():
	print "hello"

dac = DecoratorAsClass(func)
dac() # hello,hehe

参数化装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
def repeat(number=3):
	def actual_repeat(func):
		def wrap(*args,**kwargs):
			for i in range(number):
				result = func(*args,**kwargs)
			return result
		return wrap
	return actual_repeat

# 即使使用默认参数,也需要有括号
@repeat()
def foo():
	print "hehe"

保留内省

以上的装饰器会让原函数时区注释文档和函数名称,使用functools可解决问题

1
2
3
4
5
6
7
from functools import wraps

def preseving_decorator(func):
	@wraps(func)
	def wrap(*args,**kwargs):
		return func(*args,**kwargs)
	return wrap

使用场景

  • 检查参数
  • 缓存:将输出保存在某个变量中,在一定时间内,如果再输入一样的参数,那么输出直接从变量中查询
  • 代理:比如flask使用装饰器做路由分发,django使用装饰器做账号认证
  • 上下文

类装饰器

调用对象自己就是调用call方法

1
2
3
4
5
6
class Animal(object):
	def __call__(self,words):
		print "hello",words

cat = Animal()
cat("hehe") # hello,hehe

迭代器

实现了迭代器协议的容器对象

  • next 返回容器下一个元素
  • iter 返回迭代器本身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CountDown(object):
	def __init__(self, step):
		self.step = step

	def __next__(self):
		if step <= 0:
			raise StopIteration
		self.step -= 1
		return self.step

	def __iter__(self):
		return self
for element in CountDown(4):
    print(element)

yield

使用next获取yield的值

def fibonacci():
	a,b= 0,1
	while True:
		yield b
		a,b = b,z + b

fib = fibonacci()
next(fib)
next(fib)
next(fib)

使用send向yield发送参数

def psy():
    print("okok")
    while True:
        answer = (yield)
        if answer is not None:
            if answer.endswith('?'):
                print("dont ask more")
            elif 'good' in answer:
                print("nonono")
            else:
                print("whatever")

p = psy()
next(p)
p.send("hehe?")
p.send("not that good")
p.send("jkl")

super多重继承的问题

在多重继承中,在初始化的时候容易产生问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A:
    def __init__(self):
        print("A",end="")
        super().__init__()

class B:
    def __init__(self):
        print("B",end="")
        super().__init__()

class C(A,B):
    def __init__(self):
        print("C",end="")
        A.__init__(self)
        B.__init__(self)

C() # 输出是 CABB,因为在A中调用了super().__init__()

最佳实践

  • 避免多继承
  • super使用必须一致,要么全部用super,要么全不用
  • 显示的继承object,为了避免代码在python2中混用新式类和旧式类
  • 调用父类的时候查看层次结构,使用__mro__

描述符

  • __set__(self,obj,type=None)
  • __get__(self,obj,value)
  • __delete__(self,obj)

实现了__get__()__get__()的描述符是数据描述符,如果只实现了__get__()那就是非数据描述符

在每次查找对象的属性的时候,__getattribute__(),会按以下顺序查找属性

  1. 该属性是否是实例的类对象的数据描述符
  2. 如果不是,那就查看该属性能否在实例的__dict__中找到
  3. 查看该属性是否是实例的类对象的非数据描述符

python已经使用描述符将类函数绑定为实例方法

延迟求值属性

class InitOnAccess(object):
	def __init__(self,klass,*args,**kwargs):
		self.klass = klass
		self.args = args
		self.kwargs = kwargs
		self._initialized = None

	def __get__(self,instance,owner):
		if self._initialized is None:
			print('initialized!')
			self._initialized = self.klass(*self.args,**self.kwargs)
		else:
			print('cached')
		return self._initialized

class MyClass():
	lazy_bone = InitOnAccess(list,"argument")

m = MyClass()
m.lazy_bone
m.lazy_bone

property

将属性连接到方法上

class Rectangle(object):

    def __init__(self,x1,y1,x2,y2):
        self.x1,self.x2,self.y1,self.y2 = x1,x2,y1,y2

    @property
    def width(self):
        return self.x2 - self.x1

    @width.setter
    def width(self,value):
        self.x2 = self.x1 + value


rec = Rectangle(1,5,2,6)
print(rec.width)
rec.width = 10
print(rec.width)

__getattr__

当获取一个对象的某个不存在的属性的时候,就会调用这个对象的这个方法

__getattribute__

对象的所有属性都会调用这个方法,如果自定义修改的话,比较容易出现无限循环。注意在这个函数里面调用属性的时候使用super

__slots__

会限制类的实例中可以添加的属性,但是实际上这个是用来优化类的属性存储的,一旦设置了这个属性,解释器就会使用类似命名元组的存储结构进行存储类的属性,而不是使用字典去存储

解释器会忽略继承的__slots__属性

如果不把__weakref__加入__slots__中,该实例就不能作为弱引用

元类

python中一切皆对象,是对象就可以在运行时进行修改。如下图 avatar

任何一个实例是一个对象,是对象就是某一个类实例化而生成的;类也是对象,那么也是某一个类实例化生成的,最终是type的实例化,

类都有父类,最终的父类是object类。

可以使用type生成一个实例(类),

klass = type('MyClass',(object,),{'method':lambda x:print(1)})

k = klass()
k.method() # 1

自定元类的参数与type一致,并且返回另一个类对象即可

1
2
3
name:保存在__name__属性中的类的名称
bases:父类的列表,成为__bases__属性
namespace:包含主题空间的映射,将成为__dict__属性

元类的模板如下

class MetaClass(type):
        # 创建具体的类对象
	def __new__(mcs,name,bases,namespace):
		return super().__new__(mcs,name,bases,namespace)

	# 创建namespace对象
	@classmethod
	def __prepare__(mcs,name,bases,**kwargs):
		return super().__prepare__(name,bases,**kwargs)

	# 初始化,此时已经生成类对象cls
	def __init__(cls,name,bases,namespace,**kwargs):
		super().__init__(name,bases,namespace)

	# 覆写类实例创建和初始化的默认方式
	def __call__(cls,*args,**kwargs):
		return super().__call__(*args,**kwargs)

实际编写元类的如下

class RevealingClass(metaclass=RevealingMeta):
	def __new__(cls):
		return super().__new__(cls)

	def __init__(self):
		super().__init__()

# 对应元类的初始化过程 __prepare__  --> __new__ --> __init__

内置方法和实现的关系

1
2
3
4
5
6
7
8
9
10
11
abs    -->  __abs__
bool   -->  __bool__
+      -->  __add__
+=     -->  __iadd__
*      -->  __mul__
-      -->  __sub__
/      -->  __truediv__
repr   -->  __repr__ #如果没有__str__,就会使用这个
str    -->  __str__ #print的时候也会调用这个
len    -->  __len__
hash   -->  __hash__

列表推导

在某些情况下可以替换filter、map

1
2
[ord(s) for s in "hello world"]
map(lambda x:ord(x),"hello world")

可变对象

  1. 在函数的参数设置默认值是可变对象,比如是列表,那么这个函数的所有调用都会使用这一个可变对象,而不是在每次调用的时候新建一个
  2. 在使用*进行列表和元组生成的时候,当列表或者元组里面的元素是可变对象的时候,复制后的列表和元组里面是指向同一个对象的

可散列对象

一个可散列对象,在其生命周期内,散列值是不变的,这个对象实现了__hash____eq__方法,元组是不可变对象,但是当元组内部存在可变对象元素的时候,这个元组就不是可散列对象

字典推导

1
2
what = [(1,'beijing',(2,'shenzhen'))]
ever = {city:number for number,city in what}

不可变映射类型

在python3.3之后,在types模块中有MappingProxyType,这个类会返回一个只读的映射视图

from types import MappingProxyType
di = {1:"D"}
di_proxy = MappingProxyType(di)
di_proxy[2] = "C" #将会报异常

集合

集合中的元素是可散列的,但是set本身是不可散列的,frozenset是可以散列的。

集合中自带的中缀运算符

he = set([1,2,3])
ha = set([2,3,4,5,6])
he & ha
he | ha
ha - he
ha in he
ha <= he
he < ha
he >= ha
he > ha

创建集合的时候,使用一下俩种方式,一般俩说前者比后者要更高效,因为针对前者,解释器会利用BUILD_SET字节码直接创建集合,而后者是利用构造函数创建

1
2
a={1,2,3}
b=set([1,2,3])

集合推导

1
{i for i in range(10)}

字典中的散列表

散列表是一个稀疏数组,散列表里的单元叫做表元,在字典的散列表中,一个键值对占用一个表元,所有表元的大小相同。

如果俩个对象的比较(==)的时候,那么这俩个对象的散列值也要相同,

my_dict[key]中,解释器会先计算hash(key),根据值的后几位去查找表元,如果没找到,就报KeyError,如果不为空,就会在表元里面找到found_key:found_value,这个时候解释器就会检验key==found_key,如果为真,就会返回found_value。

如果key和found_key和key不匹配的话,这种情况情况就是散列冲突。原因是散列值的后几位相同。这时解释器就会在散列值中另外取几位,处理过后得到的数字当做索引,寻找新的表元。

  1. 键必须是可散列的。

 - 用户自定义对象默认是可散列的,散列值由id获取。

  1. 字典在内存上开销较大。

 - 如果需要需要存放数量巨大的记录,那么最好存放在元组或者具名元组构成的列表

  1. 键查询很快

  2. 键的次序取决于添加的顺序

  3. 向字典里面添加新的键,可能会改变已有键的顺序

 - 不要在迭代一个字典的时候修改一个字典

字符和字节

把码位转换成字节序列的叫编码,把字节序列转换成码位的过程是解码

二进制不可变bytes和可变bytearray,二者的元素都是位于0-255之间的整数。

1
bytes.fromhex("31 21 0A")

处理文本文件

在python3中,默认的open函数读取文件的时候自动做必要的解码,在写入文件的时候做必要的编码,python2需要使用io.open()

reduce

在python3中,reduce被放到functools模块中,而内置的sum函数就可以取代reduce的求和功能

all和any

1
2
3
all([]) #
all([1,2,0]) # 所有元素是真值,返回True
any([1,2,0]) # 有元素是真值,返回True

仅限关键字参数

在python3中,定义函数时若想指定仅限关键字参数,要把它们放到前面有*的参数后面。如果不想支持数量不定的定位参数,但是想支持仅限关键字参数,在签名中放一个*

def hello(a,*,b=1):
    print(a)

__defaults__ 定位参数的默认值

__kwdefaults__ 仅限关键字参数的默认值

__code__.co_varnames 参数名称,不包括变长参数

__code__.co_argcount 参数个数,不包括变长参数

from inspect import signature
sig = signature(hello)
print(str(sig)) #获取函数hello的参数部分完整的形式
print(sig.parameters) #这是一个有序字典(OrderedDict)

函数注解

def hello(name:str,where:'int > 0' = 10)  -> str:
    return "hello world"

注解只会存储在__annotations__中,解释器不会做任何其他事情

函数式编程

函数式编程关心数据的映射,命令式编程关心解决问题的步骤

函数式编程具有“函数式一等公民”的特性

operator

#包括很多常用运算符的函数
from operator import mul

#itemgetter(1) 相当于 lambda x:x[1],本身就是可迭代对象中的一个元素,itemgetter(1,0)就相当于lambda x:(x[1],x[0])
from operator import itemgetter

for c in sorted(data,key=itemgetter(1)):
   print(c)

#attrgetter(name) 相当于 lambda x:getattr(x,name),attrgetter(name,age) 相当于 lambda x:(getattr(x,name),getattr(x,age))
from operator import attrgetter

info  = attrgetter('name','age')

class Student(object):
	name = "hehe"
	age = 17

stu = Student()

print(info(stu))

from operator import methodcaller

myupper = methodcaller('upper')

myupper("haha") # 相当于 "haha".upper()

变量作用域

b=3
def hello():
    print(b)
    b = 1
# 该函数执行时会报错,因为b在函数中定义,解释器就会把b视为局部变量,在打印b的时候就会去找局部变量b,而此时b还没有定义,所以报错

闭包

函数访问在定义体之外定义的非全局变量

avatar

avg = make_averager() #执行过后,make_averager的局部作用域已经消失,对averager的来说,自由变量series已经被绑定
avg(10) # 10
avg(11) # 10.5
avg(12) # 11
print(avg.__code__.co_freevars) # (series,) 
print(avg.__closure__[0].cell_centents)

nonlocal

对于在闭包中给不可变对象赋值,会隐式地将自有变量更改为局部变量,从而导致异常

def hehe():
    count = 0
    total = 0
    def average(new):
        count += 1  # count是不可变对象,重新赋值相当于默认定义了这个变量,但是实际没有在average内部定义count,执行到这里就会报错
	total += new
	return total/count
    return average
def hehe():
    count = 0
    total = 0
    def average(new):
        nonlocal count,total  #把变量编程自有变量,每次有更新的时候就会更新绑定,python2没有这个关键字,只能通过转换成可变对象进行操作
        count += 1
        total += new
        return total/count
    return average

弱引用

del

del不是删除对象,而是删除对象的引用

weakref

python控制台会有_绑定结果不为None的表达式的值

import weakref

a = {1,2}
b = weakref.ref(a)
b()
a = {3,4}
b()
b() is None # Flase
b() is None # True

WeakValueDictionary

是可变类型的映射,值是对象的弱引用,当被引用法人对象被删除后,对应的对象的键就会被删除

list和dict不能作为弱引用的目标,但是它们的子类可以;int和tuple不能作为弱引用的目标,其子类也不可以

classmethod staticmethod

classmethod用于装饰类专属的方法,装饰的函数的第一个参数都是指代类,staticmethod就是普通的函数,只是写在了类里面。

格式化

print('1 dao = {money:0.2f}'.format(money=qian))
print(format(42,"b")) #用二进制显示数字42

对自定义类使用format,如果没有定义__format__,那么久使用继承自object中的__format__,就是str(),但是这种情况无法使用格式说明

基本的序列协议

实现__len____getitem__,从而支持索引、遍历、in操作

实现了__setitem__机会将序列变成可变序列,从而实现一些就地操作,比如就地打乱

内置类型子类

内置类型的方法不会调用子类覆盖的方法

avatar

但是__missing__方法可以正常运行

结论:不要直接继承内置的类,如果有需要,继承collections中的UserDict、UserList、UserString,内置类型dict,str,list都是使用C实现的

可迭代

iter(x)

  1. 如果对象实现了__iter__函数,那就调用它,获取一个迭代器

  2. 如果没有,但是实现了__getitem__方法,解释器会创建一个迭代器,利用这个方法将元素放进去

  3. 如果创建失败,那么抛出异常

可迭代对象

  • 实现了__iter__

  • 实现了__getitem__

Python从可迭代对象中获取迭代器。

标准的迭代器接口有俩个方法:

  • __next__:返回迭代器下一个元素,如果没有,抛出StopIteration

  • __iter__:返回self

可迭代对象不能拥有__next__方法,必须拥有__iter__方法;迭代器可以一直迭代,__iter__方法应该返回自身

iter(func,tag) # 返回一个可调用的迭代器,在迭代的时候,会一直运行func,知道func的返回值是tag,并且不输出tag

1
2
3
with open("data.txt","r") as f:
    for line in iter(f.readline,"\n"):
        print(line)

生成器

函数中使用yield,该函数就叫生成器函数,生成的对象就是生成器对象

生成器表达式

列表推导式会一次性生成一个列表,生成器表达式

def gen():
    print("1")
    yield 'A'
    print("2")
    yield 'B'
    print("3")

listQ = [x for x in gen()] # 直接生成一个列表
genQ = ( x for x in gen()) # 生成一个生成器,只在需要的时候运行生成器

上下文管理器和with

上下文管理器包括__enter____exit__俩个方法,with语句开始运行时,会调用上下文管理器对象__enter__方法,运行结束后,会调用__exit__方法

with open("text.txt") as fp: #执行with 后面的表达式得到的结果是上下文管理器对象,不过,把值绑定到目标变量上(as 子句)是在上下文管理器对象上调用 __enter__ 方法的结果
    fp.read()
class LookingClass:

    def __enter__(self):
        import sys
	self.origin_write = sys.stdout.write
	sys.stdout.write = self.reverse_write
	return 'HEHE'

    def reverse_write(self,text):
        self.origin_write(text[::-1])

    def __exit__(self,exc_type,exc_value,traceback):  #异常类,异常实例,traceback对象
        import sys
	sys.stdout.write = self.origin_write
	if exc_type is ZeroDivisionError:
	    print("No")
	    return True

contextlib

@contextmanager

通过编写一个类,并且实现__enter____exit__来编写上下文管理器相对比较麻烦,@contextmanager装饰器可以优化这个问题

import contextlib

@contextlib.contextmanager
def looking_glass():
    import sys
    origin_write = sys.stdout.write
    
    def reverse_write(text):
        origin_write(text[::-1])

    sys.stdout.write = reverse_write
    try:
        yield "ASDFG" # with语句会在这里暂停
    except Exception:
        pass
    finally:
        sys.stdout.write = origin_write # 出了with语句,就会执行这句话

with looking_glass() as what:
    print("HAHA")  # AHAH
    print(what)    # GFDSA

协程

yield

avatar

def averager():
    total = 0
    count = 0
    average = None
    while True:
        term = yield average
	total += term
	count += 1
	average = total/count

v = averager()
next(v) # 预激协程
v.send(10) # 10
v.send(12) # 11
v.send(14) # 12

当协程遇到异常会向上抛出,如果上层没有处理,那么就会导致协程退出

可以在生成器调用throw和close方法停止协程

def handle():
    while True:
        try:
	    x = yield
	except ZeroDivisionError:
	    print("okok")
	else:
	    print("x is ",x)

h = handle()
next(handle)
handle.send(1)
handle.throw(ZeroDivisionError) #通过调用throw,向协程抛出异常,由于协程有处理,所以可以继续运行
handle.throw(Exception) # 协程终止
from collections import namedtuple
Result = namedtuple('Result', 'count average')

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total/count

    return Result(count, average)

a = averager()
next(a)
a.send(10)
a.send(20)
try:
    a.send(None)
except StopIteration as exc:
    result = exec.value

yield from

def chain(*iterable):
    for it in iterable:
        yield from it

s = 'abc'
t = tuple(range(3))
print(list(chain(s,t))) # ['A', 'B', 'C', 0, 1, 2]

yield from x 会调用iter(x)

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。

avatar

对于RESULT = yield from EXPR,可以简单理解成如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_i = iter(EXPR)
try:
    _y = next(_i)
except StopIteration as _e:
    _r = _e.value
else:
    while 1:
        _s = yield _y
        try:
            _y = _i.send(_s)
        except StopIteration as _e:
            _r = _e.value
        break
RESULT = _r

元编程

所有类的继承关系的始祖都是Object,所有类型关系的始祖都是type

1
2
3
4
5
# 使用type定义类,参数是名称、继承、私有化

K1 = type('Klass',(),{"name":"K1"})
K2 = type('Klass',(K1,),{"age":1})
print(K2.name)

PyCodeObject

python源码编译后,生成一个pyc文件,存放着PyCodeObject对象

编译的时候,对于代码中的一个代码块,会创建一个PyCodeObject对象与这个段代码对应,当进入一个新的命名空间,或者说作用域,就算是新的代码块

命名空间是符号的上下文环境,符号的含义取决于命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct {
    PyObject_HEAD
    int co_argcount;        /* 位置参数个数 */
    int co_nlocals;         /* 局部变量个数 */
    int co_stacksize;       /* 栈大小 */
    int co_flags;
    PyObject *co_code;      /* 字节码指令序列 */
    PyObject *co_consts;    /* 所有常量集合 */
    PyObject *co_names;     /* 所有符号名称集合 */
    PyObject *co_varnames;  /* 局部变量名称集合 */
    PyObject *co_freevars;  /* 闭包用的变量名集合 */
    PyObject *co_cellvars;  /* 内部嵌套函数引用的变量名集合 */
    /* The rest doesn’t count for hash/cmp */
    PyObject *co_filename;  /* 代码所在文件名 */
    PyObject *co_name;      /* 模块名|函数名|类名 */
    int co_firstlineno;     /* 代码块在文件中的起始行号 */
    PyObject *co_lnotab;    /* 字节码指令和行号的对应关系 */
    void *co_zombieframe;   /* for optimization only (see frameobject.c) */
} PyCodeObject;

在python代码中,PyCodeObject对象可以通过__code__属性查看,以下是在交互命令行中查看

1
2
3
4
5
6
def h():
    print(1)

dir(h.__code__)

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']

pyc生成

主要包括三个部分

  • magic number
    • 魔数,用于和解释器自己的魔数比较,避免版本不同导致字节码指令集不兼容而无法正常运行程序
  • pyc文件创建时间
    • 可以用于判定是否需要重新编译出pyc
  • PyCodeObject对象
    • 先写入类型,比如TYPE_CODE,TYPE_INT,
    • 对数值的写入,将数值转换成16进制
    • 写入字符串,这个比较麻烦。。。

字节码

python2.5一共有104条字节码指令

官方提供dis模块,可以解析字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def h():
	a= 1
	print(2)

dis.dis(h)

2         0 LOAD_CONST               1 (1)
          2 STORE_FAST               0 (a)

3         4 LOAD_GLOBAL              0 (print)
          6 LOAD_FAST                0 (a)
          8 CALL_FUNCTION            1
         10 POP_TOP
         12 LOAD_CONST               0 (None)
         14 RETURN_VALUE

最左侧一列是字节码指令对应源码的位置,第二列是字节码在co_code中的偏移位置,第3列是字节码,最后一列显示字节码参数

虚拟机框架

字节码中没有执行环境,这里的执行环境包括命名空间,以及一些其他信息,虚拟机实际执行的是PyFrameObject。

PyFrameObject结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _frame {
	PyObject_VAR_HEAD
	struct _frame *f_back; // 执行环境链上的前一个frame
	PyCodeObject *f_code;
	PyObject *f_builtins; //builtins命名空间
	PyObject *f_globals;//global命名空间
	PyObject *f_locals; //local命名空间
	PyObject **f_valuestack; //运行时栈的栈底位置
	PyObject **f_stacktop; //运行时栈的栈顶位置
	...
	int f_lasti; //上一个字节码指令在f_code中的偏移
	int f_lineno;//当前字节码对应的源码行数
	...
	PyObject *f_localsplus[1];//动态内存,维护局部变量、cell对象集合、free对象集合、运行时栈所需要的空间
}

可以使用如下方法获取当前活跃的PyFrameObject对象

1
2
3
4
5
import sys
frame = sys._getframe()
dir(frame)

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace', 'f_trace_lines', 'f_trace_opcodes']

赋值语句就是建立约束的地方,约束的容身之所就是命名空间,命名空间是用PyDictObject实现的。

一个module内部,可能有多个命名空间,每个命名空间都和一个作用域相对应,作用域仅由源程序的文本决定。

最内嵌套作用域规则: LEGB:Local、Enclosing、Global、Builtin

PyEal_EvalFramEx

遍历字节码指令序列:

  • first_instr:字节码指令序列开始
  • f_lasti:上一条已经执行的字节码在co_code中的所以
  • next_instr:指向下一条待执行的字节码指令位置

python中的一个线程就是操作系统上的原生线程

  • 一个线程有一个PyThreadState对象,
  • PyInterpreterState是python中进程的抽象

多线程机制

全局解释器锁GIL

  • 时钟中断,即执行了多少个指令后,进行线程线程调度,sys.getcheckinterval(),默认是100

thread模块

  • start_new_thread对应thread_PyThread_start_new_thread
    • 初始化bootstate结构boot,保存线程一切信息
    • 初始化Python多线程环境
      • 创建GIL,结构体是NRMUTEX
        • owned
        • thread_id
        • hevent
          • 在win32环境下,hevent就是Win32的Event对象
      • 在获取GIL时,通过参数waitflag,0,python会检查GIL是否可用,通过owned值,如果为-1,表示可以获得
      • 当释放GIL时,会通知所有等待中的进程,至于唤醒哪个进程,就在操作系统自己的实现
    • 以boot为参数,创建操作系统原生线程

内存管理

内存分配

小块空间的内存池

block
pool

默认大小为系统的内存页,一般时4KB。一个pool管理着许多大小相同的block

struct pool_header {
	union {
		block *_padding;
		uint count;
	}ref;
	block *freeblock;
	struct pool_header *nextpool;
	struct pool_header *prevpool;
	uint arenaindex;
	uint szidx;
	uint nextoffset;
	uint maxnextoffset;
}

在pool内,block可以连续申请,当其中一个block被释放的时候,为了避免内存不连续,将这些被释放的block按顺序组成链表,freeblock就是这个链表的头

arena

pool的集合就是arena,默认大小时256KB,即64个pool

struct arena_object{
	uptr address;
	block* pool_address;
	uint nfreepools;
	uint ntotalpools;
	struct pool_header* freepools;
	struct arena_object* nextarena;
	struct arena_object* prearena;
}

多个arena构成数组,数组的首地址由arenas维护。

当arena没有与pool建立联系的时候,这时的arena处于未使用的状态,一旦建立了联系,这时的arena就是可用状态

usedpools

默认的大小块内存的边界SMALL_REQUEST_THRESHOLD为256字节,当申请的内存小于256字节时,PyObject_Malloc会在内存池中申请,当大于时,就malloc

pool状态

  • used 所有used pool都被usedpools管控
  • full
  • empty 所有的empty pool都被freepools管控

垃圾收集

主要是引用计数,标记清除和分代收集是为了打破循环引用的补充计数

可收集对象链表

被垃圾收集机制监控的container对象

  • PyGC_HEAD
  • PyObject_HEAD
  • Container object

其中PyGC_Head,除了包含可收集对象链表的前后指针外,还包含gc_ref

分代收集

在python中,有三代,实际是维护了三个链表,_PyGC_generation0、_PyGC_generation1、_PyGC_generation2

所有新创建的对戏都会被加入第0代链表中,第0代内存链表有700容量,一旦超过700,就会触发内存回收

当对第1代进行回收前,会将第0代的链表merge到第1代后面,对第2代回收也是如此

寻找root object集合

有效引用计数,即将循环引用去除,A引用了B,B引用了A,那么A、B的游戏引用计数就是0,gc_ref就是这个值

垃圾收集的第一步,就是遍历可收集对象链表,设置gc_ref,然后遍历container对象中的每一个引用,将这些引用对象的gc_ref减1,遍历完整个列表后,gc_ref不为0的对象,就是rootobject

然后,从rootobject出发,沿着引用链,一步一步标记不能回收的内存,最终,形成俩个一条reachable链表和一条unreachable链表,

对于unreachable链表,其中的对象,如果有__del__方法,那么就将其移到garbage的PyLiastObject对象中

使用C/C++开发模块

//添加头文件
#include <Python.h>
#include <stdio.h>

//编写对应的功能函数
void fly(const char *name)
{
    printf("%s is flying.\n", name);
}

//添加python调用版本,在其中调用编写的功能函数
static PyObject *bird_fly(PyObject *self, PyObject *args)
{
    const char *name;
    if (!PyArg_ParseTuple(args, "s", &name))//把python对象的args转换为C对象name,s表示是string转换成char*,其他还有:i int->int,z string or None -> char*, O :object -> PyObject*,还可以解析字典元组之类的,{s:i,s:i} :dict -> char*,int.char*,int
        return NULL;
    fly(name);//执行功能函数

	//这里是返回值,因为功能函数没有返回值,所以返回Py_None
	//如果是有返回值,那么需要使用Py_BuildValue将C对象转换成python对象,其中的格式和PyArg_ParseTuple一样
	//return (PyObject*)Py_BuildValue("i",1);
    Py_INCREF(Py_None);//显示地增加Py_None对象的引用计数
    return Py_None;
}

//添加向Python呈现C函数的方法表bird_methods
static PyMethodDef bird_methods[] = {
    { "fly", bird_fly, METH_VARARGS, "Bird fly" },//分别是python解释器调用的函数名,实际执行的函数体,函数签名类型(一般是METH_VARARGS即元组的形式传参,无参数时METH_NOARGS,也可以METH_KEYWORD,即使用python字典形式传参),函数描述
{ NULL, NULL, 0, NULL }
};

//添加模块初始化函数initbird,必须以init模块名 这样,这里使用的python2版本的初始化方式
PyMODINIT_FUNC initbird(void)
{
    PyImport_AddModule("bird");
    Py_InitModule("bird", bird_methods);
}

//python3使用如下形式
/*
static struct PyModuleDef BirdModule = {
    PyModuleDef_HEAD_INIT,//默认
    "bird",//模块名
    NULL,
    -1,
    bird_methods //上面的数组
};

void PyInit_bird(void){
    PyModule_Create(&BirdModule);
}
*/

多线程和多进程

threading

threading模块封装了很多的同步原语,

  • Lock
    • 原始锁,实际上就是使用匿名信号量实现的,一般语言里面的原始锁可以由互斥量、临界区、信号量实现的,python这里选择了匿名信号量实现,也就是说,这个锁,谁(所有线程)都可以减去获取锁acquire、就是信号量减一,释放锁release、信号量加一
  • Rlock
    • 重入锁RLock,相当于java的重入锁,基于Lock实现的,就是在acquire的时候记录当前线程id,release的时候判定线程是不是acquire的线程
  • condition,条件,维护一个锁Lock/RLock,和一个waiting池,线程通过acquire获得condition对象,当wait的时候,线程会释放condition内部的锁进入block状态,同时waiting池中记录这个线程,当notify,condition对象从waiting池中挑选一个线程,通知其acquire
  • Semaphore,信号量,基于Condition实现
  • BoundedSemaphore,防止Semaphore被无限释放
  • event 基于Condition,控制线程间运行顺序
  • Barrier 基于Condition,让特定线程先运行
  • Timer 基于Thread和Event实现,定时任务

从锁上看,python原生库提供的支持很基础,而且没有特定的优化,相对于java在锁方面,大量运用CAS原语,提供各种字节码指令,优化各种并发情况下的同步操作,以达到高性能,python在这方面就很随意,为什么呢?因为python本身有着GIL,在多线程效率方面就天生有着限制,我觉得GIL就是对单核操作系统执行多进程的模拟,另外,如果说高并发的话,python可以用协程实现,比如tornado、asyncio,底层使用是IO多路复用、select/poll/epoll/kqueue等,不过老实说,在协程方面,python又不如go、scala、erlang等完善,python自己的协程框架asyncio的三方生态太少,单说高并发,也不及Go等

创建线程调用链: start -> _thread._start_new_thread -> thread_Pythread_start_new_thread -> PyThread_start_new_thread -> pthread_create

通过这个调用链看出,在linux,python的线程实现,就是通过pthread库函数创建的,是一种用户态的线程,和linux内核进程一对一

线程池

不管是线程池还是进程池,都要维护俩个核心东西:进(线)程队列和任务队列,进(线)程队列中的进程从任务队列中取任务去执行,为了取任务的时候,各个进(线)程不冲突,需要一个锁,并将结果返回给一个对象,一般都叫future对象。