python3 面向对象编程_python面向对象编程

Python (60) 2023-03-24 16:06

本节是第五讲的第十三小节,本节为大家介绍Python语言的面向对象程序设计,包括类的继承和多态,自定义数据类型等。

继承与多态(Inheritance and Polymorphism)

Circle类继承自Point类,添加了一个额外的数据属性(radius),以及3个新方法, 此外,还重新实现了 Point类的几个方法。下面给出的是完整的类定义:

继承是通过在class行列出我们需要继承的类(或多个类)实现的。这里,我们继承了 Point类——图展示了 Circle类的继承体系。

python3 面向对象编程_python面向对象编程_https://bianchenghao6.com/blog_Python_第1张

class Circle(Point):

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

super().__init__(x, y)

self.radius = radius

def edge_distance_from_origin(self):

return abs(self.distance_from_origin() - self.radius)

def area(self):

return math.pi * (self.radius ** 2)

def circumference(self):

return 2 * math.pi * self.radius

def __eq__(self, other):

return self.radius == other.radius and super().__eq__(other)

def __repr__(self):

return "Circle({0.radius!r},{0.x!r},{0.y!r})".format(self)

def __str__(self):

return repr(self)

在__init__()方法内部,我们使用super()来调用基类的 __init__()方法从而创建并初始化self.x属性与self.y属性。但是,使用这个类时,可以提供无效的radius值, 比如-2。在下一小节中,我们将学习如何防止这一类问题,这是通过使用特性来使得属性更加强健实现的。

area()方法与circumference()方法的计算过程都是直接的,edge_distance_from_origin()方法调用了 distance_from_origin()方法,将其作为计算过程的一部分。由于Circle 类没有提供distance_from_origin()方法的实现,因此将使用基类Point实现的方法。将这一方法与__eq__()方法的重新实现相比较。这一方法将圆半径与其他圆半径进行比较,如果相等,就使用super()显式地调用基类的__eq__()方法。如果不使用super()方法,就可能进入无穷递归,因为Circle.__eq__()会对自身进行调用。此外,在super() 调用中不需要传递self参数,因为Python会自动地传递这一参数。

#下面给出两个使用实例:

p = Shape.Point(28, 45)

c = Shape.Circle(5, 28, 45)

р. distance_from_origin() # returns: 53.0

с. distance_from_origin() # returns: 53.0

我们可以对Point或Circle调用distance_from_origin()方法,因为Circles可以代表 Points。

多态意味着,给定类的任意对象在使用时都可以看作该类的任意某个基类的对象, 这也是为什么在创建子类时只需要实现我们需要的附加方法,必须重新实现的也只是那些需要替代的方法。在对方法重新实现时,如果有必要可以使用基类的方法版本, 这是通过在实现中调用super()来做到的。

对Circle这个类,我们实现了几个附加的方法,比如area()与circumference(),并重新实现了几个需要修改的方法。对__repr__()与__str__()方法进行重新实现是必要的, 因为如果不这样做,就会调用基类的相应方法,而这些方法返回的字符串代表的是 PointS而非CircleS。对__init__()方法与__eq__()方法重新实现也是必要的,因为我们必须考虑到Circles还有一个附加的属性,对这两种情况,我们都尽可能多地利用了基类中的相应实现,降低所需要的工作量。

通过上面的设计,Point类与Circle类都完全按照我们的需要进行了实现。但我们也可以提供一些额外的方法,比如,如果我们希望可以对PointS或CircleS进行排序, 就可以提供一些其他的用于比较的特殊方法。我们可能还需要做的是提供用于对Point 或Circle进行复制的方法,大多数Python类没有提供copy()方法(dict.copy()与set.copy() 是例外情况)。如果我们需要对Point或Circle进行复制,那么我们可以导入copy模块, 并使用其中的copy.copy()函数。(对Point与Circle对象,没必要使用copy.deepcopy(), 因为这两个对象只包含固定的实例变量。)

使用特性进行属性存取控制(Using Properties to Control Attributes Access)

在前面的小节中,Point类包含了一个distance_from_origin()方法,Circle类包含 area()、circumference()与 edge_distance_from_origin()等方法。所有这些方法返回的都是一个单独的float值,因此,从用户的角度看,这些类可以当作数据属性来使用,当然, 是只读的。在ShapeAlt.py文件中,提供了 Point类与Circle类的替代实现方案 这里提及的所有方法都是作为特性提供的,这使得我们可以编写类似于如下的代码:

