Python attributes 笔记

上个月在 Python Meetup 上有人分享了 Python attributes 的一些玩法,记录一下。

在 Python 里,我们所调用的全局变量,实际上是存放在 dictionary 里面的一个 key/value.

>>> s = 'abc'
>>> globals()
{'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__package__': None, 's': 'abc', '__spec__': None, '__builtins__': <module 'builtins' (built-in)>, '__doc__': None, '__name__': '__main__'}
>>> globals()['s']
'abc'

通过 dir([object]),可以看到某个对象的 attributes.

>>> s.upper()
'ABC'

>>> dir(s)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

要想获取这些 attributes, 可以通过 gettattr() 实现。可以看到, s 这个变量/对象的 attributes 大多是一些 built-in method.

>>> for attrname in dir(s):
...     print(attrname, getattr(s, attrname))
... 
__add__ <method-wrapper '__add__' of str object at 0x7fd867a74df8>
__class__ <class 'str'>
__contains__ <method-wrapper '__contains__' of str object at 0x7fd867a74df8>
__delattr__ <method-wrapper '__delattr__' of str object at 0x7fd867a74df8>
__dir__ <built-in method __dir__ of str object at 0x7fd867a74df8>
__doc__ str(object='') -> str
str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or
errors is specified, then the object must expose a data buffer
that will be decoded using the given encoding and error handler.
Otherwise, returns the result of object.__str__() (if defined)
or repr(object).
encoding defaults to sys.getdefaultencoding().
errors defaults to 'strict'.
__eq__ <method-wrapper '__eq__' of str object at 0x7fd867a74df8>
__format__ <built-in method __format__ of str object at 0x7fd867a74df8>
__ge__ <method-wrapper '__ge__' of str object at 0x7fd867a74df8>
__getattribute__ <method-wrapper '__getattribute__' of str object at 0x7fd867a74df8>
__getitem__ <method-wrapper '__getitem__' of str object at 0x7fd867a74df8>
__getnewargs__ <built-in method __getnewargs__ of str object at 0x7fd867a74df8>
__gt__ <method-wrapper '__gt__' of str object at 0x7fd867a74df8>
__hash__ <method-wrapper '__hash__' of str object at 0x7fd867a74df8>
__init__ <method-wrapper '__init__' of str object at 0x7fd867a74df8>
__iter__ <method-wrapper '__iter__' of str object at 0x7fd867a74df8>
__le__ <method-wrapper '__le__' of str object at 0x7fd867a74df8>
__len__ <method-wrapper '__len__' of str object at 0x7fd867a74df8>
__lt__ <method-wrapper '__lt__' of str object at 0x7fd867a74df8>
__mod__ <method-wrapper '__mod__' of str object at 0x7fd867a74df8>
__mul__ <method-wrapper '__mul__' of str object at 0x7fd867a74df8>
__ne__ <method-wrapper '__ne__' of str object at 0x7fd867a74df8>
__new__ <built-in method __new__ of type object at 0x7fd868e796e0>
__reduce__ <built-in method __reduce__ of str object at 0x7fd867a74df8>
__reduce_ex__ <built-in method __reduce_ex__ of str object at 0x7fd867a74df8>
__repr__ <method-wrapper '__repr__' of str object at 0x7fd867a74df8>
__rmod__ <method-wrapper '__rmod__' of str object at 0x7fd867a74df8>
__rmul__ <method-wrapper '__rmul__' of str object at 0x7fd867a74df8>
__setattr__ <method-wrapper '__setattr__' of str object at 0x7fd867a74df8>
__sizeof__ <built-in method __sizeof__ of str object at 0x7fd867a74df8>
__str__ <method-wrapper '__str__' of str object at 0x7fd867a74df8>
__subclasshook__ <built-in method __subclasshook__ of type object at 0x7fd868e796e0>
capitalize <built-in method capitalize of str object at 0x7fd867a74df8>
casefold <built-in method casefold of str object at 0x7fd867a74df8>
center <built-in method center of str object at 0x7fd867a74df8>
count <built-in method count of str object at 0x7fd867a74df8>
encode <built-in method encode of str object at 0x7fd867a74df8>
endswith <built-in method endswith of str object at 0x7fd867a74df8>
expandtabs <built-in method expandtabs of str object at 0x7fd867a74df8>
find <built-in method find of str object at 0x7fd867a74df8>
format <built-in method format of str object at 0x7fd867a74df8>
format_map <built-in method format_map of str object at 0x7fd867a74df8>
index <built-in method index of str object at 0x7fd867a74df8>
isalnum <built-in method isalnum of str object at 0x7fd867a74df8>
isalpha <built-in method isalpha of str object at 0x7fd867a74df8>
isdecimal <built-in method isdecimal of str object at 0x7fd867a74df8>
isdigit <built-in method isdigit of str object at 0x7fd867a74df8>
isidentifier <built-in method isidentifier of str object at 0x7fd867a74df8>
islower <built-in method islower of str object at 0x7fd867a74df8>
isnumeric <built-in method isnumeric of str object at 0x7fd867a74df8>
isprintable <built-in method isprintable of str object at 0x7fd867a74df8>
isspace <built-in method isspace of str object at 0x7fd867a74df8>
istitle <built-in method istitle of str object at 0x7fd867a74df8>
isupper <built-in method isupper of str object at 0x7fd867a74df8>
join <built-in method join of str object at 0x7fd867a74df8>
ljust <built-in method ljust of str object at 0x7fd867a74df8>
lower <built-in method lower of str object at 0x7fd867a74df8>
lstrip <built-in method lstrip of str object at 0x7fd867a74df8>
maketrans <built-in method maketrans of type object at 0x7fd868e796e0>
partition <built-in method partition of str object at 0x7fd867a74df8>
replace <built-in method replace of str object at 0x7fd867a74df8>
rfind <built-in method rfind of str object at 0x7fd867a74df8>
rindex <built-in method rindex of str object at 0x7fd867a74df8>
rjust <built-in method rjust of str object at 0x7fd867a74df8>
rpartition <built-in method rpartition of str object at 0x7fd867a74df8>
rsplit <built-in method rsplit of str object at 0x7fd867a74df8>
rstrip <built-in method rstrip of str object at 0x7fd867a74df8>
split <built-in method split of str object at 0x7fd867a74df8>
splitlines <built-in method splitlines of str object at 0x7fd867a74df8>
startswith <built-in method startswith of str object at 0x7fd867a74df8>
strip <built-in method strip of str object at 0x7fd867a74df8>
swapcase <built-in method swapcase of str object at 0x7fd867a74df8>
title <built-in method title of str object at 0x7fd867a74df8>
translate <built-in method translate of str object at 0x7fd867a74df8>
upper <built-in method upper of str object at 0x7fd867a74df8>
zfill <built-in method zfill of str object at 0x7fd867a74df8>

