2010-07-08 49 views
50

Về cơ bản tôi muốn làm một cái gì đó như thế này:Cách tạo thuộc tính lớp chỉ đọc bằng Python?

class foo: 
    x = 4 
    @property 
    @classmethod 
    def number(cls): 
     return x 

Sau đó, tôi muốn sau đây để làm việc:

>>> foo.number 
4 

Thật không may, trên không hoạt động. Thay vì cho tôi 4, nó cho tôi <property object at 0x101786c58>. Có cách nào để đạt được điều này không?

Trả lời

36

property descriptor luôn luôn trả về chính nó khi được truy cập từ một lớp (ví dụ: khi instanceNone trong phương thức __get__).

Nếu đó không phải là những gì bạn muốn, bạn có thể viết một mô tả mới mà luôn luôn sử dụng các đối tượng lớp (owner) thay vì dụ:

>>> class classproperty(object): 
...  def __init__(self, getter): 
...   self.getter= getter 
...  def __get__(self, instance, owner): 
...   return self.getter(owner) 
... 
>>> class Foo(object): 
...  x= 4 
...  @classproperty 
...  def number(cls): 
...   return cls.x 
... 
>>> Foo().number 
4 
>>> Foo.number 
4 
+19

Đây không phải là thuộc tính chỉ đọc. Các bộ mô tả Python cố ý không chặn các cuộc gọi cấp lớp tới 'setattr' và' delattr' bằng các phương thức '__set__' và' __delete__' của chúng. Vì vậy, 'Foo.number = 6' vẫn có thể hoạt động. – HardlyKnowEm

47

Điều này sẽ làm Foo.number một read-only tài sản:

class MetaFoo(type): 
    @property 
    def number(cls): 
     return cls.x 

class Foo(object, metaclass=MetaFoo): 
    x = 4 

print(Foo.number) 
# 4 

Foo.number = 6 
# AttributeError: can't set attribute 

Giải thích: Kịch bản thông thường khi sử dụng @property trông như thế này:

class Foo(object): 
    @property 
    def number(self): 
     ... 
foo = Foo() 

Một tài sản được xác định trong Foo là chỉ đọc đối với các phiên bản của nó. Tức là, foo.number = 6 sẽ tăng AttributeError.

Tương tự, nếu bạn muốn Foo.number để tăng AttributeError, bạn sẽ cần thiết lập thuộc tính được xác định trong type(Foo). Do đó sự cần thiết cho một metaclass.


Lưu ý rằng khả năng đọc này không miễn trừ với tin tặc. tài sản này có thể được thực hiện có thể ghi bằng cách thay đổi lớp Foo của:

class Base(type): pass 
Foo.__class__ = Base 

# makes Foo.number a normal class attribute 
Foo.number = 6 
print(Foo.number) 

in

6 

hoặc, nếu bạn muốn thực hiện Foo.number một tài sản settable,

class WritableMetaFoo(type): 
    @property 
    def number(cls): 
     return cls.x 
    @number.setter 
    def number(cls, value): 
     cls.x = value 
Foo.__class__ = WritableMetaFoo 

# Now the assignment modifies `Foo.x` 
Foo.number = 6 
print(Foo.number) 

cũng in 6 .

+0

@unutbu: Có thể thực hiện điều ngược lại hay không: điều đó khiến cho Foo.number có thể ghi mà không cần xây dựng lại lớp Foo. – user2284570

+0

@ user2284570: Bạn có thể ghi 'Foo.number' bằng cách thay đổi metaclass' Foo' từ 'MetaFoo' thành một thứ khác. Bạn có thể làm cho 'number' là một thuộc tính có thể đặt hoặc đơn giản là không đề cập đến nó, trong trường hợp đó thiết lập' Foo.number' sẽ làm cho 'Foo.number' trở thành một thuộc tính lớp bình thường. Tôi đã thêm một số mã ở trên để hiển thị những gì tôi có ý nghĩa. – unutbu

+0

@unutbu: Và tôi có thể thay đổi metaclass trở lại không? Điều này sẽ làm việc cho các đối tượng mã? – user2284570

12

