这篇文章总结python语言面向对象一些设计思想以及使用面向对象思想去设计一个对象。
在Python中,类(class)是我们实践面向对象编程时最重要的工具之一。通过类,我们可以把头脑中的抽象概念进行建模,进而实现复杂的功能。同函数一样,类的语法本身也很简单,但藏着许多值得注意的细节。
封装(encapsulation)是面向对象编程里的一个重要概念,为了更好地体现类的封装性,许多编程语言支持将属性设置为公开或私有,只是方式略有不同。比如在Java里,我们可以用public和private关键字来表达是否私有;而在Go语言中,公有/私有则是用首字母大小写来区分的
在Python里,所有的类属性和方法默认都是公开的,不过你可以通过添加双下划线前缀__的方式把它们标示为私有。举个例子:
class Foo:
def __init__(self):
self.__bar = 'baz'
上面代码中Foo类的bar就是一个私有属性,如果你尝试从外部访问它,程序就会抛出异常
foo = Foo()
print(foo._bar)
# 输出结果
AttributeError: 'Foo' object has no attribute '_bar'
虽然上面是设置私有属性的标准做法,但Python里的私有只是一个“君子协议”。虽然用属性的本名访问不了私有属性,但只要稍微调整一下名字,就可以继续操作__bar了
foo = Foo()
print(foo._Foo__bar)
# 输出结果
bar
原理说明: 当使用双下划线前缀__的方式把他们标为私有后,python其实是为他们起了一个别名,只有通过这个别名才能访问他们,间接实现了私有化。例如上面_bar属性的别名就是_Foo__bar,访问别名依旧可以输出_bar属性的值。
python为私有成员创建别名格式:Python解释器只是重新给了它一个包含当前类名的别名(_{class}__{var}),因此你仍然可以在外部用这个别名来访问和修改它。
而在日常编程中,我们极少使用双下划线来标示一个私有属性。这是因为“标准”的双下划线前缀,可能会在子类想要重写父类私有属性时带来不必要的麻烦。如果你认为某个属性是私有的,直接给它加上单下划线_前缀就够了。注意单下划线python是不会为他创建别名,因此可以直接访问。
class Foo:
name = 'zhansan'
def __init__(self):
# 单下划线
self._bar = 'bar'
foo = Foo()
# 可以直接访问单下划线属性,说明它实质上不是私有。
print(foo._bar)
# 输出结果
bar
Python语言内部大量使用了字典类型,比如一个类实例的所有成员,其实都保存在了一个名为__dict__的字典属性中。而且,不光实例有这个字典,类其实也有这个字典。
class Person:
'''
Person类
'''
def __init__(self, name, age):
self.name = name
self.age = age
def say(self):
'''
输出内容
:return:
'''
print(f"Hi, My name is {self.name}, I'm {self.age}")
查看实例和类的字典
p = Person('张三', 100)
# 对象的字典保存当前实例所有的属性
print(p.__dict__)
# 输出结果
{'name': '张三', 'age': 100}
# 类的字典保存类的注释文档、方法等数据
print(Person.__dict__)
#输出结果
{&#39;__module__&#39;: &#39;__main__&#39;, &#39;__doc__&#39;: &#39;\n Person类\n &#39;, &#39;__init__&#39;: <function Person.__init__ at 0x10f566290>, &#39;say&#39;: <function Person.say at 0x10f5663b0>, &#39;__dict__&#39;: <attribute &#39;__dict__&#39; of &#39;Person&#39; objects>, &#39;__weakref__&#39;: <attribute &#39;__weakref__&#39; of &#39;Person&#39; objects>}
我们可以使用这个dict完成一些特殊任务&#xff0c;比如下面的例子。
将字典的内容赋值到上面Person对象的属性&#xff0c;常规的做法是遍历字典&#xff0c;然后将值赋值给Person对象属性。
d &#61; {&#39;name&#39;: &#39;张三&#39;, &#39;age&#39;: 20}
for key, value in d.items():
setattr(p, key, value)
print(f&#39;输出Person对象属性值 name: {p.name}, age:{p.age}&#39;)
#输出结果
输出Person对象属性值 name: 张三, age:20
使用直接修改实例的__dict__属性来快速达到目的
d &#61; {&#39;name&#39;: &#39;张三&#39;, &#39;age&#39;: 20}
p.__dict__.update(d)
print(f&#39;输出Person对象属性值 name: {p.name}, age:{p.age}&#39;)
#输出结果
输出Person对象属性值 name: 张三, age:20
虽然两种方式都可以实现向Person对象赋值属性&#xff0c;但他们是有区别的&#xff0c;通过类属性设置行为可以通过定义__setattr__魔法方法修改&#xff0c;而dict方式赋值则不会受__setattr__魔法限制。下面通过一个例子介绍他们的区别。
class Person:
def __init__(self, name, age):
self.name &#61; name
self.age &#61; age
def __setattr__(self, name, value):
# 不允许设置年龄小于0
if name &#61;&#61; &#39;age&#39; and value < 0:
raise ValueError(f&#39;Invalid age value: {value}&#39;)
super().__setattr__(name, value)
在上面的代码里&#xff0c;Person类增加了__setattr__方法&#xff0c;实现了对age值的校验逻辑。通过对象赋值小于0将会抛出异常。
p &#61; Person(&#39;张三&#39;, 1)
p.age &#61; -1
# 输出结果
ValueError: Invalid age value: -1
虽然普通的属性赋值会被__setattr__限制&#xff0c;但如果你直接操作实例的__dict__字典&#xff0c;就可以无视这个限制&#xff1a;
p &#61; Person(&#39;张三&#39;, 1)
p.__dict__[&#39;age&#39;] &#61; -1
print(f&#39;输出age:{p.age}&#39;)
#输出结果
输出age:-1
在编写类时&#xff0c;除了普通方法以外&#xff0c;我们还常常会用到一些特殊对象&#xff0c;比如类方法、静态方法等。要定义这些对象&#xff0c;得用到特殊的装饰器。下面简单介绍这些装饰器。
类方法&#64;classmethod
当你用def在类里定义一个函数时&#xff0c;这个函数通常称作方法。调用方法需要先创建一个类实例&#xff0c;由示例调用方法。如果你不使用实例&#xff0c;而是直接用类来调用quack()&#xff0c;程序就会因为找不到类实例而报错。
不过&#xff0c;虽然普通方法无法通过类来调用&#xff0c;但你可以用&#64;classmethod装饰器定义一种特殊的方法&#xff1a;类方法&#xff08;class method&#xff09;&#xff0c;它的生命周期属于整个类不在是对象&#xff0c;因此无须实例化也可调用。
class Duck:
def __init__(self, color):
self.color &#61; color
def quack(self):
print(f"Hi, I&#39;m a {self.color} duck!")
&#64;classmethod
def create_random(cls):
"""创建一只随机颜色的鸭子"""
color &#61; random.choice([&#39;yellow&#39;, &#39;white&#39;, &#39;gray&#39;])
# 通过cls创建类对象&#xff0c;并返回该对象
return cls(color&#61;color)
上面create_random方法通过类装饰器定义为类方法&#xff0c;下面通过类来调用该方法。
# 通过类调用类方法&#xff0c;返回类对象
duck &#61; Duck.create_random()
# 通过对象调用普通方法
duck.quack()
# 输出结果
Hi, I&#39;m a yellow duck!
作为一种特殊方法&#xff0c;类方法最常见的使用场景&#xff0c;就是像上面一样定义工厂方法来生成新实例。类方法的主角是类本身&#xff0c;当你发现某个行为不属于实例&#xff0c;而是属于整个类时&#xff0c;可以考虑使用类方法。
静态方法&#64;staticmethod
class Cat:
def __init__(self, name):
self.name &#61; name
def say(self):
sound &#61; self.get_sound()
print(f&#39;{self.name}: {sound}...&#39;)
&#64;staticmethod
# 静态方法不接收当前实例作为第一个位置参数
def get_sound():
repeats &#61; random.randrange(1, 10)
return &#39; &#39;.join([&#39;Meow&#39;] * repeats)
上面get_sound方法定义为静态方法&#xff0c;下面通过对象和类来调用它。
cat &#61; Cat(&#39;波斯猫&#39;)
# 通过对象调用静态方法
cat.say()
# 输出结果
波斯猫: Meow Meow Meow Meow Meow Meow Meow...
# 通过类调用静态方法
print(Cat.get_sound())
# 输出结果
Meow Meow Meow Meow Meow Meow Meow Meow
和普通方法相比&#xff0c;静态方法不需要访问实例的任何状态&#xff0c;是一种与状态无关的方法&#xff0c;因此静态方法其实可以改写成脱离于类的外部普通函数。
选择静态方法还是普通函数&#xff0c;可以从以下几点来考虑&#xff1a;
在一个类里&#xff0c;属性和方法有着不同的职责&#xff1a;属性代表状态&#xff0c;方法代表行为。二者对外的访问接口也不一样&#xff0c;属性可以通过inst.attr的方式直接访问&#xff0c;而方法需要通过inst.method()来调用
不过&#xff0c;&#64;property装饰器模糊了属性和方法间的界限&#xff0c;使用它&#xff0c;你可以把方法通过属性的方式暴露出来。
class Attrs:
def __init__(self,name):
self.name &#61; name
&#64;property
def set_name(self):
return self.name &#43; &#39;非常棒&#39;
使用&#64;property装饰器将set_name方法定义为属性&#xff0c;下面调用该属性查看结果。
att &#61; Attrs(&#39;张三&#39;)
print(att.set_name)
# 输出结果
张三非常棒
&#64;property除了可以定义属性的读取逻辑外&#xff0c;还支持自定义写入和删除逻辑
class UpdateAttr:
def __init__(self, name):
self.name &#61; name
&#64;property
def set_name(self):
return self.name
# 经过&#64;property的装饰以后&#xff0c;set_name 已经从一个普通方法变成了property对象&#xff0c;因此这里可以使用 set_name.setter
# 定义setter方法&#xff0c;该方法会在对属性赋值时被调用
&#64;set_name.setter
def set_name(self, desc):
pass
# 定义deleter方法&#xff0c;该方法会在删除属性时被调用
&#64;set_name.deleter
def set_name(self):
raise RuntimeError(&#39;Can not delete name&#39;)
调用结果
update_attr &#61; UpdateAttr(&#39;张三&#39;)
# 为属性赋值
att &#61; update_attr.set_name &#61; &#39;非常棒&#39;
print(att)
# 输出结果
非常棒
# 删除属性
del update_attr.set_name
# 输出结果
RuntimeError: Can not delete name
&#64;property是个非常有用的装饰器&#xff0c;它让我们可以基于方法定义类属性&#xff0c;精确地控制属性的读取、赋值和删除行为&#xff0c;灵活地实现动态属性等功能。
当你决定把某个方法改成属性后&#xff0c;它的使用接口就会发生很大的变化。你需要学会判断&#xff0c;方法和属性分别适合什么样的场景。举个例子&#xff0c;假如你的类有个方法叫get_latest_items()&#xff0c;调用它会请求外部服务的数十个接口&#xff0c;耗费5&#xff5e;10秒钟。那么这时&#xff0c;盲目把这个方法改成.latest_items属性就不太恰当。人们在读取属性时&#xff0c;总是期望能迅速拿到结果&#xff0c;调用方法则不一样——快点儿慢点儿都无所谓。让自己设计的接口符合他人的使用预期&#xff0c;也是写代码时很重要的一环。
在介绍抽象类的子类化机制前&#xff0c;先看一个与他相关的例子。
class ThreeFactory:
"""在被迭代时不断返回 3
:param repeat: 重复次数
"""
def __init__(self, repeat):
self.repeat &#61; repeat
def __iter__(self):
for _ in range(self.repeat):
yield 3
tf &#61; ThreeFactory(2)
for i in tf:
print(i)
ThreeFactory是个非常简单的类&#xff0c;它所做的&#xff0c;就是迭代时不断返回数字3&#xff0c;在collections.abc模块中&#xff0c;有许多和容器相关的抽象类&#xff0c;比如代表集合的Set、代表序列的Sequence等&#xff0c;其中有一个最简单的抽象类&#xff1a;Iterable&#xff0c;它表示的是可迭代类型。假如你用isinstance()函数对上面的ThreeFactory实例做类型检查&#xff0c;会得到一个有趣的结果&#xff1a;
print(isinstance(ThreeFactory(2), Iterable))
# 输出结果
True
虽然ThreeFactory没有继承Iterable类&#xff0c;但当我们用isinstance()检查它是否属于Iterable类型时&#xff0c;结果却是True&#xff0c;这正是受了抽象类的特殊子类化机制的影响。
下面通过一个示例来实现上面子类化功能&#xff0c;通过该示例介绍子类化机制。
from abc import ABC
# 要定义一个抽象类&#xff0c;你需要继承ABC类或使用abc.ABCMeta元类
class Validator(ABC):
"""校验器抽象类"""
&#64;classmethod
def __subclasshook__(cls, C):
"""任何提供了validate 方法的类&#xff0c;都被当作 Validator 的子类"""
if any("validate" in B.__dict__ for B in C.__mro__):
return True
return NotImplemented
def validate(self, value):
raise NotImplementedError
上面代码的重点是__subclasshook__类方法。__subclasshook__是抽象类的一个特殊方法&#xff0c;当你使用isinstance检查对象是否属于某个抽象类时&#xff0c;如果后者定义了这个方法&#xff0c;那么该方法就会被触发&#xff0c;然后&#xff1a;
这意味着&#xff0c;下面这个和Validator没有继承关系的类&#xff0c;也被视作Validator的子类&#xff1a;
class StringValidator:
def validate(self, value):
pass
print(isinstance(StringValidator(), Validator))
# 输出&#xff1a;True
通过__subclasshook__类方法&#xff0c;我们可以定制抽象类的子类判断逻辑。这种子类化形式只关心结构&#xff0c;不关心真实继承关系&#xff0c;所以常被称为“结构化子类”。
这也是之前的ThreeFactory类能通过Iterable类型校验的原因&#xff0c;因为Iterable抽象类对子类只有一个要求&#xff1a;实现了__iter__方法即可。