circle = Shape.Circle(5, 28, 45) # assumes: import ShapeAlt as Shape

circle.radius # returns: 5

circle.edge_distance_from_origin # returns: 48.0

下面给出的是用于 ShapeAlt.Circle 类的 area 与 edge_distance_from_origin 这两个特性的获取者方法的实现:

@property

def area(self):

return math.pi * (self.radius ** 2)

@property

def edge_distance_from_origin(self):

return abs(self.distance_from_origin - self.radius)

如果我们只是像上面所做的提供获取者方法,那么特性是只读的。用于area 特性的代码与前面的area()方法是相同的,edge_distance_from_origin的代码则与以前的实现略有些不同,因为现在存取的是基类的distance_from_origin特性,而不是调用distance_from_origin()方法。两者最显著的差别是特性修饰器。修饰器是一个函数,该函数以一个函数或方法为参数,并返回参数的“修饰后”版本,也就是对该函数或方法进行修改后的版本。decorator是通过在名字前使用@符号引导来进行标记的,这里,我们暂且将其看做一种语法格式

property()修饰器函数是一个内置函数,至多可以接受4个参数:一个获取者函数, 一个设置者函数,一个删除者函数以及一个docstring。使用@property的效果与仅使用 一个参数(获取者函数)调用property()的效果是相同的,我们可以类似于如下的方式创建area特性:

def area(self):

return math.pi * (self.radius ** 2)

area = property(area)

我们很少使用这种语法,因为使用修饰器所需要的代码更短,也更加清晰。

在上一小节中,我们注意到,对Circle的radius属性没有进行验证,但通过将radius 转换为特性,就可以对其进行验证。这并不需要对Circle.__init__()方法进行任何改变, 存取Circle.radius属性的任何代码仍然可以正常工作而不需要改变——只不过现在 radius在设置时就会进行验证。

Python程序员通常使用特性,而非在其他面向对象程序设计语言中通常使用的显式的获取者函数与设置者函数(比如getRadius()与setRadius()函数),这是因为将数据属性转变为特性非常容易,而又不影响该类的有效使用。

为将属性转换为可读/可写的特性,我们必须创建一个私有的属性,其中实际上存放了数据,并提供获取者方法与设置者方法。下面给出的是属性radius的获取者、设置者以及docstring的完整版。

我们使用assert语句来确保radius的取值为非0以及非负值,并将该值存储于私有属性self._radius中。需要注意的是,获取者与设置者(如果需要,还有一个删除者)有同样的名称——用于对其进行区分的是修饰器,修饰器会适当地对其进行重命名,从而避免发生名冲突。

用于设置者的修饰器最初看起来有些奇怪。每个创建的特性都包含getter、setter、 deleter 等属性,因此,在使用 @property 创建了 radius 特性之后,radius.getter、radius.setter 以及radius.deleter等属性就都是可用的。radius.getter被设置为@property修饰器的获取者方法,其他两个属性由Python设置,以便其不进行任何操作(因此这两个属性不能写或删除),除非用作修饰器,这种情况下,他们实际上使用自己用于修饰的方法来替代了自身。

@property

def radius(self):

"""The circle's radius

>>> circle = Circle(-2)

Traceback (most recent call last):

...

AssertionError: radius must be nonzero and non-negative

>>> circle = Circle(4)

>>> circle.radius = -1

Traceback (most recent call last):

...

AssertionError: radius must be nonzero and non-negative

>>> circle.radius = 6

"""

return self.__radius

@radius.setter

def radius(self, radius):

assert radius > 0, "radius must be nonzero and non-negative"

self.__radius = radius

Circle 的初始化程序(Circle.__init__())包括 self.radius = radius 语句,将调用 radius 特性的设置者方法,因此,如果创建Circle时给定了无效的radius值,就会产生一个 AssertionError异常。类似地,如果试图将现有Circle设置为无效值,仍然会调用设置者方法并产生异常。 docstring中包含了 doctests,以便测试在这些情况下是否正确地产生了异常。

Point与Circle类型都是自定义数据类型,包含足够的有用的功能。我们创建的大多数数据类型都类似于此,但偶尔也需要创建完整的自定义数据类型,下一小节中将看到实例。

创建完全整合的数据类型(Creating Complete Fully Integrated Data Types)

