《流畅的Python》第一个例子中的陷阱

《流畅的Python》书中首个例子中,作者使用了类变量和实例变量的方式,可能导致混淆。当在方法中引用未定义的实例变量时,Python会查找并使用同名类变量。为保持语义清晰,应明确使用类变量。通过在__init__()方法中输出self.suits和self.ranks的id值,可以验证它们是类变量,所有对象共享。如果将suits和ranks改为实例变量,每个对象将拥有独立的内存空间。当类和实例变量同名时,实例变量会屏蔽类变量,要引用类变量需通过类名前缀。

《流畅的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']

原来,实列变量可以屏蔽同名的类变量,那么此时我们需要引用类变量怎么办呢?简单,直接在类变量前冠以类名即可。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kingdragonfly

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值