Hiểu __get__ và __set__ và mô tả Python


310

Tôi đang cố gắng hiểu mô tả của Python là gì và chúng hữu ích cho việc gì. Tôi hiểu cách họ làm việc, nhưng đây là những nghi ngờ của tôi. Hãy xem xét các mã sau đây:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)


class Temperature(object):
    celsius = Celsius()
  1. Tại sao tôi cần lớp mô tả?

  2. Cái gì instanceownerở đây? (trong __get__). Mục đích của các tham số này là gì?

  3. Làm thế nào tôi có thể gọi / sử dụng ví dụ này?

Câu trả lời:


147

Bộ mô tả là cách loại Python propertyđược triển khai. Một bộ mô tả chỉ đơn giản là thực hiện __get__, __set__v.v. và sau đó được thêm vào một lớp khác trong định nghĩa của nó (như bạn đã làm ở trên với lớp Nhiệt độ). Ví dụ:

temp=Temperature()
temp.celsius #calls celsius.__get__

Truy cập thuộc tính mà bạn đã gán bộ mô tả cho ( celsiustrong ví dụ trên) gọi phương thức mô tả thích hợp.

instancetrong __get__là thể hiện của lớp (ở trên, __get__sẽ nhận được temp, trong khi đó ownerlà lớp có mô tả (vì vậy nó sẽ là Temperature).

Bạn cần sử dụng một lớp mô tả để đóng gói logic cung cấp năng lượng cho nó. Theo cách đó, nếu bộ mô tả được sử dụng để lưu trữ một số hoạt động đắt tiền (ví dụ), nó có thể lưu trữ giá trị trên chính nó chứ không phải lớp của nó.

Một bài viết về mô tả có thể được tìm thấy ở đây .

EDIT: Như jchl đã chỉ ra trong các bình luận, nếu bạn chỉ cần thử Temperature.celsius, instancesẽ được None.


6
Sự khác biệt giữa selfvà là instancegì?
Lăng kính bổ đề

2
'dụ' có thể là thể hiện của bất kỳ lớp nào, bản thân sẽ là thể hiện của cùng một lớp.
TheBeginner

3
@LemmaPrism selflà thể hiện của bộ mô tả, instancelà thể hiện của lớp (nếu được khởi tạo) bộ mô tả nằm trong ( instance.__class__ is owner).
Tcll

Temperature.celsiusđưa ra giá trị 0.0theo mã celsius = Celsius(). Bộ mô tả Celsius được gọi, vì vậy, thể hiện của nó có giá trị init 0.0được gán cho thuộc tính lớp Nhiệt độ, celsius.
Angel Salazar

109

Tại sao tôi cần lớp mô tả?

Nó cho phép bạn kiểm soát thêm về cách các thuộc tính hoạt động. Ví dụ, nếu bạn đã quen với getters và setters trong Java, thì đó là cách của Python để làm điều đó. Một lợi thế là nó trông giống như một thuộc tính (không có thay đổi về cú pháp). Vì vậy, bạn có thể bắt đầu với một thuộc tính thông thường và sau đó, khi bạn cần làm một cái gì đó lạ mắt, hãy chuyển sang một mô tả.

Một thuộc tính chỉ là một giá trị có thể thay đổi. Một mô tả cho phép bạn thực thi mã tùy ý khi đọc hoặc cài đặt (hoặc xóa) một giá trị. Vì vậy, bạn có thể tưởng tượng việc sử dụng nó để ánh xạ một thuộc tính vào một trường trong cơ sở dữ liệu, ví dụ - một loại ORM.

Một cách sử dụng khác có thể từ chối chấp nhận một giá trị mới bằng cách ném một ngoại lệ vào __set__- chỉ thực hiện "thuộc tính" một cách hiệu quả.

Cái gì instanceownerở đây? (trong __get__). Mục đích của các tham số này là gì?

Điều này khá tinh tế (và lý do tôi viết một câu trả lời mới ở đây - tôi đã tìm thấy câu hỏi này trong khi tự hỏi điều tương tự và không tìm thấy câu trả lời tuyệt vời đó).

Một mô tả được định nghĩa trên một lớp, nhưng thường được gọi từ một thể hiện. Khi nó được gọi từ một thể hiện cả hai instanceownerđược đặt (và bạn có thể làm việc ownertừ instanceđó để nó có vẻ vô nghĩa). Nhưng khi được gọi từ một lớp, chỉ ownerđược đặt - đó là lý do tại sao nó ở đó.

Điều này chỉ cần thiết __get__vì nó là người duy nhất có thể được gọi trong một lớp. Nếu bạn đặt giá trị lớp, bạn đặt chính bộ mô tả. Tương tự để xóa. Đó là lý do tại sao ownerkhông cần thiết ở đó.

Làm thế nào tôi có thể gọi / sử dụng ví dụ này?

Chà, đây là một mẹo hay khi sử dụng các lớp tương tự:

class Celsius:

    def __get__(self, instance, owner):
        return 5 * (instance.fahrenheit - 32) / 9

    def __set__(self, instance, value):
        instance.fahrenheit = 32 + 9 * value / 5


class Temperature:

    celsius = Celsius()

    def __init__(self, initial_f):
        self.fahrenheit = initial_f


t = Temperature(212)
print(t.celsius)
t.celsius = 0
print(t.fahrenheit)

(Tôi đang sử dụng Python 3; đối với python 2, bạn cần đảm bảo các phân chia đó / 5.0/ 9.0). Điều đó mang lại:

100.0
32.0

Bây giờ có nhiều cách khác, được cho là tốt hơn để đạt được hiệu ứng tương tự ở trăn (ví dụ: nếu celsius là một thuộc tính, đó là cơ chế cơ bản giống nhau nhưng đặt tất cả nguồn bên trong lớp Nhiệt độ), nhưng điều đó cho thấy những gì có thể được thực hiện ...


2
Các chuyển đổi là sai: chúng phải là C = 5 (F − 32) / 9, F = 32 + 9C / 5.
musiphil

1
Hãy chắc chắn rằng bạn có một đối tượng của Nhiệt độ. Làm theo sau làm rối tung các công cụ. t1 = Nhiệt độ (190) in t1.celsius t1.celsius = 100 in t1.fahrenheit Bây giờ khi bạn kiểm tra t.celcius và t.fahrenheit, chúng cũng được sửa đổi. t.celcius là 115 và t.fahrenheit là 32. rõ ràng là sai. @Eric
Ishan Bhatt

1
@IshanBhatt: Tôi nghĩ đó là do lỗi được chỉ ra bởi musiphil ở trên. Ngoài ra, đây không phải là câu trả lời của tôi
Eric

69

Tôi đang cố gắng hiểu mô tả của Python là gì và chúng có thể hữu ích cho việc gì.

Mô tả là các thuộc tính lớp (như thuộc tính hoặc phương thức) với bất kỳ phương thức đặc biệt nào sau đây:

  • __get__ (phương pháp mô tả phi dữ liệu, ví dụ về phương thức / hàm)
  • __set__ (phương pháp mô tả dữ liệu, ví dụ trên một ví dụ thuộc tính)
  • __delete__ (phương pháp mô tả dữ liệu)

Các đối tượng mô tả này có thể được sử dụng làm thuộc tính trên các định nghĩa lớp đối tượng khác. (Đó là, họ sống trong __dict__đối tượng của lớp.)

Các đối tượng mô tả có thể được sử dụng để lập trình quản lý các kết quả của một tra cứu chấm (ví dụ foo.descriptor) trong một biểu thức bình thường, một bài tập và thậm chí xóa.

Chức năng / phương pháp, phương pháp ràng buộc, property, classmethod, và staticmethodtất cả sử dụng những phương pháp đặc biệt để kiểm soát cách thức chúng được truy cập thông qua tra cứu rải rác.

Một bộ mô tả dữ liệu , như property, có thể cho phép đánh giá lười biếng các thuộc tính dựa trên trạng thái đơn giản hơn của đối tượng, cho phép các cá thể sử dụng ít bộ nhớ hơn nếu bạn tính toán trước từng thuộc tính có thể.

Một mô tả dữ liệu khác, a member_descriptor, được tạo bởi __slots__, cho phép tiết kiệm bộ nhớ bằng cách cho phép lớp lưu trữ dữ liệu trong cơ sở dữ liệu giống như bộ dữ liệu thay đổi thay vì linh hoạt hơn nhưng tốn nhiều không gian hơn __dict__.

Các bộ mô tả phi dữ liệu, thường là các thể hiện, lớp và các phương thức tĩnh, nhận các đối số đầu tiên ẩn (thường được đặt tên clsself, tương ứng) từ phương thức mô tả không dữ liệu của chúng , __get__.

Hầu hết người dùng Python chỉ cần học cách sử dụng đơn giản và không cần phải tìm hiểu hoặc hiểu thêm về việc triển khai các mô tả.

Trong chiều sâu: Mô tả là gì?

Một mô tả là một đối tượng với bất kỳ phương thức nào sau đây ( __get__, __set__hoặc __delete__), được dự định sẽ được sử dụng thông qua tra cứu chấm chấm như thể nó là một thuộc tính điển hình của một thể hiện. Đối với một đối tượng chủ sở hữu obj_instance, với một descriptorđối tượng:

  • obj_instance.descriptoryêu cầu
    descriptor.__get__(self, obj_instance, owner_class)trả về một value
    Đây là cách tất cả các phương thức và gettrên một thuộc tính hoạt động.

  • obj_instance.descriptor = valuegọi
    descriptor.__set__(self, obj_instance, value)trở lại None
    Đây là cách làm việc settertrên một tài sản.

  • del obj_instance.descriptorgọi
    descriptor.__delete__(self, obj_instance)trở lại None
    Đây là cách làm việc deletertrên một tài sản.

obj_instancelà cá thể có lớp chứa đối tượng mô tả. selflà ví dụ của bộ mô tả (có lẽ chỉ là một cho lớp của obj_instance)

Để xác định điều này với mã, một đối tượng là một mô tả nếu tập hợp các thuộc tính của nó giao với bất kỳ thuộc tính bắt buộc nào:

def has_descriptor_attrs(obj):
    return set(['__get__', '__set__', '__delete__']).intersection(dir(obj))

def is_descriptor(obj):
    """obj can be instance of descriptor or the descriptor class"""
    return bool(has_descriptor_attrs(obj))

Một mô tả dữ liệu có một __set__và / hoặc __delete__.
Một Non-Data-Descriptor có không __set__hay __delete__.

def has_data_descriptor_attrs(obj):
    return set(['__set__', '__delete__']) & set(dir(obj))

def is_data_descriptor(obj):
    return bool(has_data_descriptor_attrs(obj))

Ví dụ về đối tượng mô tả dựng sẵn:

  • classmethod
  • staticmethod
  • property
  • chức năng nói chung

Mô tả phi dữ liệu

Chúng ta có thể thấy điều đó classmethodstaticmethodlà những người không mô tả dữ liệu:

>>> is_descriptor(classmethod), is_data_descriptor(classmethod)
(True, False)
>>> is_descriptor(staticmethod), is_data_descriptor(staticmethod)
(True, False)

Cả hai chỉ có __get__phương pháp:

>>> has_descriptor_attrs(classmethod), has_descriptor_attrs(staticmethod)
(set(['__get__']), set(['__get__']))

Lưu ý rằng tất cả các chức năng cũng không phải là Mô tả dữ liệu:

>>> def foo(): pass
... 
>>> is_descriptor(foo), is_data_descriptor(foo)
(True, False)

Mô tả dữ liệu, property

Tuy nhiên, propertylà một Mô tả dữ liệu:

>>> is_data_descriptor(property)
True
>>> has_descriptor_attrs(property)
set(['__set__', '__get__', '__delete__'])

Lệnh tra cứu chấm

Đây là những điểm khác biệt quan trọng , vì chúng ảnh hưởng đến thứ tự tra cứu cho một tra cứu rải rác.

obj_instance.attribute
  1. Đầu tiên, ở trên để xem liệu thuộc tính có phải là Mô tả dữ liệu trên lớp của thể hiện hay không,
  2. Nếu không, nó sẽ xem liệu thuộc tính có trong obj_instance's không __dict__, sau đó
  3. cuối cùng nó rơi trở lại một Mô tả không dữ liệu.

Hậu quả của thứ tự tra cứu này là các Mô tả / Dữ liệu không giống như các hàm / phương thức có thể bị ghi đè bởi các thể hiện .

Tóm tắt và các bước tiếp theo

Chúng tôi đã học được rằng mô tả là những vật thể với bất kỳ __get__, __set__hoặc __delete__. Các đối tượng mô tả này có thể được sử dụng làm thuộc tính trên các định nghĩa lớp đối tượng khác. Bây giờ chúng tôi sẽ xem xét cách chúng được sử dụng, sử dụng mã của bạn làm ví dụ.


Phân tích mã từ câu hỏi

Đây là mã của bạn, theo sau là câu hỏi và câu trả lời của bạn cho từng câu:

class Celsius(object):
    def __init__(self, value=0.0):
        self.value = float(value)
    def __get__(self, instance, owner):
        return self.value
    def __set__(self, instance, value):
        self.value = float(value)

class Temperature(object):
    celsius = Celsius()
  1. Tại sao tôi cần lớp mô tả?

Bộ mô tả của bạn đảm bảo bạn luôn có dấu phẩy cho thuộc tính lớp này Temperaturevà bạn không thể sử dụng delđể xóa thuộc tính:

>>> t1 = Temperature()
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

Mặt khác, các mô tả của bạn bỏ qua lớp chủ sở hữu và các thể hiện của chủ sở hữu, thay vào đó, lưu trữ trạng thái trong bộ mô tả. Bạn có thể dễ dàng chia sẻ trạng thái trên tất cả các trường hợp bằng một thuộc tính lớp đơn giản (miễn là bạn luôn đặt nó ở dạng nổi cho lớp và không bao giờ xóa nó, hoặc thoải mái với người dùng mã của bạn làm như vậy):

class Temperature(object):
    celsius = 0.0

Điều này giúp bạn có chính xác hành vi tương tự như ví dụ của bạn (xem câu trả lời cho câu hỏi 3 bên dưới), nhưng sử dụng hàm Pythons dựng sẵn ( property) và sẽ được coi là thành ngữ hơn:

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)
  1. Ví dụ và chủ sở hữu ở đây là gì? (trong nhận ). Mục đích của các tham số này là gì?