Tôi đồng ý với unubtu's answer; có vẻ như nó hoạt động, tuy nhiên, nó không hoạt động với cú pháp chính xác này trên Python 3 (đặc biệt, Python 3.4 là những gì tôi gặp khó khăn). Đây là cách người ta phải tạo mẫu theo Python 3.4 để làm cho mọi việc, có vẻ như:

class MetaFoo(type): 
    @property 
    def number(cls): 
     return cls.x 

class Foo(metaclass=MetaFoo): 
    x = 4 

print(Foo.number) 
# 4 

Foo.number = 6 
# AttributeError: can't set attribute 
2

Vấn đề với các giải pháp trên là nó sẽ không hoạt động để truy cập vào các biến lớp từ dụ biến:

print(Foo.number) 
# 4 

f = Foo() 
print(f.number) 
# 'Foo' object has no attribute 'number' 

Hơn nữa, sử dụng metaclass rõ ràng không phải là quá tốt đẹp, như sử dụng trang trí thông thường property.

Tôi đã cố gắng giải quyết vấn đề này. Dưới đây như thế nào nó hoạt động bây giờ:

@classproperty_support 
class Bar(object): 
    _bar = 1 

    @classproperty 
    def bar(cls): 
     return cls._bar 

    @bar.setter 
    def bar(cls, value): 
     cls._bar = value 


# @classproperty should act like regular class variable. 
# Asserts can be tested with it. 
# class Bar: 
#  bar = 1 


assert Bar.bar == 1 

Bar.bar = 2 
assert Bar.bar == 2 

foo = Bar() 
baz = Bar() 
assert foo.bar == 2 
assert baz.bar == 2 

Bar.bar = 50 
assert baz.bar == 50 
assert foo.bar == 50 

Như bạn thấy, chúng ta có @classproperty mà làm việc cùng một cách như @property cho các biến lớp. Điều duy nhất chúng tôi cần là trang trí lớp học bổ sung @classproperty_support.

Giải pháp cũng hoạt động cho thuộc tính lớp chỉ đọc.

Dưới đây là thực hiện:

class classproperty: 
    """ 
    Same as property(), but passes obj.__class__ instead of obj to fget/fset/fdel. 
    Original code for property emulation: 
    https://docs.python.org/3.5/howto/descriptor.html#properties 
    """ 
    def __init__(self, fget=None, fset=None, fdel=None, doc=None): 
     self.fget = fget 
     self.fset = fset 
     self.fdel = fdel 
     if doc is None and fget is not None: 
      doc = fget.__doc__ 
     self.__doc__ = doc 

    def __get__(self, obj, objtype=None): 
     if obj is None: 
      return self 
     if self.fget is None: 
      raise AttributeError("unreadable attribute") 
     return self.fget(obj.__class__) 

    def __set__(self, obj, value): 
     if self.fset is None: 
      raise AttributeError("can't set attribute") 
     self.fset(obj.__class__, value) 

    def __delete__(self, obj): 
     if self.fdel is None: 
      raise AttributeError("can't delete attribute") 
     self.fdel(obj.__class__) 

    def getter(self, fget): 
     return type(self)(fget, self.fset, self.fdel, self.__doc__) 

    def setter(self, fset): 
     return type(self)(self.fget, fset, self.fdel, self.__doc__) 

    def deleter(self, fdel): 
     return type(self)(self.fget, self.fset, fdel, self.__doc__) 


def classproperty_support(cls): 
    """ 
    Class decorator to add metaclass to our class. 
    Metaclass uses to add descriptors to class attributes, see: 
    http://stackoverflow.com/a/26634248/1113207 
    """ 
    class Meta(type): 
     pass 

    for name, obj in vars(cls).items(): 
     if isinstance(obj, classproperty): 
      setattr(Meta, name, property(obj.fget, obj.fset, obj.fdel)) 

    class Wrapper(cls, metaclass=Meta): 
     pass 
    return Wrapper 

Lưu ý: mã không được thử nghiệm nhiều, cảm thấy tự do cần lưu ý nếu nó không hoạt động như bạn mong đợi.

Các vấn đề liên quan