后端编程Python3-高级程序设计(面向对象-下)
off999 2024-10-19 07:14 19 浏览 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高级程序设计(面向对象-下),更多实操示例请参照视频讲解。跟着张员外讲编程,学习更轻松,不花钱还能学习真本领。
相关推荐
- 大文件传不动?WinRAR/7-Zip 入门到高手,这 5 个技巧让你效率翻倍
-
“这200张照片怎么传给女儿?微信发不了,邮箱附件又超限……”62岁的张阿姨对着电脑犯愁时,儿子只用了3分钟就把照片压缩成一个文件,还教她:“以后用压缩软件,比打包行李还方便!”职场人更懂这...
- 电脑解压缩软件推荐——7-Zip:免费、高效、简洁的文件管理神器
-
在日常工作中,我们经常需要处理压缩文件。无论是下载软件包、接收文件,还是存储大量数据,压缩和解压缩文件都成为了我们日常操作的一部分。而说到压缩解压软件,7-Zip绝对是一个不可忽视的名字。今天,我就来...
- 设置了加密密码zip文件要如何打开?这几个方法可以试试~
-
Zip是一种常见的压缩格式文件,文件还可以设置密码保护。那设置了密码的Zip文件要如何打开呢?不清楚的小伙伴一起来看看吧。当我们知道密码想要打开带密码的Zip文件,我们需要用到适用于Zip格式的解压缩...
- 大文件想要传输成功,怎么把ZIP文件分卷压缩
-
不知道各位小伙伴有没有这样的烦恼,发送很大很大的压缩包会受到限制,为此,想要在压缩过程中将文件拆分为几个压缩包并且同时为所有压缩包设置加密应该如何设置?方法一:使用7-Zip免费且强大的文件管理工具7...
- 高效处理 RAR 分卷压缩包:合并解压操作全攻略
-
在文件传输和存储过程中,当遇到大文件时,我们常常会使用分卷压缩的方式将其拆分成多个较小的压缩包,方便存储和传输。RAR作为一种常见的压缩格式,分卷压缩包的使用频率也很高。但很多人在拿到RAR分卷...
- 2个方法教你如何删除ZIP压缩包密码
-
zip压缩包设置了加密密码,每次解压文件都需要输入密码才能够顺利解压出文件,当压缩包文件不再需要加密的时候,大家肯定想删除压缩包密码,或是忘记了压缩包密码,想要通过删除操作将压缩包密码删除,就能够顺利...
- 速转!漏洞预警丨压缩软件Winrar目录穿越漏洞
-
WinRAR是一款功能强大的压缩包管理器,它是档案工具RAR在Windows环境下的图形界面。该软件可用于备份数据,缩减电子邮件附件的大小,解压缩从Internet上下载的RAR、ZIP及其它类...
- 文件解压方法和工具分享_文件解压工具下载
-
压缩文件减少文件大小,降低文件失效的概率,总得来说好处很多。所以很多文件我们下载下来都是压缩软件,很多小伙伴不知道怎么解压,或者不知道什么工具更好,所以今天做了文件解压方法和工具的分享给大家。一、解压...
- [python]《Python编程快速上手:让繁琐工作自动化》学习笔记3
-
1.组织文件笔记(第9章)(代码下载)1.1文件与文件路径通过importshutil调用shutil模块操作目录,shutil模块能够在Python程序中实现文件复制、移动、改名和删除;同时...
- Python内置tarfile模块:读写 tar 归档文件详解
-
一、学习目标1.1学习目标掌握Python内置模块tarfile的核心功能,包括:理解tar归档文件的原理与常见压缩格式(gzip/bz2/lzma)掌握tar文件的读写操作(创建、解压、查看、过滤...
- 使用python展开tar包_python拓展
-
类Unix的系统,打包文件经常使用的就是tar包,结合zip工具,可以方便的打包并解压。在python的标准库里面有tarfile库,可以方便实现生成了展开tar包。使用这个库最大的好处,可能就在于不...
- 银狐钓鱼再升级:白文件脚本化实现GO语言后门持久驻留
-
近期,火绒威胁情报中心监测到一批相对更为活跃的“银狐”系列变种木马。火绒安全工程师第一时间获取样本并进行分析。分析发现,该样本通过阿里云存储桶下发恶意文件,采用AppDomainManager进行白利...
- ZIP文件怎么打开?2个简单方法教你轻松搞定!
-
在日常工作和生活中,我们经常会遇到各种压缩文件,其中最常见的格式之一就是ZIP。ZIP文件通过压缩数据来减少文件大小,方便我们进行存储和传输。然而,对于初学者来说,如何打开ZIP文件可能会成为一个小小...
- Ubuntu—解压多个zip压缩文件.zip .z01 .z02
-
方法将所有zip文件放在同一目录中:zip_file.z01,zip_file.z02,zip_file.z03,...,zip_file.zip。在Zip3.0版本及以上,使用下列命令:将所有zi...
- 如何使用7-Zip对文件进行加密压缩
-
7-Zip是一款开源的文件归档工具,支持多种压缩格式,并提供了对压缩文件进行加密的功能。使用7-Zip可以轻松创建和解压.7z、.zip等格式的压缩文件,并且可以通过设置密码来保护压缩包中的...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)