在Python编程语言中,所有的数据类型(包括基本数据类型和自定义数据类型)、函数、模块、类、甚至模块、包等都被视为对象。

在一些其他编程语言中,基本数据类型(如整数、浮点数、布尔值等)和复合数据类型(如数组、结构体等)与对象有所区别,而在Python中,这种区别消失了。无论是简单的整数还是复杂的类对象,它们在Python中都是对象。

Meta&Base: Type与Object详解

由引言我们已经知道,Python中一切都是对象,而对象又分为两类:

  1. 类型对象(Type Objects):
    • “类型对象”通常指的是用来创建其他对象的对象,也就是类(class)。在Python中,类本身也是对象,由元类type来定义。类是用来实例化创建其他对象的模板,它定义了对象的属性和方法。
    • 所有的内置数据类型(如intstrlist等)和用户自定义的类都属于类型对象。这些类型对象定义了一类对象所具有的共同特征和行为。
  2. 非类型对象(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 和 type 关系图 (实线表继承关系,虚线表实例关系 Shalabh Chaturvedi)

  1. object
    • object是Python中所有类的基类(baseclass)。在Python中,一切皆对象,所有的数据类型和类都是object的子类,包括内置数据类型(如intstrlist等)和用户自定义的类。
    • object类提供了一些通用的方法,例如__init__()__str__()__repr__()等,这些方法在所有类中都是可用的,因为所有类都继承自object,即使自定义类没有显式地继承任何其他类,它也隐式地继承了object类。
  2. type
    • type是一个元类(metaclass),它用于创建所有类的类(class of classes)。在Python中,类本身也是一个对象,由元类来定义类的行为。而type就是Python内置的元类。
    • 在一般情况下,我们使用type()函数来获取对象的类型(类)。例如,type(5)将返回inttype('hello')将返回str
    • 此外,我们还可以通过type()函数来动态地创建新的类。它接受三个参数:类名、基类元组和包含属性和方法的字典。通过使用type,我们可以在运行时动态地创建新的类,这种能力被称为”metaclass programming”。

故而,一个最基本的类,要由两个组分组成。1)从object那继承类的基本特征与方法;2)从type那实例化成为类,这也是type被称为元类的原因。只要是类,就必然是元类的实例,就必然是类型对象,包含obejct;只要是类,就必然继承自object,包括type。

Attrs&Methods: 属性与方法

类有两个主要成分,其一是方法(可理解为常说的函数,方法是与对象相关联的函数。) ,其二则是属性。

类方法、实例方法、静态方法

类方法、实例方法和静态方法是三种不同类型的方法,它们具有以下区别:

  1. 实例方法(Instance Method):

    • 定义方式:实例方法是最常见的方法类型,定义时必须包含一个self参数,代表类的==实例本身==。
    • 调用方式:实例方法只能通过类的实例来调用,而不能通过类名直接调用。
    • 访问权限:实例方法可以访问和修改类的实例属性以及调用其他实例方法。
    • 用途:实例方法通常用于操作和处理类的实例,以及实现类的特定行为。
      1
      2
      3
      4
      5
      6
      class MyClass:
      def instance_method(self, x):
      return self.x + x

      obj = MyClass()
      result = obj.instance_method(5) # 通过实例调用实例方法
  2. 类方法(Class Method):

    • 定义方式:类方法使用@classmethod装饰器定义,第一个参数通常为cls,表示类本身。
    • 调用方式:类方法可以通过类名直接调用,也可以通过类的实例调用。
    • 访问权限:类方法可以访问和修改类的属性,但不能访问实例的属性,也不能调用实例方法。
    • 用途:类方法通常用于处理和修改类级别的属性,执行与类相关的操作。
      1
      2
      3
      4
      5
      6
      7
      8
      class MyClass:
      class_variable = 10

      @classmethod
      def class_method(cls, x):
      return cls.class_variable + x

      result = MyClass.class_method(5) # 通过类名直接调用类方法
  3. 静态方法(Static Method):

    • 定义方式:静态方法使用@staticmethod装饰器定义,没有特殊的参数,与类和实例无关。
    • 调用方式:静态方法可以通过类名直接调用,也可以通过类的实例调用。仅仅能通过”\.method()”的方法进行访问。
    • 访问权限:静态方法不能访问类和实例的属性,它们与类和实例无关。
    • 用途:静态方法通常用于实现与类和实例无关的一般性功能,类似于普通函数,但是定义在类中,仅为了方便封装。
      1
      2
      3
      4
      5
      6
      class MyClass:
      @staticmethod
      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
16
class 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
2
from types import MethodType  
worker0.say_age = MethodType(say_age, worker0)

类属性&对象属性

  1. 定义位置:

    • 类属性:类属性是定义在类中,类的所有实例共享同一个类属性。它们属于类本身而不是实例。
    • 对象属性:对象属性是定义在类的方法中或在 __init__ 构造方法中,每个实例都有自己的一份对象属性。它们属于类的每个实例。
      1
      2
      3
      4
      5
      6
      7
      8
      class MyClass:
      class_attribute = "This is a class attribute."

      def __init__(self, value):
      self.instance_attribute = value

      obj1 = MyClass(42)
      obj2 = MyClass(100)
  2. 访问方式:

    • 类属性:可以通过类名直接访问类属性,也可以通过实例对象访问。当通过实例对象访问类属性时,如果实例对象中没有同名的对象属性,会自动访问类属性。
    • 对象属性:只能通过实例对象访问对象属性,每个实例都有自己的一份对象属性,彼此之间不共享。
      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
  3. 更新方式:
    • 类属性:可以通过类名直接更新类属性的值,更新后所有实例共享更新后的值。
    • 对象属性:只能通过实例对象来更新对象属性的值,更新只影响当前实例本身,不会影响其他实例。
      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类中的私有属性严格来讲并不是私有属性,可以说是只防君子,不防小人。同样的,既有类的私有属性也有实例的私有属性:

  1. 类的私有属性:类的私有属性是在类定义中使用双下划线 __ 开头的属性,它们通常被称为”名称修饰”(Name Mangling),并且在类外部无法直接访问。Python 会将属性名转换为 _ClassName__attribute 的形式,其中 ClassName 是实例所属的类的名称,这样就使属性在外部变得不容易访问。虽然属性名称发生了变化,但实际上它不是严格意义上的”私有属性”,因为在某些情况下,仍然可以通过特定的方式访问到。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class 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.
  2. 实例的私有属性也是一样:
    1
    2
    3
    4
    5
    6
    7
    class 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
22
class 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 # 调用 delarg

property函数赋值的变量即为属性({obj}.{varname})。或者使用装饰器实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class C():
def __init__(self):
self.__arg = 0

@property
def argopt(self):
return self.__arg

@argopt.setter
def argopt(self, value):
self.__arg = value

@argopt.deleter
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__ 中列出的属性,并且不能再动态添加其他属性。
    1. __slots__ 属性只对当前类的实例对象有效,对于继承的子类不起作用。
    2. __slots__ 中定义的属性名不包括继承的父类属性,即继承的属性不受限制。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      class 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
    13
    class 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
    18
    def 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
    14
    class 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
      14
      class 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
    14
    class 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(),它们均是基于类的属性方法来实现的。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class 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 newarg
    无论属性是否存在均会调用自定义的 getattribute 方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class 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