La classe non è acqua. (Class will out) – Italian saying
我可能会写一整本关于面向对象编程(OOP和类的书。在本章中,我面临着在广度和深度之间找到平衡的艰巨挑战。有太多的事情要说,如果我深入描述的话,其中的很多事情要比这一整章要花更多的时间。因此,我将试着给你们一个我认为是基本原理的良好全景,加上一些在下一章中可能有用的东西。Python 的官方文档将有助于填补空白。
在本章中,我们将介绍以下主题:
- 装饰师
- 面向对象编程与 Python
- 遍历器
在第五章节省时间和内存中,我测量了各种表达式的执行时间。如果您还记得的话,我必须将一个变量初始化为开始时间,并从执行后的当前时间中减去它,以计算经过的时间。每次测量后,我都会把它打印在控制台上。那太乏味了。
每当你发现自己在重复一些事情时,就会响起警钟。你能把代码放在函数中避免重复吗?大多数情况下,答案是是,所以让我们看一个例子:
# decorators/time.measure.start.py
from time import sleep, time
def f():
sleep(.3)
def g():
sleep(.5)
t = time()
f()
print('f took:', time() - t) # f took: 0.3001396656036377
t = time()
g()
print('g took:', time() - t) # g took: 0.5039339065551758
在前面的代码中,我定义了两个函数,f
和g
,它们除了睡眠之外什么都不做(分别是 0.3 秒和 0.5 秒)。我使用了sleep
函数在所需的时间内暂停执行。请注意时间度量是如何非常精确的。现在,我们如何避免重复这些代码和计算?第一种可能的方法是:
# decorators/time.measure.dry.py
from time import sleep, time
def f():
sleep(.3)
def g():
sleep(.5)
def measure(func):
t = time()
func()
print(func.__name__, 'took:', time() - t)
measure(f) # f took: 0.30434322357177734
measure(g) # g took: 0.5048270225524902
啊,现在好多了。整个计时机制已封装到一个函数中,因此我们不会重复代码。我们动态地打印函数名,并且很容易编写代码。如果我们需要将参数传递给我们度量的函数,该怎么办?这段代码会变得更加复杂,所以让我们看一个例子:
# decorators/time.measure.arguments.py
from time import sleep, time
def f(sleep_time=0.1):
sleep(sleep_time)
def measure(func, *args, **kwargs):
t = time()
func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
measure(f, sleep_time=0.3) # f took: 0.30056095123291016
measure(f, 0.2) # f took: 0.2033553123474121
现在,f
希望被喂食sleep_time
(默认值为0.1
,所以我们不再需要g
。我还必须更改measure
函数,以便它现在可以接受函数、任何变量位置参数和任何变量关键字参数。这样,无论我们用什么方法调用measure
,我们都会将这些参数重定向到我们内部对func
的调用。
这很好,但我们可以把它推得更远一点。比如说,我们想在f
函数中内置计时行为,这样我们就可以调用它并采取相应的措施。我们可以这样做:
# decorators/time.measure.deco1.py
from time import sleep, time
def f(sleep_time=0.1):
sleep(sleep_time)
def measure(func):
def wrapper(*args, **kwargs):
t = time()
func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
return wrapper
f = measure(f) # decoration point
f(0.2) # f took: 0.20372915267944336
f(sleep_time=0.3) # f took: 0.30455899238586426
print(f.__name__) # wrapper <- ouch!
前面的代码可能不那么简单。让我们看看这里发生了什么。魔法就在装饰点。当我们将f
作为参数调用measure
返回的内容时,我们基本上会将f
重新赋值。在measure
中,我们定义另一个函数wrapper
,然后返回它。所以,净效果是,在装饰点之后,当我们调用f
时,实际上我们调用wrapper
。由于wrapper
内部正在调用func
,也就是f
,我们实际上正在这样关闭循环。如果你不相信我,看看最后一行。
wrapper
实际上是。。。包装纸。它接受变量和位置参数,并使用它们调用f
。它还围绕调用进行时间度量计算。
这种技术被称为装饰,而measure
实际上是装饰。这种范式变得如此流行和广泛使用,以至于在某种程度上,Python 为其添加了一种特殊的语法(请参见https://www.python.org/dev/peps/pep-0318/ )。让我们探讨三种情况:一个 decorator、两个 decorator 和一个接受参数的 decorator:
# decorators/syntax.py
def func(arg1, arg2, ...):
pass
func = decorator(func)
# is equivalent to the following:
@decorator
def func(arg1, arg2, ...):
pass
基本上,我们没有手动将函数重新分配给 decorator 返回的内容,而是使用特殊语法@decorator_name
预先定义函数。
我们可以通过以下方式将多个装饰器应用于同一个函数:
# decorators/syntax.py
def func(arg1, arg2, ...):
pass
func = deco1(deco2(func))
# is equivalent to the following:
@deco1
@deco2
def func(arg1, arg2, ...):
pass
应用多个装饰器时,请注意顺序。在上例中,func
首先用deco2
修饰,结果用deco1
修饰。一个好的经验法则是:装饰器离函数越近,应用它的时间越早。
一些装饰程序可以接受参数。这种技术通常用于生产其他装饰物。让我们看看语法,然后我们将看到一个示例:
# decorators/syntax.py
def func(arg1, arg2, ...):
pass
func = decoarg(arg_a, arg_b)(func)
# is equivalent to the following:
@decoarg(arg_a, arg_b)
def func(arg1, arg2, ...):
pass
正如你所看到的,这个案例有点不同。首先,使用给定的参数调用decoarg
,然后使用func
调用其返回值(实际的装饰器)。在我给你们举另一个例子之前,让我们先解决一个困扰我的问题。我不想在装饰时丢失原始函数名和 docstring(以及其他属性,请查看文档了解详细信息)。但是因为在我们的装饰器中,我们返回了wrapper
,所以func
中的原始属性丢失,并且f
最终被分配了wrapper
的属性。美丽的functools
模块很容易修复这个问题。我将修复最后一个示例,并重写其语法以使用@
运算符:
# decorators/time.measure.deco2.py
from time import sleep, time
from functools import wraps
def measure(func):
@wraps(func)
def wrapper(*args, **kwargs):
t = time()
func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
return wrapper
@measure
def f(sleep_time=0.1):
"""I'm a cat. I love to sleep! """
sleep(sleep_time)
f(sleep_time=0.3) # f took: 0.3010902404785156
print(f.__name__, ':', f.__doc__) # f : I'm a cat. I love to sleep!
现在我们在谈!正如您所看到的,我们所需要做的就是告诉 Python,wrapper
实际上包装了func
(通过wraps
函数),您可以看到原始名称和 docstring 现在得到了维护。
让我们看另一个例子。我想要一个装饰器,当函数的结果大于某个阈值时打印错误消息。我还将借此机会向您展示如何同时应用两个装饰师:
# decorators/two.decorators.py
from time import sleep, time
from functools import wraps
def measure(func):
@wraps(func)
def wrapper(*args, **kwargs):
t = time()
result = func(*args, **kwargs)
print(func.__name__, 'took:', time() - t)
return result
return wrapper
def max_result(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if result > 100:
print('Result is too big ({0}). Max allowed is 100.'
.format(result))
return result
return wrapper
@measure
@max_result
def cube(n):
return n ** 3
print(cube(2))
print(cube(5))
Take your time in studying the preceding example until you are sure you understand it well. If you do, I don't think there is any decorator you now won't be able to write.
我必须增强measure
装饰器,使其wrapper
现在返回调用func
的结果。max_result
装饰师也会这样做,但在返回之前,它会检查result
是否大于100
,这是允许的最大值。我用他们两个装饰了cube
。首先应用max_result
,然后应用measure
。运行此代码会产生以下结果:
$ python two.decorators.py
cube took: 3.0994415283203125e-06
8
Result is too big (125). Max allowed is 100.
cube took: 1.0013580322265625e-05
125
为方便起见,我用一个空行分隔了两次通话的结果。在第一次调用中,结果是8
,它通过了阈值检查。测量并打印运行时间。最后,我们打印结果(8
。
第二次调用的结果是125
,所以打印错误消息,返回结果,然后轮到measure
,再次打印运行时间,最后打印结果(125
。
如果我使用相同的两个修饰符(但顺序不同)修饰cube
函数,错误消息将跟随打印运行时间的行,而不是在它之前。
现在让我们简化这个示例,回到单个装饰器:max_result
。我想这样做,我可以用不同的阈值装饰不同的函数,我不想为每个阈值编写一个装饰器。让我们修改一下max_result
,这样我们就可以动态地修饰指定阈值的函数:
# decorators/decorators.factory.py
from functools import wraps
def max_result(threshold):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
if result > threshold:
print(
'Result is too big ({0}). Max allowed is {1}.'
.format(result, threshold))
return result
return wrapper
return decorator
@max_result(75)
def cube(n):
return n ** 3
print(cube(5))
前面的代码展示了如何编写一个装饰工厂。如果您还记得,使用接受参数的修饰符来装饰函数与编写func = decorator(argA, argB)(func)
是一样的,因此当我们用max_result(75)
装饰cube
时,我们是在做cube = max_result(75)(cube)
。
让我们一步一步地看看发生了什么。当我们呼叫max_result(75)
时,我们进入它的身体。内部定义了一个decorator
函数,该函数将函数作为其唯一参数。在该函数中,执行通常的装饰技巧。我们定义了wrapper
,在其中我们检查原始函数调用的结果。这种方法的美妙之处在于,从最内层,我们仍然可以同时称为func
和threshold
,这允许我们动态设置阈值。
wrapper
返回result
、decorator
返回wrapper
、max_result
返回decorator
。这意味着我们的cube = max_result(75)(cube)
呼叫实际上变成了cube = decorator(cube)
。不只是任何一个decorator
,而是threshold
的值为75
的一个。这是通过一个名为闭包的机制实现的,它不在本章的范围内,但仍然非常有趣,所以我提到了它,供您对其进行一些研究。
运行最后一个示例会产生以下结果:
$ python decorators.factory.py
Result is too big (125). Max allowed is 75.
125
前面的代码允许我根据自己的意愿使用具有不同阈值的max_result
装饰器,如下所示:
# decorators/decorators.factory.py
@max_result(75)
def cube(n):
return n ** 3
@max_result(100)
def square(n):
return n ** 2
@max_result(1000)
def multiply(a, b):
return a * b
请注意,每个装饰都使用不同的threshold
值。
修饰符在 Python 中非常流行。它们经常被使用,并且简化(我敢说是美化)了很多代码。
这是一个相当长的旅程,希望很好,到现在为止,我们应该准备好探索 OOP。我将使用 Kindler 的定义,E。;克里维,I.(2011)。国际通用系统杂志的复杂控制系统的面向对象仿真,并将其应用于 Python:
Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which are data structures that contain data, in the form of attributes, and code, in the form of functions known as methods. A distinguishing feature of objects is that an object's method can access and often modify the data attributes of the object with which they are associated (objects have a notion of "self"). In OO programming, computer programs are designed by making them out of objects that interact with one another.
Python 完全支持这个范例。实际上,正如我们已经说过的,Python 中的所有东西都是一个对象,因此这表明 OOP 不仅受到 Python 的支持,而且是其核心的一部分。
OOP 中的两个主要角色是对象和类。类用于创建对象(对象是从中创建它们的类的实例),所以我们可以将它们视为实例工厂。当对象由类创建时,它们继承类属性和方法。它们代表程序域中的具体项目。
我将从用 Python 编写的最简单的类开始:
# oop/simplest.class.py
class Simplest(): # when empty, the braces are optional
pass
print(type(Simplest)) # what type is this object?
simp = Simplest() # we create an instance of Simplest: simp
print(type(simp)) # what type is simp?
# is simp an instance of Simplest?
print(type(simp) == Simplest) # There's a better way for this
让我们运行前面的代码并逐行解释:
$ python simplest.class.py
<class 'type'>
<class '__main__.Simplest'>
True
我定义的Simplest
类的主体中只有pass
指令,这意味着它没有任何自定义属性或方法。如果为空,则名称后的括号是可选的。我将打印它的类型(__main__
是顶级代码执行的作用域的名称),并且我知道,在注释中,我编写了对象,而不是类。事实证明,正如你从print
的结果中所看到的,类实际上是对象。准确地说,它们是type
的实例。解释这一概念将引导我们讨论元类和元编程,这两个高级概念需要牢牢掌握基础知识才能理解,并且超出了本章的范围。和往常一样,我提到它是为了给你们留下一个指针,当你们准备好深入挖掘的时候。
让我们回到示例:我使用Simplest
创建了一个实例simp
。您可以看到,创建实例的语法与调用函数的语法相同。然后我们打印出simp
属于什么类型,并验证simp
实际上是Simplest
的一个实例。我将在本章后面向您展示一种更好的方法。
到目前为止,一切都很简单。但是,当我们写class ClassName(): pass
时会发生什么?好的,Python 所做的是创建一个类对象并为其指定一个名称。这与我们使用def
声明函数时发生的情况非常相似。
创建类对象后(通常在第一次导入模块时发生),它基本上表示一个名称空间。我们可以调用该类来创建其实例。每个实例都继承类属性和方法,并被赋予自己的名称空间。我们已经知道,要遍历名称空间,我们所需要做的就是使用点(.
操作符)。
让我们看另一个例子:
# oop/class.namespaces.py
class Person:
species = 'Human'
print(Person.species) # Human
Person.alive = True # Added dynamically!
print(Person.alive) # True
man = Person()
print(man.species) # Human (inherited)
print(man.alive) # True (inherited)
Person.alive = False
print(man.alive) # False (inherited)
man.name = 'Darth'
man.surname = 'Vader'
print(man.name, man.surname) # Darth Vader
在前面的示例中,我定义了一个名为species
的类属性。类主体中定义的任何变量都是属于该类的属性。在代码中,我还定义了Person.alive
,这是另一个类属性。您可以看到,从类中访问该属性没有限制。您可以看到,man
是Person
的一个实例,它继承了这两个元素,并在它们发生变化时立即反映出来。
man
还有两个属于自己名称空间的属性,因此称为实例属性:name
和surname
。
Class attributes are shared among all instances, while instance attributes are not; therefore, you should use class attributes to provide the states and behaviors to be shared by all instances, and use instance attributes for data that belongs just to one specific object.
搜索对象中的属性时,如果未找到该属性,Python 将在用于创建该对象的类中继续搜索(并一直搜索,直到找到该属性或到达继承链的末尾)。这将导致一个有趣的阴影行为。让我们看另一个例子:
# oop/class.attribute.shadowing.py
class Point:
x = 10
y = 7
p = Point()
print(p.x) # 10 (from class attribute)
print(p.y) # 7 (from class attribute)
p.x = 12 # p gets its own `x` attribute
print(p.x) # 12 (now found on the instance)
print(Point.x) # 10 (class attribute still the same)
del p.x # we delete instance attribute
print(p.x) # 10 (now search has to go again to find class attr)
p.z = 3 # let's make it a 3D point
print(p.z) # 3
print(Point.z)
# AttributeError: type object 'Point' has no attribute 'z'
前面的代码非常有趣。我们已经定义了一个名为Point
的类,它有两个类属性x
和y
。当我们创建一个实例p
时,您可以看到我们可以从p
名称空间(p.x
和p.y
打印x
和y
。当我们这样做时,Python 在实例上找不到任何x
或y
属性,因此搜索类,并在那里找到它们。
然后我们通过分配p.x = 12
给p
它自己的x
属性。这种行为一开始可能看起来有点奇怪,但如果你仔细想想,它与当外部有全局x = 10
时声明x = 12
的函数中发生的情况完全相同。我们知道x = 12
不会影响全局,对于类和实例来说,它是完全相同的。
分配p.x = 12
后,打印时,搜索不需要读取类属性,因为x
在实例上找到,所以打印出12
。我们还打印了Point.x
,它在类名称空间中表示x
。
然后,我们从p
的名称空间中删除x
,这意味着,在下一行,当我们再次打印它时,Python 将再次在类中搜索它,因为在实例中再也找不到它了。
最后三行告诉您,将属性分配给实例并不意味着它们将在类中找到。实例获取类中的任何内容,但事实并非如此。
你认为把x
和y
坐标作为类属性怎么样?你认为这是个好主意吗?如果您添加了另一个Point
实例,会怎么样?这有助于说明为什么类属性非常有用吗?
在类方法中,我们可以通过一个特殊的参数来引用一个实例,该参数按约定称为self
。self
始终是实例方法的第一个属性。让我们一起研究一下这种行为,以及如何共享,不仅是属性,而且是所有实例的方法:
# oop/class.self.py
class Square:
side = 8
def area(self): # self is a reference to an instance
return self.side ** 2
sq = Square()
print(sq.area()) # 64 (side is found on the class)
print(Square.area(sq)) # 64 (equivalent to sq.area())
sq.side = 10
print(sq.area()) # 100 (side is found on the instance)
注意sq
是如何使用area
方法的。这两个调用,Square.area(sq)
和sq.area()
是等价的,它告诉我们机制是如何工作的。您可以将实例传递给方法调用(Square.area(sq)
),方法调用中的名称为self
,或者您可以使用更舒适的语法sq.area()
,Python 将在幕后为您翻译。
让我们看一个更好的例子:
# oop/class.price.py
class Price:
def final_price(self, vat, discount=0):
"""Returns price after applying vat and fixed discount."""
return (self.net_price * (100 + vat) / 100) - discount
p1 = Price()
p1.net_price = 100
print(Price.final_price(p1, 20, 10)) # 110 (100 * 1.2 - 10)
print(p1.final_price(20, 10)) # equivalent
前面的代码告诉您,在声明方法时,没有任何东西阻止我们使用参数。我们可以使用与函数完全相同的语法,但我们需要记住,第一个参数始终是实例。我们不必称之为self
,但这是公约,这是遵守公约非常重要的少数情况之一。
您是否注意到,在呼叫p1.final_price(...)
之前,我们必须将net_price
分配给p1
吗?有一个更好的方法。在其他语言中,这将被称为构造函数,但在 Python 中,它不是。它实际上是一个初始值设定项,因为它在已经创建的实例上工作,因此被称为__init__
。这是一种魔法方法,在创建对象后立即运行。Python 对象还有一个__new__
方法,它是实际的构造函数。在实践中,必须重写它并不常见,但这是一种主要用于编写元类的实践,正如我们所提到的,这是一个相当高级的主题,我们在本书中不会探讨:
# oop/class.init.py
class Rectangle:
def __init__(self, side_a, side_b):
self.side_a = side_a
self.side_b = side_b
def area(self):
return self.side_a * self.side_b
r1 = Rectangle(10, 4)
print(r1.side_a, r1.side_b) # 10 4
print(r1.area()) # 40
r2 = Rectangle(7, 3)
print(r2.area()) # 21
事情终于开始成形了。创建对象时,__init__
方法会自动为我们运行。在本例中,我对其进行了编码,以便在创建对象时(通过像函数一样调用类名),我们将参数传递给创建调用,就像在任何常规函数调用中一样。我们传递参数的方式遵循__init__
方法的签名,因此,在两个创建语句中,10
和7
将分别是r1
和r2
的side_a
,而4
和3
将是side_b
。您可以看到,r1
和r2
对area()
的调用反映了它们有不同的实例参数。以这种方式设置对象更方便、更好。
现在应该很清楚了:OOP 是关于代码重用的。我们定义一个类,创建实例,这些实例使用只在类中定义的方法。根据初始化器设置实例的方式,它们的行为会有所不同。
但这只是故事的一半,OOP 更强大。我们有两个主要的设计结构需要利用:继承和组合。
继承意味着两个对象通过Is-A类型的关系进行关联。另一方面,组合意味着两个对象通过Has-a类型的关系进行关联。用一个例子很容易解释:
# oop/class_inheritance.py
class Engine:
def start(self):
pass
def stop(self):
pass
class ElectricEngine(Engine): # Is-A Engine
pass
class V8Engine(Engine): # Is-A Engine
pass
class Car:
engine_cls = Engine
def __init__(self):
self.engine = self.engine_cls() # Has-A Engine
def start(self):
print(
'Starting engine {0} for car {1}... Wroom, wroom!'
.format(
self.engine.__class__.__name__,
self.__class__.__name__)
)
self.engine.start()
def stop(self):
self.engine.stop()
class RaceCar(Car): # Is-A Car
engine_cls = V8Engine
class CityCar(Car): # Is-A Car
engine_cls = ElectricEngine
class F1Car(RaceCar): # Is-A RaceCar and also Is-A Car
pass # engine_cls same as parent
car = Car()
racecar = RaceCar()
citycar = CityCar()
f1car = F1Car()
cars = [car, racecar, citycar, f1car]
for car in cars:
car.start()
""" Prints:
Starting engine Engine for car Car... Wroom, wroom!
Starting engine V8Engine for car RaceCar... Wroom, wroom!
Starting engine ElectricEngine for car CityCar... Wroom, wroom!
Starting engine V8Engine for car F1Car... Wroom, wroom!
"""
前面的示例向您展示了对象之间的是一种和是一种类型的关系。首先,让我们考虑一下。它是一个简单的类,有两个方法,start
和stop
。然后我们定义了ElectricEngine
和V8Engine
,它们都继承自Engine
。您可以看到,当我们定义它们时,我们将Engine
放在类名后面的括号内。
这意味着ElectricEngine
和V8Engine
都从Engine
类继承属性和方法,据说这是它们的基类。
汽车也是如此。Car
是RaceCar
和CityCar
的基类。RaceCar
也是F1Car
的基类。另一种说法是F1Car
继承自RaceCar
,后者继承自Car
。因此,F1Car
是一个RaceCar
和RaceCar
是一个Car
。由于传递性,我们可以说F1Car
也是一个Car
。CityCar
也是,是一个Car
。
当我们定义class A(B): pass
时,我们说A
是B
的子,而B
是A
的父。父和基类是同义词,是子和派生的。另外,我们说一个类从另一个类继承,或者说它扩展了它。
这就是继承机制。
另一方面,让我们回到代码。每个类都有一个 class 属性engine_cls
,它是我们想要分配给每种类型汽车的引擎类的引用。Car
有一个通用Engine
,而两辆赛车有一个强大的 V8 引擎,而城市车有一个电动引擎。
当使用初始值设定项方法__init__
创建汽车时,我们将创建分配给汽车的任何引擎类的实例,并将其设置为engine
实例属性。
在所有类实例中共享engine_cls
是有意义的,因为汽车的相同实例很可能具有相同类型的引擎。另一方面,将一个引擎(任何Engine
类的实例)作为类属性是不好的,因为我们将在所有实例之间共享一个引擎,这是不正确的。
汽车与其发动机之间的关系类型为Has-a型。一辆汽车有一个发动机。这被称为合成,反映了物体可以由许多其他物体构成的事实。汽车有一个发动机、齿轮、车轮、车架、车门、座椅等。
在设计 OOP 代码时,以这种方式描述对象非常重要,这样我们就可以正确地使用继承和组合来以最佳方式构造代码。
Notice how I had to avoid having dots in the class_inheritance.py
script name, as dots in module names make it imports difficult. Most modules in the source code of the book are meant to be run as standalone scripts, therefore I chose to add dots to enhance readability when possible, but in general, you want to avoid dots in your module names.
在我们结束这一段之前,让我们用另一个例子来检查我是否告诉了你真相:
# oop/class.issubclass.isinstance.py
from class_inheritance import Car, RaceCar, F1Car
car = Car()
racecar = RaceCar()
f1car = F1Car()
cars = [(car, 'car'), (racecar, 'racecar'), (f1car, 'f1car')]
car_classes = [Car, RaceCar, F1Car]
for car, car_name in cars:
for class_ in car_classes:
belongs = isinstance(car, class_)
msg = 'is a' if belongs else 'is not a'
print(car_name, msg, class_.__name__)
""" Prints:
car is a Car
car is not a RaceCar
car is not a F1Car
racecar is a Car
racecar is a RaceCar
racecar is not a F1Car
f1car is a Car
f1car is a RaceCar
f1car is a F1Car
"""
如您所见,car
只是Car
的一个实例,而racecar
是RaceCar
的一个实例(扩展为Car
),而f1car
是F1Car
(扩展为RaceCar
和Car
)。香蕉是香蕉的一个实例。但是,它也是一种水果。还有,它是食物,对吗?这是相同的概念。要检查对象是否是类的实例,请使用isinstance
方法。建议与透明类型比较:(type(object) == Class)
。
Notice I have left out the prints you get when instantiating the cars. We saw them in the previous example.
我们还要检查继承性–相同的设置,for
循环中的不同逻辑:
# oop/class.issubclass.isinstance.py
for class1 in car_classes:
for class2 in car_classes:
is_subclass = issubclass(class1, class2)
msg = '{0} a subclass of'.format(
'is' if is_subclass else 'is not')
print(class1.__name__, msg, class2.__name__)
""" Prints:
Car is a subclass of Car
Car is not a subclass of RaceCar
Car is not a subclass of F1Car
RaceCar is a subclass of Car
RaceCar is a subclass of RaceCar
RaceCar is not a subclass of F1Car
F1Car is a subclass of Car
F1Car is a subclass of RaceCar
F1Car is a subclass of F1Car
"""
有趣的是,我们了解到类是自身的一个子类。检查前面示例的输出,查看它是否与我提供的解释相匹配。
One thing to notice about conventions is that class names are always written using CapWords
, which means ThisWayIsCorrect
, as opposed to functions and methods, which are written this_way_is_correct
. Also, when in the code, you want to use a name that is a Python-reserved keyword or a built-in function or class, the convention is to add a trailing underscore to the name. In the first for
loop example, I'm looping through the class names using for class_ in ...
, because class
is a reserved word. But you already knew all this because you have thoroughly studied PEP8, right?
为了帮助您了解Is-A和Has-A之间的区别,请看下图:
我们已经看到了类声明,比如class ClassA: pass
和class ClassB(BaseClassName): pass
。当我们没有显式指定基类时,Python 会将特殊的对象类设置为我们定义的基类。最终,所有类都派生自一个对象。请注意,如果不指定基类,则括号是可选的。
因此,书写class A: pass
或class A(): pass
或class A(object): pass
是完全相同的事情。对象类是一个特殊的类,因为它具有所有 Python 类通用的方法,并且不允许您对其设置任何属性。
让我们看看如何从类中访问基类:
# oop/super.duplication.py
class Book:
def __init__(self, title, publisher, pages):
self.title = title
self.publisher = publisher
self.pages = pages
class Ebook(Book):
def __init__(self, title, publisher, pages, format_):
self.title = title
self.publisher = publisher
self.pages = pages
self.format_ = format_
看看前面的代码。其中三个输入参数在Ebook
中重复。这是一种非常糟糕的做法,因为我们现在有两套指令在做同样的事情。此外,Book.__init__
签名的任何变更不会反映在Ebook
中。我们知道Ebook
是一个Book
,因此我们可能希望在儿童课程中反映变化。
让我们看看解决此问题的一种方法:
# oop/super.explicit.py
class Book:
def __init__(self, title, publisher, pages):
self.title = title
self.publisher = publisher
self.pages = pages
class Ebook(Book):
def __init__(self, title, publisher, pages, format_):
Book.__init__(self, title, publisher, pages)
self.format_ = format_
ebook = Ebook(
'Learn Python Programming', 'Packt Publishing', 500, 'PDF')
print(ebook.title) # Learn Python Programming
print(ebook.publisher) # Packt Publishing
print(ebook.pages) # 500
print(ebook.format_) # PDF
现在,这样更好了。我们已经消除了这种令人讨厌的重复。基本上,我们告诉 Python 调用Book
类的__init__
方法,然后将self
提供给调用,确保将该调用绑定到当前实例。
如果我们在Book
的__init__
方法中修改逻辑,我们不需要触摸Ebook
,它会自动适应变化。
这种方法很好,但我们仍然可以做得更好。假设我们把Book
的名字改为Liber
,因为我们爱上了拉丁语。我们必须改变Ebook
的__init__
方法来反映这种变化。这可以通过使用super
来避免:
# oop/super.implicit.py
class Book:
def __init__(self, title, publisher, pages):
self.title = title
self.publisher = publisher
self.pages = pages
class Ebook(Book):
def __init__(self, title, publisher, pages, format_):
super().__init__(title, publisher, pages)
# Another way to do the same thing is:
# super(Ebook, self).__init__(title, publisher, pages)
self.format_ = format_
ebook = Ebook(
'Learn Python Programming', 'Packt Publishing', 500, 'PDF')
print(ebook.title) # Learn Python Programming
print(ebook.publisher) # Packt Publishing
print(ebook.pages) # 500
print(ebook.format_) # PDF
super
是一个返回代理对象的函数,该代理对象将方法调用委托给父类或同级类。在这种情况下,它会将对__init__
的调用委托给Book
类,这种方法的美妙之处在于,现在我们甚至可以自由地将Book
更改为Liber
,而无需触及Ebook
的__init__
方法中的逻辑。
既然我们知道了如何从子类访问基类,那么让我们来探索 Python 的多重继承。
除了使用多个基类组成一个类之外,这里感兴趣的是如何执行属性搜索。请看下图:
如您所见,Shape
和Plotter
充当所有其他类的基类。Polygon
直接从他们那里继承,RegularPolygon
从Polygon
继承,RegularHexagon
和Square
都从RegulaPolygon
继承。还要注意的是Shape
和Plotter
隐式继承自object
,因此我们有一个称为菱形的路径,或者更简单地说,有多条路径可以到达基类。稍后我们将了解这一点的重要性。让我们将其转换为一些简单的代码:
# oop/multiple.inheritance.py
class Shape:
geometric_type = 'Generic Shape'
def area(self): # This acts as placeholder for the interface
raise NotImplementedError
def get_geometric_type(self):
return self.geometric_type
class Plotter:
def plot(self, ratio, topleft):
# Imagine some nice plotting logic here...
print('Plotting at {}, ratio {}.'.format(
topleft, ratio))
class Polygon(Shape, Plotter): # base class for polygons
geometric_type = 'Polygon'
class RegularPolygon(Polygon): # Is-A Polygon
geometric_type = 'Regular Polygon'
def __init__(self, side):
self.side = side
class RegularHexagon(RegularPolygon): # Is-A RegularPolygon
geometric_type = 'RegularHexagon'
def area(self):
return 1.5 * (3 ** .5 * self.side ** 2)
class Square(RegularPolygon): # Is-A RegularPolygon
geometric_type = 'Square'
def area(self):
return self.side * self.side
hexagon = RegularHexagon(10)
print(hexagon.area()) # 259.8076211353316
print(hexagon.get_geometric_type()) # RegularHexagon
hexagon.plot(0.8, (75, 77)) # Plotting at (75, 77), ratio 0.8.
square = Square(12)
print(square.area()) # 144
print(square.get_geometric_type()) # Square
square.plot(0.93, (74, 75)) # Plotting at (74, 75), ratio 0.93.
请看前面的代码:Shape
类有一个属性geometric_type
,两个方法area
和get_geometric_type
。使用基类(例如我们的示例中的Shape
)来定义接口——子类必须为其提供实现的方法,这是非常常见的。有不同的更好的方法可以做到这一点,但我希望这个例子尽可能简单。
我们还有Plotter
类,它添加了plot
方法,从而为从它继承的任何类提供绘图功能。当然,在本例中,plot
实现只是一个虚拟的print
。第一个有趣的类是Polygon
,它继承自Shape
和Plotter
。
多边形有很多种类型,其中一种是规则多边形,它是等角的(所有角度都相等)和等边的(所有边都相等),因此我们创建了继承自Polygon
的RegularPolygon
类。对于所有边都相等的正多边形,我们可以在RegularPolygon
上实现一个简单的__init__
方法,该方法取边的长度。最后,我们创建了RegularHexagon
和Square
类,它们都继承自RegularPolygon
。
这个结构相当长,但希望在设计代码时能让您了解如何专门化对象的分类。
现在,请看最后八行。注意,当我在hexagon
和square
上调用area
方法时,我得到了这两个方法的正确区域。这是因为它们都提供了正确的实现逻辑。此外,我可以对它们调用get_geometric_type
,即使它们的类中没有定义它,Python 必须一直到Shape
才能找到它的实现。注意,即使在Shape
类中提供了实现,用于返回值的self.geometric_type
也正确地取自调用方实例。
plot
方法调用也很有趣,它向您展示了如何使用对象不具备的功能来丰富对象。这种技术在 Django(我们将探讨第 14 章、web 开发)等 web 框架中非常流行,它提供了名为mixins的特殊类,您可以直接使用这些类的功能。您所要做的就是将所需的 mixin 定义为您自己的基类之一,就是这样。
多重继承功能强大,但也会变得非常混乱,因此我们需要确保了解使用它时会发生什么。
现在,我们知道,当您请求someobject.attribute
而在该对象上找不到attribute
时,Python 开始在创建someobject
的类中搜索。如果它也不在那里,Python 会在继承链上搜索,直到找到attribute
或到达object
类。如果继承链只由单个继承步骤组成,这很容易理解,这意味着类只有一个父级。然而,当涉及到多重继承时,有些情况下,如果找不到属性,就无法直接预测下一个要搜索的类。
Python 提供了一种始终知道在属性查找中搜索类的顺序的方法:方法解析顺序(MRO。
The MRO is the order in which base classes are searched for a member during lookup. From version 2.3, Python uses an algorithm called C3, which guarantees monotonicity.
In Python 2.2, new-style classes were introduced. The way you write a new-style class in Python 2.* is to define it with an explicit object
base class. Classic classes were not explicitly inheriting from object
and have been removed in Python 3. One of the differences between classic and new-style classes in Python 2.* is that new-style classes are searched with the new MRO.
关于上一个示例,让我们看看Square
类的 MRO:
# oop/multiple.inheritance.py
print(square.__class__.__mro__)
# prints:
# (<class '__main__.Square'>, <class '__main__.RegularPolygon'>,
# <class '__main__.Polygon'>, <class '__main__.Shape'>,
# <class '__main__.Plotter'>, <class 'object'>)
要获得类的 MRO,我们可以从实例到它的__class__
属性,再从实例到它的__mro__
属性。或者,我们可以直接调用Square.__mro__
或Square.mro()
,但如果必须动态调用,则更有可能是对象而不是类。
请注意,唯一值得怀疑的是Polygon
之后的二分法,其中继承链分为两种方式:一种通向Shape
,另一种通向Plotter
。通过扫描 MRO 中的Square
类,我们知道Shape
是在Plotter
之前搜索的。
为什么这很重要?那么,考虑下面的代码:
# oop/mro.simple.py
class A:
label = 'a'
class B(A):
label = 'b'
class C(A):
label = 'c'
class D(B, C):
pass
d = D()
print(d.label) # Hypothetically this could be either 'b' or 'c'
B
和C
都继承自A
,而D
都继承自B
和C
。这意味着对label
属性的查找可以通过B
或C
到达顶部(A
。根据第一个到达的结果,我们得到了不同的结果。
因此,在前面的示例中,我们得到了'b'
,这是我们所期望的,因为B
是D
基类中最左边的一个。但是如果我从B
中删除label
属性会发生什么?这将是一个令人困惑的情况:算法会一直到A
还是先到C
?让我们来了解一下:
# oop/mro.py
class A:
label = 'a'
class B(A):
pass # was: label = 'b'
class C(A):
label = 'c'
class D(B, C):
pass
d = D()
print(d.label) # 'c'
print(d.__class__.mro()) # notice another way to get the MRO
# prints:
# [<class '__main__.D'>, <class '__main__.B'>,
# <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
因此,我们了解到 MRO 是D
-B
-C
-A
-object
,这意味着当我们请求d.label
时,我们得到'c'
,这是正确的。
在日常编程中,处理 MRO 是不常见的,但是当您第一次从一个框架中反对一些混合时,我保证您会很高兴我花了一段时间来解释它。
到目前为止,我们已经用数据和实例方法的形式对类进行了属性编码,但是我们可以在类中放置另外两种类型的方法:静态方法和类方法。
您可能还记得,当您创建类对象时,Python 会为其指定一个名称。该名称充当名称空间,有时将功能分组在其下是有意义的。静态方法非常适合此用例,因为与实例方法不同,它们不传递任何特殊参数。让我们看一个虚构的StringUtil
类的例子:
# oop/static.methods.py
class StringUtil:
@staticmethod
def is_palindrome(s, case_insensitive=True):
# we allow only letters and numbers
s = ''.join(c for c in s if c.isalnum()) # Study this!
# For case insensitive comparison, we lower-case s
if case_insensitive:
s = s.lower()
for c in range(len(s) // 2):
if s[c] != s[-c -1]:
return False
return True
@staticmethod
def get_unique_words(sentence):
return set(sentence.split())
print(StringUtil.is_palindrome(
'Radar', case_insensitive=False)) # False: Case Sensitive
print(StringUtil.is_palindrome('A nut for a jar of tuna')) # True
print(StringUtil.is_palindrome('Never Odd, Or Even!')) # True
print(StringUtil.is_palindrome(
'In Girum Imus Nocte Et Consumimur Igni') # Latin! Show-off!
) # True
print(StringUtil.get_unique_words(
'I love palindromes. I really really love them!'))
# {'them!', 'really', 'palindromes.', 'I', 'love'}
前面的代码非常有趣。首先,我们了解到静态方法是通过简单地对它们应用staticmethod
装饰器来创建的。您可以看到,它们没有传递任何特殊参数,因此,除了装饰之外,它们实际上看起来就像函数。
我们有一个类,StringUtil
,它充当函数的容器。另一种方法是使用一个单独的模块,其中包含函数。大多数时候,这确实是一个偏好的问题。
is_palindrome
中的逻辑现在应该很容易理解,但是,以防万一,让我们来看看。首先,我们删除s
中既不是字母也不是数字的所有字符。为了做到这一点,我们使用字符串对象的join
方法(在本例中为空字符串对象)。通过对空字符串调用join
,结果是传递给join
的 iterable 中的所有元素都将连接在一起。我们为join
提供一个生成器表达式,该表达式表示从s
中提取任何字符,如果该字符是字母数字或数字。这是因为,在回文句子中,我们想要丢弃任何不是字符或数字的东西。
然后,如果case_insensitive
是True
,我们将小写s
,然后我们继续检查它是否是回文。为了做到这一点,我们比较第一个字符和最后一个字符,然后比较第二个字符和第二个字符到最后一个字符,依此类推。如果在任何时候我们发现差异,这意味着字符串不是回文,因此我们可以返回False
。另一方面,如果我们正常退出for
循环,这意味着没有发现差异,因此我们可以说字符串是回文。
请注意,无论字符串的长度如何,此代码都能正常工作;也就是说,如果长度是奇数或偶数。len(s) // 2
达到s
的一半,如果s
长度为奇数,中间一个不会被检查(如雷达中,D未被检查),但我们不在乎;它会和它自己比较,所以它总是通过检查。
get_unique_words
要简单得多:它只返回一个集合,我们将一个句子中的单词添加到该集合的列表中。set
类为我们删除了任何重复,因此我们不需要做任何其他事情。
StringUtil
类为处理字符串的方法提供了一个很好的容器名称空间。我本可以用MathUtil
类编写一个类似的示例,并使用一些静态方法处理数字,但我想向您展示一些不同的东西。
类方法与静态方法稍有不同,因为与实例方法一样,它们也接受一个特殊的第一个参数,但在本例中,它是类对象本身。编码类方法的一个非常常见的用例是为类提供工厂功能。让我们看一个例子:
# oop/class.methods.factory.py
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
@classmethod
def from_tuple(cls, coords): # cls is Point
return cls(*coords)
@classmethod
def from_point(cls, point): # cls is Point
return cls(point.x, point.y)
p = Point.from_tuple((3, 7))
print(p.x, p.y) # 3 7
q = Point.from_point(p)
print(q.x, q.y) # 3 7
在前面的代码中,我向您展示了如何使用类方法为类创建工厂。在这种情况下,我们希望通过传递两个坐标来创建一个Point
实例(常规创建p = Point(3, 7)
),但我们也希望能够通过传递一个元组(Point.from_tuple
)或另一个实例(Point.from_point
)来创建一个实例。
在这两个类方法中,cls
参数引用Point
类。与 instance 方法一样,它将self
作为第一个参数,而 class 方法则使用cls
参数。self
和cls
都是根据一个惯例命名的,你不必被迫遵守,但强烈鼓励你尊重这个惯例。这是任何 Python 程序员都不会改变的,因为它是解析器、linter 和任何能够自动对代码执行某些操作的工具所期望的强大约定,所以最好还是坚持下去。
类和静态方法很好地结合在一起。静态方法实际上非常有助于分解类方法的逻辑以改进其布局。让我们看一个重构StringUtil
类的示例:
# oop/class.methods.split.py
class StringUtil:
@classmethod
def is_palindrome(cls, s, case_insensitive=True):
s = cls._strip_string(s)
# For case insensitive comparison, we lower-case s
if case_insensitive:
s = s.lower()
return cls._is_palindrome(s)
@staticmethod
def _strip_string(s):
return ''.join(c for c in s if c.isalnum())
@staticmethod
def _is_palindrome(s):
for c in range(len(s) // 2):
if s[c] != s[-c -1]:
return False
return True
@staticmethod
def get_unique_words(sentence):
return set(sentence.split())
print(StringUtil.is_palindrome('A nut for a jar of tuna')) # True
print(StringUtil.is_palindrome('A nut for a jar of beans')) # False
将此代码与以前的版本进行比较。首先,请注意,尽管is_palindrome
现在是一个类方法,但我们调用它的方式与调用静态方法时的方式相同。我们之所以将其改为类方法,是因为在分解出两段逻辑(_strip_string
和_is_palindrome
之后,我们需要获得对它们的引用,如果我们的方法中没有cls
,唯一的选择就是这样调用它们:StringUtil._strip_string(...)
和StringUtil._is_palindrome(...)
,这不是好的做法,因为我们会在is_palindrome
方法中硬编码类名,因此每当我们想要更改类名时,我们就不得不修改它。使用cls
将作为类名,这意味着我们的代码不需要任何修改。
请注意,新逻辑的阅读效果比以前的版本要好得多。此外,请注意,通过使用前导下划线命名分解的方法,我暗示这些方法不应该从类外调用,但这将是下一段的主题。
如果你有 java、C++、C++等语言的背景,那么你就知道程序员允许将隐私状态分配给属性(数据和方法)。对于这一点,每种语言都有自己的风格,但要点是公共属性可以从代码中的任何一点访问,而私有属性只能在其定义的范围内访问。
在 Python 中,没有这样的东西。一切都是公开的;因此,我们依赖于约定和一种称为名称混乱的机制。
约定如下:如果属性的名称没有前导下划线,则将其视为公共名称。这意味着您可以访问它并自由修改它。当名称有一个前导下划线时,该属性被视为私有属性,这意味着该属性可能要在内部使用,您不应该从外部使用或修改它。私有属性的一个非常常见的用例是应该由公共属性使用的辅助方法(可能在调用链中与其他方法一起使用),以及内部数据,例如缩放因子,或者理想情况下我们将放入常量中的任何其他数据(一个无法更改的变量,但是,令人惊讶的是,Python 也没有这些变量)。
这种特点通常会吓跑其他背景的人;他们因缺乏隐私而感到威胁。老实说,在我对 Python 的整个职业体验中,我从来没有听到有人尖叫过“哦,天哪,我们有一个可怕的 bug,因为 Python 缺少私有属性!”我发誓,一次也没有。
这就是说,要求隐私实际上是有道理的,因为没有隐私,你就有可能在你的代码中引入真正的 bug。让我告诉你我的意思:
# oop/private.attrs.py
class A:
def __init__(self, factor):
self._factor = factor
def op1(self):
print('Op1 with factor {}...'.format(self._factor))
class B(A):
def op2(self, factor):
self._factor = factor
print('Op2 with factor {}...'.format(self._factor))
obj = B(100)
obj.op1() # Op1 with factor 100...
obj.op2(42) # Op2 with factor 42...
obj.op1() # Op1 with factor 42... <- This is BAD
在前面的代码中,我们有一个名为_factor
的属性,让我们假设它非常重要,在创建实例后不会在运行时修改它,因为op1
依赖于它才能正常工作。我们用一个前导下划线来命名它,但这里的问题是,当我们调用obj.op2(42)
时,我们会对它进行修改,这反映在对op1
的后续调用中。
让我们通过添加另一个前导下划线来修复此不希望出现的行为:
# oop/private.attrs.fixed.py
class A:
def __init__(self, factor):
self.__factor = factor
def op1(self):
print('Op1 with factor {}...'.format(self.__factor))
class B(A):
def op2(self, factor):
self.__factor = factor
print('Op2 with factor {}...'.format(self.__factor))
obj = B(100)
obj.op1() # Op1 with factor 100...
obj.op2(42) # Op2 with factor 42...
obj.op1() # Op1 with factor 100... <- Wohoo! Now it's GOOD!
哇,看那个!现在它正按预期工作。Python 是一种神奇的东西,在本例中,正在发生的是名称篡改机制已经开始发挥作用。
名称混乱意味着,任何具有至少两个前导下划线和最多一个尾随下划线的属性名称(如__my_attr
)都将替换为包含下划线和实际名称前的类名的名称,如_ClassName__my_attr
。
这意味着,当您从类继承时,损坏机制会在基类和子类中为私有属性提供两个不同的名称,以避免名称冲突。每个类和实例对象都将对其属性的引用存储在一个名为__dict__
的特殊属性中,因此让我们检查obj.__dict__
以查看名称在操作中的损坏:
# oop/private.attrs.py
print(obj.__dict__.keys())
# dict_keys(['_factor'])
这就是我们在这个例子的问题版本中发现的_factor
属性。但是看看正在使用__factor
的那个:
# oop/private.attrs.fixed.py
print(obj.__dict__.keys())
# dict_keys(['_A__factor', '_B__factor'])
看见obj
现在有两个属性,_A__factor
(在A
类中被损坏)和_B__factor
(在B
类中被损坏)。这是一种机制,可以确保当您执行obj.__factor = 42
操作时A
中的__factor
不会发生变化,因为您实际上正在触摸_B__factor
,这样_A__factor
就安全可靠了。
如果您正在设计一个包含其他开发人员使用和扩展的类的库,那么您需要记住这一点,以避免无意中重写属性。像这样的 bug 非常微妙,很难被发现。
还有一件事是犯罪的,更不用说是装饰师。假设您在Person
类中有一个age
属性,并且在某个时刻您希望确保在更改其值时,您还检查age
是否在适当的范围内,例如[18,99]。您可以编写访问器方法,例如get_age()
和set_age(...)
(也称为getter和setter,并将逻辑放在那里。get_age()
很可能只返回age
,而set_age(...)
也会进行范围检查。问题是,您可能已经有很多代码直接访问age
属性,这意味着您现在要进行一些繁琐的重构。Java 等语言通过默认使用访问器模式克服了这个问题。许多 Java集成开发环境(IDEs通过动态为您编写 getter 和 setter 访问器方法存根来自动完成属性声明。
Python 更聪明,它通过property
装饰器实现了这一点。当您用property
装饰一个方法时,您可以使用该方法的名称,就像它是一个数据属性一样。因此,最好不要将需要一段时间才能完成的逻辑放入此类方法中,因为通过将它们作为属性访问,我们不希望等待。
让我们看一个例子:
# oop/property.py
class Person:
def __init__(self, age):
self.age = age # anyone can modify this freely
class PersonWithAccessors:
def __init__(self, age):
self._age = age
def get_age(self):
return self._age
def set_age(self, age):
if 18 <= age <= 99:
self._age = age
else:
raise ValueError('Age must be within [18, 99]')
class PersonPythonic:
def __init__(self, age):
self._age = age
@property
def age(self):
return self._age
@age.setter
def age(self, age):
if 18 <= age <= 99:
self._age = age
else:
raise ValueError('Age must be within [18, 99]')
person = PersonPythonic(39)
print(person.age) # 39 - Notice we access as data attribute
person.age = 42 # Notice we access as data attribute
print(person.age) # 42
person.age = 100 # ValueError: Age must be within [18, 99]
Person
类可能是我们编写的第一个版本。然后我们意识到我们需要将范围逻辑放在适当的位置,因此,使用另一种语言,我们必须将Person
重写为PersonWithAccessors
类,并重构所有使用Person.age
的代码。在 Python 中,我们将Person
重写为PersonPythonic
(当然,您通常不会更改名称),以便年龄存储在一个私有_age
变量中,我们使用该修饰定义属性 getter 和 setter,这允许我们像以前一样继续使用person
实例。getter 是在访问属性进行读取时调用的方法。另一方面,setter 是一种方法,当我们访问一个属性来编写它时,就会调用它。在其他语言中,如 Java,通常将它们定义为get_age()
和set_age(int value)
,但我发现 Python 语法更简洁。它允许您开始编写简单的代码并在以后进行重构,只有当您需要它时,才有必要使用访问器污染您的代码,因为它们在将来可能会有所帮助。
property
装饰器还允许只读数据(无 setter)和删除属性时的特殊操作。请参考官方文件进行更深入的挖掘。
我发现 Python 的操作符重载的方法非常出色。重载运算符意味着根据其使用的上下文赋予其含义。例如,+
运算符在处理数字时表示加法,但在处理序列时表示串联。
在 Python 中,当您使用操作符时,很可能是在幕后调用某些对象的特殊方法。例如,a[k]
调用大致翻译为type(a).__getitem__(a, k)
。
例如,让我们创建一个存储字符串的类,如果'42'
是该字符串的一部分,则该类的计算结果为True
,否则为False
。另外,让我们为类指定一个与存储字符串对应的长度属性:
# oop/operator.overloading.py
class Weird:
def __init__(self, s):
self._s = s
def __len__(self):
return len(self._s)
def __bool__(self):
return '42' in self._s
weird = Weird('Hello! I am 9 years old!')
print(len(weird)) # 24
print(bool(weird)) # False
weird2 = Weird('Hello! I am 42 years old!')
print(len(weird2)) # 25
print(bool(weird2)) # True
那很有趣,不是吗?有关为类提供自定义运算符实现而可以重写的神奇方法的完整列表,请参阅官方文档中的 Python 数据模型。
多态性一词来源于希腊语polys(许多,很多)和morphē(形式,形状),其含义是为不同类型的实体提供单一接口。
在我们的汽车示例中,我们称之为engine.start()
,不管它是什么类型的发动机。只要它公开 start 方法,我们就可以调用它。这就是多态性在起作用。
在其他语言(如 Java)中,为了使函数能够接受不同的类型并对其调用方法,这些类型需要以共享接口的方式进行编码。通过这种方式,编译器知道无论函数所提供的对象的类型如何,该方法都是可用的(当然,只要它扩展了适当的接口)。
在 Python 中,情况有所不同。多态性是隐式的,没有什么可以阻止您对对象调用方法;因此,从技术上讲,不需要实现接口或其他模式。
有一种特殊的多态性称为特殊多态性,这就是我们在最后一段中看到的:运算符重载。这是操作员根据输入的数据类型改变形状的能力。
多态性还允许 Python 程序员简单地使用从对象公开的接口(方法和属性),而不必检查它是从哪个类实例化的。这使得代码更紧凑,感觉更自然。
我不能在多态性上花费太多的时间,但我鼓励你自己去检查它,它将扩展你对 OOP 的理解。祝你好运
在我们离开 OOP 领域之前,我想提到最后一件事:数据类。PEP557 在 Python 3.7 中引入(https://www.python.org/dev/peps/pep-0557/ ),可以描述为可变命名元组,默认为
。让我们深入了解一个示例:
# oop/dataclass.py
from dataclasses import dataclass
@dataclass
class Body:
'''Class to represent a physical body.'''
name: str
mass: float = 0\. # Kg
speed: float = 1\. # m/s
def kinetic_energy(self) -> float:
return (self.mass * self.speed ** 2) / 2
body = Body('Ball', 19, 3.1415)
print(body.kinetic_energy()) # 93.755711375 Joule
print(body) # Body(name='Ball', mass=19, speed=3.1415)
在前面的代码中,我创建了一个类来表示一个物理体,使用一种方法可以计算它的动能(使用著名的公式Ek=½mv2。请注意,name
应该是一个字符串,而mass
和speed
都是浮点数,并且都有一个默认值。同样有趣的是,我不必编写任何__init__
方法,它是由dataclass
装饰器为我编写的,还有用于比较和生成对象的字符串表示的方法(在最后一行由print
隐式调用)。
如果您感兴趣,可以阅读 PEP557 中的所有规范,但现在请记住,如果您需要,数据类可能会提供一个更好、更强大的命名元组替代方案。
现在我们有了所有的工具来了解如何编写自己的自定义迭代器。让我们首先定义一个 iterable 和一个迭代器:
- Iterable:如果一个对象能够一次返回一个成员,则称其为 Iterable。列表、元组、字符串和字典都是可编辑的。定义
__iter__
或__getitem__
方法的自定义对象也是可重用的。 - 迭代器:表示数据流的对象称为迭代器。需要一个自定义迭代器来为
__iter__
提供一个返回对象本身的实现,并为__next__
提供一个返回数据流的下一项直到流耗尽的实现,此时所有对__next__
的连续调用只会引发StopIteration
异常。内置函数,如iter
和next
被映射为在幕后对对象调用__iter__
和__next__
。
让我们编写一个迭代器,首先返回字符串中的所有奇数字符,然后返回偶数字符:
# iterators/iterator.py
class OddEven:
def __init__(self, data):
self._data = data
self.indexes = (list(range(0, len(data), 2)) +
list(range(1, len(data), 2)))
def __iter__(self):
return self
def __next__(self):
if self.indexes:
return self._data[self.indexes.pop(0)]
raise StopIteration
oddeven = OddEven('ThIsIsCoOl!')
print(''.join(c for c in oddeven)) # TIICO!hssol
oddeven = OddEven('HoLa') # or manually...
it = iter(oddeven) # this calls oddeven.__iter__ internally
print(next(it)) # H
print(next(it)) # L
print(next(it)) # o
print(next(it)) # a
因此,我们需要为返回对象本身的__iter__
提供一个实现,然后为__next__
提供一个实现。让我们看一下。需要发生的是_data[0]
、_data[2]
、_data[4]
、_data[1]
、_data[3]
、_data[5]
的回归。。。直到我们返回了数据中的每一项。为了做到这一点,我们准备了一个列表和索引,比如[0
、2
、4
、6
、1
、3
、5
、…],当其中至少有一个元素时,我们弹出第一个元素并从该位置的数据返回该元素,从而实现了我们的目标。当indexes
为空时,我们根据迭代器协议的要求提出StopIteration
。
还有其他方法可以达到相同的结果,所以请尝试自己编写不同的代码。确保最终结果适用于所有边缘情况、空序列、长度为1
、2
的序列等。
在本章中,我们研究了装饰器,发现了使用装饰器的原因,并介绍了几个同时使用一个或多个装饰器的示例。我们还看到了接受参数的装饰器,这些参数通常用作装饰器工厂。
我们初步了解了 Python 中的面向对象编程。我们涵盖了所有的基础知识,因此您现在应该能够理解未来章节中的代码。我们讨论了可以在类中编写的各种方法和属性,我们探讨了继承与组合、方法重写、属性、运算符重载和多态性。
最后,我们简要介绍了迭代器的基础,现在您对生成器有了更深入的了解。
在下一章中,我们将看到如何处理文件以及如何以几种不同的方式和格式保存数据。