创建一个完整的数据类型时,有两种可能的方法。一种是从头开始创建,虽然该数据类型必然要继承自object (就像所有Python类一样),但是该数据类型所需要的每个数据属性以及方法(除__new()__之外)都必须提供。另一种方法是继承自现有的数据类型,这种数据类型与我们需要创建的数据类型相似,对这种情况,所需要的工作是重新实现那些需要不同行为的方法,其他方法则直接继承。

在下面介绍的内容中,我们将从头实现一个FuzzyBool数据类型,再之后我们将实现同样的数据类型,但使用了继承机制,以降低工作量。内置的bool类型是二值型 的(True或False),但在AI (人工智能)的某些领域使用的是模糊型布尔值,这种类型值包括“true”与“false”,但还包括一些介于二者之间的值。在我们的实现中,我们使用浮点值0.0表示False, 1.0表示True, 0.5表示50%的True, 0.25表示25%的 True,依此类推。下面给出了一些使用实例(两种实现的工作方式一样):

a = FuzzyBool.FuzzyBool(.875)

b = FuzzyBool.FuzzyBool(.25)

a >= b #returns: True

bool(a), bool(b) #returns:(True,false)

~a #returns:FuzzyBool(0.125)

a & b #returns:FuzzyBool(0.25)

b |= FuzzyBool.FuzzyBooI(.5) #returns:FuzzyBool(0.5)

"a={0:.1%} b={1:.0%}".format(a,b) #returns:'a=87.5% b=50%'

我们希望FuzzBool类型支持比较操作符(<、<=、==、!=、>=、>)的全集,也支持3种基本的逻辑操作符,即not (~)、and (&)以及or (|)。除逻辑操作符外,我们还希望提供两个其他的逻辑方法,conjunction()方法与disjunction()方法,这两个方法可以接受我们所需要数量的FuzzyBools,在处理后返回作为结果的FuzzyBool值。为使得该数据类型完整,需要提供到bool、 int、float以及str等数据类型的转换功能,还需要一个可使用eval()评估的表象形式,最后的一个需求是FuzzyBool要提供对str.format()格式规约的支持,FuzzyBoolS可用作字典的键或集合的成员,FuzzyBoolS是固定的但由于可以使用增强的赋值操作符(&=与|=),使得这种数据类型的使用也很便利。

__del__()特殊方法

在销毁对象时,会调用特殊方法__del__(self)— 至少在理论上如此.在实践中, 也可能从不调用__del__(self),即便在程序终止时也是如此.进一步地说,如果我们使用语句del x,实际上发生的处理是对象引用x被删除,并且x所指向的对象的对象引用计数减去1,只有在引用计数变为0时,才可能调用__del__()方法,但Python 并不保证一定调用该方法。鉴于此,实际编程时极少对__del__()方法进行重新实现 。该方法也不应该用于释放资源,因此,该方法不适合用于关闭文件、断开网络连接,或断开数据库连接.

Python提供了两种用于确保资源被正确释放的机制。一种方法是使用try ... finally语句块,我们在前面已经有所了解,另一种方法是将上下文对象与with语句一起使用。

特殊方法 使用 描述

__bool__(self) bool(x) 如果提供,就返回x的真值,对if x:...是有用的

__format__(self,format_spec) "{0}".format(x) 为自定义类提供str.format()支持

__hash__(self) hash(x) 如果提供,那么x可用作字典的键或存放在集合中

__init__(self,args) x = X(args) 对象初始化时调用

__new__(cls, args) x = X(args) 创建对象时调用

__repr__(self) repr(x) 返回x的字符串表示,在可能的地方eval(repr(x)) = x

__repr__(self) ascii(x) 仅使用ASCII字符返回x的字符串表示

__str__(self) str(x) 返回x适合阅读的字符串表示形式

数值型与位逻辑运算的特殊方法

特殊方法 使用 特殊方法 使用

__abs__(self) abs(x) __complex__(self) complex(x)

__float __(self) float(x) __int__(self) int(x)

__index__(self) bin(x) oct(x) hex(x) __round__(self,digits) round(x, digits)

__pos__(self) +x __neg__(self) -x

__add__(self,other) x + y __sub__(self,other) x-y

__iadd__(self,other) x+=y __isub__(self,other) x-=y

__radd__(self,other) y + x __rsub__(self,other) y-x

__mul__(self,other) x * y __mod__(self,other) x%y

__imul__(self,other) x *=y __imod__(self,other) x%=y

__rmul__(self,other) y * x __rmod__(self,other) y%x

