现在的位置: 首页 > 移动开发 > 正文

什么叫魔法函数?

2020年02月14日 移动开发 ⁄ 共 5130字 ⁄ 字号 评论关闭

  什么叫魔法函数(magic method)

  类中诸如__getitem__以双下划线开始的一些特殊方法方法称为双下方法(dunder method),Python解释器遇到这些特殊方法时,会激活一些基本的对象操作,例如:obj[key]背后的方法是__getitem__,实际编程使用的是:my_collection[key],解释器实际调用的是my_collection.__getitem__(key)。类中有很多这些特殊方法,它们也有一个特殊的昵称:魔法函数(magic method).

  这些特殊方法名,能让我们定义的对象实现和支持很多语言框架,并与之交互,如:迭代、集合类、属性访问、运算符重载、函数和方法的调用、对象的创建和销毁、字符串表示形式和格式化、管理上下文(即with块)。可能这些语言框架我们不会都熟悉,在后面的内容中遇到了,我再详细介绍。

  一摞Python风格的纸牌

  我们通过“一摞Python风格的纸牌”引入两个魔法函数:__getitem__、__len__,了解特殊方法的强大。

  **实例描述:**这是一摞有序的纸牌,类名设置为FrenchDeck,一摞牌中有52张卡片(Card),每一个卡片又是一个类,一个Card类设置两个属性:rank:卡片对应的数字,如:A、1、2、…、J、Q、K;suit:卡片对应的花色,如:spades(黑桃)、diamonds(方块)、clubs(梅花)、hearts(红桃)。

  涉及技术:

  collections.namedtuple 创建一个简单的Card类 (namedtuple:用以构建只有少数属性但是没有方法的对象,比如数据库条目)

  random.choice 从一个序列中随机选择一个元素

  内建函数sorted() 对纸牌根据要求进行排序

  创建类:

  import collections

  # 通过random.choice 从一个序列中随机选出一个元素

  from random import choice

  Card = collections.namedtuple('Card', ['rank', 'suit']) # 创建一个类,有两个属性rank、suit

  class FrenchDeck:

  # 定义基本属性

  ranks = [str(n) for n in range(2, 11)] + list('JQKA') # 一副牌中的数字

  suits = 'spades diamonds clubs hearts'.split() # 一副牌中的颜色

  # 定义私有属性, 双下划线,私有属性在类外部无法直接使用,私有方法也是双下划线开头, 如: __fun(self)

  __cards = []

  def __init__(self):

  self.__cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks] # 通过构造函数初始化属性

  def __len__(self):

  """

  可以使用len(FrenchDeck()) 调用

  """

  return len(self.__cards)

  def __getitem__(self, position):

  """

  可以使用FrenchDeck()[index] index为索引的方式获取相关数据,也就是说从一桌牌中抽取一张牌

  """

  return self.__cards[position]

  魔法函数使用

  1.通过len方法查看一摞牌有多少张

  # 实例化该类

  deck = FrenchDeck()

  # 查看一桌牌牌数

  print(len(deck))

  2.通过deck[index]获取这一桌牌中第一张牌、最后一张牌

  print(deck[0], deck[-1])

  print(choice(deck))

  3.随机抽取一张纸牌

  print(choice(deck))

  4.由于__getitem__把[]操作交给了self.__cards列表操作,所以deck类也自然而然地支持切片(slicing)

  # 一摞牌最上面3张 只看牌面是A的牌

  print(deck[:3])

  print(deck[12::13]) # 先抽出索引是12的那张牌,然后每隔13张牌取一张

  5.卡牌迭代,仅仅实现__getitem__就可以使这一摞牌变成可以迭代的了。迭代通常是隐式的(一个集合类型没有实现__contains__方法),也就是说in运算符就会按顺序对整个集合做一次迭代搜索

  for card in deck:

  print(card)

  6.反向迭代,列表的逆序

  for card in reversed(deck):

  print(card)

  7.判断一个Object是否在这个集合中

  print(Card('Q', 'hearts') in deck) # 创建一个Card('Q', 'hearts')类

  print(Card("7", "beasts") in deck)

  8.对纸牌进行排序,排序规则:点数从小到大,相同点数看花色,黑桃、红桃、方块、梅花依次减小,假设梅花2的大小是0, 黑体A是51

  suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

  def spades_high(card):

  """

  按照规则给扑克牌排序的函数

  """

  rank_value = FrenchDeck.ranks.index(card.rank) # 获取当前纸牌rank的索引

  return rank_value * len(suit_values) + suit_values[card.suit]

  for card in sorted(deck, key=spades_high):

  print(card)

  在当前设计中FrenchDeck是不能洗牌的,因为这摞牌是不可变的(immutable),除非我们破坏这个类的封装性,改变__cards进行操作后面会介绍如何使用__setitem__方法实现洗牌功能。

  如何使用特殊方法

  特殊方法的存在是为了Python解释器调用的,我们并不需要调用它们,如:my_object.__len__(),虽然这样写程序不会报错,我们最好还是使用len(my_object)的方式。如果my_object是自己创建的类,在执行len(my_object)时,解释器会自己调用你实现的__len__方法。

  如果使用的是内置的类型,比如列表、字符串等,那么CPython读取对应内存中长度可变的内置对象的C语言结构体。

  很多特殊方法的调用是隐式的,比如for i in x:这个语句,背后其实用的是iter(x),而这个函数的背后则是x.__iter__()方法,前提是这个方法你实现了。

  通过内置函数(如:len、iter、str等)来使用特殊方法是最好的选择。其速度更快。当然也不要随意添加特殊方法,如:__foo__之类的,虽然这个特殊方法现在还没有被Python内部使用,但是以后就不一定了。

  模拟数值类型

  利用特殊方法,可以让自定义对象通过加号"+"(或者其他运算符号)进行运算。这节我们借用一个二维向量(vector)类来展示特殊方法的使用。

  这里的向量就是欧几里得集合中常用的概念,在数学和物理中经常使用,如下图,其中Vector(2,4) + Vector(2,1)=Vector(4,5),说明:虽然Python内置的complex类可以用来表示二维向量,但是我们这个自定义的类可以扩展到n维向量。

  在书写vector类之前,我们需要做出以下思考:

  1.两个向量相加后还是一个向量,即有如下效果

  v1 = Vector(2,4)

  v2 = Vector(2,1)

  v1 + v2 # Vector(4, 5)

  2.内置函数abs,如果输入时整数或者浮点数,它返回的时输入值的绝对值,如果输入的是复数(complex number),返回这个复数的模,在设计这个vector类时需要将api与其保持一致性,书写vector类时需要对abs函数做相关处理,即在碰到abs函数时,也应该返回该向量的模,即有如下效果

  v = Vector(3, 4)

  print(abs(v)) # 5.0

  3.利用*运算符来实现向量的标量乘法(向量与数的乘法,得到的结果向量与原向量保持一致,模变大),即有如下效果

  v * 3 # Vector(9,12)

  abs(v*3) # 15.0

  涉及知识:

  math.hypot math.hypot(x, y) 返回欧几里德范数 sqrt(x*x + y*y)

  我们暂时先考虑这么多,先实现一个简单的向量,其中使用了:__repr__、__abs__、__add__、__mul__。

  from math import hypot

  class Vector:

  # Vector构造函数

  def __init__(self, x=0, y=0):

  self.x = x

  self.y = y

  # 1 以字符串形式打印向量,否则打印:类型、地址等信息

  def __repr__(self):

  return 'Vector(%r, %r)' % (self.x, self.y)

  # return 'Vector({}, {})'.format(self.x, self.y)

  # 实现向量abs取模

  def __abs__(self):

  return hypot(self.x, self.y)

  # 实现向量相加

  def __add__(self, other):

  x = self.x + other.x

  y = self.y + other.y

  return Vector(x, y)

  # 实现向量乘以一个标量

  def __mul__(self, scalar):

  return Vector(self.x * scalar, self.y * scalar)

  **说明:**在使用向量与标量相乘的时候,标量只能在向量后。也就是说忽略了乘法的交换律,在后面会进一步介绍使用__rmul__来解决这个问题。

  字符串表示形式

  Python有一个内置函数repr,它能吧一个对象用字符串的形式表达出来以便辨认,这就是”字符串表示形式“。repr就是通过__repr__这个特殊方法来得到一个对象的字符串形式,如果没有实现__repr__,那么打印对象时,得到的字符串可能有如下形式:,字符串返回也可以使用str.format()进行字符修改。

  在__repr__实现中,通过%r或{}占位,就暗示Vector(1, 2) 与 Vector('1’, '2’)是不一样的,这保证了数据类型的稳定,因为向量的构造函数只接受数值,不接受字符串。

  __repr__与__str__具有相同的功能,但是运行中还是有区别的,后者是在str()函数被使用时,或是在用print函数打印一个对象的时候才被调用,它返回的字符串对终端用户更友好。但只想实现两个特殊方法中的一个,__repre__是更好的选择,因为如果一个对象没有__str__函数,而Python又需要调用它的时候,解释器会用__repr__作为替代。

  算数运算符

  通过__add__、__mul__在上例中实现了+ 和* 这两个运算符。这两个方法的返回值都是新创建的向量对象,被操作的两个向量原封未动,代码只读取了其对应的数值,这就是中缀运算符的基本原则。

  自定义的布尔值

  尽管Python里有bool类型,但实际上任何对象都可以用于需要布尔值的上下文中(如:if 、while、and、or、not运算符)。为了判断一个值x为真还是假,Python会调用bool(x),这个函数只能返回True或False。

  默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对__bool__或者__len__函数有自己的实现。bool(x)的背后是调用x.__bool__()的结果,如果不存在__bool__方法,那么bool(x)会尝试调用x.__len__()。若返回0,则bool会返回False,否则返回True。

  对__bool__的实现很简单,如果一个向量的模是0,那么就返回一个False,其他情况返回True。因为__bool__函数的返回类型应该是布尔型,所以我们通过bool(abs(self))把模值变成布尔值。(return bool(self.x or self.y)会更快。

抱歉!评论已关闭.