Python面向对象:Digging into details
在Python编程语言中,所有的数据类型(包括基本数据类型和自定义数据类型)、函数、模块、类、甚至模块、包等都被视为对象。
在一些其他编程语言中,基本数据类型(如整数、浮点数、布尔值等)和复合数据类型(如数组、结构体等)与对象有所区别,而在Python中,这种区别消失了。无论是简单的整数还是复杂的类对象,它们在Python中都是对象。
Meta&Base: Type与Object详解
由引言我们已经知道,Python中一切都是对象,而对象又分为两类:
- 类型对象(Type Objects):
- “类型对象”通常指的是用来创建其他对象的对象,也就是类(class)。在Python中,类本身也是对象,由元类
type
来定义。类是用来实例化创建其他对象的模板,它定义了对象的属性和方法。 - 所有的内置数据类型(如
int
、str
、list
等)和用户自定义的类都属于类型对象。这些类型对象定义了一类对象所具有的共同特征和行为。
- “类型对象”通常指的是用来创建其他对象的对象,也就是类(class)。在Python中,类本身也是对象,由元类
- 非类型对象(Non-Type Objects):
- “非类型对象”指的是类实例化后得到的对象,也就是我们常见的数据类型和对象实例。这些对象是类的实例,它们继承了所属类的属性和方法,并具有自己的特定值和状态。
- 除了类实例外,Python中的一些原始数据类型(如整数、浮点数、字符串等)也属于非类型对象。
1
2
3
4
5
6
7
8
9# 类型对象示例
class MyClass: # 这是一个类,是类型对象
def method(self):
print("This is a method.")
# 非类型对象示例
obj1 = MyClass() # 创建MyClass的实例,是非类型对象
obj2 = [1, 2, 3] # 创建一个列表,是非类型对象
x = 10 # x是整数,是非类型对象
然而,“类型”到底指什么?
object
:object
是Python中所有类的基类(baseclass)。在Python中,一切皆对象,所有的数据类型和类都是object
的子类,包括内置数据类型(如int
、str
、list
等)和用户自定义的类。object
类提供了一些通用的方法,例如__init__()
、__str__()
、__repr__()
等,这些方法在所有类中都是可用的,因为所有类都继承自object
,即使自定义类没有显式地继承任何其他类,它也隐式地继承了object
类。
type
:type
是一个元类(metaclass),它用于创建所有类的类(class of classes)。在Python中,类本身也是一个对象,由元类来定义类的行为。而type
就是Python内置的元类。- 在一般情况下,我们使用
type()
函数来获取对象的类型(类)。例如,type(5)
将返回int
,type('hello')
将返回str
。 - 此外,我们还可以通过
type()
函数来动态地创建新的类。它接受三个参数:类名、基类元组和包含属性和方法的字典。通过使用type
,我们可以在运行时动态地创建新的类,这种能力被称为”metaclass programming”。
故而,一个最基本的类,要由两个组分组成。1)从object那继承类的基本特征与方法;2)从type那实例化成为类,这也是type被称为元类的原因。只要是类,就必然是元类的实例,就必然是类型对象,包含obejct;只要是类,就必然继承自object,包括type。
Attrs&Methods: 属性与方法
类有两个主要成分,其一是方法(可理解为常说的函数,方法是与对象相关联的函数。) ,其二则是属性。
类方法、实例方法、静态方法
类方法、实例方法和静态方法是三种不同类型的方法,它们具有以下区别:
实例方法(Instance Method):
- 定义方式:实例方法是最常见的方法类型,定义时必须包含一个
self
参数,代表类的==实例本身==。 - 调用方式:实例方法只能通过类的实例来调用,而不能通过类名直接调用。
- 访问权限:实例方法可以访问和修改类的实例属性以及调用其他实例方法。
- 用途:实例方法通常用于操作和处理类的实例,以及实现类的特定行为。
1
2
3
4
5
6class MyClass:
def instance_method(self, x):
return self.x + x
obj = MyClass()
result = obj.instance_method(5) # 通过实例调用实例方法
- 定义方式:实例方法是最常见的方法类型,定义时必须包含一个
类方法(Class Method):
- 定义方式:类方法使用
@classmethod
装饰器定义,第一个参数通常为cls
,表示类本身。 - 调用方式:类方法可以通过类名直接调用,也可以通过类的实例调用。
- 访问权限:类方法可以访问和修改类的属性,但不能访问实例的属性,也不能调用实例方法。
- 用途:类方法通常用于处理和修改类级别的属性,执行与类相关的操作。
1
2
3
4
5
6
7
8class MyClass:
class_variable = 10
def class_method(cls, x):
return cls.class_variable + x
result = MyClass.class_method(5) # 通过类名直接调用类方法
- 定义方式:类方法使用
静态方法(Static Method):
- 定义方式:静态方法使用
@staticmethod
装饰器定义,没有特殊的参数,与类和实例无关。 - 调用方式:静态方法可以通过类名直接调用,也可以通过类的实例调用。仅仅能通过”\
.method()”的方法进行访问。 - 访问权限:静态方法不能访问类和实例的属性,它们与类和实例无关。
- 用途:静态方法通常用于实现与类和实例无关的一般性功能,类似于普通函数,但是定义在类中,仅为了方便封装。
1
2
3
4
5
6class MyClass:
def static_method(x):
return x * 2
result = MyClass.static_method(3) # 通过类名直接调用静态方法
- 定义方式:静态方法使用
总结区别:
- 实例方法是与类的实例绑定的,需要通过类的实例来调用,可以访问实例属性和调用其他实例方法。
- 类方法是与类绑定的,可以通过类名直接调用或通过类的实例调用,可以访问类的属性,但不能访问实例的属性。
- 静态方法与类和实例无关,可以通过类名直接调用或通过类的实例调用,不能访问类和实例的属性。
对象中的函数被称为 method 方法类型,普通函数类型为 function。普通函数会将self
作为一个普通形参而非对象来处理,因此直接使用function来更新method会出现错误。如下例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Employee():
__class_version = "v1.0"
def __init__(self, id, name):
self.id = id
self.name = name
def say_age(self):
print("My id is %d" % self.age)
worker0 = Employee(0, "John")
worker0.age = 25
def say_age(self):
print("My id is %d" % self.age)
worker0.say_age = say_age
print(type(worker0.say_age))
print(type(say_age))
worker0.say_age()
会出现如下错误,因为say_age
是一个function
而非method
,无法识别self
。
正确的做法应当是利用types
库中的MethodType
函数:1
2from types import MethodType
worker0.say_age = MethodType(say_age, worker0)
类属性&对象属性
定义位置:
- 类属性:类属性是定义在类中,类的所有实例共享同一个类属性。它们属于类本身而不是实例。
- 对象属性:对象属性是定义在类的方法中或在
__init__
构造方法中,每个实例都有自己的一份对象属性。它们属于类的每个实例。1
2
3
4
5
6
7
8class MyClass:
class_attribute = "This is a class attribute."
def __init__(self, value):
self.instance_attribute = value
obj1 = MyClass(42)
obj2 = MyClass(100)
访问方式:
- 类属性:可以通过类名直接访问类属性,也可以通过实例对象访问。当通过实例对象访问类属性时,如果实例对象中没有同名的对象属性,会自动访问类属性。
- 对象属性:只能通过实例对象访问对象属性,每个实例都有自己的一份对象属性,彼此之间不共享。
1
2
3
4
5
6
7# 访问类属性
print(MyClass.class_attribute) # 输出: This is a class attribute.
print(obj1.class_attribute) # 输出: This is a class attribute.
# 访问对象属性
print(obj1.instance_attribute) # 输出: 42
print(obj2.instance_attribute) # 输出: 100
- 更新方式:
- 类属性:可以通过类名直接更新类属性的值,更新后所有实例共享更新后的值。
- 对象属性:只能通过实例对象来更新对象属性的值,更新只影响当前实例本身,不会影响其他实例。
1
2
3
4
5
6
7
8
9# 更新类属性
MyClass.class_attribute = "Updated class attribute."
print(MyClass.class_attribute) # 输出: Updated class attribute.
print(obj1.class_attribute) # 输出: Updated class attribute.
# 更新对象属性
obj1.instance_attribute = 50
print(obj1.instance_attribute) # 输出: 50
print(obj2.instance_attribute) # 输出: 100
还有一点需要注意的是,类属性在创建实例时并不会被单独创建,都是引用的类的属性。但通过实例来改变类属性时,我们并非修改了类的属性,而是将其拷贝了一份并进行修改,与类的属性解除了绑定,再对类属性做修改将不再影响到这一实例的同名属性值。
私有属性
Python类中的私有属性严格来讲并不是私有属性,可以说是只防君子,不防小人。同样的,既有类的私有属性也有实例的私有属性:
- 类的私有属性:类的私有属性是在类定义中使用双下划线
__
开头的属性,它们通常被称为”名称修饰”(Name Mangling),并且在类外部无法直接访问。Python 会将属性名转换为_ClassName__attribute
的形式,其中ClassName
是实例所属的类的名称,这样就使属性在外部变得不容易访问。虽然属性名称发生了变化,但实际上它不是严格意义上的”私有属性”,因为在某些情况下,仍然可以通过特定的方式访问到。1
2
3
4
5
6
7
8
9class MyClass:
__private_attribute = "I am a private attribute."
obj = MyClass()
# 无法直接访问类的私有属性
print(obj.__private_attribute)
# 报错:'MyClass' object has no attribute '__private_attribute'
print(obj._MyClass__private_attribute)
# 输出: I am a private attribute. - 实例的私有属性也是一样:在上面的示例中,
1
2
3
4
5
6
7class MyClass:
def __init__(self):
self.__private_attribute = "I am a private attribute."
obj = MyClass()
# 实际上是访问了名称修饰后的属性
print(obj._MyClass__private_attribute) # 输出: I am a private attribute.__private_attribute
是实例obj
的私有属性。虽然我们在类的外部无法直接访问它,但通过名称修饰后的_MyClass__private_attribute
,我们仍然可以在一定条件下访问到它。
毫无疑问,我们虽然能访问并修改私有属性,但这会带来隐患(可能对一个只读属性赋值,也可以动态绑定一个私有属性)。标准的访问方式应该是通过类方法进行(cls.__private_variable)。
更加靠谱的方法是将实例方法属性化。以下内容来自Python 从入门到深入 1.0 文档1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class C():
def __init__(self):
self.__arg = 0
def getarg(self):
return self.__arg
def setarg(self, value):
self.__arg = value
def delarg(self):
del self.__arg
arg = property(fget=getarg, fset=setarg, fdel=delarg, doc="'arg' property.")
c = C()
c.arg = 10 # 调用 setarg
print(c.arg) # 调用 getarg
c.setarg(20) # 调用 setarg
print(c.getarg()) # 调用 getarg
del c.arg # 调用 delargproperty
函数赋值的变量即为属性({obj}.{varname}
)。或者使用装饰器实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class C():
def __init__(self):
self.__arg = 0
def argopt(self):
return self.__arg
def argopt(self, value):
self.__arg = value
def argopt(self):
del self.__arg
c = C()
c.argopt = 10
print(c.argopt)
del c.argopt
需要注意的是并没有@{}.getter
,而是直接@property
;此外,要访问该属性,我们要用@property
装饰的那个函数的名称(这里为argopt
),通过调用属性的方式调用方法,就是实例方法的属性化;且大多数情况下需要用到@property
时是希望利用其readonly属性,故并不是每一个都需要设置setter&deleter
。
基类属性
__slots__
属性是一个包含属性名称的元组,指定了允许的属性列表。当一个类定义了__slots__
属性后,它的实例对象只能拥有__slots__
中列出的属性,并且不能再动态添加其他属性。__slots__
属性只对当前类的实例对象有效,对于继承的子类不起作用。__slots__
中定义的属性名不包括继承的父类属性,即继承的属性不受限制。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Parent:
__slots__ = ('name', 'age')
class Child(Parent):
__slots__ = ('gender', 'address')
obj = Child()
obj.name = "Alice"
obj.age = 30
obj.gender = "female"
obj.address = "123 Main St."
# 子类 Child 的实例对象只能拥有子类定义的属性,不会继承父类 Parent 的 __slots__
print(obj.name) # 输出: Alice
print(obj.age) # 输出: 30
print(obj.gender) # 输出: female
print(obj.address) # 输出: 123 Main St.
# 无法添加其他属性,因为 __slots__ 限制了对象的属性
# obj.email = "alice@example.com" # 报错:'Child' object has no attribute 'email'
__dir__
是一个特殊方法(也称为魔法方法),用于返回对象的属性列表。当调用内置函数dir(obj)
时,实际上会调用obj.__dir__()
方法来获取对象的属性列表。1
2
3
4
5
6
7
8
9
10
11
12
13class MyClass:
def __init__(self):
self.name = "Alice"
self.age = 30
def __dir__(self):
# 自定义 __dir__ 方法返回属性列表
return ['name', 'age', 'gender']
obj = MyClass()
# 调用 dir(obj) 实际上调用了 obj.__dir__() 方法
print(dir(obj))__doc__
是一个特殊属性(也称为魔法属性),用于存储类或函数的文档字符串(docstring)。当你定义类、函数或模块时,可以在其开头的第一个字符串中编写文档字符串。这个字符串会被Python 解释器识别为==文档字符串==,并将其存储在对象的__doc__
属性中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18def my_function(a, b):
"""
This function calculates the sum of two numbers.
Parameters:
a (int): The first number.
b (int): The second number.
Returns:
int: The sum of the two numbers.
"""
return a + b
print(my_function.__doc__)
# This function calculates the sum of two numbers.
# Parameters:
# a (int): The first number.
# b (int): The second number.
# Returns:
# int: The sum of the two numbers.__str__
用于定义对象的字符串表示形式。当一个对象被传递给内置的str()
函数时,Python 解释器会尝试调用该对象的__str__()
方法,以获取对象的字符串表示形式。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"Person: {self.name}, Age: {self.age}"
person = Person("Alice", 30)
# 调用 str(person) 实际上调用了 person.__str__() 方法
print(str(person))
# Person: Alice, Age: 30
# 为对象的 "informal" 字符串表示形式,通常用于提供对象的可读性好的字符串形式。__repr__
用于定义对象的 “official” 字符串表示形式。当一个对象被传递给内置的repr()
函数时,Python 解释器会尝试调用该对象的__repr__()
方法,以获取对象的官方字符串表示形式。如果一个对象没有定义__repr__
方法,Python 解释器会尝试调用__str__
方法。如果__str__
方法也没有定义,Python 将使用默认的__repr__
方法,==其结果是对象的内存地址表示==。- 如果一个对象同时定义了
__repr__
和__str__
方法,那么在普通上下文中,__str__
方法会优先于__repr__
方法被调用。1
2
3
4
5
6
7
8
9
10
11
12
13
14class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
point = Point(3, 5)
# 调用 repr(point) 实际上调用了 point.__repr__() 方法
print(repr(point))
# Point(x=3, y=5)
# 上为官方表达,供调试时查看对象详细信息用
- 如果一个对象同时定义了
__call__
用于使对象可以像函数一样被调用。如果一个对象定义了__call__
方法,那么该对象可以被当作函数一样调用,就像调用普通函数一样。(如一个神经网络就会定义一个__call__
方法,以前向运算产生输出值)1
2
3
4
5
6
7
8
9
10
11
12
13
14class Adder:
def __init__(self, base):
self.base = base
def __call__(self, x):
return self.base + x
# 创建 Adder 实例
adder = Adder(10)
# 调用 Adder 实例,实际上调用了 adder.__call__(5) 方法
result = adder(5)
print(result) # 输出: 15__delattr__
,__setattr__
, 和__getattribute__
分别用于自定义对象属性的设置、访问和删除行为。它们提供了对对象属性的高级控制。Python 提供了三个内置方法 getattr(),setattr() 和 hasattr(),它们均是基于类的属性方法来实现的。无论属性是否存在均会调用自定义的 getattribute 方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class C():
def __init__(self):
self.hello = "123"
def __delattr__(self, name):
print("delattr %s" % name)
super().__delattr__(name) # 调用 object 的 __delattr__
def __setattr__(self, attr, value):
print("setattr %s" % attr)
super().__setattr__(attr, value)# 调用 object 的 __setattr__
c = C()
del c.hello # 调用类对象的 __delattr__
print(c.hello) # 报错 hello 属性不存在
c.newarg = "100" # 调用类对象的 __setattr__
>>>
delattr hello
setattr newarg1
2
3
4
5
6
7
8
9
10
11
12
13
14class C():
def __init__(self):
self.hello = "123"
def __getattribute__(self, name):
print("getattribute %s" % name)
return super().__getattribute__(name)
print(C().hello)
print(C().ARG) # 报错没有 ARG 属性
>>>
getattribute hello
getattribute ARG