__floordiv__(self,other) x//y __truediv__(self,other) x/y

__ifloordiv__(self,other) x//=y __itruediv__(self,other) x/=y

__rfloordiv__(self,other) y//x __rtruediv__(self,other) y/x

__divmod__(self,other) divmod(x, y) __rdivmod__(self,other) divmod(y, x)

__pow__(self,other) x ** y __and__(self,other) x&y

__ipow__(self,other) x **=y __iand__(self,other) x&=y

__rpow__(self,other) y ** x __rand__(self,other) y&x

__xor__(self,other) x^y __or__(self,other) x|y

__ixor__(self,other) x^=y __ior__(self,other) x|=y

__rxor__(self,other) y^x __ror__(self,other) y|x

__Ishift__(self,other) x<<y __rshift__(self,other) x>>y

__ilshift__(self,other) x<<=y __irshift__(self,other) x>>=y

__rlshift__(self,other) y<<x __rrshift__(self,other) y>>x

__invert__(self) ~x

从头开始创建数据类型(Creating Data Types from Scratch)

从头开始创建FuzzyBool意味着,我们必须提供一个属性来存放FuzzyBool的值,并 提供该数据类型所需要的所有方法。下面给出的是取自FuzzyBooLpy程序的class行:

class FuzzyBool:

def _init_(self, value=0.0):

self.__value = value if 0.0 <= value <= 1.0 else 0.0

在设计这一数据类型时,我们将值属性设置为私有的,因为我们需要FuzzyBool是一 种固定的数据类型,因此,如果允许对该值进行存取就会导致错误。另外,如果给定了一 个取值范围之外的值,我们就强制其取值为一个fail-safe值,0.0 (false)。在前面介绍的ShapeAlt.Circle类中,我们使用了更严格的策略,在创建新Circle对象时,如果给定的是无效的radius值,就会产生一个异常。图以展示了 FuzzyBool数据类型的继承树。

python3 面向对象编程_python面向对象编程_https://bianchenghao6.com/blog_Python_第2张

FuzzyBool类的继承体系

最简单的逻辑操作符是逻辑非,即NOT,为此,我们使用了位逻辑倒置操作符(~):

def __invert__(self):

return FuzzyBool(1.0 - self.__value)

位逻辑AND操作符(&)是由特殊方法__and__()提供的,增强版(&=)则是由特殊方法__iand__()提供的:

def __and__(self,other):

return FuzzyBool(min(self.__value, other.__va!ue))

def __iand__(self, other):

self.__value = min(self.__value, other.__value)

return self

位操作符AND以两个FuzzyBool值为基础,运算后返回一个新FuzzyBool值,其增强的赋值操作版则对私有值进行更新。严格地讲,这并不是一种固定数据类型的行为,但与其他一些Python固定数据类型也是匹配的,比如int, int类型在进行操作时, 比如使用+=操作符,看起来是改变了操作符左边的操作数,实际上进行了重新绑定, 使其指向存放加运算结果的新int对象。对上面给出的实例,则不需要进行重新绑定, 因为我们确实改变了 FuzzyBool变量本身。返回self的目的是为了支持下一步的其他 操作。

我们也可以实现__rand__()方法,在self与other代表的是不同的数据类型,并且没有为这两种数据类型实现__and__()方法时,就会调用__rand__()方法。对FuzzyBool 类而言,这是不需要的。大多数用于二元操作符的特殊方法都同时具备“i” (in-place) 与“r”(反射,即互换操作数)这两个版本。

我们没有展示用于位操作符|的__or__()方法,也没有展示用于in-place |= operator 的__ior__()方法,因为这两个方法与AND对应的方法是类似的,不同之处就在于这里取值时取的是self与other的最大值,而非最小值。

def __repr__(self):

return ("{0}({1})".format(self.__class__.__name__,self.__value))

我们创建了一种可进行eval()的表象形式,比如,给定f= FuzzyBool.FuzzyBool (.75),则 repr(f)将产生字符串'FuzzyBool(0.75)'。

所有对象都具备Python自动提供的某些特殊方法,其中的一种方法称为__class__, 实际上是对对象类的一个对象引用。所有类都有一个私有的__name__属性,也是由 Python自动提供的。我们使用这些属性来提供类名(用于其表象形式)。这意味着, 如果FuzzyBool类被继承时只是添加了一些额外的方法,那么继承而来的__repr__()方 法可以正确工作,而不需要对其进行重新实现,因为其可以获得子类的类名。

