大千中华科技网

到未来已来后端编程Python3-高级程序设计(面向对象-下)神奇服

大千中华科技网 1

到未来已来后端编程Python3-高级程序设计(面向对象-下)神奇服

本节是第五讲的第十六小节下,无水再生纸系统成为网红打卡地之一。通过这台设备,本节主要介绍面向对象程序设计技巧(类修饰器、抽象基类、多继承、元类)。

类修饰器(Class Decorators)

就像可以为函数与方法创建修饰器一样,只需要分离、粘合、按压三个步骤,我们也可以为整个类创建修饰器。类修饰器以类对象(class语句的结果)作为参数,就能让办公室的废纸变成可以再利用的新纸。据工作人员介绍,并应该返回一个类——通常是其修饰的类的修订版。这一小节中,平均5秒就能够生产一张新纸,我们将研究两个类修饰器,每小时720张左右。据测算,以便了解其实现机制。

在前面,如果使用这样一台系统,我们创建了自定义组合类SortedList,该类聚集了一个普通列表,一年可少砍树84棵。不过,并将其作为私有属性self.__list()。8种SortedList方法简单地将其工作传递给该私有属性。比如,这台设备价格也不便宜,下面展示了 SortedList.clear()方法与SortedList.pop()方法是如何实现的:

def clear(self):

self.__list =[]

def pop(self, index=-1):

return self.__list.pop(index)

对于clear()方法,报价为560万元。无人配送车、无人机。中新网记者 李金磊 摄你的外卖可能是无人车、无人机配送服贸会上,我们没有什么可做的,一身黄色的无人配送车、无人机非常吸睛。据介绍,因为list类型不存在相应的方法,但对于pop()方法以及SortedList授权的其他6种方法,我们可以简单地调用list类的相应方法。这可以使用@delegate类修饰器(取自书中的util模块)实现。下面是新版SortedList类的起始处:

