定制类
Who are you to pass judgment on servants of anothers? It is before their own lord that they stand or fall. And they will be upheld, for the Lord is able to make them stand.
定制类
类是对象,类也是对象类型。字符串、列表、字典等是Python中内置的对象类型,除此之外,我们可以编写类,自定义对象类型。
类和对象类型
如果时至今日,你还没有充分理解类和对象类型的问题,可以再看看如下内容。
>>> class C1(object): pass #Python 3: class C1: pass>>> class C2(object): pass #Python 3: class C2: pass>>> a = C1()>>> b = C2()>>> type(a)<class '__main__.C1'>>>> type(b)<class '__main__.C2'>
type()
是我们已经知晓了的内建函数,它返回的是对象类型。a = C1()
,是实例化,创建了一个实例,也是一个赋值语句,将变量a
与类C1()
建立了引用关系,这就和以前a = 2
的效果是一样的。所以,我们可以通过type(a)
来得到实例或者说是这个变量所引用对象的类型。
在Python中,还有一个函数,专门来判断一个对象是不是另一个给定类的实例。
>>> help(isinstance)Help on built-in function isinstance in module __builtin__:isinstance(...) isinstance(object, class-or-type-or-tuple) -> bool Return whether an object is an instance of a class or of a subclass thereof. With a type as second argument, return whether that is the object's type. The form using a tuple, isinstance(x, (A, B, ...)), is a shortcut for isinstance(x, A) or isinstance(x, B) or ... (etc.).
这是Python 2下的帮助文档信息,Python 3下的内容与之类似。
从isinstance()
的名字上就能知道它是干什么的。
用它可以判断一个对象是否是一个类或者子类的实例,如果第二个参数是类型,也可以判断是否为该类型。
>>> isinstance(a, C1)True>>> isinstance(a, C2)False
a
是类C1
的实例,不是C2
的实例。类似操作,还可以这么做:
>>> m = 1>>> isinstance(m, int)True>>> isinstance(m, float)False
用以前的话说,m
所引用的对象是整数型,简说成m
是整数型。但是,如果从instance()
的操作中看,m
和a
是等效的,你可以认为a
是C1类型的对象。
由此,我们进一步理解了类就是一种对象类型的。
定制类
必须要定制类,因为这个世界太复杂。
定制类,就要用到类的特殊方法,比如初始化函数__init__
,虽然用途很 广泛,但仅仅用它还嫌不够,还要用到其它的特殊方法。
在Python的官方网站上,专门有介绍Special method names的章节,读者可以去仔细阅读。恕我不一一介绍。
本节中,仅根据例子中的问题,使用某个特殊方法。
#!/usr/bin/env pythonclass RoundFloat(object): #Python 3: class RoundFloat: def __init__(self, val): assert isinstance(val, float), "value must be a float." self.value = round(val, 2) def __str__(self): return "{:.2f}".format(self.value) __repr__ = __str__if __name__ == "__main__": r = RoundFloat(2.185) print r #Python 3: print(r) print type(r) #Python 3: print(type(r))
上述程序中的类RoundFloat
的作用是定义了一种两位小数的浮点数类型,利用这个类,能够得到两位小数的浮点数。
在初始化函数中assert isinstance(val, float), "value must be a float."
是对输入的数据类型进行判断,如果不是浮点数就会抛出异常提示。关于assert
(断言)可以参看后续内容。
方法__str__()
是一个特殊方法。实现这个方法,目的就是能够得到打印的内容。这里就是将前面四舍五入保留了两位小数的浮点数,以小数点后有两位小数的形式输出。
__repr__ = __str__
的含义是在类被调用,即向变量提供__str__()
里的内容。
执行程序,结果是:
2.19<class '__main__.RoundFloat'>
如果是RoundFloat(2.185)
,返回的结果是2.00
。
对比看,int(2.34)
和RoundFloat(2.185)
完全等效,即int
是对象类型,也是数据转换的函数;RoundFloat
具有同样的功能。RoundFloat
就是我们新定义的对象类型。
仿照上面的做法,我们还可以定制一个专门显示分数的类。
如你所知,如果在Python中直接输入状如3/2,它不会是一个分数,而是按照除法进行处理。但,分数的显示和使用,是显而易见的,Python的内置对象类型中又没有分数类型(不仅Python,相当多的高级语言都没有)。所以,有必要自定义一个相关的类型。
仿照前面定制类的方式,写出这样一段代码。
#!/usr/bin/env python#coding: utf-8class Fraction(object): #Python 3: class Fraction: def __init__(self, number, denom=1): self.number = number self.denom = denom def __str__(self): return str(self.number) + '/' + str(self.denom) __repr__ = __str__if __name__ == "__main__": f = Fraction(2, 3) print f #Python 3: print(f)#output: 2/3
类Fraction
就是自定义的分数类型。由此可见,自定义类是相当重要和必要的。
在这个基础上,继续将分数问题深入研究——分数相加。1/2 + 1/3 = 5/6
,计算过程如下:
- 通分,即分母为原来两个分数的分母的最小公倍数,得到
3/6 + 2/6
; - 分子相加,得到上述两个分数的和。
这样,我们将问题分解,找出一个关键,就是“通分”,而通分的关键是找出两个整数的最小公倍数。
如何找最小公倍数?步骤如下:
- 计算两个数的最大公约数,假设a和b,最大公约数(greatest common divisor)用gcd(a, b)表示;
- 最小公倍数和最大公约数的关系是:lcm(a, b) = |a * b| / gcd(a, b),lcm(a, b)表示这两个数的最小公倍数(lowest common multiple)。
读者不妨从新审视一番上述问题解决的思路。原始问题是计算两个分数的加法,然后将这个问题分解,再将分解之后的问题再分解。最终我们解决问题的基石是计算最大公约数。像这样解决问题的方法,我们称之为分治法,即一个复杂问题,分解为若干个简单问题,然后把简单问题组合起来,就解决了那个复杂问题——分而治之。
分解到最小的问题,就可以用编写函数的方式解决了。所以,先计算最大公约数和最小公倍数。
#!/usr/bin/env python#coding: utf-8def gcd(a, b): #最大公约数 if not a > b: a, b = b, a while b != 0: remainder = a % b a, b = b, remainder return adef lcm(a, b): #最小公倍数 return (a * b) / gcd(a,b)if __name__ == "__main__": print gcd(8, 20) #Python 3: print(gcd(8, 20)) print lcm(8, 20) #Python 3: print(lcm(8, 20))#output: #4#40
如此,完成了最小公倍数的计算。然后,在前面定制的分数类的基础上,就可以制作两个分数相加的计算了。
#!/usr/bin/env python#coding: utf-8def gcd(a, b): if not a > b: a, b = b, a while b != 0: remainder = a % b a, b = b, remainder return adef lcm(a, b): return (a * b) / gcd(a,b)class Fraction(object): #Python 3: class Frraction: def __init__(self, number, denom=1): self.number = number self.denom = denom def __str__(self): return str(self.number) + '/' + str(self.denom) __repr__ = __str__ def __add__(self, other): lcm_num = lcm(self.denom, other.denom) number_sum = (lcm_num / self.denom * self.number) + (lcm_num / other.denom * other.number) return Fraction(number_sum, lcm_num)if __name__ == "__main__": m = Fraction(1, 3) n = Fraction(1, 2) s = m + n print m,"+",n,"=",s
较之以前,增加了一个特殊方法__add__()
,它就是实现相加的特殊方法。在类中,有规定了加减乘除等运算的特殊方法。
在Python中,如果要实现某种运算,必须要有运算符,这是毫无疑问已经很熟悉的了。但是,这些运算符之所以能够被使用,都是因为有一些特殊方法才得以实现的。以下表格中列出几种常见运算符所对应的特殊方法,供参考。
二元运算符 | 特殊方法 |
---|---|
+ | __add__ , __radd__ |
- | __sub__ , __rsub__ |
* | __mul__ , __rmul__ |
/ | __div__ , __rdiv__ , __truediv__ , __rtruediv__ |
// | __floordiv__ , __rfloordiv__ |
% | __mod__ , __rmod__ |
** | __pow__ , __rpow__ |
<< | __lshift__ , __rlshift__ |
>> | __rshift__ , __rrshift__ |
& | __and__ , __rand__ |
== | __eq__ |
!=,<> | __ne__ |
> | __get__ |
< | __lt__ |
>= | __ge__ |
<= | __le__ |
以“+”为例,不论是实现1 + 2
还是'abc' + 'xyz'
,都是要执行1.__add__(2)
或者'abc'.__add__('xyz')
操作。也就是两个对象是否能进行加法运算,首先就要看相应的对象是否有__add__()
方法(读者不妨在交互模式中使用dir()
,看一看整数、字符串是否有__add__()
方法),一旦相应的对象有__add__()
方法,即使这个对象从数学上不可加,我们都可以用加法的形式,来表达obj.__add__()
所定义的操作。在Python中,运算符起到简化书写的功能,但它依靠特殊方法实现。
所以,在刚才自定义的类Fraction
中,为了实现分数加法,我们重写了__add__()
方法,也可以称之为运算符重载(对于Python是否支持重载,也是一个争论话题)。
就这样,我们解决了分数相加的问题。
但,上述加法还不是很完美,还可以有很多优化的地方,比如分数结果要化成最简分数等等。真正要做好一个分数运算的类,还有很多工作。
不过,在Python中,其实不用你自己做了,自由高手做好。标准库中就有相应模块解决此问题。
>>> from fractions import Fraction>>> m, n = Fraction(1, 3), Fraction(1, 2)>>> m + nFraction(5, 6)>>> print m + n #Python 3: print(m + n)5/6>>> a, b = Fraction(1, 3), Fraction(1, 6)>>> print a + b #Python 3: print(a + b)1/2
Python的魅力之一,就是它强大的标准库和第三方库,让你省心省力。