def __str__(self):

return str(self.__value)

对字符串形式,我们只是简单地返回已格式化为字符串表示的浮点值。我们并不是必须要使用super()以防止无限递归,因为我们是对self.__value属性调用str(),而不是对实例本身。

def __bool__(self):

return self.__value > 0.5

def __int__(self):

return round(self.__value)

def __float__(self):

return self.__value

特殊方法__bool__()将实例转换为Boolean,因此必须总是返回True或False;特殊方法__int__()则提供了到整型的转换功能。我们使用的是内置的round()函数,因为int() 只是简单地进行截取(因此对1.0以外的任意FuzzyBool值都将返回0)。浮点类型的转换是容易的,因为值本身就是一个浮点数

def __It__(self, other):

return self.__value < other.__value

def __eq__(self, other):

return self.__value == other.__value

为提供完整的比较操作符集(<、<=、==、!=、>=、>),有必要实现其中至少3 种,即<、<=与=,因为Python可以从〈推导出〉,从==推导出!=,从<=推导出>=。 这里我们只展示了其中的两种,因为所有这些操作符的实现都是非常类似的。默认情况下,自定义类的实例支持操作符==(总是返回False),并且是可哈希运算的(因此可用作字典的键,也可以添加到集合中),但是如果我们重新实现了特殊方法__eq__()以便提供正确的相等性测试功能。

def __hash__(self):

return hash(id(self))

实例就不再是可哈希运算的,不过这可以通过提供一个__hash__()特殊方法来弥补,如上面的代码所示。

Python提供了用于字符串、数字、固定集合以及其他类的哈希函数,这里我们只是简单地使用内置的hash()函数(该函数可用于任何具备__hash__()特殊方法的数据类型),并根据对象独一无二的ID计算哈希值。(注意,不能使用私有的self.__value, 因为该值可能作为增强赋值操作的结果并发生变化,而对象的哈希值必须保持不变。)

内置的id()函数会对作为参数的对象返回一个独一无二的整数,这一整数通常是该对象在内存中的地址,但是我们能做的只是假定没有哪两个对象会有相同的ID。实 际上,is操作符使用id()函数来确定两个对象引用是否指向相同的对象。

def __format__(self, format_spec):

return format(self.__value, format_spec)

只有在进行类定义时,才真正需要内置的format()函数,该函数以一个单独的对象以及一个可选的格式规约为参数,并返回该对象经过格式处理后生成的字符串。

在某对象用在格式化字符串中时,该对象的__format__()方法将被调用,并以对象以及格式规约为参数,该方法将返回进行了适当格式化处理的实例,就像前面我们看到的一样。

所有内置的类都包含适当的__format__()方法。这里,我们使用的是float.__format__() 方法,为其传递的参数是给定的浮点值以及相应的格式化字符串。使用类似于如下的形式, 也可以达到同样的效果:

def __format__(self, format_spec):

return self.__value.__format__(format_spec)

使用format()函数需要略少一些的键盘输入,并且更易于阅读。没有任何因素要求我们必须使用format()函数,因此,我们可以实现自己的格式规约语言,并在 __format__()方法内部对其进行解释——只要我们返回一个字符串即可:

@staticmethod

def conjunction(*fuzzies):

return FuzzyBool(min([float(x) for x in fuzzies]))

内置的staticmethod()函数的设计目标是用作修饰器,就像我们这里所做的一样。 简单地看,静态方法也是方法,并且不获取和使用self或Python专门传递的任何其他第一个参数。

操作符&可以进行结链处理,因此,给定FuzzyBool型变量f、g与h,我们可以通过f&g&h将其连接在一起。对数量较少的变量,这种方式可以正常工作,如果变量个数较多,比如十几个或更多,这种方法的效率就会很低,因为每个符号&都代表一个函数调用。实际上,我们可以使用一个单独的函数调用FuzzyBool.FuzzyBool.conjunction(f, g, h)来达到同样的目的。使用FuzzyBool实例,可以将这种调用表述得更加清晰,但由于静态方法不能获取self,因此,如果我们使用实例调用这个函数, 该实例本身也需要在函数中进行处理,就必须将该实例自身也作为参数传递给该函数, 比如 f.conjunction(f, g, h)。

我们没有展示相应的disjunction()方法,因为除了名字不同外,唯一的区别就是这个函数使用的是max()而非min()。