instancelà ví dụ của chủ sở hữu đang gọi mô tả. Chủ sở hữu là lớp trong đó đối tượng mô tả được sử dụng để quản lý quyền truy cập vào điểm dữ liệu. Xem các mô tả về các phương pháp đặc biệt xác định các mô tả bên cạnh đoạn đầu tiên của câu trả lời này để biết thêm các tên biến mô tả.

  1. Làm thế nào tôi có thể gọi / sử dụng ví dụ này?

Đây là một minh chứng:

>>> t1 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1
>>> 
>>> t1.celsius
1.0
>>> t2 = Temperature()
>>> t2.celsius
1.0

Bạn không thể xóa thuộc tính:

>>> del t2.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delete__

Và bạn không thể chỉ định một biến không thể chuyển đổi thành float:

>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __set__
ValueError: invalid literal for float(): 0x02

Mặt khác, những gì bạn có ở đây là trạng thái toàn cầu cho tất cả các trường hợp, được quản lý bằng cách gán cho bất kỳ trường hợp nào.

Cách dự kiến ​​mà hầu hết các lập trình viên Python có kinh nghiệm sẽ thực hiện kết quả này là sử dụng trình propertytrang trí, sử dụng cùng một mô tả dưới mui xe, nhưng đưa hành vi vào việc thực hiện lớp chủ sở hữu (một lần nữa, như được định nghĩa ở trên):

