《流畅的Python》是一本不可多得的好书,然而不知是有意或出于无意,作者在书中留下了一些陷阱,让我们从全书的第一个例子说起。
下面是本书第一个例子的源码:
import collections
Card=collections.namedtuple('Card',['rank','suit'])
class FrenchDeck:
ranks=[str(n) for n in range(2,11)] + list('JQKA') #1
suits='spades diamonds clubs hearts'.split() #2
def __init__(self):
self._cards=[Card(rank,suit) for suit in self.suits
for rank in self.ranks] #3
def __len__(self):
return len(self._cards)
def __getitem__(self,position):
return self._cards[position]
整个例子短小紧凑,但是仔细端详,会发现#1和#2两句声明的是两个类变量,而#3句却是用实例变量的方式来引用之。
从Python默认的行为看,这种情况是允许的。就是说,如果在方法中引用了某个没有定义的实例变量,那么会去查找同名的类变量,如果找到,则会获取同名类变量的值,否则引发异常。不信,你把ranks和suits两个类变量删掉试试,再次创建FrenchDeck类的对象,Python马上给你脸色看。
所以为了保持语义上的一致,#3一句如下书写更为清晰准确:
self._cards=[Card(rank,suit) for suit in FrenchDeck.suits
for rank in FrenchDeck.ranks] #3
但是,这个问题还没有结束,我们接着往下看。
回到原程序,如何确认__init__()方法中引用的是类变量呢?
方法很简单,我们在__init__()函数中的#3句之后加上一句代码,使得现在的__init__()函数看起来是这样的:
def __init__(self):
self._cards=[Card(rank,suit) for suit in self.suits
for rank in self.ranks] #3
print(id(self.suits))
按F5,在打开的python shell中,我们创建两个FrenchDeck类的变量:
>>>deck1=FrenchDeck()
>>>deck2=FrenchDeck()
因为我们在__init__()函数中,添加了输出self.suits的id值,所以创建FrenchDeck类的对象时就会打印这个值,结果deck1和deck2对象打印出的值相等。这就对了,类变量只创建一次,并且在所有对象中共享。
同样,可以确认ranks变量也是类变量。
还有疑问,对吗?好的,程序员就是要有探究到底的精神,我们再来做一个实验,把源代码改为如下:
import collections
Card=collections.namedtuple('Card',['rank','suit'])
class FrenchDeck:
def __init__(self):
self.ranks=[str(n) for n in range(2,11)] + list('JQKA') #1
self.suits='spades diamonds clubs hearts'.split() #2
self._cards=[Card(rank,suit) for suit in self.suits
for rank in self.ranks] #3
print(id(self.suits))
def __len__(self):
return len(self._cards)
def __getitem__(self,position):
return self._cards[position]
#1和#2两句彻底把suits和ranks声明为实例变量,我们再创建FrenchDeck类的两个对象,来检查suits的id值,这次发现两个对象中的suits的id值不一样了。因为每个对象都为各自的实列变量开辟内存空间。同样也可以通过检查ranks的id值来确认这一点。
最后一个问题,如果在一个类的定义中,出现同名的类变量和实列变量,那情况又会如何呢?
做个试验就可以搞定这个问题,我们把源码改为如下:
import collections
Card=collections.namedtuple('Card',['rank','suit'])
class FrenchDeck:
ranks=[str(n) for n in range(2,11)] + list('JQKA')
suits='spades diamonds clubs hearts'.split()
def __init__(self):
self.ranks=[str(n) for n in range(10)]
self.suits=[str(n*2) for n in range(10)]
self._cards=[Card(rank,suit) for suit in self.suits
for rank in self.ranks]
print(self.suits)
def __len__(self):
return len(self._cards)
def __getitem__(self,position):
return self._cards[position]
F5运行后,创建FrenchDeck类的实列,这时候输出的是:
['0', '2', '4', '6', '8', '10', '12', '14', '16', '18']
原来,实列变量可以屏蔽同名的类变量,那么此时我们需要引用类变量怎么办呢?简单,直接在类变量前冠以类名即可。
《流畅的Python》书中首个例子中,作者使用了类变量和实例变量的方式,可能导致混淆。当在方法中引用未定义的实例变量时,Python会查找并使用同名类变量。为保持语义清晰,应明确使用类变量。通过在__init__()方法中输出self.suits和self.ranks的id值,可以验证它们是类变量,所有对象共享。如果将suits和ranks改为实例变量,每个对象将拥有独立的内存空间。当类和实例变量同名时,实例变量会屏蔽类变量,要引用类变量需通过类名前缀。
600

被折叠的 条评论
为什么被折叠?



