在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势
接下来,我们需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL。
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。保护不同的数据的安全,就应该加不同的锁。
每执行一个python程序,就是开启一个进程,在一个python的进程内,不仅有其主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,所有的线程都运行在这一个进程内,所以:
== 1、所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
2、所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。==
GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图
有了GIL的存在,python有这两个特点:
1、进程可以利用多核,但是开销大
2、多线程开销小,却无法利用多核优势
也就是说Python中的多线程是假的多线程,Python解释器虽然可以开启多个线程,但同一时间只有一个线程能在解释器中执行,而做到这一点正是由于GIL锁的存在,它的存在使得CPU的资源同一时间只会给一个线程使用,而由于开启线程的开销小,所以多线程才能有一片用武之地,不然就真的是鸡肋了。
而python的多线程到底有没有用,我们需要看任务是I/O密集型,还是计算密集型
1、如果是CPU密集型代码(循环、计算等),由于计算工作量多和大,计算很快就会达到100,然后触发GIL的释放与在竞争,多个线程来回切换损耗资源,所以在多线程遇到CPU密集型代码时,单线程会比多线程的快。
2、如果是I\O密集型代码(文件处理、网络爬虫),开启多线程实际上是并发(不是并行),IO操作会进行IO等待,线程A等待时,自动切换到线程B,这样就提升了效率,比单线程快很多
而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。
多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低
“python下想要充分利用多核CPU,就用多进程”,原因是什么呢?
原因是:每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,(多线程并不是真正意义上的并行执行,只能算并发执行)所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。
由于现实生活中运行的总进程数量总是多于 CPU核心数量!因此,严格来说并没有真正意义上的并行。现在运行的程序都是轮询调度产生的并行假象,所以如何根据实际情况选择多进程或是多线程。
1.多线程使用场景:IO密集型
2.多进程使用场景:CPU密集型
Python 直接赋值、浅拷贝、深拷贝直接赋值:其实就是对象的引用(别名)。
浅拷贝(copy):拷贝父对象,不会拷贝对象的内部的子对象。
深拷贝(deepcopy): copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象。
字典浅拷贝实例:
>>>a = {1: [1,2,3]}
>>> b = a.copy()
>>> a, b
({1: [1, 2, 3]}, {1: [1, 2, 3]})
>>> a[1].append(4)
>>> a, b
({1: [1, 2, 3, 4]}, {1: [1, 2, 3, 4]})
深度拷贝需要引入 copy 模块:
>>>import copy
>>> c = copy.deepcopy(a)
>>> a, c
({1: [1, 2, 3, 4]}, {1: [1, 2, 3, 4]})
>>> a[1].append(5)
>>> a, c
({1: [1, 2, 3, 4, 5]}, {1: [1, 2, 3, 4]})
解析
1、b = a: 赋值引用,a 和 b 都指向同一个对象。
2、b = a.copy(): 浅拷贝, a 和 b 是一个独立的对象,但他们的子对象还是指向统一对象(是引用)。
b = copy.deepcopy(a): 深度拷贝, a 和 b 完全拷贝了父对象及其子对象,两者是完全独立的。
#!/usr/bin/python
# -*-coding:utf-8 -*-
import copy
a = [1, 2, 3, 4, ['a', 'b']] #原始对象
b = a #赋值,传对象的引用
c = copy.copy(a) #对象拷贝,浅拷贝
d = copy.deepcopy(a) #对象拷贝,深拷贝
a.append(5) #修改对象a
a[4].append('c') #修改对象a中的['a', 'b']数组对象
print( 'a = ', a )
print( 'b = ', b )
print( 'c = ', c )
print( 'd = ', d )
运行结果:
('a = ', [1, 2, 3, 4, ['a', 'b', 'c'], 5])
('b = ', [1, 2, 3, 4, ['a', 'b', 'c'], 5])
('c = ', [1, 2, 3, 4, ['a', 'b', 'c']])
('d = ', [1, 2, 3, 4, ['a', 'b']])
私有属性
xx:公有变量
_x:单前置下划线,私有化属性或方法,from somemodule import * 禁止导入,类对象和子类可以访问
__xx:双前置下划线,避免与子类中的属性命名冲突,无法在外部直接访问(名字重整所以访问不到)
xx:双前后下划线,用户名字空间的魔法对象或属性,例如:int, __不要自己发明这样的名字
xx_:单后置下划线,用于避免与Python关键词的冲突
魔法方法__init__我们很熟悉了,它在对象初始化的时候调用,我们一般将它理解为"构造函数".
实际上, 当我们调用x = SomeClass()的时候调用,__init__并不是第一个执行的, __new__才是。所以准确来说,是__new__和__init__共同构成了"构造函数".
__new__是用来创建类并返回这个类的实例, 而__init__只是将传入的参数来初始化该实例.
new__在创建一个实例的过程中必定会被调用,但__init__就不一定,比如通过pickle.load的方式反序列化一个实例时就不会调用__init。
__new__方法总是需要返回该类的一个实例,而__init__不能返回除了None的任何值。比如下面例子:
class Foo(object):
def __init__(self):
print ("foo __init__")
return None # 必须返回None,否则抛TypeError
def __del__(self):
print ("foo __del__")
a = Foo()
实际中,你很少会用到__new__,除非你希望能够控制类的创建。
如果要讲解__new__,往往需要牵扯到metaclass(元类)的介绍。
对于__new__的重载,
Python文档中
也有了详细的记载
在对象的生命周期结束时, __del__会被调用,可以将__del__理解为"析构函数".
__del__定义的是当一个对象进行垃圾回收时候的行为。
有一点容易被人误解, 实际上,x.del() 并不是对于del x的实现,但是往往执行del x时会调用x.del().
怎么来理解这句话呢? 继续用上面的Foo类的代码为例:
class Foo(object):
def __init__(self):
print ("foo __init__")
return None # 必须返回None,否则抛TypeError
def __del__(self):
print ("foo __del__")
a = Foo()
a.__del__()
print(a)
del a
print(a)
运行结果:
NameError: name 'a' is not defined
foo __init__
foo __del__
foo __del__
如果调用了foo.del(),对象本身仍然存在. 但是调用了del foo, 就再也没有foo这个对象了.
请注意,如果解释器退出的时候对象还存在,就不能保证 del 被确切的执行了。所以__del__并不能替代良好的编程习惯。
比如,在处理socket时,及时关闭结束的连接。
总有人要吐槽Python缺少对于类的封装,比如希望Python能够定义私有属性,然后提供公共可访问的getter和 setter。Python其实可以通过魔术方法来实现封装。
getattr(self, name)
该方法定义了你试图访问一个不存在的属性时的行为。因此,重载该方法可以实现捕获错误拼写然后进行重定向, 或者对一些废弃的属性进行警告。
setattr(self, name, value)
setattr 是实现封装的解决方案,它定义了你对属性进行赋值和修改操作时的行为。
不管对象的某个属性是否存在,它都允许你为该属性进行赋值,因此你可以为属性的值进行自定义操作。有一点需要注意,实现__setattr__时要避免"无限递归"的错误,下面的代码示例中会提到。
delattr(self, name)
__delattr__与__setattr__很像,只是它定义的是你删除属性时的行为。实现__delattr__是同时要避免"无限递归"的错误。
getattribute(self, name)
__getattribute__定义了你的属性被访问时的行为,相比较,__getattr__只有该属性不存在时才会起作用。
因此,在支持__getattribute__的Python版本,调用__getattr__前必定会调用 getattribute。getattribute__同样要避免"无限递归"的错误。
需要提醒的是,最好不要尝试去实现__getattribute,因为很少见到这种做法,而且很容易出bug。
例子说明__setattr__的无限递归错误:
def __setattr__(self, name, value):
self.name = value
# 每一次属性赋值时, __setattr__都会被调用,因此不断调用自身导致无限递归了。
因此正确的写法应该是:
def __setattr__(self, name, value):
self.__dict__[name] = value
__delattr__如果在其实现中出现del self.name 这样的代码也会出现"无限递归"错误,这是一样的原因。
下面的例子很好的说明了上面介绍的4个魔术方法的调用情况:
class Access(object):
def __getattr__(self, name):
print ('__getattr__')
return super(Access, self).__getattr__(name)
def __setattr__(self, name, value):
print ('__setattr__')
return super(Access, self).__setattr__(name, value)
def __delattr__(self, name):
print ('__delattr__')
return super(Access, self).__delattr__(name)
def __getattribute__(self, name):
print ('__getattribute__')
return super(Access, self).__getattribute__(name)
access = Access()
access.attr1 = True # __setattr__调用
print(access.attr1)# 属性存在,只有__getattribute__调用
try:
access.attr2 # 属性不存在, 先调用__getattribute__, 后调用__getattr__
except AttributeError:
print("不存在")
del access.attr1 # __delattr__调用
运行结果:
__setattr__
__getattribute__
True
__getattribute__
__getattr__
不存在
__delattr__
描述符对像
我们从一个例子来入手,介绍什么是描述符,并介绍__get__, set, delete 的使用。(放在这里介绍是为了跟上一小节介绍的魔术方法作对比)
我们知道,距离既可以用单位"米"表示,也可以用单位"英尺"表示。
现在我们定义一个类来表示距离,它有两个属性: 米和英尺
class Meter(object):
'''Descriptor for a meter.'''
def __init__(self, value=0.0):
self.value = float(value)
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = value
class Foot(object):
'''Descriptor for a foot.'''
def __get__(self, instance, owner):
return instance.meter * 3.2808
def __set__(self, instance, value):
instance.meter = float(value) / 3.2808
class Distance(object):
meter = Meter()
foot = Foot()
d = Distance()
print (d.meter, d.foot) # 0.0, 0.0
d.meter = 1
print (d.meter, d.foot) # 1.0 3.2808
d.meter = 2
print (d.meter, d.foot) # 2.0 6.5616
运行结果:
0.0 0.0
1 3.2808
2 6.5616
在上面例子中,在还没有对Distance的实例赋值前, 我们认为meter和foot应该是各自类的实例对象, 但是输出却是数值。这是因为__get__发挥了作用.
我们只是修改了meter,并且将其赋值成为int,但foot也修改了。这是__set__发挥了作用.
描述器对象(Meter、Foot)不能独立存在, 它需要被另一个所有者类(Distance)所持有。
描述器对象可以访问到其拥有者实例的属性,比如例子中Foot的instance.meter。
在面向对象编程时,如果一个类的属性有相互依赖的关系时,使用描述器来编写代码可以很巧妙的组织逻辑。
在Django的ORM中, models.Model中的IntegerField等, 就是通过描述器来实现功能的。
一个类要成为描述器,必须实现__get__, set, delete 中的至少一个方法。下面简单介绍下:
get(self, instance, owner)
参数instance是拥有者类的实例。参数owner是拥有者类本身。
__get__在其拥有者对其读值的时候调用。
set(self, instance, value)
__set__在其拥有者对其进行修改值的时候调用。
delete(self, instance)
__delete__在其拥有者对其进行删除的时候调用。
构造自定义容器在Python中,常见的容器类型有: dict, tuple, list, string。
其中tuple, string是不可变容器,dict, list是可变容器。
可变容器和不可变容器的区别在于,不可变容器一旦赋值后,不可对其中的某个元素进行修改。
比如定义了l = [1, 2, 3]和t = (1, 2, 3)后, 执行l[0] = 0是可以的,但执行t[0] = 0则会报错。
如果我们要自定义一些数据结构,使之能够跟以上的容器类型表现一样,那就需要去实现某些协议。
这里的协议跟其他语言中所谓的"接口"概念很像,一样的需要你去实现才行,只不过没那么正式而已。
如果要自定义不可变容器类型,只需要定义__len__ 和 getitem__方法;
如果要自定义可变容器类型,还需要在不可变容器类型的基础上增加定义__setitem 和 delitem。
如果你希望你的自定义数据结构还支持"可迭代", 那就还需要定义__iter__。
len(self)
需要返回数值类型,以表示容器的长度。该方法在可变容器和不可变容器中必须实现。
getitem(self, key)
当你执行self[key]的时候,调用的就是该方法。该方法在可变容器和不可变容器中也都必须实现。
调用的时候,如果key的类型错误,该方法应该抛出TypeError;
如果没法返回key对应的数值时,该方法应该抛出ValueErro。
setitem(self, key, value)
当你执行self[key] = value时,调用的是该方法。
delitem(self, key)
当你执行del self[key]的时候,调用的是该方法。
iter(self)
该方法需要返回一个迭代器(iterator)。当你执行for x in container: 或者使用iter(container)时,该方法被调用。
reversed(self)
如果想要该数据结构被內建函数reversed()支持,就还需要实现该方法。
contains(self, item)
如果定义了该方法,那么在执行item in container 或者 item not in container时该方法就会被调用。
如果没有定义,那么Python会迭代容器中的元素来一个一个比较,从而决定返回True或者False。
missing(self, key)
dict字典类型会有该方法,它定义了key如果在容器中找不到时触发的行为。
比如d = {‘a’: 1}, 当你执行d[notexist]时,d.missing(‘notexist’)就会被调用。
下面举例,使用上面讲的魔术方法来实现Haskell语言中的一个数据结构。
class FunctionalList:
''' 实现了内置类型list的功能,并丰富了一些其他方法: head, tail, init, last, drop, take'''
def __init__(self, values=None):
if values is None:
self.values = []
else:
self.values = values
def __len__(self):
return len(self.values)
def __getitem__(self, key):
return self.values[key]
def __setitem__(self, key, value):
self.values[key] = value
def __delitem__(self, key):
del self.values[key]
def __iter__(self):
return iter(self.values)
def __reversed__(self):
return FunctionalList(reversed(self.values))
def append(self, value):
self.values.append(value)
def head(self):
# 获取第一个元素
return self.values[0]
def tail(self):
# 获取第一个元素之后的所有元素
return self.values[1:]
def init(self):
# 获取最后一个元素之前的所有元素
return self.values[:-1]
def last(self):
# 获取最后一个元素
return self.values[-1]
def drop(self, n):
# 获取所有元素,除了前N个
return self.values[n:]
def take(self, n):
# 获取前N个元素
return self.values[:n]
# 我们再举个例子,实现Perl语言的AutoVivification,它会在你每次引用一个值未定义的属性时为你自动创建数组或者字典。
class AutoVivification(dict):
"""Implementation of perl's autovivification feature."""
def __missing__(self, key):
value = self[key] = type(self)()
return value
weather = AutoVivification()
weather['china']['guangdong']['shenzhen'] = 'sunny'
weather['china']['hubei']['wuhan'] = 'windy'
weather['USA']['California']['Los Angeles'] = 'sunny'
print (weather)
# 结果输出:{'china': {'hubei': {'wuhan': 'windy'}, 'guangdong': {'shenzhen': 'sunny'}}, 'USA': {'California': {'Los Angeles': 'sunny'}}}
运行结果:
{'china': {'guangdong': {'shenzhen': 'sunny'}, 'hubei': {'wuhan': 'windy'}}, 'USA': {'California': {'Los Angeles': 'sunny'}}}
在Python中,关于自定义容器的实现还有更多实用的例子,但只有很少一部分能够集成在Python标准库中,比如
Counter
OrderedDict
with声明是从Python2.5开始引进的关键词。你应该遇过这样子的代码:
with open('foo.txt') as bar:
# do something with bar
在with声明的代码段中,我们可以做一些对象的开始操作和清除操作,还能对异常进行处理。
这需要实现两个魔术方法: enter 和 exit。
enter(self)
__enter__会返回一个值,并赋值给as关键词之后的变量。在这里,你可以定义代码段开始的一些操作。
exit(self, exception_type, exception_value, traceback)
__exit__定义了代码段结束后的一些操作,可以这里执行一些清除操作,或者做一些代码段结束后需要立即执行的命令,比如文件的关闭,socket断开等。如果代码段成功结束,那么exception_type, exception_value, traceback 三个参数传进来时都将为None。如果代码段抛出异常,那么传进来的三个参数将分别为: 异常的类型,异常的值,异常的追踪栈。
如果__exit__返回True, 那么with声明下的代码段的一切异常将会被屏蔽。
如果__exit__返回None, 那么如果有异常,异常将正常抛出,这时候with的作用将不会显现出来。
举例说明:
这该示例中,IndexError始终会被隐藏,而TypeError始终会抛出。
class DemoManager(object):
def __enter__(self):
pass
def __exit__(self, ex_type, ex_value, ex_tb):
if ex_type is IndexError:
print (ex_value.__class__)
return True
if ex_type is TypeError:
print (ex_value.__class__)
return # return None
with DemoManager() as nothing:
data = [1, 2, 3]
data[4] # raise IndexError, 该异常被__exit__处理了
with DemoManager() as nothing:
data = [1, 2, 3]
data['a'] # raise TypeError, 该异常没有被__exit__处理
'''
输出:
Traceback (most recent call last):
...
'''
运行结果:
Traceback (most recent call last):
...
对象的序列化
Python对象的序列化操作是pickling进行的。pickling非常的重要,以至于Python对此有单独的模块pickle,还有一些相关的魔术方法。使用pickling, 你可以将数据存储在文件中,之后又从文件中进行恢复。
下面举例来描述pickle的操作。从该例子中也可以看出,如果通过pickle.load 初始化一个对象, 并不会调用__init__方法。
# -*- coding: utf-8 -*-
from datetime import datetime
import pickle
class Distance(object):
def __init__(self, meter):
print ('distance __init__')
self.meter = meter
data = {
'foo': [1, 2, 3],
'bar': ('Hello', 'world!'),
'baz': True,
'dt': datetime(2016, 10,1),
'distance': Distance(1.78),
}
print ('before dump:', data)
with open('data.pkl', 'wb') as jar:
pickle.dump(data, jar) # 将数据存储在文件中
del data
print ('data is deleted!')
with open('data.pkl', 'rb') as jar:
data = pickle.load(jar) # 从文件中恢复数据
print('after load:', data)
运行结果:
distance __init__
before dump: {'foo': [1, 2, 3], 'bar': ('Hello', 'world!'), 'baz': True, 'dt': datetime.datetime(2016, 10, 1, 0, 0), 'distance': }
data is deleted!
after load: {'foo': [1, 2, 3], 'bar': ('Hello', 'world!'), 'baz': True, 'dt': datetime.datetime(2016, 10, 1, 0, 0), 'distance': }
值得一提,从其他文件进行pickle.load操作时,需要注意有恶意代码的可能性。另外,Python的各个版本之间,pickle文件可能是互不兼容的。
pickling并不是Python的內建类型,它支持所有实现pickle协议(可理解为接口)的类。pickle协议有以下几个可选方法来自定义Python对象的行为。
getinitargs(self)
如果你希望unpickle时,init__方法能够调用,那么就需要定义__getinitargs, 该方法需要返回一系列参数的元组,这些参数就是传给__init__的参数。
该方法只对old-style class有效。所谓old-style class,指的是不继承自任何对象的类,往往定义时这样表示: class A:, 而非class A(object):
getnewargs(self)
跟__getinitargs__很类似,只不过返回的参数元组将传值给__new__
getstate(self)
在调用pickle.dump时,默认是对象的__dict__属性被存储,如果你要修改这种行为,可以在__getstate__方法中返回一个state。state将在调用pickle.load时传值给__setstate__
setstate(self, state)
一般来说,定义了__getstate__,就需要相应地定义__setstate__来对__getstate__返回的state进行处理。
reduce(self)
如果pickle的数据包含了自定义的扩展类(比如使用C语言实现的Python扩展类)时,就需要通过实现__reduce__方法来控制行为了。由于使用过于生僻,这里就不展开继续讲解了。
令人容易混淆的是,我们知道, reduce()是Python的一个內建函数, 需要指出__reduce__并非定义了reduce()的行为,二者没有关系。
reduce_ex(self)
reduce_ex 是为了兼容性而存在的, 如果定义了__reduce_ex__, 它将代替__reduce__ 执行。
下面的代码示例很有意思,我们定义了一个类Slate(中文是板岩的意思)。这个类能够记录历史上每次写入给它的值,但每次pickle.dump时当前值就会被清空,仅保留了历史。
import pickle
import time
class Slate:
'''Class to store a string and a changelog, and forget its value when pickled.'''
def __init__(self, value):
self.value = value
self.last_change = time.time()
self.history = []
def change(self, new_value):
# 修改value, 将上次的valeu记录在history
self.history.append((self.last_change, self.value))
self.value = new_value
self.last_change = time.time()
def print_changes(self):
print ('Changelog for Slate object:')
for k, v in self.history:
print ('%s %s' % (k, v))
def __getstate__(self):
# 故意不返回self.value和self.last_change,
# 以便每次unpickle时清空当前的状态,仅仅保留history
return self.history
def __setstate__(self, state):
self.history = state
self.value, self.last_change = None, None
slate = Slate(0)
time.sleep(0.5)
slate.change(100)
time.sleep(0.5)
slate.change(200)
slate.change(300)
slate.print_changes() # 与下面的输出历史对比
with open('slate.pkl', 'wb') as jar:
pickle.dump(slate, jar)
del slate # delete it
with open('slate.pkl', 'rb') as jar:
slate = pickle.load(jar)
print ('current value:', slate.value) # None
print (slate.print_changes() ) # 输出历史记录与上面一致
运行结果:
Changelog for Slate object:
1586677795.3922954 0
1586677795.8929865 100
1586677796.3938012 200
current value: None
Changelog for Slate object:
1586677795.3922954 0
1586677795.8929865 100
1586677796.3938012 200
None
运算符相关的魔术方法
运算符相关的魔术方法实在太多了,也很好理解,不打算多讲。在其他语言里,也有重载运算符的操作,所以我们对这些魔术方法已经很了解了。
比较运算符cmp(self, other)
如果该方法返回负数,说明self other; 返回0说明self == other。
强烈不推荐来定义__cmp__, 取而代之, 最好分别定义__lt__等方法从而实现比较功能。
__cmp__在Python3中被废弃了。
eq(self, other)
定义了比较操作符==的行为.
ne(self, other)
定义了比较操作符!=的行为.
lt(self, other)
定义了比较操作符<的行为.
gt(self, other)
定义了比较操作符>的行为.
le(self, other)
定义了比较操作符<=的行为.
ge(self, other)
定义了比较操作符>=的行为.
下面我们定义一种类型Word, 它会使用单词的长度来进行大小的比较, 而不是采用str的比较方式。
但是为了避免 Word(‘bar’) == Word(‘foo’) 这种违背直觉的情况出现,并没有定义__eq__, 因此Word会使用它的父类(str)中的__eq__来进行比较。
下面的例子中也可以看出: 在编程语言中, 如果a >=b and a <= b, 并不能推导出a == b这样的结论。
class Word(str):
'''存储单词的类,定义比较单词的几种方法'''
def __new__(cls, word):
# 注意我们必须要用到__new__方法,因为str是不可变类型
# 所以我们必须在创建的时候将它初始化
if ' ' in word:
print ("Value contains spaces. Truncating to first space.")
word = word[:word.index(' ')] # 单词是第一个空格之前的所有字符
return str.__new__(cls, word)
def __gt__(self, other):
return len(self) > len(other)
def __lt__(self, other):
return len(self) = len(other)
def __le__(self, other):
return len(self) <= len(other)
print ('foo < fool:', Word('foo') fool:', Word('foolish') > Word('fool')) # True
print ('bar >= foo:', Word('bar') >= Word('foo')) # True
print ('bar <= foo:', Word('bar') <= Word('foo')) # True
print ('bar == foo:', Word('bar') == Word('foo')) # False, 用了str内置的比较方法来进行比较
print ('bar != foo:', Word('bar') != Word('foo')) # True
运行结果:
foo fool: True
bar >= foo: True
bar <= foo: True
bar == foo: False
bar != foo: True
一元运算符和函数
pos(self)
实现了’+'号一元运算符(比如+some_object)
neg(self)
实现了’-'号一元运算符(比如-some_object)
invert(self)
实现了号(波浪号)一元运算符(比如some_object)
abs(self)
实现了abs()內建函数.
round(self, n)
实现了round()内建函数. 参数n表示四舍五进的精度.
floor(self)
实现了math.floor(), 向下取整.
ceil(self)
实现了math.ceil(), 向上取整.
trunc(self)
实现了math.trunc(), 向0取整.
算术运算符add(self, other)
实现了加号运算.
sub(self, other)
实现了减号运算.
mul(self, other)
实现了乘法运算.
floordiv(self, other)
实现了//运算符.
div(self, other)
实现了/运算符. 该方法在Python3中废弃. 原因是Python3中,division默认就是true division.
truediv(self, other)
实现了true division. 只有你声明了from future import division该方法才会生效.
mod(self, other)
实现了%运算符, 取余运算.
divmod(self, other)
实现了divmod()內建函数.
pow(self, other)
实现了**操作. N次方操作.
rshift(self, other)
实现了位操作<<.
and(self, other)
实现了位操作&.
or(self, other)
实现了位操作|
xor(self, other)
实现了位操作^
这里只需要解释一下概念即可。
假设针对some_object这个对象:
some_object + other
上面的代码非常正常地实现了some_object的__add__方法。那么如果遇到相反的情况呢?
other + some_object
这时候,如果other没有定义__add__方法,但是some_object定义了__radd__, 那么上面的代码照样可以运行。
这里的__radd__(self, other)就是__add__(self, other)的反算术运算符。
所以,类比的,我们就知道了更多的反算术运算符, 就不一一展开了:
rmul(self, other)
rmul(self, other)
rfloordiv(self, other)
rdiv(self, other)
rtruediv(self, other)
rmod(self, other)
rdivmod(self, other)
rpow(self, other)
rlshift(self, other)
rrshift(self, other)
rand(self, other)
ror(self, other)
rxor(self, other)
这也是只要理解了概念就容易掌握的运算。举个例子:
x = 5
x += 1 # 这里的+=就是增量赋值,将x+1赋值给了x
因此对于a += b, iadd 将返回a + b, 并赋值给a。
所以很容易理解下面的魔术方法了:
iadd(self, other)
isub(self, other)
imul(self, other)
ifloordiv(self, other)
idiv(self, other)
itruediv(self, other)
imod(self, other)
ipow(self, other)
ilshift(self, other)
irshift(self, other)
iand(self, other)
ior(self, other)
ixor(self, other)
int(self)
实现了类型转化为int的行为.
long(self)
实现了类型转化为long的行为.
float(self)
实现了类型转化为float的行为.
complex(self)
实现了类型转化为complex(复数, 也即1+2j这样的虚数)的行为.
oct(self)
实现了类型转化为八进制数的行为.
hex(self)
实现了类型转化为十六进制数的行为.
index(self)
在切片运算中将对象转化为int, 因此该方法的返回值必须是int。用一个例子来解释这个用法。
class Thing(object):
def __index__(self):
return 1
thing = Thing()
list_ = ['a', 'b', 'c']
print (list_[thing]) # 'b'
print (list_[thing:thing]) # []
运行结果:
b
[]
可能有的人会想,list_[thing]为什么不是相当于list_[int(thing)]呢? 通过实现Thing的__int__方法能否达到这个目的呢?
显然不能。如果真的是这样的话,那么list_[1.1:2.2]这样的写法也应该是通过的。
而实际上,该写法会抛出TypeError: slice indices must be integers or None or have an index method
下面我们再做个例子,如果对一个dict对象执行dict_[thing]会怎么样呢?
dict_ = {1: 'apple', 2: 'banana', 3: 'cat'}
print dict_[thing] # raise KeyError
这个时候就不是调用__index__了。虽然list和dict都实现了__getitem__方法, 但是它们的实现方式是不一样的。
如果希望上面例子能够正常执行, 需要实现Thing的__hash__ 和 __eq__方法.
class Thing(object):
def __hash__(self):
return 1
def __eq__(self, other):
return hash(self) == hash(other)
thing = Thing()
dict_ = {1: 'apple', 2: 'banana', 3: 'cat'}
print (dict_[thing]) # apple
运行结果:
apple
实现了混合模式运算。
要了解这个方法,需要先了解coerce()内建函数:
官方文档
上的解释是, coerce(x, y)返回一组数字类型的参数, 它们被转化为同一种类型,以便它们可以使用相同的算术运算符进行操作。如果过程中转化失败,抛出TypeError。
比如对于coerce(10, 10.1), 因为10和10.1在进行算术运算时,会先将10转为10.0再来运算。因此coerce(10, 10.1)返回值是(10.0, 10.1).
__coerce__在Python3中废弃了。
其他魔方方法还没讲到的魔术方法还有很多,但有些我觉得很简单,或者很少见,就不再累赘展开说明了。
str(self)
对实例使用str()时调用。
repr(self)
对实例使用repr()时调用。str()和repr()都是返回一个代表该实例的字符串,
unicode(self)
对实例使用unicode()时调用。unicode()与str()的区别在于: 前者返回值是unicode, 后者返回值是str。unicode和str都是basestring的子类。
当你对一个类只定义了__str__但没定义__unicode__时,__unicode__会根据__str__的返回值自动实现,即return unicode(self.str());
但返回来则不成立。
class StrDemo2:
def __str__(self):
return 'StrDemo2'
class StrDemo3:
def __unicode__(self):
return u'StrDemo3'
demo2 = StrDemo2()
print (str(demo2)) # StrDemo2
print (unicode(demo2)) # StrDemo2
demo3 = StrDemo3()
print (str(demo3)) #
print (unicode(demo3)) # StrDemo3
运行结果:
StrDemo2
NameError: name 'unicode' is not defined
format(self, formatstr)
“Hello, {0:abc}”.format(a)等价于format(a, “abc”), 等价于a.format(“abc”)。
这在需要格式化展示对象的时候非常有用,比如格式化时间对象。
hash(self)
对实例使用hash()时调用, 返回值是数值类型。
nonzero(self)
对实例使用bool()时调用, 返回True或者False。
你可能会问, 为什么不是命名为__bool__? 我也不知道。
我只知道该方法在Python3中改名为__bool__了。
dir(self)
对实例使用dir()时调用。通常实现该方法是没必要的。
sizeof(self)
对实例使用sys.getsizeof()时调用。返回对象的大小,单位是bytes。
instancecheck(self, instance)
对实例调用isinstance(instance, class)时调用。 返回值是布尔值。它会判断instance是否是该类的实例。
subclasscheck(self, subclass)
对实例使用issubclass(subclass, class)时调用。返回值是布尔值。它会判断subclass否是该类的子类。
copy(self)
对实例使用copy.copy()时调用。返回"浅复制"的对象。
deepcopy(self, memodict={})
对实例使用copy.deepcopy()时调用。返回"深复制"的对象。
call(self, [args…])
该方法允许类的实例跟函数一样表现:
class XClass:
def __call__(self, a, b):
return a + b
def add(a, b):
return a + b
x = XClass()
print ('x(1, 2)', x(1, 2))
print ('callable(x)', callable(x)) # True
print ('add(1, 2)', add(1, 2))
print ('callable(add)', callable(add)) # True
运行结果:
x(1, 2) 3
callable(x) True
add(1, 2) 3
callable(add) True
Python3中的差异
Python3中,str与unicode的区别被废除了,因而__unicode__没有了,取而代之地出现了__bytes__.
Python3中,division默认就是true division, 因而__div__废弃.
__coerce__因存在冗余而废弃.
__cmp__因存在冗余而废弃.
nonzero__改名为__bool.