class Temperature(object):
    _celsius = 0.0
    @property
    def celsius(self):
        return type(self)._celsius
    @celsius.setter
    def celsius(self, value):
        type(self)._celsius = float(value)

Có hành vi dự kiến ​​chính xác giống như đoạn mã gốc:

>>> t1 = Temperature()
>>> t2 = Temperature()
>>> t1.celsius
0.0
>>> t1.celsius = 1.0
>>> t2.celsius
1.0
>>> del t1.celsius
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> t1.celsius = '0x02'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in celsius
ValueError: invalid literal for float(): 0x02

Phần kết luận

Chúng tôi đã đề cập đến các thuộc tính xác định mô tả, sự khác biệt giữa mô tả dữ liệu và không mô tả dữ liệu, các đối tượng dựng sẵn sử dụng chúng và các câu hỏi cụ thể về việc sử dụng.

Vì vậy, một lần nữa, bạn sẽ sử dụng ví dụ của câu hỏi như thế nào? Tôi hy vọng bạn sẽ không. Tôi hy vọng bạn sẽ bắt đầu với đề xuất đầu tiên của tôi (một thuộc tính lớp đơn giản) và chuyển sang đề xuất thứ hai (trang trí thuộc tính) nếu bạn cảm thấy cần thiết.


1
Thật tuyệt, tôi đã học được nhiều nhất từ ​​câu trả lời này (chắc chắn cũng học được từ những người khác). Một câu hỏi về tuyên bố này "Cách mong đợi mà hầu hết các lập trình viên Python có kinh nghiệm sẽ thực hiện được kết quả này ...". Lớp Temeperature bạn xác định trước và sau câu lệnh giống hệt nhau. Tôi đã bỏ lỡ những gì bạn nhận được ở đây?
Yolo Voe