有些程序员认为使用静态方法不够Python化,只有在将代码从其他语言(比如 C++或Java)进行转换,或有某个方法不适用self的情况下,才使用静态方法。在Python 中,一般来说,较好的做法是创建一个模块函数,而不是使用静态方法,或使用一个类方法。

类似地,如果我们创建一个变量,该变量在某个类定义内部,但在任何方法之外, 该变量就是一个静态(类)变量。对常量而言,一般使用私有的模块全局变量更加方便,但对在某个类的所有实例之间进行数据共享而言,类变量通常是有用的。

至此,我们完全实现了 “从头”开始实现FuzzyBool类。我们必须重新实现15个 方法(如果我们实现所有4个比较操作符,就需要17个方法),还需要实现两个静态方法。在接下来的内容中,我们将展示一种替代的实现方案,该方案以对float的继承 为基础,其中只需要重新实现8个方法和2个模块函数——并取消其中的32个方法。

在大多数面向对象语言中,继承机制用于创建新类,新类继承了父类的所有方法 与属性,还包含一些我们希望新类具备的额外的方法与属性。Python完全支持这一点, 允许我们实现新方法,或对继承而来的方法进行重新实现,以便对类行为进行修改。 除此之外,Python还允许我们有效地取消某些方法,也就是说,新类可以摒弃某些继承而来但不需要的方法。这种做法并不会吸引面向对象的忠实信徒,因为这样做会打破多态性,但对Python而言,至少在偶尔有些情况下,这是一种有用的技术。

从其他数据类型创建数据类型(Creating Data Types from Other Data Types)

这里给出的FuzzyBool实现在文件FuzzyBoolAlt.py中,与前面给出的实现相比, 一个明显的差别就是这里不再提供静态方法conjunction()与disjunction(),而是以模块函数的形式进行提供的,比如:

def conjunction(*fuzzies):

return FuzzyBool(min(fuzzies))

这里的代码比前面的实现方案要简单得多,因为FuzzyBoolAlt.FuzzyBool对象是 float的子类,因此可以直接用于float相关的操作,而不需要进行任何转换操作。对这一函数的访问也比前面要更加清晰,这里不再需要同时指定模块与类(或使用实例),只需要先执行import FuzzyBoolAlt,就可以使用语句 FuzzyBoolAlt.conjunction()进行相关调用。

python3 面向对象编程_python面向对象编程_https://bianchenghao6.com/blog_Python_第3张

下面给出的是FuzzyBool的类定义行及其__new__()方法:

class FuzzyBool(float):

def __new__(cls, value=0.0):

return super().__new__(cls, value if 0.0 <= value <= 1.0 else 0.0)

创建一个新类时,通常是可变的,并依赖于object.__new__()来创建原始的、尚未初始化的对象。但对于固定类,我们需要在一个步骤中同时完成创建与初始化,因为对固定对象而言,一旦创建,就不能更改。

在任意对象创建之前,会调用__new__()方法(因为对象的创建就是__new__()所要做的事情),因此,该方法不能接受self对象作为参数,因为尚未存在。实际上, __new__()是一个类方法一这些方法与通常的方法类似,区别在于,类方法是对类本身进行调用的,而非实例,并且Python将调用的类作为这些方法的第一个参数。将变量名cls用于类仅仅是一个常规的做法,就像self作为对象本身的名称一样。

因此,当我们使用语句f=FuzzyBool(0.7)时,实际上,Python会调用FuzzyBool.__new__(FuzzyBool, 0.7)来创建一个新对象也就是fuzzy 之后调用fuzzy.__init__()进行更进一步的初始化操作,最后返回一个到fuzzy对象的对象引用——f所指向的也就是这一对象引用。__new__()的大部分工作都是传递给基类实现的,即object.__new__() 我们所做的就是确保值在有效的取值范围内。

类方法是使用内置的classmethod()函数(用作修饰器)建立的。为了方便,我们不需要在def __new__()前面写上@classmethod,因为Python已然知道这一方法总是一个类方法。当然,如果需要创建其他类方法,我们确实需要使用该修饰器。

既然我们已经展示了类方法,现在就可以明确一下不同方法间的差别。类方法的第一个参数是由Python指定的,也就是方法所属的类;通常方法的第一个参数也是由 Python指定的,是调用该方法的实例;静态方法则没有Python指定的第一个参数。所有这些类型的方法都接受我们传递给其的任意参数(如果是类方法或通常方法,会将 我们传递的参数依次作为其第二个参数、第三个参数……如果是静态方法就将我们传 递的参数依次作为其第一个参数、第二个参数……)。

