1. Python对象模型
- Python中
一切皆为对象
。 - 对象拥有三个特性:
id
、类型
和值
。 -
类
把数据与功能绑定在一起。创建新类就是创建新的对象类型,从而创建该类型的新实例
。
类型对象与实例化对象
- Python中的类型也是一种对象 ,称为
类型对象
。
>>> int
<class 'int'>
>>> type(int)
<class 'type'>
>>> type(type)
<class 'type'>
-
type对象
是所有类型对象的元类。 - 通过
类型对象
实例化的对象称为实例化对象
:
>>> int('1')
1
>>> type(1)
<class 'int'>
-
object对象
是所有对象的基类:
>>> issubclass(int,object)
True
>>> type(object)
<class 'type'>
>>> issubclass(type, object)
True
函数对象
- Python中函数也是对象,称为
函数对象
:
def func(x, y):
return x + y
print(type(func))
>>>
<class 'function'>
- 我们也可以使用lambda定义函数:
func = lambda x,y: x + y
print func(3,4)
- 函数可以作为一个对象,进行参数传递:
def print_func_output(f, x, y):
print(f(x, y))
func = lambda x,y: x + y
f = func
print_func_output(f, 1, 2)
>>>
3
- Python中所提供的map(), filter(), reduce()等函数的第一个参数都是一个函数对象:
# map()将第一个参数中的函数应用于列表的每一个元素,返回一个迭代器
[2, 4, 6]
>>> list(map((lambda x: x * 2), [1, 2, 3]))
# filter()将第一个参数中的函数应用于列表的每一个元素,过滤掉函数结果为False的元素,返回一个迭代器
>>> list(filter((lambda x: x % 2 == 0), [1, 2, 3, 4, 5]))
[2, 4]
# reduce累进地将第一个参数中的函数作用于列表的每一个元素。
>>> from functools import reduce
>>> reduce((lambda x, y: x + y), [1, 2, 3])
6
2. 类型对象与实例对象
类型对象
- 每个自定义类型对象都有一个
属性空间
,信息存储于 dict 中:
class Person:
num = 0
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
print('My name is ' + self.name)
@classmethod
def get_num(cls):
return cls.num
@staticmethod
def say_hello():
print('Hello')
for k, v in Person.__dict__.items():
print(f'{k}:{v}')
>>>
__module__:__main__
num:0
__init__:<function Person.__init__ at 0x000001EE4E9C12F0>
introduce:<function Person.introduce at 0x000001EE4E9C1378>
get_num:<classmethod object at 0x000001EE4E9BE9B0>
say_hello:<staticmethod object at 0x000001EE4E9BEC50>
__dict__:<attribute '__dict__' of 'Person' objects>
__weakref__:<attribute '__weakref__' of 'Person' objects>
__doc__:None
由此可见, 类的属性空间中存放了类变量,静态方法,类方法,实例方法和一些内置属性。这些方法都是以一个普通的函数对象方式保存在类的属性空间中。
静态方法
其实就是普通的函数,只是在类的定义体中,而不是模块层定义。它的第一个参数不是特殊的值,不能访问实例变量,可以通过类名访问类变量。可以通过类名的方式调用静态方法:
Person.say_hello()
>>>
Hello
-
类方法
只能访问类变量,不能访问实例变量。它的第一个参数是类本身。但在调用类方法时不需要为该参数传递值:
print(Person.get_num())
>>>
0
-
实例方法
可以访问实例变量,它的第一个参数是当前对象。我们可以通过对象名调用实例方法,也可以通过类来调用,但需要显式传递 self 参数:
p = Person('Alice', '20')
Person.introduce(p)
p.introduce()
>>>
My name is Alice
My name is Alice
实例对象
- 实例对象的属性信息也存储于 dict 中:
p = Person('Alice', '20')
for k, v in p.__dict__.items():
print(f'{k}:{v}')
>>>
name:Alice
age:20
- 通过实例对象访问属性时,默认先在实例对象的属性空间字典中查找;再到类型对象的属性空间查找:
class Person:
name = 'person'
num = 0
def __init__(self, name):
self.name = name
p = Person('Alice')
print(p.name)
print(p.num)
>>>
Alice
0
属性和方法可见度
- Python的属性/方法可见度只有public和private两种。
- 在定义属性或方法时,在属性/方法名前面加了2个下划线'__'。则表明该属性/方法是私有属性/方法。不能在对象上通过 . 操作符直接访问:
class Person:
def __init__(self, name, age):
self.name = name
self.__age = age
def __get_age():
return self.__age()
p = Person('Alice', '20')
p.__age
p.__get_age()
>>>
AttributeError: 'Person' object has no attribute '__age'
AttributeError: 'Person' object has no attribute '__get_age'
- 但子类之所以无法直接访问私有属性/方法,是因为私有属性/方法名被更换成了
_类名__属性名
,与我们所访问的属性/方法名不同所致。Python并未从语法上严格保证其私有性:
print(p.__dict__)
print(p._Person__age)
print(p._Person__get_age())
>>>
{'name': 'Alice', '_Person__age': '20'}
20
20
- 因此Python中也可以用单下划线
_
开头的字段,通过约定表示不希望类的用户直接访问该属性。
@property和setter方法
- 通过@property和setter方法可以在对属性的访问和设置时设置自定义的行为:
class Person:
def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height
@property
def age(self):
return self.__age
@age.setter
def age(self, value):
if value > 0:
self.__age = value
else:
raise ValueError('Age must > 0')
p = Person('Alice', 20, 160)
print(p.age)
p.age = -10
>>>
Querying age
20
ValueError: Age must > 0
- @property和setter方法的一个缺点是不能复用,例如我们想对height属性实现同样的方法,则需要再创建两个方法。描述符可以解决这个问题。
描述符
-
描述符
是实现了描述符协议
的类。描述符协议方法有:
__get__(),在设置属性时调用该方法;
__set__() ,在读取属性时调用该方法;
__delete__() ,在删除属性时调用该方法;
- 类里实现了上述其中一个方法,就称该类为描述符。使用描述符能够对多个属性运用相同的自定义存取逻辑:
class PositiveNumber: # 描述符类
def __init__(self, attr_name):
self.attr_name = attr_name # attr_name为托管实例中存储值的属性的名称。
def __set__(self, instance, value): # instance是托管实例,self是描述符实例。
if value > 0:
instance.__dict__[self.attr_name] = value # 此处必须直接存入__dict__,否则会调用setattr函数会导致无限递归。
else:
raise ValueError(f'{self.attr_name} must > 0')
def __get__(self, instance, owner): # instance是托管实例,owner是托管类
print(f'Querying {self.attr_name}')
return instance.__dict__[self.attr_name]
class Person: # 托管类
age = PositiveNumber('age')
height = PositiveNumber('height')
def __init__(self, name, age, height):
self.name = name
self.age = age
self.height = height
p = Person('Alice', 20, 160)
print(p.age)
p.height = -10
>>>
Querying age
20
ValueError: age must > 0
- 如果一个类同时定义了
__get__
方法和__set__
方法,则称之为数据描述符
,如果只有__get__
方法,则称之为非数据描述符
。 - 每次使用类名.属性名,或者 getattr(类名,属性名)的调用方式访问属性时,属性查找优先级顺序为:数据描述符 > 实例的
__dict__
> 非数据描述符 > 类的__dict__
__getattribut__
, __setattr__
和__getattr__
,
- 程序每次访问属性时都会调用
__getattribut__
。每次设置属性时都会调用__setattr__
。 - 如果类定义了
__getattr__
,在查找不到该属性时,将会调用这个方法:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __getattr__(self, item):
print(f'Person do not have field {item}')
p = Person('Alice', 20)
p.height
>>>
Person do not have attr height
继承
- 加入继承关系后的完整属性查找优先级顺序为:
数据描述符 > 实例的__dict__
> 非数据描述符 > 类的__dict__
> 父类 - >父类的父类 ->Object->调用类的__getattr__
->若仍不存在,会引发一个 AttributeError 异常
- 一个问题
class Parent(object):
x = 1
class Child1(Parent):
pass
class Child2(Parent):
pass
print Parent.x, Child1.x, Child2.x
Child1.x = 2
print Parent.x, Child1.x, Child2.x
Parent.x = 3
print Parent.x, Child1.x, Child2.x
>>>
1 1 1
1 2 1
3 2 3
这个答案的关键是,在 Python 中,类变量在内部是作为字典处理的。如果一个变量的名字没有在当前类的字典中发现,将搜索祖先类(比如父类)直到被引用的变量名被找到。
因此,在父类中设置 x = 1 会使得类变量 X 在引用该类和其任何子类中的值为 1。这就是因为第一个 print 语句的输出是1 1 1。
随后,如果任何它的子类重写了该值(例如,我们执行语句 Child1.x = 2),然后,该值仅仅在子类中被改变。这就是为什么第二个 print 语句的输出是 1 2 1
最后,如果该值在父类中被改变(例如,我们执行语句 Parent.x = 3),这个改变会影响到任何未重写该值的子类当中的值(在这个示例中被影响的子类是 Child2)。这就是为什么第三个 print 输出是 3 2 3。
3. 函数:作用域,闭包,装饰器
作用域
-
Python作用域
分为:
- local,局部作用域,即当前函数作用域
- enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域
- global,全局作用域,即代码所在模块的作用域
- built-in,内置作用域,系统固定模块的作用域
- 在表达式中引用变量时,Python解释器会按照上述顺序遍历各作用域以解释该引用,如果都没有定义过名称相符的变量,将抛出NameError异常。
闭包
-
闭包
是一种定义在某个作用域中的函数,这种函数引用了那个作用域里面的变量:
def func_outer(prefix):
def func_inner(name):
return prefix + ' ' + name
return func_inner
func_inner = func_outer('Hello')
print(func_inner('A'))
>>>
Hello A
-
nonlocal关键字
可以让相关变量赋值时从local作用域延伸到enclosing作用域中查找该变量:
def func_outer(prefix):
count = 0
def func_inner(name):
nonlocal count
count += 1
return '{} {}, id {}'.format(prefix, name, count)
return func_inner
func_inner = func_outer('Hello')
print(func_inner('A'))
print(func_inner('B'))
>>>
Hello A, id 1
Hello B, id 2
-
global关键字
可以让相关变量赋值时从enclosing作用域延伸到global作用域中查找该变量:
a = 1
def func():
global a
a = 2
func()
print(a)
>>>
2
装饰器
-
装饰器
一般用来修饰函数。能够在执行受到封装的原函数执行之前和执行完毕后运行一些附加代码,实现公共功能,达到代码复用的目的:
# 定义一个装饰器
def trace(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print(f'function: {func.__name__}, parameter: {args},{kwargs}, result: {res}')
return res
return wrapper
# 使用@修饰函数,其效果等于以该函数为参数调用修饰器,
# 再把修饰器所返回的结果赋给同一个作用域中与原函数同名的变量,
# 即: fibonacci = trace(fibonacci)
@trace
def fibonacci(n):
if n in (0, 1):
return n
return fibonacci(n - 2) + fibonacci(n - 1)
>>>
function: fibonacci, parameter: (1,),{}, result: 1
function: fibonacci, parameter: (0,),{}, result: 0
function: fibonacci, parameter: (1,),{}, result: 1
function: fibonacci, parameter: (2,),{}, result: 1
function: fibonacci, parameter: (3,),{}, result: 2
4. 列表推导式,生成器,迭代器
列表推导式
-
列表推导式
用于生成一个list
>>> ['x' for n in range(5)]
['x', 'x', 'x', 'x', 'x']
>>> a=[1,2,3,4,5]
>>> [x for x in a if x % 2 == 0]
[2, 4]
- 列表推导式在推导过程中,对于输入序列中的每个值可能都要创建仅含一项元素的全新列表。输入数据非常多时,会消耗大量内存。
生成器,迭代器
- 将列表推导式所用的方括号变成圆括号,就构成了
生成器
。它们区别在于,生成器表达式运行时不会把整个输出序列呈现出来,而是返回一个迭代器
,这个迭代器每次根据生成器表达式产生一项数据。
>>> a=[1,2,3,4,5]
>>> it = (x for x in a if x % 2 == 0)
>>> next(it)
2
>>> next(it)
4
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
-
enumerate
可用将各种迭代器(也包括序列和支持迭代的对象)包装为生成器:
a = [1, 2, 3, 4, 5]
it = (x for x in a if x % 2 == 0)
for index, value in enumerate(it):
print('{}:{}'.format(index, value))
for index, value in enumerate(it):
print('{}:{}'.format(index, value))
>>>
0:2
1:4
- 需注意,迭代器只能产生一轮结果,继续执行第二轮将不会输出,也不会报错
-
zip
函数可用于同时把两个或两个以上的迭代器封装为生成器,将值汇聚成元组(tuple)。当其中一个遍历结束时,zip将不再产生元组:
a = [1, 2, 3, 4, 5]
it1 = (x for x in a if x % 2 == 0)
it2 = (x for x in a if x % 2 != 0)
for v1, v2 in zip(it1, it2):
print('{},{}'.format(v1, v2))
>>>
2,1
4,3
- 使用
yield关键字
的函数也称为生成器。每次在返回的迭代器上调用next函数时,迭代器会把生成器推进到下一个yield关键字处:
def func():
for i in range(1000):
yield [i] * 1000
gen = func()
for item in gen:
print(item)
5. 并发与并行
6. 模块加载
-
import关键字
用于导入模块 - 引入模块时,Python会按照深度优先的顺序执行下列操作:
1)在由sys.path所指定的路径中,搜寻待引入的模块。
2)从模块中加载代码,并保证这段代码能够正确编译。
3)创建与该模块对应的空对象。
4)把这个空的模块对象,添加到sys.modules里
5)运行模块对象中的代码,以定义其内容。
- 需注意
循环依赖
的问题。解决方法包括:1)调整引入顺序 2)动态引入,即在函数或方法内部使用import,会等到真正运行相关代码时,才触发模块的引入操作。但多次调用会带来更多的import开销
7. 内存管理
引用计数
- Python使用
引用计数
,为每个对象维护引用次数,并据此回收不再需要的垃圾对象。当引用次数变为 0 时就将资源释放: - sys.getrefcount可输出变量的引用计数。
>>> import sys
>>> s = '123'
>>> sys.getrefcount(s)
2
>>> n1 = s
>>> sys.getrefcount(s)
3
>>> l = [s]
>>> sys.getrefcount(s)
4
>>> del n1
>>> sys.getrefcount(s)
3
>>> del l
>>> sys.getrefcount(s)
2
- 当一个对象作为参数传个函数后,它的引用计数将加一;当函数返回,局部名字空间销毁后,对象引用计数又减一。
标记清除法
- Python还采用
标记清除法
来回收存在循环引用的垃圾对象。 - 将程序内部对象跟踪起来,是实现垃圾回收的第一步。一个对象是否需要跟踪,取决于它会不会形成循环引用。按照引用特征,Python 对象可以分为两类:
1)内向型对象 ,例如 int 、float 、 str 等,这类对象不会引用其他对象,因此无法形成循环引用,无须跟踪;
2)外向型对象 ,例如 tuple 、 list 、 dict 等容器对象,以及函数、类实例等复杂对象,这类对象一般都会引用其他对象,存在形成循环引用的风险,因此是垃圾回收算法关注的重点;
Python 为外向型对象分配内存时,对象头部之前预留了一些内存空间,以便垃圾回收模块用链表将它们跟踪起来。
标记清除法
首先找出根对象 ( root object )
集合。所谓根对象,就是指被全局引用或者在栈中引用的对象,这部对象是不能被删除的。根对象集合不难确定:我们只需遍历每个对象引用的对象,将它们的引用计数减一,最后计数不为零的就是根对象。根对象本身是
可达的 ( reachable )
,不能删除;被根对象引用的对象也是可达的,同样不能删除;以此类推。沿着引用关系遍历,遍历到的所有对象都是可达的,不能删除。不可达 ( unreachable )
的垃圾对象,就可以被安全回收。
分代回收机制
- 如果每次执行标记清除法时,都需要遍历所有对象,会影响程序性能。为了提高垃圾回收效率,Python 还引入了
分代回收机制
对象分为若干“代”( generation ),每次只处理某个代中的对象,因此 GC 卡顿时间更短。 - 一个对象存活的时间越长,它下一刻被释放的概率就越低,可以适当降低回收频率。因此,Python根据对象的存活时间进行分代,分为:
初生代
、中生代
和老生代
- 回收策略:
1)每新增 701 个需要 GC 的对象,触发一次新生代 GC ;
2)每执行 11 次新生代 GC ,触发一次中生代 GC ;
3)每执行 11 次中生代 GC ,触发一次老生代 GC (老生代 GC 还受其他策略影响,频率更低);
4)执行某个生代 GC 前,年轻生代对象链表也移入该代,一起 GC ;
5)一个对象创建后,随着时间推移将被逐步移入老生代,回收频率逐渐降低;