1
@YoloVoe không, đúng vậy, tôi đã thêm một số thông báo chính xác để nhấn mạnh rằng đó là sự lặp lại của những điều trên.
Aaron Hall

1
Đây là một câu trả lời TUYỆT VỜI. Tôi sẽ cần đọc nó thêm một vài lần nữa nhưng tôi cảm thấy sự hiểu biết của tôi về Python chỉ tăng lên vài bậc
Lucas Young

20

Trước khi đi vào chi tiết về các mô tả, điều quan trọng là phải biết cách tra cứu thuộc tính trong Python hoạt động. Điều này giả định rằng lớp không có siêu dữ liệu và nó sử dụng triển khai mặc định __getattribute__(cả hai có thể được sử dụng để "tùy chỉnh" hành vi).

Minh họa tốt nhất về tra cứu thuộc tính (trong Python 3.x hoặc cho các lớp kiểu mới trong Python 2.x) trong trường hợp này là từ Tìm hiểu siêu dữ liệu Python (codelog của ionel) . Hình ảnh sử dụng :thay thế cho "tra cứu thuộc tính không thể tùy chỉnh".

Điều này thể hiện việc tra cứu của một thuộc tính foobartrên một instancecủa Class:

nhập mô tả hình ảnh ở đây

Hai điều kiện quan trọng ở đây:

  • Nếu lớp của instancecó một mục nhập cho tên thuộc tính và nó có __get____set__.
  • Nếu instancekhông có mục nhập cho tên thuộc tính nhưng lớp có một và nó có __get__.

