如果说object
是所有对象(objects)的父类,那么type
就是所有类(classes)的父亲了。 所有对象继承自object
,所有类继承自type
type是可用于动态创建新类的内置项。好吧,它实际上有两个用途: 1.如果给定单个参数,它将返回该参数的“类型”,即用于创建该对象的类:
>>> x = 5
>>> type(x)
<class 'int'>
>>> type(x) is int
True
>>> type(x)(42.0) # Same as int(42.0)
42
2.如果给定三个参数,它将创建一个新类。 name定义类的名称 bases定义基类,即超类 dict定义所有类属性和方法。
类定义:
class MyClass(MySuperClass):
def x(self):
print('x')
等价于
def x_function(self):
print('x')
MyClass = type('MyClass', (MySuperClass,), {'x': x_function})
例如,这可能是实现collections.namedtuple
类的一种方法,该类采用类名和属性元组。
内置函数hash和
id``构成了判断Python中对象相等的骨干。
默认情况下,Python 对象不具有可比性,除非它们完全相同。如果您尝试创建两个object()
并检查它们是否相等
>>> x = object()
>>> y = object()
>>> x == x
True
>>> y == y
True
>>> x == y # Comparing two objects
False
结果将始终是False
。这来自这样一个事实,即object
通过身份(identity)来比较自己:他们只是与自己相等。
要理解为什么对象只与它们自己进行比较,我们必须理解关键字is
。
Python的is
运算符用于检查两个值是否引用内存中相同的确切对象。将 Python 对象想象成空间中的箱子, 变量、数组索引等被命名为指向这些对象的箭头。
例如:
>>> x = object()
>>> y = object()
>>> z = y
>>> x is y
False
>>> y is z
True
在上面的代码中,有两个单独的对象和三个标签x
,y
,z
,并指向这两个对象:x
指向第一个对象,y
,z
都指向另一个对象。
>>> del x
这将删除箭头x
。对象本身不受分配或删除的影响,只有箭头受其影响。但是现在没有箭头指向第一个物体,让它活着是没有意义的。因此,Python的“垃圾收集器(gc)”丢掉了它。现在我们只剩下一个object。
>>> y = 5
现在箭头y
已更改为指向整数对象5
。 z
仍然指向第二个object
,所以它仍然活着。
>>> z = y * 2
现在z
指向另一个新对象10
,它存储在内存的某个地方。现在第二个object
也没有指向它的东西,所以也将被垃圾收集丢掉。 为了能够验证所有这些,我们可以使用内置函数id
。id
获得对象在内存中的确切位置,表示为数字。
>>> x = object()
>>> y = object()
>>> z = y
>>> id(x)
139737240793600
>>> id(y)
139737240793616
>>> id(z)
139737240793616 # Notice the numbers!
>>> x is y
False
>>> id(x) == id(y)
False
>>> y is z
True
>>> id(y) == id(z)
True
相同的对象,id
相同。不同的对象,id
不同。就是这么简单。
对于object
而言,==
和is
的行为相同,因为object
的==
行为定义为比较id:
class object:
def __eq__(self, other):
return self is other
object的实际实现是用C语言编写的。 和==不同,is运算符无法重载。
另一方面,如果容器类型可以相互替换,则它们是相等的。 很好的例子是具有相同索引处具有相同项目的列表,或包含完全相同值的集合。
>>> x = [1, 2, 3]
>>> y = [1, 2, 3]
>>> x is y
False # Different objects,
>>> x == y
True # Yet, equal.
这些可以通过以下方式定义:
class list:
def __eq__(self, other):
if len(self) != len(other):
return False
return all(x == y for x, y in zip(self, other))
# Can also be written as:
return all(self[i] == other[i] for i in range(len(self)))
同样,集合是无序的,因此即使它们的位置也无关紧要,只有它们的“存在”才重要:
class set:
def __eq__(self, other):
if len(self) != len(other):
return False
return all(item in other for item in self)
现在,与“等价”的概念相关,Python有了哈希的概念。任何一段数据的“哈希”是指看起来非常随机的预先计算的值,但它可用于识别该数据段(在某种程度上)。
哈希有两个特定的属性:
这意味着,如果两个值具有相同的哈希值,则它们很可能也具有相同的值。
比较哈希是检查“存在”的一种非常快速的方法。这是字典和集用来几乎立即查找其中的值的内容:
>>> import timeit
>>> timeit.timeit('999 in l', setup='l = list(range(1000))')
12.224023487000522 # 12 seconds to run a million times
>>> timeit.timeit('999 in s', setup='s = set(range(1000))')
0.06099735599855194 # 0.06 seconds for the same thing
set 解决方案的运行速度比列表解决方案快数百倍! 这是因为它们使用哈希值作为“索引”的替代品,并且如果相同哈希的值已经存储在集合/字典中,Python可以快速检查它是否是同一项目。此过程使检查是否存在几乎是即时的。
你有没有想过Python如何存储对象,它们的变量及方法?我们知道所有对象都有自己的属性和方法,但是Python究竟如何跟踪它们呢?
简单的答案是,所有内容都存储在字典中。vars
方法公开存储在对象和类中的变量。
>>> class C:
... some_constant = 42
... def __init__(self, x, y):
... self.x = x
... self.y = y
... def some_method(self):
... pass
...
>>> c = C(x=3, y=5)
>>> vars(c)
{'x': 3, 'y': 5}
>>> vars(C)
mappingproxy(
{'__module__': '__main__', 'some_constant': 42,
'__init__': <function C.__init__ at 0x7fd27fc66d30>,
'some_method': <function C.some_method at 0x7fd27f350ca0>,
'__dict__': <attribute '__dict__' of 'C' objects>,
'__weakref__': <attribute '__weakref__' of 'C' objects>,
'__doc__': None
})
如您所见,与对象相关的属性x
和y
存储在其对象自己的字典中,方法实际上作为函数存储在类的字典中。这是有道理的,因为函数本身的代码不会为每个对象而改变,只有传递给它的变量才会改变。
这可以通过c.method(x)
与C.method(c,x)
相同来证明
>>> class C:
... def function(self, x):
... print(f'self={self}, x={x}')
>>> c = C()
>>> C.function(c, 5)
self=<__main__.C object at 0x7f90762461f0>, x=5
>>> c.function(5)
self=<__main__.C object at 0x7f90762461f0>, x=5
它表明,在类中定义的函数实际上只是一个函数,只是self
作为第一个参数,传递的是对象。对象语法c.method(x)
只是一种更简洁的编写方式,实际和C.method(c,x)
一样。
现在,这是一个略有不同的问题。如果vars
显示一个类中的所有方法,那么下面这个问题是为什么:
>>> class C:
... def function(self, x): pass
...
>>> vars(C)
mappingproxy({
'__module__': '__main__',
'function': <function C.function at 0x7f607ddedb80>,
'__dict__': <attribute '__dict__' of 'C' objects>,
'__weakref__': <attribute '__weakref__' of 'C' objects>,
'__doc__': None
})
>>> c = C()
>>> vars(c)
{}
>>> c.__class__
<class '__main__.C'>
🤔 __class__
即不在c
的字典,也不在C
的字典。那它是从哪里来的?
如果你想要一个明确的答案,知道一个对象上可以访问哪些属性,你可以使用dir
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'function']
那么多出来的属性来自哪里呢?好吧,故事稍微复杂一些:Python支持继承。
默认情况下,python 中的所有对象都继承自object
类,实际上,__class__
定义在object
:
>>> '__class__' in vars(object)
True
>>> vars(object).keys()
dict_keys(['__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__',
'__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__',
'__init__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__',
'__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__', '__doc__'])
这确实涵盖了我们在dir(c)
的输出中看到的所有内容。 既然我已经提到了继承,我想我也应该详细说明“方法解析顺序(method resolution order,MRO)”是如何工作的。 简而言之,MRO 是对象从中继承属性和方法的类的列表。下面是一个简单示例:
>>> class A:
... def __init__(self):
... self.x = 'x'
... self.y = 'y'
...
>>> class B(A):
... def __init__(self):
... self.z = 'z'
...
>>> a = A()
>>> b = B()
>>> B.mro()
[<class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
>>> dir(b)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__', '__weakref__', 'x', 'y', 'z']
>>> set(dir(b)) - set(dir(a)) # all values in dir(b) that are not in dir(a)
{'z'}
>>> vars(b).keys()
dict_keys(['z'])
>>> set(dir(a)) - set(dir(object))
{'x', 'y'}
>>> vars(a).keys()
dict_keys(['x', 'y'])
因此,每个继承级别都会将较新的方法添加到dir
列表中,dir
在子类上显示在其方法解析顺序中找到的所有方法。这就是Python在REPL中建议方法补全的方式:
>>> class A:
... x = 'x'
...
>>> class B(A):
... y = 'y'
...
>>> b = B()
>>> b. # Press <tab> twice here
b.x b.y # autocompletion!
补充内容:slots? __slots__
很有趣。 看一下这个奇怪的行为:
>>> x = object()
>>> x.foo = 5
AttributeError: 'object' object has no attribute 'foo'
>>> class C:
... pass
...
>>> c = C()
>>> c.foo = 5
>>> # works?
由于某种奇怪的原因,无法将任意变量赋给object
,但是可以赋给自己创建的类的对象。 这是object
的特性吗?
>>> x = list()
>>> x.foo = 5
AttributeError: 'list' object has no attribute 'foo'
不,list
也不行。 现在该说回slots了。 下面用自定义的类来说明__slots__
:
>>> class C:
... __slots__ = ()
...
>>> c = C()
>>> c.foo = 5
AttributeError: 'C' object has no attribute 'foo'
现在这是很长的解释:
Python实际上有两种将数据存储在对象内的方法:作为字典(像大多数情况一样)和作为“结构”。结构是一种 C 语言数据类型,本质上可以将其视为来自 Python 的元组。字典使用更多的内存,因为它们可以根据需要扩展,并依靠额外的空间来获得快速访问数据的可靠性,这就是字典的方式。另一方面,结构具有固定的大小,并且无法扩展,但是当它们一个接一个地打包这些值而不会浪费任何空间时,它们占用的内存量最小。
这两种不同的存储数据方式,在Python中用__dict__
和__slots__
呈现。通常,实例属性(如self.foo
)存储在__dict__
字典中。除非你定义了__slots__
属性,此时对象只能有预定义的属性。 让我们用例子说明:
>>> class NormalClass:
... classvar = 'foo'
... def __init__(self):
... self.x = 1
... self.y = 2
...
>>> n = NormalClass()
>>> n.__dict__
{'x': 1, 'y': 2} # Note that `classvar` variable isn't here.
>>> # That is stored in `NormalClass.__dict__`
>>> class SlottedClass:
... __slots__ = ('x', 'y')
... classvar = 'foo' # This is fine.
... def __init__(self):
... self.x = 1
... self.y = 2
... # Trying to create `self.z` here will cause the same
... # `AttributeError` as before.
...
>>> s = SlottedClass()
>>> s.__dict__
AttributeError: 'SlottedClass' object has no attribute '__dict__'
>>> s.__slots__
('x', 'y')
所以创建了__slots__
后就不会有__dict__
,也就意味着不会有字典可供添加新属性。也就节省了内存。 关于slots行为的一个视频
在我们已经看到对象与底下的字典几乎相同,让我们在它们之间再画几条线。
我们知道,访问和重新分配字典中的属性是使用索引完成的:
>>> dictionary = {'property': 42}
>>> dictionary['property']
42
在对象上,它是通过.
运算符完成的:
>>> class C:
... prop = 42
...
>>> C.prop
42
您甚至可以设置和删除对象的属性:
>>> C.prop = 84
>>> C.prop
84
>>> del C.prop
>>> C.prop
AttributeError: type object 'C' has no attribute 'prop'
但是字典要灵活得多:例如,您可以检查字典中是否存在属性:
>>> d = {}
>>> 'prop' in d
False
>>> d['prop'] = 'exists'
>>> 'prop' in d
True
您可以使用 try-catch 在对象中执行此操作:
>>> class X:
... pass
...
>>> x = X()
>>> try:
... print(x.prop)
>>> except AttributeError:
... print("prop doesn't exist.")
prop doesn't exist.
但是更推荐hasattr
方法
>>> class X:
... pass
...
>>> x = X()
>>> hasattr(x, 'prop')
False
>>> x.prop = 'exists'
>>> hasattr(x, 'prop')
True
字典可以做的另一件事是使用变量为字典编制索引。你真的不能用物体做到这一点,对吧?让我们试试:
>>> class X:
... value = 42
...
>>> x = X()
>>> attr_name = 'value'
>>> x.attr_name
AttributeError: 'X' object has no attribute 'attr_name'
是的,它不采用变量的值。这应该是显而易见的。但是要实际执行此操作,您可以使用 getattr
,它接收一个字符串,就像字典键一样:
>>> class X:
... value = 42
...
>>> x = X()
>>> getattr(x, 'value')
42
>>> attr_name = 'value'
>>> getattr(x, attr_name)
42 # It works!
setattr
和delattr
以相同的方式工作:它们将属性名称作为字符串,并相应地设置/删除相应的属性。
>>> class X:
... value = 42
...
>>> x = X()
>>> setattr(x, 'value', 84)
>>> x.value
84
>>> delattr(x, 'value') # deletes the attribute completety
>>> hasattr(x, 'value')
False # `value` no longer exists on the object.
让我们尝试用以下函数之一构建一些有意义的东西:
有时您需要创建一个必须重载的函数,以便直接获取值或获取“工厂”对象,例如,它可以是对象或函数,它可以按需生成所需的值。让我们尝试实现该模式:
class api:
"""A dummy API."""
def send(item):
print(f'Uploaded {item!r}!')
def upload_data(item):
"""Uploads the provided value to our database."""
if hasattr(item, 'get_value'):
data = item.get_value()
api.send(data)
else:
api.send(item)
upload_data
函数通过检查它是否有get_value
方法来检查我们是否获得了工厂对象。如果是这样,则该函数用于获取要上载的实际值。让我们尝试使用它!
>>> import json
>>> class DataCollector:
... def __init__(self):
... self.items = []
... def add_item(self, item):
... self.items.append(item)
... def get_value(self):
... return json.dumps(self.items)
...
>>> upload_data('some text')
Uploaded 'some text'!
>>> collector = DataCollector()
>>> collector.add_item(42)
>>> collector.add_item(1000)
>>> upload_data(collector)
Uploaded '[42, 1000]'!
super
是 Python 引用超类的方式,例如使用其方法。 例如,下面的类封装了两个项目求和的逻辑:
class Sum:
def __init__(self, x, y):
self.x = x
self.y = y
def perform(self):
return self.x + self.y
使用这个类非常简单:
>>> s = Sum(2, 3)
>>> s.perform()
5
现在假设您要创建一个子类DoubleSum
继承Sum
类,DpubleSum
类具有相同的perform
接口,但它返回双倍的值。你会用super
实现:
class DoubleSum(Sum):
def perform(self):
parent_sum = super().perform()
return 2 * parent_sum
我们不需要定义任何已经定义的东西:我们不需要定义__init__
,我们也不必重写sum逻辑。我们只需要借助父类。
>>> d = DoubleSum(3, 5)
>>> d.perform()
16
我们正在达到所有类和对象相关的内置函数的末尾,最后一个是这三个装饰器。 property @property
是当您要为对象中的属性定义获取器(getter)和设置器(setter)时要使用的修饰符。 getters 和 setter 提供了一种在尝试读取或修改对象的属性时添加验证或运行一些额外代码的方法。
这是通过将属性转换为一组函数来完成的:一个函数在您尝试访问属性时运行,另一个函数在您尝试更改其值时运行。
让我们看一个示例,其中我们尝试确保学生的“marks”属性始终设置为正数,因为标记不能为负数:
class Student:
def __init__(self):
self._marks = 0
@property
def marks(self):
return self._marks
@marks.setter
def marks(self, new_value):
# Doing validation
if new_value < 0:
raise ValueError('marks cannot be negative')
# before actually setting the value.
self._marks = new_value
设置marks:
>>> student = Student()
>>> student.marks
0
>>> student.marks = 85
>>> student.marks
85
>>> student.marks = -10
ValueError: marks cannot be negative
classmethod:
@classmethod
可以在方法上使用,以使其成为类方法:以便它获得对类对象的引用,而不是实例(self)。
一个简单的例子是创建一个返回类名的函数:
>>> class C:
... @classmethod
... def class_name(cls):
... return cls.__name__
...
>>> x = C()
>>> x.class_name
'C'
staticmethod:@staticmethod
用于将方法转换为静态方法:一个等效于位于类中的函数的方法,独立于任何类或对象属性。使用这个可以完全摆脱传递给方法的第一个参数self
。 例如,我们可以做一个做一些数据验证:
class API:
@staticmethod
def is_valid_title(title_text):
"""Checks whether the string can be used as a blog title."""
return title_text.istitle() and len(title_text) < 60
这些内置是使用一个非常高级的主题(称为描述符(descriptor))创建的。老实说,描述符是一个非常高级的主题,以至于试图在这里涵盖它除了已经被告知的内容之外没有任何用处。我计划在将来的某个时候写一篇关于描述符及其用法的详细文章,所以请继续关注! 译者注:描述符是属性控制的工具,它的内容很多,感兴趣的可以去官方教程、Effective-python90的第46小节、流畅的Python的第20章以及知乎上很多描述符的文章深入了解。