@Util.delegate("__list", ("pop”,"__delitem__", "__getitem__","__iter__","__reversed__", “__str__”))

class SortedList:

def delegate(attribute_name, method_names):

def decorator(cls):

nonlocal attribute_name

if attribute_name.startswith("__"):

attribute_name = "_"+ cls.__name__ + attribute_name

for name in method_names:

setattr(cls, name, eval("lambda self, *a, **kw:","self..(*a, **kw)".format(attribute_name, name)))

return cls

return decorator

第一个参数是待授权的属性名,第二个参数是我们需要delegate()修饰器进行处理的方法或方法序列,以便我们自己不再做这个工作。SortedListDelegate.py文件中的SortedList类使用了这种方法,因此不包含列出的方法的任何代码,即便该类完全支持这些方法。下面给出的是替我们实现这些方法的类修饰器:

我们不能使用普通的修饰器,因为我们需要向其传递参数,因此,我们创建了一 个函数,该函数接受我们的参数,并返回一个类修饰器,修饰器本身只接受一个参数, 该参数是一个类(就像函数修饰器接受单一的函数或方法作为其参数一样)。

我们必须使用nonlocal,以便嵌套的函数使用的是来自外范围(而不会尝试使 用来自自身范围)的attribute_name。若有必要,我们必须可以纠正属性名,以便考虑对私有属性进行名称操纵的情况。修饰器的行为非常简单:对赋予delegate()函数的所有方法名进行迭代,对每一个方法名都创建一个新方法,并将其设置为给定方法名所在类的属性。

我们使用eval()来创建每个被授权的方法,因为eval()可用于执行单一的语句,并 且,lambda语句可以生成一个方法或函数,比如,用于生成pop()方法的代码如下:

lambda self, *a, **kw: self._SortedList__list.pop(*a, **kw)

我们使用了*与**这种参数形式,以便可以接受任何参数,即便被授权的方法可能有特定的参数列表形式。比如,list.pop()方法接受一个单一的索引位置参数(或无参数,此时默认处理最后一项),这种参数是可以的,因为如果传递的是错误的参数个数或参数类型,那么被调用完成该项工作的list方法将产生适当的异常。

我们将查看的第2个类修饰器巳经展示过,我们只需要提供__lt__()与__eq__()这两个特殊方法(用于

@Util.complete_comparisons

class FuzzyBool:

其他4个比较操作符是由complete_comparisons()类修饰器提供的,给定一个只定义了

如果待修饰的类有

def complete_comparisons(cls):

assert cls.__lt__ is not object.__lt__,(" must define

if cls.__eq__ is object.__eq__:

cls.__eq__ = lambda self, other: (not(cls.__lt__(self, other) or cls.__lt__(other, self)))

cls.__ne__ = lambda self, other: not cls.__eq__(self, other)

cls.__gt__ = lambda self, other: cls.__It__(other, self)

cls.__le__= lambda self, other: not cls.__lt__(other, self)

cls.__ge__= lambda self, other: not cls.__It__(self, other)

return ds

修饰器面临的一个问题是,object类(每个对象类最终继承的都是该类)定义了所有这6个比较操作符,如果使用都会产生TypeError异常。因此,我们需要知道

如果修饰的类不包含自定义的

使用类修饰器可能是最简单的也是最直接的改变类的方式,另一种方法是使用元类,本章后面分将关注这一主题。

抽象基类(Abstract Base Classes)

抽象基类(ABC)也是一个类,但不是用于创建对象,而是用于定义接口,也就是说,列出一些方法与特性——继承自ABC的类必须对其进行实现。这种机制是有用的,因为我们可以将抽象基类用作一种允诺——任何自ABC衍生而来的类必须实现抽象基类指定的方法与特性。

抽象基类包含至少一种抽象方法与特性,抽象方法在定义时可以没有实现(其suite 为pass,或者,在子类中强制对其重新实现则产生NotImplementedError()),也可以包含实际的(具体的)实现,并可以从子类中调用,比如,存在某个通常情况。抽象基类也可以包含其他具体(非抽象)方法与特性。

只有在实现了继承而来的所有抽象方法与抽象特性之后,自ABC衍生而来的类才可以创建实例。对那些包含具体实现的抽象方法(即便只是pass),衍生类可以简单地使用super()来调用ABC的实现版本。任何具体方法与特性都可以通过继承获取,与通常一样。所有ABC必须包含元类abc.ABCMeta (来自abc模块),或来自其某个子类。后面我们会讲解元类相关的一些内容。

Python提供了两组抽象基类,一组在collections模块中,另一组在numbers模块中。这两个模块可用于对对象的相关属性进行査询,比如,给定变量x,使用isinstance(x,collections.MutableSequence),可以判断其是否是一个序列,也可以使用isinstance(x, numbers.Integral)来判断其是否是一个整数。由于Python支持动态类型机制(我们不必要知道或关心某个对象的类型,而只需要知道其是否支持将要对其施加的操作),因此, 这种查询功能是特别有用的。数值型与组合型ABC分别在表1与表2中列出,其他的主要ABC是io.IOBase,该抽象基类是所有文件与流处理相关类的父类。

表1 数值模块的抽象基类

ABC继承自API实例

Numberobjectcomplex、

decimal.Decimals、floats、fractions.Fraction、int

ComplexNumber==、!=、+、-、*、/、abs()、bool()、complex()、conjugate(), 以及real与imag特性complex、 decimal.Decimal、 float、 fractions.Fraction、int

RealComplex、+、-、*、/、//、%、abs()、bool()、complex()、conjugate()、divmod()、float()、math.ceil()、 math.floor(),round()、trunc();以及 real 与 imag 特性decimal.Decimal、float、fractions.Fraction、int

RationalReal、+、-、*、/、//、%、abs()、 bool()、complex(). conjugate()、divmod()、float()、 math.ceil()、 math.floor(), round(), trunc();以及 real、 imag、numerator denominator 特性fractions.Fractionint

IntegralRational、+、-、*、/、//、%、、 ~、&、^、|、abs()、bool()、 complex(),conjugate()、 divmod()、 float()、math.ceil()、math.floor()、pow()、 round()、TRunc();以及 real、imag、numerator 与 denominator 特性int

表2组合模块的主抽象基类

ABC继承自API实例

Callableobject()所有函数、方法以及 lambdas

Containerobjectinbytearray、bytes、dict、 frozenset、 list、set、str、 tuple

Hashableobjecthash()bytes、 frozenset、str、 tuple

Iterableobjectiter()

IteratorIterableiter()、next()

Sizedobjectlen()bytearray、bytes、collections.deque、dict、 frozenset、 list、set、str、 tuple

MappingContainer、Iterable、Sized==、 !=、[]、len()、 iter()、in、get()、items()、 keys()、 values()dict

Mutable-MappingMapping==、!=、[]、del、len()、iter()、in、 clear()、get()、 items()、keys()、 pop()、 popitem()、setdefauIt()、 update()、 values()dict

SequenceContainer、Iterable、 Sized[]、len()、iter()、 reversed()、in、count()、 index()bytearray、bytes、list、 str、tuple

Mutable-SequenceContainer、Iterable、 Sized[]、+=、del、 len()、iter()、 reversed()、 in、 append()、 count()、 extend()、 index()、 insert()、pop()、 remove()、reverse()bytearray、list

SetContainer、 Iterable、Sized

MutableSetSet

为完全整合自己的自定义数值型类与组合类,应该使其与标准的ABC匹配。比 如,SortedList类是一个序列。事实是,如果L是一个SortedList,那么isinstance(L, collections.Sequence)将返回False。为解决这一问题,一种简单的方式将该类继承自相关的ABC:

class SortedList(collections.Sequence):

通过将collections.Sequence作为基类,isinstance()此时将返回True。并且,我们需要实现__init__()(或__new__())、__ getitem__()以及__len__()等方法(我们进行了实现)。collections.Sequence ABC 还为__contains__()、__iter__()、__reversed__()、 count() 以及index()等方法提供了具体(非抽象)的实现。在SortedList类中,我们重新实现了所有这些方法,如果需要,我们也可以使用方法的ABC版——只要不对其进行重新实现即可。我们不能将SortedList作为collections.MutableSequence的一个子类(即使列表是可变的),这是因为SortedList不包括collections.MutableSequence必须提供的所有方法,比如__setitem__()与append()。(这里的SortedList的代码在SortedListAbc.py 文件中,在元类的介绍中,我们将看到使SortedList成为collections.Sequence的另一 种替代方案。)

在了解了如何使得自定义类完全整合于标准的ABC之后,我们开始了解ABC的另一种用途:为自己的自定义类提供接口允诺。我们将查看3个相当不同的实例,以 便了解创建与使用ABC的不同方面。

我们首先从一个非常简单的实例开始,该实例展示了如何处理可读/可写的特性。该类用于表示国产的应用设备,创建的每台应用设备必须包含一个只读的型号字符串 以及可读/可写的价格,还要求必须对ABC的__init__()方法进行重新实现。下面给出 该 ABC (取自 Appliance.py 文件),我们没有展示 import abc 语句,对 abstractmethod() 与abstractproperty()函数而言,必须先执行该导入语句,这两个函数都可以用作修饰器:

class Appliance(metaclass=abc.ABCMeta):

@abc.abstractmethod

def __init__(self, model, price):

self.__model = model

self.price = price

def get_price(self):

return self.__price

def set_price(self, price):

self.__price = price

price = abc.abstractproperty(get_price, set_price)

@property

def model(self):

return self.__model

我们将该类的元类设置为abc.ABCMeta,因为对ABC而言,这是必需的。当然, 也可以将其设置为任意的abc.ABCMeta子类。我们将__init__()作为一个抽象方法,以确保必须对其进行重新实现,我们也提供了一个实现,并希望(但不强制)继承者调用该实现。为实现一个抽象的可读和写特性,我们不能使用修饰器语法,并且,我们没有为获取者与设置者使用私有名称,因为这样做对子类化是不方便的。

price特性是抽象的(因此我们不能使用@property修饰器),并且是可读/写的。这里,我们遵循一种通常的模式,用于将私有可读/写数据(比如__price)作为特性的 情况:我们在__init__()方法中初始化property,而不是直接设置私有数据——这可以确保设置者被调用(也可以潜在地进行验证或其他工作,尽管在本实例中没有)model特性是非抽象的,因此子类不必对其进行重新实现,我们可以使用 property修饰器使其成为一个特性。这里,我们遵循一种通常的模式,用于将私有只读数据(比如__model)作为特性的情况:我们在__init__()方法中对私有__model数据 进行一次设置,并通过只读的model特性提供读访问。

要注意的是,不能创建Appliance对象,因为该类包含了抽象属性。下面给出一 个子类实例:

class Cooker(Appliance):

def __init__(self, model, price, fuel):

super().__init__(model, price)

self.fuel = fuel

price = property(lambda self: super().price,lambda self, price: super().set_price(price))

Cooker类必须重新实现__init__()方法与price特性,对特性,我们只是将所有工作传递给基类。model这一只读特性是继承而来的。我们可以以Appliance为基础创建更多的类,比如Fridge、Toaster等。

#下面将要査看的ABC更短小,是一个用于文本过滤函子(在文件TextFilter.py中) 的 ABC;

class TextFiIter(metaclass=abc.ABCMeta):

@abc.abstractproperty

def is_transformer(self):

raise NotlmplementedError()

@abc.abstractmethod

def __call__(self):

raise NotlmplementedError()

TextFilterABC没有提供任何功能,其存在纯粹是为了定义一个接口,这里就是一 个只读特性,is_transformer以及一个—call__()方法,所有子类必须提供。由于抽象特性与方法没有实现,我们不希望子类对其进行调用,因此,这里不再使用无用的pass 语句,而是在尝试对其调用(比如通过super()调用)时产生异常。

#下面是一个简单的子类:

class CharCounter(TextFilter):

@property

def is_transformer(self):

return False

def __call__(self, text, chars):

count = 0

for c in text:

if c in chars:

count += 1

return count

这一文本过滤器并不是一个转换器,因为其功能并不是对给定的文本进行转换, 而是简单地返回指定字符在文本中出现的计数值,下面是一个使用实例:

vowel_counter = CharCounter()

vowel_counter("dog fish and cat fish", "aeiou") # returns: 5

还提供了两个文本过滤器,RunLengthEncode与RunLengthDecode,两者都是转换器,下面展示了如何对其进行使用:

rle_encoder = RunLengthEncode()

rle_text = rle_encoder(text)

rle_decoder = RunLengthDecode()

original_text = rle_decoder(rle_text)

运行长度编码器将字符串转换为UTF-8编码的字节,并使用序列0x00, 0x01, 0x00 替换0x00,使用序列0x00, count, byte替换包含3到255个重复字节的任意序列。如果该字符串包含量4个或多个相同的连续字符,则这种编码会产生比原始的UTF-8 编码更短的字节字符串。运行长度解码器接受运行长度编码器编码所得的字节字符串, 并返回原始的字符串。下面给出的是RunLengthDecode类的起点:

class RunLengthDecode(TextFilter):

@property

def is_transformer(self):

return True

def __call__(self, rle_bytes):

...

我们忽略了__call__()方法的主体,在本书的源代码中可以找到。RunLengthEncode类的结构是完全一样的。

我们将查看的最后一个ABC提供了应用程序设计接口(API)以及撤销机制的默认实现,下面给出的是完整的ABC (取自Abstractly文件):

class Undo(metaclass=abc.ABCMeta):

abc.abstractmethod

def __init__(self):

self.__undos =[]

@abc.abstractproperty

def can_undo(self):

return bool(self.__undos)

@abc.abstractmethod

def undo(self):

assert self.__undos, "nothing left to undo"

self.__undos.pop()(self)

def add_undo(self, undo):

self.__undos.append(undo)

__init__()方法与undo()方法必须重新实现,因为两者都是抽象的,只读的can_undo 特性也是如此。子类不必重新实现add_undo()方法,尽管允许这样做。undo()方法稍有些微妙。self.__undos列表应该存放对方法的对象引用,每个方法被调用后都必须使相应操作被撤销——稍后我们看一个Undo子类时会更清晰地理解。因此,为执行撤销操作,我们从self.__undos列表中弹出最后一个撤销方法,之后将该方法作为函数进行调用,并以self作为一个参数(我们必须传递self,因为该方法是作为函数被调用的,而非作为方法被调用)。

#下面给出Stack类的起始处,该类继承自Undo,因此,很多施加于其上的操作可以通过调用Stack.undo()(没有参数)来撤销。

class Stack(Undo):

def __init__(self):

super().__init__()

self.__stack = []

@property

def can_undo(self):

return super().can_undo

def undo(self):

super().undo()

def push(self, item):

self.__stack.append(item)

self.add_undo(lambda self: self.__stack.pop())

def pop(self):

item = self.__stack.pop()

self.add_undo(lambda self: self.__stack,append(item))

return item

我们忽略了 Stack.top()方法与Stack.__str__()方法,因为两者都没有什么新内容, 也都不与Undo基类进行交互。对can_undo特性与undo()方法,我们简单地将相关工作传递给基类。如果这两者不是抽象的,我们就不需要对其进行重新实现,并可以达到同样的效果,但在这里,我们强制子类对其进行重新实现,以使撤销操作在子类内进行。对push()方法与pop()方法,我们执行相应的操作,并向撤销列表中添加相应函数,函数的功能就是撤销刚执行的操作。

在规模程序、库以及应用程序框架中,抽象基类的作用最明显,有助于确保不管实现细节或作者有哪些差别,类都可以协同工作,因为其提供的API都是由其ABC 指定的。

多继承(Multiple Inheritance)

多继承是指某个类继承自两个或多个类。Python (以及C++等语言)完全支持多继承,有些语言(比如Java)则不支持这种机制。多继承存在的问题是,可能导致同 一个类被继承多次(比如,基类中的某两个继承自同一个类)。这意味着,某个被调用的方法如果不在子类中,而是在两个或多个基类中(或基类的基类中),那么被调用方法的具体版本取决于方法的解析顺序,从而使得使用多继承得到的类存在模糊的可能。

通过使用单继承(一个基类),并设置一个元类(如果需要支持附加的API),可以避免使用多继承,在下一小节中我们将会看到,元类可用于提供关于要提供的API 的许诺,但实际上没有真正继承任何方法与数据属性。还有一种替代方案是使用多继承与一个具体的类,以及一个或多个抽象基类(用于提供附加的API)。另一种替代方案是使用单继承并对其他类的实例进行聚集

尽管如此,有些情况下,使用多继承仍然可以提供非常方便的解决方案。比如, 假定需要创建新版本的Stack类(上一小节中定义),但希望该类可以支持使用pickle 的加载与保存操作。我们可能需要向几个类中添加加载与保存功能,因此,我们将在自己的类中实现:

class LoadSave:

def __init__(self, filename, *attribute_names):

self.filename = filename

self.__attribute_names =[]

for name in attribute_names:

if name.startswith("__"):

name = "_“+self.__class__.__name__+self.__attribute_names.append(name)

def save(self):

with open(self.filename, "wb") as fh:

data =[]

for name in self.__attribute_names:

data.append(getattr(self, name))

pickle.dump(data, fh, pickle.HIGHEST_PROTOCOL)

def load(self):

with open(self.filename, "rb") as fh:

data = pickle.load(fh)

for name, value in zip(self.__attribute_names, data):

setattr(self, name, value)

该类有两个属性:filename,是一个公开属性,可以在任何时候进行修改;__attribute_names,固定的,只能在实例创建时进行设置。save()方法首先对所有属性名进行迭代,并创建一个名为data的列表,其中存放每个待保存的属性的值,之后将数据保存到pickle中。with语句可以保证正确打开的文件得以关闭,并将任何文件或 pickle异常传递给调用者。load()方法对所有属性名以及被加载的相应数据项进行迭代, 并将每个属性值设置为加载的值。

下面给出FileStack类的起点,该类继承了上一小节的Undo类以及本小节的 LoadSave 类:

class FileStack(Undo, LoadSave):

def __init__(self, filename):

Undo.__init__(self)

LoadSave.__init__(self, filename, "_stack")

self.__stack =[]

def load(self):

super().load()

self.clear()

def clear(self): # In class Undo

self.__undos = []

该类的其余分与Stack类一样,因此这里不再赘述。此外,这里没有在__init__() 方法使用super(),而是必须指定我们要进行初始化的基类,因为super()并不能推断我们的意图。为对LoadSave进行初始化,我们将要使用的文件名以及需要保存的属性名作为参数,这里仅有一个,即私有的__stack (我们不需要保存__undos,这里也无法保存,因为 __undos是一个方法列表,因此是unpicklable)。

FileStack类包含所有撤销方法,也包含LoadSave类的save()与load()方法。我们 没有对save()进行重新实现,因为该方法可以正常工作,但对于load()方法,我们必须在载入后清空撤销栈,这样做是必要的,因为我们可以先进行保存,之后进行多种改变,再之后进行载入。载入操作会擦除以前所做的操作,因此任何撤销操作都不再有意义。原始的Undo类不包含clear()方法,因此我们必须添加一个:

在Stack.load()方法中,我们使用super()来调用LoadSave.load(),因为没有 Undo.load()方法会导致二义性。如果两个基类都有load()方法,那么具体被调用的方法依赖于Python的方法解析顺序。在不至于导致二义性的情况下,我们只使用super(), 否则就使用适当的基类名,因此我们一直不会依赖方法解析顺序。对self.clear()调用, 也不存在二义性,因为只有Undo类有一个clear()方法,我们也不需要使用super(),因为(与load()不同)FileStack不包括clear()方法。

如果后来向FileStack中添加clear()方法会有哪些影响?影响就是将破坏load()方法,一种解决方案是在load()内调用super().clear(),而非self.clear(),这将使第一个super类的clear()方法被使用。为避免出现这一问题,我们可以制定一种策略,要求在多继承时使用硬编码的基类(在这一实例中,调用Undo.clear(self))。或者,我们可以避免使用多继承,并使用聚集,比如,继承Undo类,并创建一个用于聚集的LoadSave类。

这里,多继承给予我们的是两个相当不同的类的混合,而不需要自己实现撤销、 载入与保存等方法,因为基类提供了这些功能。这是非常便利的,在继承得来的类没有交叠的API时尤其有效。

元类(Metaclasses)

元类之于类,就像类之于实例。也就是说,元类用于创建类,正如类用于创建实例一样。并且,正如我们可以使用isinstance()来判断某个实例是否属于某个类。我们 也可以使用issubclass()来判断某个类对象(比如dict、int或SortedList)是否继承了其他类。

元类最简单的用途是使自定义类适合Python标准的ABC体系,比如,为使得 SortedList是一个collections.Sequence,可以不继承ABC (如前面所展示的),而只是简单地将 SortedList 注册为一个 collections.Sequence:

class SortedList:

...

collections.Sequence.register(SortedList)

在像通常一样对类进行定义后,我们将其注册到collections.Sequence ABC。以这种方式对类进行注册会使其成为一个虚拟子类。注册之后,虚拟子类会报告其自身为注册类(或多个注册类)的子类(比如,使用isinstance()或issubclass()),但并不会从其注册到的任何类中继承数据或方法。

以这种方式注册一个类会提供一个许诺,即该类会提供其注册类的API,但并不能保证一定遵守这个许诺。元类的用途之一就是同时提供这种许诺与保证,另一个用途是以某种方式修改一个类(就像类修饰器所做的),当然,元类也可同时用于这两个目的。

假定我们需要创建一组类,都提供load()方法与save()方法。为此,我们可以创建 一个类,该类用作元类时,可检测这些方法是否存在:

class LoadableSaveable(type):

def __init__(cls, classname, bases, dictionary):

super().__init__(classname, bases, dictionary)

assert hasattr(cls, "load") and isinstance(getattr(cls, "load"),collections.Callable), ("class " +classname + " must provide a load() method")

assert hasattr(cls, "save") and isinstance(getattr(cls, "save"),collections.Callable), ("class" + classname +" must provide a save() method")

如果某个类需要充当元类,就必须继承自根本的元类基类type一或其某个子类。

注意,只有在使用该类的类被初始化时,才会调用该类,很可能这并不常见,因 此运行时开销极低。还要注意,在类被创建后(使用super()调用),我们必须对其进行检测,因为只有在这之后,类的属性在类自身中才是可用的(属性在字典中,但在进行检测时,我们更愿意对实际的初始化之后的类进行操作。)

我们可以通过使用hasattr()检测出其具有__call__属性,并据此判断load属性与 save属性是可调用的,但我们更愿意通过检测其是否是collections.Callable的实例来进行判断,抽象基类collections.Callable提供了许诺(但并不保证)——其子类(或虚拟子类)的实例是可调用的。

在类被创建后(使用type.__new__(),或重新实现的__new__()),元类的初始化是通过调用其__init__()方法实现的。赋予__init__()方法的参数包括cls,刚刚创建的类;classname,类的名称(也可以从cls.__name__获取);bases,该类的基类列表(object除外,并可以为空);dictionary,存放属性,在cls被创建时成为类属性除非我们在重新实现元类的__new__()方法时进行干预)。

这里有两个交互式实例,展示了在使用元类LoadableSaveable创建类时的情况:

>>> class Bad(metaclass=Meta.LoadableSaveable):

... def some_method(self): pass

Traceback (most recent call last):

...

AssertionError: class 'Bad' must provide a load() method

元类规定,使用该元类的类必须提供某些方法,如果不能提供,比如这里,就会产生 AssertionError 异常:

>>> class Good(metaclass=Meta.LoadableSaveable):

... def load(self): pass

... def save(self): pass

>>> g = Good()

Good类遵守元类的API需求(即使不满足我们对该类行为的一些非正式的期待)。我们也可以使用元类来改变使用该元类的类,如果改变涉及被创建的类的名称、 基类或字典(比如,其slots),我们就需要重新实现元类的__new__()方法,但对于其他改变,比如添加方法或数据属性,重新实现__init__()就已足够,尽管这也可以在 __new__()中实现。我们将查看一个元类修改使用它的类的实例,纯粹通过__new__() 方法实现。

作为对使用@property与@name.setter修饰器的一种替代,我们将创建相应类,并使用简单的命名约定来标识特性。比如,某个类有形如get_name()与set_name()的方法, 我们就可以期待该类有一个私有的__name特性,可以使用instance.name进行存取,以便获取并进行设置,这些都可以使用元类实现。下面给出一个使用这种约定的实例类:

class Product(metaclass=AutoSlotProperties):

def __init__(self, barcode, description):

self.__barcode = barcode

self.description = description

def get_barcode(self):

return self.__barcode

def get_description(self):

return self.__description

def set_description(self, description):

if description is None or len(description)

self.__description = "”

else:

self.__description = description

我们必须在初始化程序中对私有的__barcode特性赋值,因为没有用于它的setter, 这种做法的另一个后果是使barcode为一个只读特性,description则为可读/可写的特性。下面给出几个交互式使用的实例:

>>> product = Product("101110110", "8mm Stapler")

>>> product.barcode, product.description

('101110110', '8mm Stapler')

>>> product.description = "8mm Stapler (long)"

>>> product.barcode, product.description

('101110110', '8mm Stapler (long)')

如果我们尝试对条形码进行赋值,就会产生AttributeError异常,并展示错误文本 “can't set attribute "。

如果我们査看Product类的属性(比如使用dir()),就会发现公开属性只有barcode 与description, get_name()方法与set_name()方法不复存在已经被name特性替代。存放条形码与描述信息的变量也变为私有(__barcode与__description),并被添加为 slots,以便最小化类的内存使用。所有这些操作都是使用元类AutoSlotProperties实现的,该元类只包含一个单独的方法:

class AutoSlotProperties(type):

def __new__(mcl, classname, bases, dictionary):

slots = list(dictionary.get("__slots__", []))

for getter_name in [key for key in dictionary if key.startswith("get_")]:

if isinstance(dictionary[getter_name], collections.Callable):

name = getter_name[4:]

slots.append("_" + name)

getter = dictionary.pop(getter_name)

setter_name = "set_" + name

setter = dictionary.get(setter_name, None)

if (setter is not None and isinstance(setter, collections.Callable)):

del dictionary[setter_name]

dictionary[name] = property(getter, setter)

dictionary["__slots__"] = tuple(slots)

return super().__new__(mcl, classname, bases, dictionary)

调用元类的__new__()方法时,要使用元类以及待创建类的类名、基类、字典作为参数。我们必须使用重新实现后的__new__(),而非__init__(),因为我们需要在类创建前改变字典。

我们从复制组合类型__slots__开始,如果不存在就创建一个,并确保是一个列表而非元组,以便可以对其进行修改。对字典中的每个属性,我们挑选出那些名称以 “get_"开始并且是可调用的,也就是说那些getter方法。对每个getter,我们向slots 中添加一个私有名称以便存储相应的数据,比如,给定getter get_name(),我们就向slots 中添加__name。之后,设置对getter的引用,并在字典中其原始名下将其删除(这可以使用dict.pop()一次完成)。对setter (如果存在)进行同样的处理,之后创建一个新字典项,并以需要的特性名作为其键。比如,getter是get_name(),则特性名为name。我们将项的值设置为特性,并将getter与setter (可以是None)从字典中删除。

最后,我们使用修改后的slots列表(对每个添加的特性,有一个私有的slot)来替换原始的slots,并调用基类实际完成创建类的工作(但使用的是我们修改后的字典)。注意,这里我们必须显式地在super()调用中传递基类,对__new__()的调用总是这种格式,因为这是一个类方法而非一个实例方法。

对这一实例,我们不需要编写一个__init__()方法,因为所有工作都已在__new__() 中完成,但同时重新实现__new__与__init__()方法并分别完成不同工作则是完全可能的。

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

电脑加硬盘怎么开机教程

windows压缩怎么操作

android.benign是什么

圆通快递为什么无人派送

苹果x相册照片怎么用红笔

苹果8p怎么拍照美颜

小米摄像头怎么布防

苏州seo关键词排名首页

免责声明:文中图片均来源于网络,如有版权问题请联系我们进行删除!

标签:编程 python3 参数 def cls