Đó là nơi mô tả đi vào nó:

  • Mô tả dữ liệu có cả __get____set__.
  • Mô tả phi dữ liệu mà chỉ có __get__.

Trong cả hai trường hợp, giá trị trả về đi qua __get__được gọi với thể hiện là đối số thứ nhất và lớp là đối số thứ hai.

Việc tra cứu thậm chí còn phức tạp hơn đối với việc tra cứu thuộc tính lớp (xem ví dụ tra cứu thuộc tính Class (trong blog đã đề cập ở trên) ).

Hãy chuyển sang các câu hỏi cụ thể của bạn:

Tại sao tôi cần lớp mô tả?

Trong hầu hết các trường hợp, bạn không cần phải viết các lớp mô tả! Tuy nhiên, bạn có thể là một người dùng cuối rất thường xuyên. Ví dụ chức năng. Các hàm là các mô tả, đó là cách các hàm có thể được sử dụng như các phương thức selfđược truyền hoàn toàn làm đối số đầu tiên.

def test_function(self):
    return self

class TestClass(object):
    def test_method(self):
        ...

Nếu bạn tìm kiếm test_methodmột ví dụ, bạn sẽ nhận lại "phương thức ràng buộc":

>>> instance = TestClass()
>>> instance.test_method
<bound method TestClass.test_method of <__main__.TestClass object at ...>>

