Python attributes 笔记

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

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

>>> s = 'abc'
>>> globals()
{'__loader__': , '__package__': None, 's': 'abc', '__spec__': None, '__builtins__': , '__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__ 
__class__ 
__contains__ 
__delattr__ 
__dir__ 
__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__ 
__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 

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

>>> s.upper

>>> s.upper()
'ABC'
>>> s.upper.__call__

每个对象的 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 "", line 1, in 
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__': , 'hello': , '__init__': , '__dict__': })
>>> 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