百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

后端编程Python3-高级程序设计(面向对象-下)

off999 2024-10-19 07:14 12 浏览 0 评论

本节是第五讲的第十六小节下,本节主要介绍面向对象程序设计技巧(类修饰器、抽象基类、多继承、元类)。

类修饰器(Class Decorators)

就像可以为函数与方法创建修饰器一样,我们也可以为整个类创建修饰器。类修饰器以类对象(class语句的结果)作为参数,并应该返回一个类——通常是其修饰的类的修订版。这一小节中,我们将研究两个类修饰器,以便了解其实现机制。

在前面,我们创建了自定义组合类SortedList,该类聚集了一个普通列表,并将其作为私有属性self.__list()。 8种SortedList方法简单地将其工作传递给该私有属性。 比如,下面展示了 SortedList.clear()方法与SortedList.pop()方法是如何实现的:

def clear(self):

self.__list =[]

def pop(self, index=-1):

return self.__list.pop(index)

对于clear()方法,我们没有什么可做的,因为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.{0}.{1}(*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()类修饰器提供的,给定一个只定义了< (或<与==)的类,则修饰器将生成未给出的其他比较操作符,这是通过如下的一些逻辑等价关系实现的:

如果待修饰的类有<与==操作符,那么修饰器将使用这两个操作符;如果只提供了< 操作符,就回退到使用<完成所有任务的情况。(实际上,提供了<,则Python会自动地生成>;提供了==,则Python会自动生成!=,因此只要实现3个操作符<、<=与==,Python 就完全可以推断出其他操作符。然而,通过使用类修饰器,可以将实现操作符的工作量最小化到只有<,这是方便的,并可以确保所有比较操作符使用相容的逻辑。)

def complete_comparisons(cls):

assert cls.__lt__ is not object.__lt__,("{0} must define < and ideally ==".format(cls.__name__)

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异常。因此,我们需要知道< 与 ==是否巳被重新实现(因此是可用的),通过将类中相关的正在进行修饰的特殊方法对象中的方法进行比较,就可以很容易地做到。

如果修饰的类不包含自定义的<,那么断言将失败,因为这是修饰器的最小需求。 如果有一个自定义的==,我们就使用,否则,就创建一个。之后,所有其他方法都被创建,而修饰的类(现在包含所有6个比较方法)将被返回。

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

抽象基类(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 实例

Number object complex、

decimal.Decimals、floats、fractions.Fraction、int

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

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

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

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

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

ABC 继承自 API 实例

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

Container object in bytearray、bytes、dict、 frozenset、 list、set、str、 tuple

Hashable object hash() bytes、 frozenset、str、 tuple

Iterable object iter()

Iterator Iterable iter()、next()

Sized object len() bytearray、bytes、collections.deque、dict、 frozenset、 list、set、str、 tuple

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

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

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

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

Set Container、 Iterable、Sized <、<=、==、!=、=>、 >、&、|、^、len()、iter()、in、isdisjoint() frozenset、set

MutableSet Set <、<=、==、!=、=>、>、 &、|、^、&=、|=、^=、-=、len()、iter()、in、 add()、 clear()、 discard()、isdisjoint()、 pop()、remove() set

为完全整合自己的自定义数值型类与组合类,应该使其与标准的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()调用)时产生异常。

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

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) < 3:

self.__description = "<Invalid 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高级程序设计(面向对象-下),更多实操示例请参照视频讲解。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。

相关推荐

每天一个 Python 库:datetime 模块全攻略,时间操作太丝滑!

在日常开发中,时间处理是绕不开的一块,比如:生成时间戳比较两个时间差转换为可读格式接口传参/前端展示/日志记录今天我们就用一个案例+代码+思维导图,带你完全搞定datetime模块的用法!...

字节跳动!2023全套Python入门笔记合集

学完python出来,已经工作3年啦,最近有很多小伙伴问我,学习python有什么用其实能做的有很多可以提高工作效率增强逻辑思维还能做爬虫网站数据分析等等!!最近也是整理了很多适合零基...

为什么你觉得Matplotlib用起来困难?因为你还没看过这个思维导图

前言Matplotlib是一个流行的Python库,可以很容易地用于创建数据可视化。然而,设置数据、参数、图形和绘图在每次执行新项目时都可能变得非常混乱和繁琐。而且由于应用不同,我们不知道选择哪一个图...

Python新手必看!30分钟搞懂break/continue(附5个实战案例)

一、跳转语句的使命当程序需要提前结束循环或跳过特定迭代时,break和continue就是你的代码急刹按钮和跳步指令。就像在迷宫探险中:break=发现出口立即离开continue=跳过陷阱继续前进二...

刘心向学(24)Python中的数据类(python中5种简单的数据类型)

分享兴趣,传播快乐,增长见闻,留下美好!亲爱的您,这里是LearningYard新学苑。今天小编为大家带来文章“刘心向学(24)Python中的数据类”欢迎您的访问。Shareinterest,...

刘心向学(25)Python中的虚拟环境(python虚拟环境安装和配置)

分享兴趣,传播快乐,增长见闻,留下美好!亲爱的您,这里是LearningYard新学苑。今天小编为大家带来文章“刘心向学(25)Python中的虚拟环境”欢迎您的访问。Shareinte...

栋察宇宙(八):Python 中的 wordcloud 库学习介绍

分享乐趣,传播快乐,增长见识,留下美好。亲爱的您,这里是LearingYard学苑!今天小编为大家带来“Python中的wordcloud库学习介绍”欢迎您的访问!Sharethefun,...

AI在用|ChatGPT、Claude 3助攻,1分钟GET高颜值思维导图

机器之能报道编辑:Cardinal以大模型、AIGC为代表的人工智能浪潮已经在悄然改变着我们生活及工作方式,但绝大部分人依然不知道该如何使用。因此,我们推出了「AI在用」专栏,通过直观、有趣且简洁的人...

使用DeepSeek + Python开发AI思维导图应用,非常强!

最近基于Deepseek+PythonWeb技术开发了一个AI对话自动生成思维导图的应用,用来展示下如何基于低门槛的Python相关技术栈,高效结合deepseek实现从应用场景到实际应用的快速落地...

10幅思维导图告诉你 - Python 核心知识体系

首先,按顺序依次展示了以下内容的一系列思维导图:基础知识,数据类型(数字,字符串,列表,元组,字典,集合),条件&循环,文件对象,错误&异常,函数,模块,面向对象编程;接着,结合这些思维导图主要参考的...

Python基础核心思维导图,让你轻松入门

Python基础核心思维导图【高清图文末获取】学习路线图就给大家看到这里了,需要的小伙伴下方获取获取方式看下方图片...

Python基础核心思维导图,学会事半功倍

Python基础核心思维导图【高清图文末获取】学习路线图就给大家看到这里了,需要的小伙伴下方获取获取方式看下方图片...

硬核!288页Python核心知识笔记(附思维导图,建议收藏)

今天就给大家分享一份288页Python核心知识笔记,相较于部分朋友乱糟糟的笔记,这份笔记更够系统地总结相关知识,巩固Python知识体系。文末获取完整版PDF该笔记学习思维导图:目录内容展示【领取方...

Python学习知识思维导图(高效学习)

Python学习知识思维导图python基础知识python数据类型条件循环列表元组字典集合字符串序列函数面向对象编程模块错误异常文件对象#python##python自学##编程#...

别找了!288页Python核心知识笔记(附思维导图,建议收藏)

今天就给大家分享一份288页Python核心知识笔记,相较于部分朋友乱糟糟的笔记,这份笔记更够系统地总结相关知识,巩固Python知识体系。文末获取完整版PDF该笔记学习思维导图:目录内容展示【领取方...

取消回复欢迎 发表评论: