0%

类的属性

在 Python 中,数据属性和处理数据的方法统称为属性(attribute),方法也可称为方法属性,本质上是可调用的(callable)属性。Python 提供了丰富的 API 用于控制访问属性,以及实现动态属性。即使访问不存在的属性,也可以通过特殊方法实现“虚拟属性”,从而即时计算属性的值。

处理属性的特殊属性

为了方便处理属性,Python 定义了一些特殊属性,包括:

  • __class__:对象所属类的引用。obj.__class__type(obj) 效果一致。类和类的实例都具有属性,有些属性只能在类中查询,比如特殊方法;
  • __dict__:存储类或实例的可写属性的字典。如果设置了 __slots__ 属性,实例可能没有 __dict__ 属性;
  • __slots__:类可以定义这个属性,限制实例能拥有哪些属性。该属性的值可以是个可迭代对象,但通常会使用元组。如果类设置了 __slots__ 属性且 __slots__ 中不包含 '__dict__',那么该类的实例没有 __dict__ 属性。

__dict__

默认情况下,Python 会使用名为 __dict__ 的字典存储类和实例中的可写属性。其中,类属性字典由名为 mappingproxy 的代理对象包装,mappingproxy 定义在 collections.abc 模块中,特别指代类属性字典的类型:mappingproxy = type(type.__dict__)。类属性字典包含显式定义在类中的字段和方法,以及一些可写的特殊属性,包括模块、字典、弱引用和文档字符串。

1
2
3
4
5
6
7
8
9
10
11
12
>>> class Foo:
... a = 1
... def __init__(self):
... self.b = 2
...
>>> Foo.__dict__
mappingproxy({'__module__': '__main__',
'a': 1,
'__init__': <function Foo.__init__ at 0x1051fe8b0>,
'__dict__': <attribute '__dict__' of 'Foo' objects>,
'__weakref__': <attribute '__weakref__' of 'Foo' objects>,
'__doc__': None})

类属性不仅限于类字典中所展示的,还包含一些不可变的类属性,比如所属类的引用 __class__,直接父类组成的元组 __bases__ 等。

实例属性字典则是普通的字典类型,为实例属性赋值,会动态的修改实例字典。如果属性不存在,则将其添加到字典中,包括在初始化方法 __init__ 中赋值的实例属性。

阅读全文 »

可调用对象

除了用户定义的函数,调用运算符,即 “()” 括号对,还能应用到其他对象上。我们将能应用调用运算符的对象称为可调用对象,通过内置的 callable() 方法可以判断对象是否是可调用对象。在 Python 3 的数据模型文档中,一共列出了 7 种可调用对象:

  • 内置函数和内置方法:使用 C 语言(CPython)实现的函数和方法,如 len()alist.append()
  • 用户定义的函数:包括使用 def 创建的普通函数和 lambda 创建的匿名函数;
  • 实例方法与类方法:定义在类中的方法,实例方法是指第一个参数为 self 的方法,类方法是指第一个参数为 cls 的方法;
  • :对类使用调用运算符,如 C(),会执行类的 __new__ 方法创建类的实例,然后执行 __init__ 初始化;
  • 类的实例:如果类定义了 __call__ 方法,那它的实例可以作为函数调用;
  • 生成器函数:内部使用了 yield 关键字的函数,调用生成器函数会返回生成器对象;
  • 协程函数和异步生成器函数:从 Python 3.5 开始支持使用 async def 关键字来定义协程函数,如果内部包含 yield 关键字则被称为异步生成器函数。该函数被调用时会返回一个异步迭代器对象。

自定义的可调用类型

在装饰器一节,我们已经认识到了,装饰器不仅可以是函数,也可以是类。任何类只要实现了 __call__ 方法,那它就是可调用对象,就可以表现的如同函数。因此,我们可以编写用户自定义的可调用类型,将其用在任何期待函数的地方。下面我将通过 Java 和 Python 两种语言,展现它们在可调用类型上的异同。

