在学习面向对象程序设计时,我们通常会学到存取方法,它们是名称类似于getHeight和setHeight的方法,用于获取和设置属性(这些属性可能是私有的)。如果访问给定的时必须采取特定的措施,那么像这样封装状态变量(属性)很重要。例如,请看下面的Rectangle类:
class Rectangle:
def __init__(self):
self.width = 0
self.height = 0
def set_size(self, size):
self.width, self.height = size
def get_size(self):
return self.width, self.height
下面的示例演示了如何使用这个类:
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.get_size()
(10, 5)
>>> r.set_size((150, 100))
>>> r.width
150
get_size和set_size是假想属性size的存取方法,这个属性是一个由width和height组成的元组。(可以将这个属性替换成更有趣的属性,如矩形的面积或其对角线的长度。)这些代码并非完全错误,但存在缺陷。使用这个类时,程序员应无需关心它是如何实现的(封装)。如果有一天你想修改实现,让size成为真正的属性,而width和height是动态计算出来的,就需要提供访问width和height的存取方法,使用这个类的程序也必须重写。应让客户端代码(使用你所编写代码的代码)能够以同样的方式对待所有的属性。
那么如何解决这个问题呢?给所有的属性都提供存取方法吗?这当然并非不可能,但如果有大量简单的属性,这样做就不现实(而且有点傻),因为需要编写大量这样的存取方法,除了获取和设置属性外什么也不做。这将引入复制并粘贴(重复代码)的坏味。显然很糟糕(虽然在有些语言中,这样的问题很常见)。所幸Python能够替你隐藏存取方法,让所有的属性看起来都一样。通过存取方法定义的属性通常称为特性(property)。
在Python中,实际上有两种创建特定的机制,我将重点介绍较新的那种——函数property,它只能用于新式类。随后,我将简单说明如何使用魔法方法来实现特性。
函数property使用起来很简单。如果你编写了一个类,如前面的Rectangle类,只需再添加一行代码。
class Rectangle:
def __init__(self):
self.width = 0
self.height = 0
def set_size(self, size):
self.width, self.height = size
def get_size(self):
return self.width, self.height
size = property(get_size, set_size)
在这个新版的Rectangle方法中,通过调用函数property并将存取方法作为参数(获取方法在前,设置方法在后)创建了一个特性,然后将名称size关联到这个特性。这样,你就能以同样的方式对待width、height和size,而无需关心它们是如何实现的。
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.get_size()
(10, 5)
>>> r.size = 150, 100
>>> r.width
150
如你所见,属性size依然受制于get_size和set_size执行的计算,但看起来就像普通属性一样。
注意 如果特性的行为怪异,务必确保你使用的是新式类(通过直接或间接地继承object或直接设置__metaclass__)。不然,特性的获取方法依然正常,但设置方法可能不正常(是否如此取决于使用的Python版本)。这可能有点令人迷惑。
实际上,调用函数property时,还可不指定参数、指定一个参数、指定三个参数或指定四个参数。如果没有指定任何参数,创建的特性将既不可读也不可写。如果只指定一个参数(获取方法),创建的特性将是只读的。第三个参数是可选的,指定用于删除属性的方法(这个方法不接受任何参数)。第四个参数也是可选的,指定一个文档字符串。这些参数分别名为fget、fset、fdel和doc。如果你要创建一个只可写且带文档字符串的特性,可使用它们作为关键字参数来实现。
本节虽然很短(旨在说明函数property很简单),却非常重要。这里要说明的是,对于新式类,应使用特性而不是存取方法。
你可能很好奇,想知道特性是如何完成其魔法的,下面就来说一说。如果你对此不感兴趣,可跳过这些内容。
property其实并不是函数,而是一个类。它的实例包含一些魔法方法,而所有的魔法都是有这些方法完成的。这些魔法方法为__get__、__set__、__delete__,它们一道定义了所谓描述符协议。只要对象实现了这些方法中的任何一个,它就是一个描述符。描述符的独特之处在于其访问方式。例如,读取属性(具体来说,是实例中访问类中定义的属性)时,如果它关联的是一个实现了__get__的对象,将不会返回这个对象,而是调用方法__get__并将其结果返回。实际上,这是隐藏在特性、关联的方法、静态方法和类方法以及super后面的机制。
有关描述符的详细信息,请参阅Descriptor HowTo Guide(http://docs.python.org/3/howto/descriptor.html)。
可以拦截对对象的所有访问企图,其用途之一是在旧式类中实现特性(在旧式类中,函数property的行为可能不符合预期)。要在属性被访问时执行一段代码,必须使用一些魔法方法。下面四个魔法方法提供了你需要的所有功能(在旧式类中,只需使用后面三个)。
相比函数property,这些魔法方法使用起来要棘手些(从某种程度上来说,效率也更低),但它们很有用,因为你可在这些方法中编写处理多个特性的代码。然而,在可能的情况下,还是使用函数property吧。
再来看前面的Rectangle示例,但这里使用的是魔法方法:
class Rectangle:
def __init__(self):
self.width = 0
self.height = 0
def __setattr__(self, name, value):
if name == 'size':
self.width, self.height = value
else:
self.__dict__[name] = value
def __getattr__(self, name):
if name == 'size':
return self.height, self.width
else:
raise AttributeError()
如你所见,这个版本需要处理额外的管理细节。对于这个代码示例,需要注意如下两点。
注意 前面说过,编写方法__setattr__时需要避开无限循环陷阱,编写__getattribute__时亦如此。由于它拦截对所有属性的访问(在新式类中),因此将拦截对__dict__的访问!在__getattribute__中访问当前实例的属性时,唯一安全的方式是使用超类的方法__getattribute__(使用super)。
之前粗略地提及了迭代器(和可迭代对象),本节将更详细地介绍。对于魔法方法,这里只介绍__iter__,它就是迭代器协议的基础。
迭代(iterate)意味着重复多次,就像循环那样。有些人可能之前只使用for循环迭代过序列和字典,但实际上也可迭代其他对象:实现了方法__iter__的对象。
方法__iter__返回一个迭代器,它是包含方法__next__的对象,而调用这个方法时可不提供任何参数。当你调用方法__next__时,迭代器应返回下一个值。如果迭代器没有可供返回的值,应引发StopIteration异常。你还可使用内置的便利函数next,在这种情况下,next(it)与it.__next__()等效。
注意 在Python3中,迭代器协议有细微的变化。在以前的迭代器协议中,要求迭代器对象包含方法next而不是__next__。
这有什么意义呢?为何不使用列表呢?因为在很多情况下,使用列表都有点像大炮打蚊子。例如,如果你有一个可逐个计算值的函数,你可能只想逐个的获取值,而不是使用列表一次性获取。这是因为如果有很多值,列表可能占用太多的内存。但还有其他原因:使用迭代器更通用、更简单、更优雅。下面来看一个不能使用列表的示例,因为如果使用,这个列表的长度必须是无穷大的!
这个“列表”为斐波那契数列,表示该数列的迭代器如下:
class Fibs:
def __init__(self):
self.a = 0
self.b = 1
def __next__(self):
self.a, self.b = self.b, self.a+self.b
return self.a
def __iter__(self):
return self
注意到这个迭代器实现了方法__iter__,而这个方法返回迭代器本身。在很多情况下,都在另一个对象中实现返回迭代器的方法__iter__,并在for循环中使用这个对象。但推荐在迭代器中也实现方法__iter__(并像刚才那样让它返回self),这样迭代器就可直接用于for循环中。
注意 更正规的定义是,实现了方法__iter__的对象是可迭代的,而实现了方法__next__的对象是迭代器。
首先,创建一个Fibs对象。
>>> fibs = Fibs()
然后就可在for循环中使用这个对象,如找出第一个大于1000的斐波那契数。
>>> for f in fibs:
... if f > 1000:
... print(f)
... break
...
1597
这个方法之所以会停止,是因为其中包含break语句;否则,这个for循环将没完没了的执行。
提示 通过对可迭代对象调用内置函数iter,可获得一个迭代器。
>>> it = iter([1, 2, 3])
>>> next(it)
1
>>> next(it)
2
还可使用它从函数或其他可调用对象创建可迭代对象,详情请参阅库参考手册。
除了对迭代器和可迭代对象进行迭代(通常这样做)之外,还可将它们转换成序列。在可以使用序列的情况下,大多也可使用迭代器或可迭代对象(诸如索引和切片等操作除外)。一个这样的例子是使用构造函数list显式地将迭代器转换为列表。
>>> class TestIterator:
... value = 0
... def __next__(self):
... self.value += 1
... if self.value > 10:
... raise StopIteration
... return self.value
... def __iter__(self):
... return self
...
>>> ti = TestIterator()
>>> list(ti)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
特殊(魔法)名称的用途很多,前面展示的只是冰山一角。魔法方法大多是为非常高级的用途准备的,因此这里不详细介绍。然而,如果你感兴趣,可以模拟数字,让对象像函数一样被调用,影响对象的比较方式,等等。要更详细的了解有哪些魔法方法,可参阅“Python Reference Manual”的Special method names一节。
本文分享自 Python机器学习算法说书人 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有