上个月在 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