假设现有一副扑克,要求按照 A, 2 ~ 10, J, Q, K 的顺序进行排序。在 Java 中,可以通过 Collections.sort() 集合类的接口对一个集合进行排序。Python 也提供了内置的 sorted() 方法,对可迭代对象进行排序。但两种语言都不支持直接对字符串和数字类型进行比较,所以还需要实现特定的排序逻辑。

Java 中要实现排序逻辑通常有两种方法。一种是让类实现 Comparable 接口,重写其中的 compareTo() 抽象方法:

1
2
3
4
5
6
public class Poker implements Comparable<Poker> {
@Override
public int compareTo(Poker otherPoker) {
// return ...
}
}

这里重点想展示第二种方法:新建一个实现了 Comparator 接口的比较器类,重写其 compare() 抽象方法。

1
2
3
4
5
6
7
8
9
public class PokerComparator implements Comparator<Poker> {
@Override
public int compare(Poker firstPoker, Poker secondPoker) {
// return ...
}
}

PokerComparator pokerComparator = new PokerComparator();
Collections.sort(pokers, pokerComparator);
阅读全文 »

序列

之前我们已经讨论过,Python 的“序列协议”是指:任何类,只要使用标准的签名和语义实现了 __getitem____len__ 方法,就能用在任何期待序列的地方,解释器会为这些类做特殊的支持,比如支持迭代和 in 运算符。序列协议的接口定义可以查阅官方的 CPython API 接口文档:Python/C API Reference Manual – Sequence Protocol,其中有这样一个函数:

1
2
int PySequence_Check(PyObject *o)
/* Return 1 if the object provides sequence protocol, and 0 otherwise. Note that it returns 1 for Python classes with a __getitem__() method unless they are dict subclasses since in general case it is impossible to determine what the type of keys it supports. This function always succeeds. */

这个函数的作用是检查并返回对象是否支持序列协议 —— 只在实现了 __getitem__ 方法且不是字典子类时才返回 1。这也符合我们之前所说的,协议是非正式的,没有强制力,只要你知道类的具体使用场景,可以只实现协议的一部分。比如,仅为了支持迭代,甚至不需要提供 __len__ 方法。

Python 常用的内置序列类型包括:字符串 str、列表 list、元组 tuple 和范围 range。尽管字典 dict 和集合 set 实现了序列协议中的 __getitem____len__ 方法,但它们并不算序列类型,因为它们的特征与序列有本质差异,比如这两个类型不支持通过整数下标索引访问元素,不支持切片,并且字典和集合内的元素是无序的。

序列类 Sequence,定义在标准库 collections.abc 模块中,继承自 Reversible 和 Collection 类,而 Collection 又继承自 Sized、Iterable 和 Container,体现了序列类可反转、具有规模、可迭代和是一个容器的语义。

collections.abc 模块的源码中,我们还能了解到序列类包含哪些子类。除了显示继承了 Sequence 的子类,如 ByteString 和 MutableSequence,还有通过 register 关键字绑定为 Sequence 虚拟子类的一些内置类型,在绑定虚拟子类一节中也提到过这一点。

1
2
3
4
5
>>> from collections.abc import Sequence
>>> all([issubclass(i, Sequence) for i in (str, list, tuple, range, bytes, bytearray, memoryview)])
True
>>> any([issubclass(i, Sequence) for i in (dict, set)])
False

上面列表推导表达式中的所有类型都是定义在 builtsin 模块中的内置类型,可以看到,除了 dict 和 set 之外,第二行的所有内置类型都是序列类型。除此之外,标准库中还定义了其他序列类型,比如 array 模块的 array 数组类型,collections 模块中的 deque 双端队列类型。

对于这些序列类型,按照序列内可容纳的类型,可以划分为以下两组:

阅读全文 »

前言

运算符重载这个语言特性其实一直备受争议,鉴于太多 C++ 程序员滥用这个特性,Java 之父 James Gosling 很干脆的决定不为 Java 提供运算符重载功能。但另一方面,正确的使用运算符重载确实能提高代码的可读性和灵活性。为此,Python 施加了一些限制,在灵活性、可用性和安全性之间做到了平衡。主要包括:

  • 不能重载内置类型的运算符
  • 不能新建运算符,只能重载现有的
  • is、and、or 和 not 运算符不能重载(但位运算符 &、| 和 ~ 可以)