def __invert__(self):

return FuzzyBool(1.0 - float(self))

与前面类似,这一方法用于提供对位操作符NOT (〜)的支持。需要注意的是,这里没用存取私有属性(存放FuzzyBool的值),而是直接使用self。这是因为,FuzzyBool 是从float继承而来的。这意味着,在可以使用float的任何地方,都可以使用FuzzyBool ——当然,这些地方不能使用FuzzyBool 已取消的方法。

def __and__(self, other):

return FuzzyBool(min(selft,other))

def __iand__(self, other):

return FuzzyBool(min(self, other))

上面定义的逻辑与前面也是类似的(尽管代码有一些差别),就像__invert__()方法 一样,我们可以直接使用self与other,尽管是floatS类型。我们忽略了OR对应的方法,因为与上面的定义相比,不同之处仅在于名称(__or__()与__ior__()),另外就是使用了max()而非min()。

def __repr__(self):

return ("{0}({1})".format(self.__class__.__name__,super()._repr_()))

我们必须重新实现__repr__()方法,因为该方法的基类版本只是以字符串形式返回数字,而我们需要类名。为使得表示是可进行eval()处理的,对str.format()的第二个参数,我们不能只是传递self,因为那将会导致对这个__repr__()方法的无限递归调用, 因此,我们调用的是基类的实现版本。我们并不是必须要重新实现__str__()方法,因为该方法的基类版本已经足够,可以用在FuzzyBool.__str__()的重新实现中。

def __bool__(self):

return self > 0.5

def __int__(self):

return round(self)

在Boolean上下文中使用浮点数时,如果浮点数的值为0.0,就代表False,其他情况则代表True。对FuzzyBoolS而言,这种方式是不合适的,因此我们必须重新实现这一方法。类似地,int(self)的作用也只是简单的剪切,将除1.0之外的任意值转换为0,因此,这里使用round()函数,该函数的作用是四舍五入,对0到0.5之间的值产生0,对0.5至1 (包括边界值)之间的值则产生1.0。

我们没有重新实现__hash__()方法、__format__()方法,以及用于提供比较操作符的任何其他方法,因为float基类提供的所有这些方法对FuzzyBoolS都可以正确工作。

我们重新实现的方法提供了对FuzzyBool类的完整实现——与前面从头实现的方案相比,所需要的代码大为减少。然而,这一新的FuzzyBool类也继承了30多个对FuzzyBoolS而言没有意义的多余的方法,比如,基本的数值型操作符与移位操作符(+、 -、*、/、<<、>>等),应用于FuzzyBoolS都是没有意义的。下面展示了怎样取消加法:

def __add__(self, other):

raise NotlmplementedError()

我们还必须为__iadd__()以及__radd__()等方法编写类似的代码,以便完全防止加法的使用。(注意,NotlmplementedError是一个标准的异常,与内置的Notlmplemented 对象是不同的。)另外一种替代的方法是产生NotlmplementedError异常,特别是如果需要更紧密地模拟Python内置类的行为时,可以产生一个TypeError异常。下面展示了如何使得FuzzyBool.__add__()的行为就像内置的类遇到了无效操作符:

def __add__(self, other):

raise TypeError("unsupported operand type(s) for +:'{0}' and '{1}'".format(self.__class__.__name__, other.__class__.__name__))

对单值操作,我们希望以模拟内置类型的方式取消其方法实现,代码也稍简单一些:

def __neg__(self):