在这些 method 后面,加上括号,就是调用这个 method.

>>> s.upper
<built-in method upper of str object at 0x7fd867a74df8>
>>> s.upper()
'ABC'
>>> s.upper.__call__
<method-wrapper '__call__' of builtin_function_or_method object at 0x7fd867a0efc0>

每个对象的 attribute 都是可以自由修改的。比如,我们可以把 Python “升级” 到 Python 4:

>>> import sys
>>> sys.version
'3.5.2 |Anaconda 4.2.0 (64-bit)| (default, Jul  2 2016, 17:53:06) \n[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]'
>>> sys.version = '4.0.0'
>>> sys.version
'4.0.0'

又或者捏造一个 attribute.

>>> def foo():
...     return 5
... 
>>> foo.x = 100
>>> foo.x
100
>>> foo()
5

又或者直接访问对象里的一个值。

>>> class Foo(object):
...     def __init__(self, x):
...             self.x = x
...     
...     def __add__(self, other):
...             return Foo(self.x + other.x)
... 
>>> f = Foo(10)
>>> f.x
10

嗯,对象里面的变量,其实还是一个 Dictionary.

>>> class Foo(object):
...     pass
... 
>>> f = Foo()
>>> f.x = 100
>>> f.y = {'a':1, 'b':2, 'c':3}
>>> vars(f)
{'y': {'c': 3, 'a': 1, 'b': 2}, 'x': 100}
>>> 
>>> g = Foo()
>>> g.a = [1,2,3]
>>> g.b = 'hello'
>>> vars(g)
{'a': [1, 2, 3], 'b': 'hello'}

每个对象里的 attributes, 通常由 __init__ 来添加。

>>> class Foo(object):
...     def __init__(self, x, y):
...             self.x = x
...             self.y = y
... 
>>> f = Foo(10, [1,2,3])
>>> vars(f)
{'y': [1, 2, 3], 'x': 10}

再玩点别的。创建一个 Class Person, 记录每个人的信息。