Python 的运算符重载非常方便,只需要重写对应的特殊方法。在上面一节我们已经介绍了如何重载一个向量类的 “+” 和 “==” 运算符,实现还算简单,接下来我们考虑一个更复杂的情形:不只限于二维向量相加的 Vector 类,以引入 Python 运算符重载更全面的知识点。

改进版的 Vector

考虑到高维向量的应用场景,我们应当支持不同维度向量的相加操作,并且为低维向量的缺失值做默认添 0 处理,这也是一些统计分析应用的常用缺失值处理方式。基于此,首先要确定的便是,Vector 类的构造函数不再只接收固定数量和位置的参数,而应当接收可变参数。

通常情况下,Python 函数接收可变参数有两种处理方式。一种是接收不定长参数,即 *args,这样我们就可以用类似 Vector(1, 2)Vector(1, 2, 3) 的方式来初始化不同维数的向量类。在这种情况下,函数会将不定长参数打包成名为 args 的元组进行处理,当然能满足迭代的需求。虽然这种方式看上去很直观,但考虑到向量类从功能上讲也是一个序列类,而 Python 中的内置序列类型的构造方法基本上都是接收可迭代对象(Iterable)作为参数,考虑到一致性我们也采取这种形式,并且通过重写 __repr__ 输出更直观的向量类的数学表示形式。

1
2
3
4
5
6
class Vector:
def __init__(self, components: Iterable):
self._components = array('i', components)

def __repr__(self):
return str(tuple(self._components))

为了方便之后对向量分量的处理,将其保存在一个数组中,第一个参数 ‘i’ 标明这是一个整型数组。这样做还有一个好处就是,保证了向量序列的不可变性,这一点同 Python 内置类型不可变列表 tuple 类似。如此定义后,我们可以这样实例化 Vector 类:

1
2
3
4
5
6
7
>>> from vector import Vector
>>> Vector([1, 2])
(1, 2)
>>> Vector((1, 2, 3))
(1, 2, 3)
>>> Vector(range(4))
(0, 1, 2, 3)
阅读全文 »

Python 是动态强类型语言

Dynamic programming language: In computer science, a dynamic programming language is a class of high-level programming languages, which at runtime execute many common programming behaviours that static programming languages perform during compilation. These behaviors could include an extension of the program, by adding new code, by extending objects and definitions, or by modifying the type system.

以上内容摘自维基百科对于动态编程语言(Dynamic programming language)的定义。动态语言是相对于静态语言而言的。相比之下,静态语言有更严格的语法限制,在编译阶段就能够确定数据类型,典型的静态语言包括 C、C++ 和 Java 等。这一类语言的优势在于代码结构规范,易于调试和重构。缺点则是语法冗杂,编码方式不灵活。

而动态语言最典型的特点在于不需要编码时指定数据类型,类型信息由运行时推断得出。常见的动态语言都是一些脚本语言,比如 JavaScript、Python、PHP 等。这类语言虽然调试和重构的支持不如静态语言,但由于没有类型约束编码更加灵活。

Python 就是一门动态编程语言,编码时不用指定类型,且运行时可以变更数据类型:

1
2
3
4
5
6
>>> a = 1
>>> type(a)
<class 'int'>
>>> a = '1'
>>> type(a)
<class 'str'>

尽管 “PEP 484 – Type Hints” 引入了类型提示,但它明确指出:Python 依旧是一门动态类型语言,作者从未打算强制要求使用类型提示,甚至不会把它变成约定。但是 API 作者能够添加可选的类型注解,执行某种静态类型检查。

另外值得注意的是,虽然 Python 支持运行时变更数据类型,但变量所指向的内存地址空间已经在变更时发生了变化。也就是说,数据类型变更后不再指向原先的内存地址空间。我们可以用查看对象内存地址的 id() 函数加以验证:

1
2
3
4
5
6
>>> a = '123456'
>>> id(a)
4316699376
>>> a = 123456
>>> id(a)
4316579216

强弱类型

阅读全文 »