raise TypeError("bad operand type for unary-: '{0}"'.format(self.__class__.__name__)

对比较操作符,有一种更简单的惯用做法,比如,如果需要取消实现操作符==, 可以使用如下语句:

def __eq__(self, other):

return Notlmplemented

如果实现了某个比较操作符(<、<=、==、!=、>=、>)的方法返回一个内置的 Notlmplemented对象,并且存在使用该方法的尝试,那么Python首先通过交换操作数 (这种情况下,other对象有一个适当的比较方法,因为self对象没有)来反转这种比较,如果这种做法不能正确工作,Python就产生一个TypeError异常,并给出异常消息解释说给定类型的操作数不支持这一操作。对于我们不需要的所有不能比较的方法, 我们必须或者产生NotlmplementedError异常,或者产生TypeError异常,就像前面对 __add__()与__neg__()方法所做的那样。

像上面这样取消实现每个不需要的方法是很乏味的,尽管这种做法确实有效并且易于理解。这里我们将展示一种可用于取消实现方法的高级技术——该技术在FuzzyBoolAlt模块中使用。

下面给出用于取消实现两个我们不需要的单值操作的代码:

for name, operator in (("__neg__","-"),("__index__", "index()”)):

message = ("bad operand type for unary {0}: '{{self}}"'.format(operator))

exec("def {0}(self): raise TypeError(\"{1}\".format("

"self=self.__class__,__name__))".format(name, message))

内置的exec()函数动态地执行从给定对象传递来的代码。这里给定的是一个字符串对象,但也可以传递其他类型的对象。默认情况下,代码是在某个封闭范围内的上下文中执行的,这里是在FuzzyBool类的定义范围之内,因此,执行的def语句会创建我们所需要的FuzzyBool方法。

在FuzzyBoolAlt模块被导入时,代码只执行一次。 下面给出的是为第一个元组("__neg__","-")生成的代码:

def __neg__(self):

raise TypeError("bad operand type for unary-: '{self}'".format(self=self.__class__.__name__))

我们已经使得异常、错误消息与Python为自己的类型使用的消息匹配,用于处理二元方法以及n元函数(比如pow())的代码遵循类似的模式,但使用的是不同的错误信息。为保持完整性,下面给岀使用的代码:

for name, operator in (("__xor__","^"),("__ixor__", "^="),

("__add__","+"),("__iadd__", "+="),("__radd__","+"),

("__sub__”,"-"),("__isub__", "-="),("__rsub__","-"),

("__mul__",”*”),("__imul__","*="), ("__rmul__”, "*”),

("__pow__", "**"),(”__ipow__“,"**="),

("__rpow__","**"), ("__floordiv__","//"),

("__truediv__","/"),("__itruediv__","/="),

("__rtruediv__", "/"),("__divmod__”,"divmod()"),

(”__rdivmod__”,"divmod()"),("__mod__",“%”),

("__imod__","%=”),(”__rmod__",”%”),

("__lshift__","<<"),(”__ilshift__","<<="),

("__rlshift__”,"<<"),("__rshift__”,">>"),

("__irshift__”,">>="),("__rrshift__”,">>")):

message = ("unsupported operand type(s) for {0}:"

"'({self}}'{{join})}{{args}}".format(operator))

exec("def {0}(self, *args):\n"

" types = [\"'\"+ arg.__class__.__name__+ \"'\" ”

"for arg in args]\n"

" raise TypeError(\"{1}\".format("

"self=self.__class__.__name__,"

”join=(\" and\" if len(args) == 1 else \",\"),"

"args=\",\".join(types)))".format(name, message))

上面的代码比前面要稍复杂一些,因为对于二元操作符,输出的消息中必须将两种类型分列为typel与type2,对于3个或更多的类型,则必须列为type1、type2、 type3 来模拟内置的行为,下面给出为第一个元组("__xor__","^") 生成的代码:

def __xor__(self,*args):

types = [""' + arg.__class__.__name__+ '"" for arg in args]

raise TypeError("unsupported operand type(s) for ^:"

”'{self}'{join}{args}”.format(self=self.__class__.__name__,

join=(" and" if len(args) == 1 else ","),

args=",".join(types)))

这里使用的两个for ... in循环语句块可以简单地剪切与粘贴,我们可以从第一个循环中移除或向其中添加单值操作符与方法,也可以从第二个循环中移除或向其中添加二元或n元操作符与方法,以便取消实现任何我们不需要的方法。

根据上面的代码,如果有两个FuzzyBools, f与g,并尝试使用f+g将其进行相加, 就会产生 TypeError 异常,并给出错误消息“unsupported operand type(s) for +: 'Fuzzy Bool' and 'FuzzyBool"',这些都是我们所期望的行为。

总而言之,以第一种方案创建FuzzyBool类的做法更常见,并可以用于几乎所有目的。然而,如果我们需要创建一个固定类,可以使用的方法是重新实现object._ new_()(该方法继承自Python的某种固定类型,比如float、int、str或tuple),之后 实现我们需要的所有其他方法。这种方法的不足是需要取消实现一些方法一这种做法会破坏多态性,因此,在大多数情况下,使用聚集(就像在第一种FuzzyBool实现中)是一种更好的方法。

以上内容部分摘自视频课程05后端编程Python-13面向对象(下),更多实操示例请参照视频讲解。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。

发表回复