>>> class Person(object):
...     def __init__(self, name):
...             self.name = name
...     
...     def hello(self):
...             return "Hello, {}".format(self.name)
... 
>>> p1 = Person('name1')
>>> p1.hello()
'Hello, name1'

然后添加一个统计人口的功能。

>>> class Person(object):
...     population = 0
...     def __init__(self, name):
...             self.name = name
...             Person.population = self.population + 1 ##<<---- 有没有不对劲的感觉?
...     def hello(self):
...             return "Hello, {}".format(self.name)
... 

# 然而输出是正确的
>>> print("population = {}".format(Person.population))
population = 0
>>> p1 = Person('name1')
>>> p2 = Person('name2')
>>> print("population = {}".format(Person.population))
population = 2
>>> print("p1.population = {}".format(p1.population))
p1.population = 2
>>> print("p2.population = {}".format(p2.population))
p2.population = 2

在加人口数字的时候,出现了 self.population + 1, 而 self 里面是没有 population 这个 attribute 的,这发生了什么?

在访问 self.population 的时候, Python 发现 self 里面没有 population 这个 attribute, 它就会往上一层,在 Person 这个 Class 里面找,所以实际上在这里 self.population 指向的就是 Person.population. 再举个例子:

>>> p1.thing
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute 'thing'
>>> 
>>> Person.thing = 'hello'
>>> p1.thing
'hello'

呃,那要是 Class 存在继承关系呢?

>>> class Person(object):
...     def __init__(self, name):
...             self.name = name
...     def hello(self):
...             return "Hello, {}".format(self.name)
... 
>>> class Employee(Person):
...     def __init__(self, name, id_number):
...             Person.__init__(self, name)
...             self.id_number = id_number
... 
>>> e = Employee('emp1', 1)
>>> e.hello()
'Hello, emp1'
>>> Person.hello(e)
'Hello, emp1'

>>> Person.__dict__
mappingproxy({'__doc__': None, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'Person' objects>, 'hello': <function Person.hello at 0x7fd867a1e950>, '__init__': <function Person.__init__ at 0x7fd867a1e510>, '__dict__': <attribute '__dict__' of 'Person' objects>})
>>> Person.__dict__['hello'](e)
'Hello, emp1'

同理, Employee 里面没有 hello 这个 attribute, 当调用 e.hello() 的时候,实际上 Python 会往上找到 Person 的 hello,然后进行调用。

前面提到,任何人都能随意访问和修改某个 attribute 的值,那有方法可以限制它吗? 比如,我们提供了一个调节空调温度的接口:

>>> class Thermostat(object):
...     def __init__(self):
...             self.temp = 20
... 
>>> t = Thermostat()
>>> t.temp = 100
>>> t.temp = 0

然而,我们并不想用户将温度设置为 100 度或者 0 度。我们可以通过下面这样的方法伪装一下:

>>> class Thermostat(object):
...     def __init__(self):
...             self._temp = 20 # want it to be private
...     
...     @property
...     def temp(self):
...             print("getting temp")
...             return self._temp
...     
...     @temp.setter
...     def temp(self, new_temp):
...             print("setting temp")
...             if new_temp > 35:
...                     print("Too high!")
...                     new_temp = 35
...             elif new_temp < 0:
...                     print("Too low!")
...                     new_temp = 0
...             
...             self._temp = new_temp
... 
>>> t = Thermostat()
>>> t.temp = 100
setting temp
Too high!
>>> t.temp
getting temp
35
>>> t.temp = -30
setting temp
Too low!
>>> t.temp
getting temp
0

既然用户是通过 t.temp = 100 来设置温度的,我们可以干脆把 temp 这个 attribute 指向一个专门的对象。用户把它当成 int 来用就行。

>>> class Temp(object):
...     def __init__(self):
...             self.temp = {}
...     def __get__(self, obj, objtype):
...             return self.temp[obj]
...     def __set__(self, obj, newval):
...             if newval > 35:
...                     newval = 35
...             if newval < 0:
...                     newval = 0
...             self.temp[obj] = newval
... 
>>> class Thermostat(object):
...     temp = Temp()
... 
>>> t1 = Thermostat()
>>> t2 = Thermostat()
>>> t1.temp = 100
>>> t2.temp = 20
>>> 
>>> t1.temp
35
>>> t2.temp
20

参考资料

https://github.com/littlepea/beijing-python-meetup/blob/master/2017/201705_attributes_talk/Beijing%20Python.ipynb