前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >魔法方法(2)

魔法方法(2)

作者头像
不可言诉的深渊
发布2019-07-26 17:26:26
6770
发布2019-07-26 17:26:26
举报

特性

在学习面向对象程序设计时,我们通常会学到存取方法,它们是名称类似于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

函数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工作原理

你可能很好奇,想知道特性是如何完成其魔法的,下面就来说一说。如果你对此不感兴趣,可跳过这些内容。

property其实并不是函数,而是一个类。它的实例包含一些魔法方法,而所有的魔法都是有这些方法完成的。这些魔法方法为__get__、__set__、__delete__,它们一道定义了所谓描述符协议。只要对象实现了这些方法中的任何一个,它就是一个描述符。描述符的独特之处在于其访问方式。例如,读取属性(具体来说,是实例中访问类中定义的属性)时,如果它关联的是一个实现了__get__的对象,将不会返回这个对象,而是调用方法__get__并将其结果返回。实际上,这是隐藏在特性、关联的方法、静态方法和类方法以及super后面的机制。

有关描述符的详细信息,请参阅Descriptor HowTo Guide(http://docs.python.org/3/howto/descriptor.html)。


__getattr__、__setattr__等方法

可以拦截对对象的所有访问企图,其用途之一是在旧式类中实现特性(在旧式类中,函数property的行为可能不符合预期)。要在属性被访问时执行一段代码,必须使用一些魔法方法。下面四个魔法方法提供了你需要的所有功能(在旧式类中,只需使用后面三个)。

  • __getattribute__(self, name):在属性被访问时自动调用(只适用于新式类)。
  • __getattr__(self, name):在属性被访问而对象没有这样的属性时自动调用。
  • __setattr__(self, name, value):试图给属性赋值时自动调用。
  • __delattr__(self, name):试图删除属性时自动调用。

相比函数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()

如你所见,这个版本需要处理额外的管理细节。对于这个代码示例,需要注意如下两点。

  • 即便涉及的属性不是size,也将调用方法__setattr__。因此这个方法必须考虑如下两种情形:如果涉及的属性为size,就执行与以前一样的操作;否则就使用魔法属性__dict__。__dict__属性是一个字典,其中包含所有的实例属性。之所以使用它而不是执行常规属性赋值,是因为旨在避免再次调用__setattr__,进而导致无限循环。
  • 仅当没有找到指定的属性时,才会调用方法__getattr__。这意味着如果指定的名称不是size,这个方法将引发AttributeError异常。这在要让类能够正确的支持hasattr和getattr等内置函数时很重要。如果指定的名称为size,就使用前一个实现中的表达式。

注意 前面说过,编写方法__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一节。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python机器学习算法说书人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 特性
    • 函数property
      • __getattr__、__setattr__等方法
      • 迭代器
        • 迭代器协议
          • 从迭代器创建序列
          • 其他魔法方法
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档