Tương tự, bạn cũng có thể liên kết một hàm bằng cách gọi __get__phương thức của nó theo cách thủ công (không thực sự được khuyến nghị, chỉ nhằm mục đích minh họa):

>>> test_function.__get__(instance, TestClass)
<bound method test_function of <__main__.TestClass object at ...>>

Bạn thậm chí có thể gọi đây là "phương pháp tự ràng buộc":

>>> test_function.__get__(instance, TestClass)()
<__main__.TestClass at ...>

Lưu ý rằng tôi không cung cấp bất kỳ đối số nào và hàm đã trả về thể hiện mà tôi đã ràng buộc!

Chức năng là mô tả phi dữ liệu !

Một số ví dụ tích hợp của bộ mô tả dữ liệu sẽ là property. Bỏ qua getter, setterdeleterbộ propertymô tả là (từ Hướng dẫn cách mô tả "Thuộc tính" ):

class Property(object):
    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)

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

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

Kể từ đó là một dữ liệu mô tả nó gọi bất cứ khi nào bạn nhìn lên "tên" của propertyvà nó chỉ đơn giản là các đại biểu đến các chức năng trang trí bằng @property, @name.setter@name.deleter(nếu có).

Có một số mô tả khác trong thư viện tiêu chuẩn, ví dụ staticmethod, classmethod.

Điểm của mô tả là dễ dàng (mặc dù bạn hiếm khi cần chúng): Mã phổ biến trừu tượng để truy cập thuộc tính. propertylà một sự trừu tượng hóa cho truy cập biến thể hiện, functioncung cấp một sự trừu tượng hóa cho các phương thức, staticmethodcung cấp một sự trừu tượng hóa cho các phương thức không cần truy cập cá thể và classmethodcung cấp một sự trừu tượng hóa cho các phương thức cần truy cập lớp thay vì truy cập cá thể (điều này được đơn giản hóa một chút).

Một ví dụ khác sẽ là một tài sản lớp .

Một ví dụ thú vị (sử dụng __set_name__từ Python 3.6) cũng có thể là một thuộc tính chỉ cho phép một loại cụ thể:

class TypedProperty(object):
    __slots__ = ('_name', '_type')
    def __init__(self, typ):
        self._type = typ

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        return instance.__dict__[self._name]

    def __set__(self, instance, value):
        if not isinstance(value, self._type):
            raise TypeError(f"Expected class {self._type}, got {type(value)}")
        instance.__dict__[self._name] = value

    def __delete__(self, instance):
        del instance.__dict__[self._name]

    def __set_name__(self, klass, name):
        self._name = name

Sau đó, bạn có thể sử dụng bộ mô tả trong một lớp:

class Test(object):
    int_prop = TypedProperty(int)

Và chơi một chút với nó:

>>> t = Test()
>>> t.int_prop = 10
>>> t.int_prop
10

>>> t.int_prop = 20.0
TypeError: Expected class <class 'int'>, got <class 'float'>

Hoặc một "tài sản lười biếng":

class LazyProperty(object):
    __slots__ = ('_fget', '_name')
    def __init__(self, fget):
        self._fget = fget

    def __get__(self, instance, klass=None):
        if instance is None:
            return self
        try:
            return instance.__dict__[self._name]
        except KeyError:
            value = self._fget(instance)
            instance.__dict__[self._name] = value
            return value

    def __set_name__(self, klass, name):
        self._name = name

class Test(object):
    @LazyProperty
    def lazy(self):
        print('calculating')
        return 10

>>> t = Test()
>>> t.lazy
calculating
10
>>> t.lazy
10

Đây là những trường hợp di chuyển logic vào một mô tả chung có thể có ý nghĩa, tuy nhiên người ta cũng có thể giải quyết chúng (nhưng có thể bằng cách lặp lại một số mã) bằng các phương tiện khác.

Cái gì instanceownerở đây? (trong __get__). Mục đích của các tham số này là gì?

Nó phụ thuộc vào cách bạn tìm kiếm thuộc tính. Nếu bạn tìm thuộc tính trên một thể hiện thì:

  • đối số thứ hai là ví dụ mà bạn tra cứu thuộc tính
  • đối số thứ ba là lớp của thể hiện

Trong trường hợp bạn tra cứu thuộc tính trên lớp (giả sử mô tả được xác định trên lớp):

  • đối số thứ hai là None
  • đối số thứ ba là lớp nơi bạn tra cứu thuộc tính

Vì vậy, về cơ bản, đối số thứ ba là cần thiết nếu bạn muốn tùy chỉnh hành vi khi bạn thực hiện tra cứu cấp lớp (vì instancenó là None).

Làm thế nào tôi có thể gọi / sử dụng ví dụ này?

Ví dụ của bạn về cơ bản là một thuộc tính chỉ cho phép các giá trị có thể được chuyển đổi thành floatvà được chia sẻ giữa tất cả các phiên bản của lớp (và trên lớp - mặc dù người ta chỉ có thể sử dụng quyền truy cập "đọc" trên lớp nếu không bạn sẽ thay thế đối tượng mô tả ):

>>> t1 = Temperature()
>>> t2 = Temperature()

>>> t1.celsius = 20   # setting it on one instance
>>> t2.celsius        # looking it up on another instance
20.0

>>> Temperature.celsius  # looking it up on the class
20.0

Đó là lý do tại sao các mô tả thường sử dụng đối số thứ hai ( instance) để lưu trữ giá trị để tránh chia sẻ nó. Tuy nhiên, trong một số trường hợp, việc chia sẻ giá trị giữa các trường hợp có thể được mong muốn (mặc dù tôi không thể nghĩ ra một kịch bản tại thời điểm này). Tuy nhiên, thực tế nó không có ý nghĩa đối với một tài sản celsius trên một lớp nhiệt độ ... ngoại trừ có thể là bài tập thuần túy học thuật.


không chắc chắn nếu nền trong suốt của đồ họa thực sự bị ảnh hưởng trong chế độ tối nên được báo cáo là lỗi đối với stackoverflow.
Áo thun

@Tloverman Tôi nghĩ đây là một vấn đề với hình ảnh. Nó không hoàn toàn trong suốt ... Tôi đã lấy nó của bài đăng trên blog và không biết làm thế nào để tạo lại nó với nền trong suốt thích hợp. Thật tệ khi nó trông rất kỳ lạ với nền tối :(
MSeifert

9

Tại sao tôi cần lớp mô tả?

Lấy cảm hứng từ Python thông thạo bởi Buciano Ramalho

Hình ảnh bạn có một lớp học như thế này

class LineItem:
     price = 10.9
     weight = 2.1
     def __init__(self, name, price, weight):
          self.name = name
          self.price = price
          self.weight = weight

item = LineItem("apple", 2.9, 2.1)
item.price = -0.9  # it's price is negative, you need to refund to your customer even you delivered the apple :(
item.weight = -0.8 # negative weight, it doesn't make sense

Chúng ta nên xác nhận trọng lượng và giá để tránh gán cho chúng một số âm, chúng ta có thể viết ít mã hơn nếu chúng ta sử dụng mô tả làm proxy như thế này

class Quantity(object):
    __index = 0

    def __init__(self):
        self.__index = self.__class__.__index
        self._storage_name = "quantity#{}".format(self.__index)
        self.__class__.__index += 1

    def __set__(self, instance, value):
        if value > 0:
            setattr(instance, self._storage_name, value)
        else:
           raise ValueError('value should >0')

   def __get__(self, instance, owner):
        return getattr(instance, self._storage_name)

sau đó định nghĩa lớp LineItem như thế này:

class LineItem(object):
     weight = Quantity()
     price = Quantity()

     def __init__(self, name, weight, price):
         self.name = name
         self.weight = weight
         self.price = price

và chúng ta có thể mở rộng lớp Số lượng để xác thực phổ biến hơn


1
Trường hợp sử dụng thú vị, vì nó cho thấy cách sử dụng bộ mô tả để tương tác với nhiều phiên bản của người dùng. Ban đầu tôi không hiểu điểm quan trọng: Một thuộc tính có bộ mô tả phải được tạo trong không gian tên lớp (ví dụ: weight = Quantity()nhưng các giá trị phải được đặt trong không gian tên thể hiện chỉ bằng cách sử dụng self(ví dụ self.weight = 4), nếu không thì thuộc tính sẽ được trả về giá trị mới và phần mô tả sẽ bị loại bỏ. Rất vui!
phút

Tôi không thể hiểu một điều. Bạn đang định nghĩa weight = Quantity()là biến lớp và của nó __get____set__đang làm việc với biến thể hiện. Làm sao?
Kỹ thuật viên

0

Tôi đã thử (với những thay đổi nhỏ như được đề xuất) mã từ câu trả lời của Andrew Cooke. (Tôi đang chạy trăn 2.7).

Mật mã:

#!/usr/bin/env python
class Celsius:
    def __get__(self, instance, owner): return 9 * (instance.fahrenheit + 32) / 5.0
    def __set__(self, instance, value): instance.fahrenheit = 32 + 5 * value / 9.0

class Temperature:
    def __init__(self, initial_f): self.fahrenheit = initial_f
    celsius = Celsius()

if __name__ == "__main__":

    t = Temperature(212)
    print(t.celsius)
    t.celsius = 0
    print(t.fahrenheit)

Kết quả:

C:\Users\gkuhn\Desktop>python test2.py
<__main__.Celsius instance at 0x02E95A80>
212

Với Python trước 3, hãy đảm bảo rằng bạn phân lớp từ đối tượng sẽ làm cho bộ mô tả hoạt động chính xác vì ma thuật get không hoạt động đối với các lớp kiểu cũ.


1
Mô tả chỉ làm việc với các lớp phong cách mới. Đối với python 2.x, điều này có nghĩa là lấy lớp của bạn từ "đối tượng", mặc định trong Python 3.
Ivo van der Wijk

0

Bạn sẽ thấy https://docs.python.org/3/howto/descriptor.html#properies

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    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)

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

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

    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__)

1
Điều này không trả lời câu hỏi hoặc cung cấp bất kỳ thông tin hữu ích.
Sebastian Nielsen
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.