python - matplotlib tutorial




Pythonproperty مقابل getters و setters (8)

فيما يلي سؤال تصميم بيثون نقي:

class MyClass(object):
    ...
    def get_my_attr(self):
        ...

    def set_my_attr(self, value):
        ...

و

class MyClass(object):
    ...        
    @property
    def my_attr(self):
        ...

    @my_attr.setter
    def my_attr(self, value):
        ...

تتيح لنا بايثون القيام بذلك بأي طريقة. إذا كنت ستقوم بتصميم برنامج بايثون ، ما هو المنهج الذي ستستخدمه ولماذا؟


[ TL ، DR؟ يمكنك تخطي إلى نهاية مثال التعليمات برمجية .]

إنني في الواقع أفضّل استخدام لغة مختلفة ، وهي مشاركة صغيرة لاستخدامها كواحد ، ولكن من الجيد أن يكون لديك حالة استخدام أكثر تعقيدًا.

قليلا من الخلفية أولا.

تفيد الخصائص في أنها تسمح لنا بالتعامل مع كل من الإعداد والحصول على القيم بطريقة برمجية ولكن مع السماح بالوصول إلى السمات كسمات. يمكننا تحويل "يحصل" على "حسابات" (بشكل أساسي) ويمكننا تحويل "مجموعات" إلى "أحداث". لذلك دعنا نقول أن لدينا الطبقة التالية ، التي قمت بترميزها مع getters و setters تشبه جافا.

class Example(object):
    def __init__(self, x=None, y=None):
        self.x = x
        self.y = y

    def getX(self):
        return self.x or self.defaultX()

    def getY(self):
        return self.y or self.defaultY()

    def setX(self, x):
        self.x = x

    def setY(self, y):
        self.y = y

    def defaultX(self):
        return someDefaultComputationForX()

    def defaultY(self):
        return someDefaultComputationForY()

قد تتساءل لماذا لم أتصل بـ defaultX و defaultY في طريقة init الخاصة بالكائن. والسبب في ذلك هو أنه في حالتنا ، أريد أن أفترض أن أساليب someDefaultComputation ترجع القيم التي تتغير مع مرور الوقت ، مثل الطابع الزمني ، وكلما لم يتم تعيين x (أو y) (حيث ، من أجل هذا المثال ، "لم يتم التعيين") يعني "تعيين إلى بلا") أريد قيمة الحساب الافتراضي لـ x (أو y).

لذلك هذا هو عرجاء لعدد من الأسباب المذكورة أعلاه. سأعيد كتابتها باستخدام الخصائص:

class Example(object):
    def __init__(self, x=None, y=None):
        self._x = x
        self._y = y

    @property
    def x(self):
        return self.x or self.defaultX()

    @x.setter
    def x(self, value):
        self._x = value

    @property
    def y(self):
        return self.y or self.defaultY()

    @y.setter
    def y(self, value):
        self._y = value

    # default{XY} as before.

ما الذي اكتسبناه؟ لقد اكتسبنا القدرة على الإشارة إلى هذه السمات كسمات على الرغم من أننا ، من وراء الكواليس ، ينتهي بنا الأمر إلى تشغيل الأساليب.

وبالطبع فإن القوة الحقيقية للعقارات هي أننا نرغب عمومًا في أن تقوم هذه الطرق بعمل شيء ما بالإضافة إلى الحصول على القيم وتحديدها (وإلا لن يكون هناك أي فائدة في استخدام الخصائص). أنا فعلت هذا في مثال بلدي getter. نحن نستخدم بشكل أساسي هيئة وظيفية لالتقاط افتراضي عندما لا يتم تعيين القيمة. هذا هو نمط شائع جدا.

ولكن ماذا نخسر ، وماذا لا يمكننا أن نفعل؟

إن الإزعاج الرئيسي ، من وجهة نظري ، هو أنه إذا قمت بتعريف أحد الأشخاص (كما نفعل هنا) يجب عليك أيضًا تحديد أداة ضبط. [1] هذا هو الضجيج الزائد الذي يفسد الكود.

إزعاج آخر هو أنه لا يزال يتعين علينا تهيئة قيمتي x و y في init . (حسنا ، بالطبع يمكننا إضافتها باستخدام setattr () ولكن هذا هو أكثر رمز إضافي.)

ثالثًا ، على عكس المثال الذي يشبه جافا ، لا يمكن أن يقبل الحاصلون على المعلمات الأخرى. الآن أستطيع أن أسمعك تقول بالفعل ، حسنا ، إذا كان أخذ المعلمات فهو ليس غات! بالمعنى الرسمي ، هذا صحيح. ولكن من الناحية العملية ، لا يوجد سبب يجعلنا غير قادرين على جعل معلمة ذات سمة معينة - مثل x - وتعيين قيمتها لبعض المعلمات المحددة.

من الجميل أن نفعل شيئًا مثل:

e.x[a,b,c] = 10
e.x[d,e,f] = 20

فمثلا. أقرب ما يمكن أن نحصل عليه هو تجاوز المهمة بما يعني بعض الدلالات الخاصة:

e.x = [a,b,c,10]
e.x = [d,e,f,30]

وبالطبع ، تأكد من أن مضيفنا يعرف كيفية استخلاص القيم الثلاث الأولى كمفتاح لقاموس وتعيين قيمته إلى رقم أو شيء ما.

ولكن حتى لو فعلنا ذلك ، ما زلنا لا نستطيع دعمه مع خصائص لأنه لا توجد طريقة للحصول على القيمة لأننا لا نستطيع تمرير المعلمات على الإطلاق إلى الثرت. لذلك كان علينا أن نعيد كل شيء ، بإدخال عدم التماثل.

يتيح لنا أداة getter / setter على نمط جافا التعامل مع هذا ، ولكننا نعود إلى الحاجة إلى getter / setters.

في رأيي ، ما نريده حقًا هو شيء يستحوذ على المتطلبات التالية:

  • يحدد المستخدمون طريقة واحدة فقط لسمة معينة ويمكن أن يشيروا إلى ما إذا كانت السمة للقراءة فقط أم للقراءة والكتابة. تفشل الخصائص في هذا الاختبار إذا كانت السمة قابلة للكتابة.

  • ليست هناك حاجة للمستخدم لتحديد متغير إضافي وراء وظيفة ، لذلك نحن لا نحتاج إلى init أو setattr في التعليمات البرمجية. المتغير موجود فقط بحقيقة لقد أنشأنا هذه الخاصية المميزة الجديدة.

  • يتم تنفيذ أي رمز افتراضي للسمة في نص الأسلوب نفسه.

  • يمكننا تعيين السمة كسمة ورجوعها كسمات.

  • يمكننا معلمة السمة.

من حيث الكود ، نريد طريقة للكتابة:

def x(self, *args):
    return defaultX()

وتكون قادرًا على القيام بما يلي:

print e.x     -> The default at time T0
e.x = 1
print e.x     -> 1
e.x = None
print e.x     -> The default at time T1

وهكذا دواليك.

كما نرغب أيضًا في إجراء هذا الإجراء للحالة الخاصة لخاصية سمة parameterizable ، ولكن مع السماح لحالة التعيين الافتراضية بالعمل. سترى كيف تعاملت مع هذا أدناه.

الآن إلى النقطة (yay! النقطة!). الحل الذي توصلت إليه لهذا هو على النحو التالي.

نقوم بإنشاء كائن جديد ليحل محل مفهوم الملكية. الهدف من هذا العنصر هو تخزين قيمة المتغير الذي تم ضبطه عليه ، لكنه يحافظ أيضًا على مؤشر على الكود يعرف كيفية حساب افتراضي. وتتمثل مهمتها في تخزين القيمة المحددة أو تشغيل الطريقة إذا لم يتم تعيين هذه القيمة.

دعونا نسميها UberProperty.

class UberProperty(object):

    def __init__(self, method):
        self.method = method 
        self.value = None
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def clearValue(self):
        self.value = None
        self.isSet = False

أفترض أن الطريقة هنا هي طريقة الصف ، والقيمة هي قيمة UberProperty ، ولقد أضفت isSet لأن None قد يكون قيمة حقيقية وهذا يتيح لنا طريقة نظيفة للإعلان عن وجود "لا قيمة". طريقة أخرى هي الحارس من نوع ما.

هذا يعطينا في الأساس كائنًا يمكنه فعل ما نريد ، ولكن كيف نضعه بالفعل في صفنا؟ حسنا ، خصائص استخدام الديكورين. لماذا لا نستطيع ذلك؟ دعونا نرى كيف قد يبدو (من هنا سألتزم باستخدام "سمة" واحدة فقط ، x).

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

هذا لا يعمل في الواقع حتى الآن ، بالطبع. علينا تنفيذ uberProperty والتأكد من أنه يعالج كل من يحصل ومجموعات.

لنبدأ مع يحصل.

كانت المحاولة الأولى لي ببساطة إنشاء كائن UberProperty جديد وإعادته:

def uberProperty (f): return UberProperty (f)

اكتشفت بسرعة ، بالطبع ، أن هذا لا ينجح: بيثون لا تربط بين الكائن القابل للاستدعاء ، وأحتاج إلى الكائن من أجل استدعاء الوظيفة. حتى إن إنشاء الديكور في الفصل لا يعمل ، فبالرغم من أننا نمتلك الصف الآن ، إلا أننا لا نملك حتى الآن كائنًا للعمل معه.

لذلك نحن بحاجة إلى أن نكون قادرين على فعل المزيد هنا. نحن نعلم أن الطريقة لا تحتاج إلا إلى تمثيلها مرة واحدة ، لذلك دعونا نمضي قدمًا ونحافظ على ديكورنا ، لكن قم بتعديل UberProperty لتخزين فقط مرجع الأسلوب:

class UberProperty(object):

    def __init__(self, method):
        self.method = method 

هو أيضا غير قابل للاستدعاء ، لذلك في الوقت الحالي لا شيء يعمل.

كيف نكمل الصورة؟ حسنًا ، ماذا سننتهي عند إنشاء فئة المثال باستخدام مصممنا الجديد:

class Example(object):

    @uberProperty
    def x(self):
        return defaultX()

print Example.x     <__main__.UberProperty object at 0x10e1fb8d0>
print Example().x   <__main__.UberProperty object at 0x10e1fb8d0>

في كلتا الحالتين ، نعود إلى UberProperty وهو بالطبع غير قابل للاستدعاء ، لذلك هذا ليس كثير الاستخدام.

ما نحتاج إليه هو طريقة بديلة لربط مثيل UberProperty الذي تم إنشاؤه بواسطة الديكور بعد أن تم إنشاء الفئة إلى كائن من الصف قبل أن يتم إرجاع هذا الكائن إلى المستخدم للاستخدام. نعم ، نعم ، هذه دعوة أولية ، يا صاح.

دعونا نكتب ما نريد أن تكون نتيجة اكتشافنا أولاً. نحن ملزمون UberProperty على سبيل المثال ، لذلك شيء واضح للعودة سيكون BoundUberProperty. هذا هو المكان الذي سنحافظ فيه بالفعل على حالة السمة x.

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

الآن نحن التمثيل. كيف يمكن الحصول على هذه الأشياء؟ هناك عدة طرق ، ولكن أسهل طريقة لشرح يستخدم أسلوب init للقيام بهذا التعيين. وبحلول الوقت الذي يطلق عليه init يسمى لدينا الديكور ، ولكن فقط بحاجة للنظر من dict الكائن وتحديث أي سمات حيث قيمة السمة من نوع UberProperty.

الآن ، خصائص uber-cool باردة وسنريد استخدامها كثيرًا ، لذلك من المنطقي إنشاء فئة أساسية تقوم بذلك لكل الفئات الفرعية. أعتقد أنك تعرف ما سيتم استدعاء الطبقة الأساسية.

class UberObject(object):
    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)

نضيف هذا ، ونغير مثالنا في الوراثة من UberObject ، و ...

e = Example()
print e.x               -> <__main__.BoundUberProperty object at 0x104604c90>

بعد تعديل x ليكون:

@uberProperty
def x(self):
    return *datetime.datetime.now()*

يمكننا إجراء اختبار بسيط:

print e.x.getValue()
print e.x.getValue()
e.x.setValue(datetime.date(2013, 5, 31))
print e.x.getValue()
e.x.clearValue()
print e.x.getValue()

ونحصل على الناتج الذي أردناه:

2013-05-31 00:05:13.985813
2013-05-31 00:05:13.986290
2013-05-31
2013-05-31 00:05:13.986310

(جي ، أنا أعمل في وقت متأخر.)

لاحظ أنني استخدمت getValue و setValue و clearValue هنا. هذا لأنني لم أرتبط بعد بالوسائل لإعادتها تلقائيًا.

لكنني أعتقد أن هذا مكان جيد للتوقف الآن ، لأنني أشعر بالتعب. يمكنك أيضًا رؤية أن الوظيفة الأساسية التي نريدها موجودة ؛ والباقي هو نافذة خلع الملابس. ضمادة نافذة مهمة للاستخدام ، ولكن يمكن أن تنتظر حتى يكون لدي تغيير لتحديث المنشور.

سأنهي المثال التالي في النشر التالي عن طريق تناول هذه الأشياء:

  • نحتاج إلى التأكد من أن init الخاص بـ UberObject يتم استدعائه دائمًا بواسطة الفئات الفرعية.

    • لذلك إما أن نجبرها على أن تسمى في مكان ما أو نمنعها من التنفيذ.
    • سنرى كيف نفعل ذلك مع metaclass.
  • نحتاج إلى التأكد من أننا نتعامل مع الحالة الشائعة التي يكون فيها شخص ما "مستعارًا" لوظيفة إلى شيء آخر ، مثل:

      class Example(object):
          @uberProperty
          def x(self):
              ...
    
          y = x
    
  • نحتاج ex إلى إرجاع exgetValue () بشكل افتراضي.

    • ما سنراه في الواقع هو منطقة واحدة يفشل فيها النموذج.
    • اتضح أننا سنحتاج دائمًا إلى استخدام استدعاء دالة للحصول على القيمة.
    • ولكن يمكننا جعله يبدو وكأنه استدعاء دالة عادية وتجنب الاضطرار إلى استخدام exgetValue (). (فعل هذا واضح ، إذا لم تكن قد أصلحته بالفعل.)
  • نحتاج إلى دعم الإعداد المباشر ، كما هو الحال في ex = <newvalue> . يمكننا القيام بذلك في الصف الأصلي أيضًا ، ولكننا سنحتاج إلى تحديث شفرة init الخاصة بنا للتعامل معها.

  • أخيرًا ، سنقوم بإضافة سمات ذات معلمات. يجب أن يكون واضحًا كيف سنفعل هذا أيضًا.

إليك الشفرة كما هي موجودة الآن:

import datetime

class UberObject(object):
    def uberSetter(self, value):
        print 'setting'

    def uberGetter(self):
        return self

    def __init__(self):
        for k in dir(self):
            v = getattr(self, k)
            if isinstance(v, UberProperty):
                v = BoundUberProperty(self, v)
                setattr(self, k, v)


class UberProperty(object):
    def __init__(self, method):
        self.method = method

class BoundUberProperty(object):
    def __init__(self, obj, uberProperty):
        self.obj = obj
        self.uberProperty = uberProperty
        self.isSet = False

    def setValue(self, value):
        self.value = value
        self.isSet = True

    def getValue(self):
        return self.value if self.isSet else self.uberProperty.method(self.obj)

    def clearValue(self):
        del self.value
        self.isSet = False

    def uberProperty(f):
        return UberProperty(f)

class Example(UberObject):

    @uberProperty
    def x(self):
        return datetime.datetime.now()

آدم

[1] قد أكون خلف ما إذا كانت هذه هي الحالة.


أشعر أن الخصائص تدور حول السماح لك بالحصول على النفقات العامة للكتابة والحراس والمستأجرين فقط عندما تحتاج إليها بالفعل.

تنصح ثقافة البرمجة جافا بشدة بعدم منح حق الوصول إلى الخصائص ، وبدلاً من ذلك ، انتقل من خلال getters و setters ، وفقط تلك التي تحتاجها بالفعل. انها مطول قليلا لتكتب دائما هذه القطع الواضحة من التعليمات البرمجية ، وتلاحظ أن 70 ٪ من الوقت لا يتم استبدالها أبدا بمنطق غير عادي.

في بيثون ، يهتم الناس بالفعل بهذا النوع من النفقات العامة ، بحيث يمكنك تبني الممارسة التالية:

  • لا تستخدمي المستفيدين والمستوطنين في البداية ، إذا لم تكن هناك حاجة إليها
  • استخدم @property لتطبيقها بدون تغيير صيغة بقية الشفرة.

أنا أفضل استخدام لا في معظم الحالات. المشكلة مع الخصائص هي أنها تجعل الطبقة أقل شفافية. خاصة ، هذه مشكلة إذا كنت تثير استثناءً من أداة ضبط. على سبيل المثال ، إذا كان لديك حساب Account.email:

class Account(object):
    @property
    def email(self):
        return self._email

    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError('Invalid email address.')
        self._email = value

ثم لا يتوقع مستخدم الفئة أن تعيين قيمة للخاصية قد يتسبب في حدوث استثناء:

a = Account()
a.email = 'badaddress'
--> ValueError: Invalid email address.

نتيجة لذلك ، قد لا تتم معالجة الاستثناء ، ويمكن أن يتم نشره بشكل كبير في سلسلة المكالمات بحيث يتم التعامل معه بشكل صحيح ، أو ينتج عنه تتبع غير مفيد جدًا لمستخدم البرنامج (وهو أمر شائع جدًا في عالم python و java ).

وأود أيضا تجنب استخدام getters و setters:

  • لأن تحديدها لجميع العقارات مقدمًا هو مضيعة للوقت جدًا ،
  • يجعل مقدار التعليمات البرمجية أطول بدون داع ، مما يجعل فهم الشفرة وصيانتها أكثر صعوبة ،
  • إذا كنت تعرفهم عن الخصائص فقط حسب الحاجة ، فإن واجهة الفصل ستتغير ، مما سيؤذي جميع مستخدمي الفصل

بدلاً من الخصائص والممتلكات / المستأجرين ، أفضّل تنفيذ المنطق المعقد في أماكن محددة بشكل جيد مثل طريقة التحقق من الصحة:

class Account(object):
    ...
    def validate(self):
        if '@' not in self.email:
            raise ValueError('Invalid email address.')

أو طريقة Account.Safe مشابهة.

لاحظ أنني لا أحاول أن أقول أنه لا توجد حالات تكون فيها الخصائص مفيدة ، ولكن فقط قد تكون أفضل حالًا إذا كان بإمكانك جعل صفوفك بسيطة وشفافة بما يكفي بحيث لا تحتاج إليها.


أنا مندهش من أن لا أحد قد ذكر أن الخصائص مرتبطة بطبقة وصفية ، و يحصلون على هذه الفكرة بالضبط في وظائفهم - أن هم وظائف ويمكن استخدامها في:

  • التحقق من صحة
  • تغيير البيانات
  • نوع البط (نوع الإكراه إلى نوع آخر)

يقدم هذا طريقة ذكية لإخفاء تفاصيل التنفيذ ورمز السرعة مثل التعبير العادي ، أو النوع ، أو المحاولة .. باستثناء الكتل أو التأكيدات أو القيم المحسوبة.

بشكل عام ، قد يكون عمل CRUD على كائن ما أمرًا عاديًا في كثير من الأحيان ، ولكن يجب مراعاة أمثلة البيانات التي ستستمر في قاعدة بيانات علائقية. يمكن لـ ORM's إخفاء تفاصيل التنفيذ الخاصة بـ vernaculars SQL الخاصة في الطرق المنضمة إلى fget و fset و fdel المعرفة في فئة الخاصية التي ستدير الفظيعة إذا .. elif .. آخر السلالم القبيحة جدا في كود OO - تعريض البساطة و أنيقة self.variable = something وتجنب تفاصيل المطور باستخدام ORM.

إذا كان أحد يفكر في الخصائص فقط مثل بعض بقايا من عبودية و لغة الانضباط (أي جافا) فهي تفتقر إلى نقطة واصفات.


الإجابة المختصرة هي: خصائص الفوز بالأيادي . دائما.

هناك في بعض الأحيان حاجة إلى الحاصلين على الجوائز والمستأجرين ، لكن في تلك الفترة ، كنت سأخفيهم إلى العالم الخارجي. هناك الكثير من الطرق للقيام بذلك في بيثون ( getattr ، setattr ، __getattribute__ ، إلخ ...) ، ولكن هناك طريقة مختصرة ونظيفة للغاية:

def set_email(self, value):
    if '@' not in value:
        raise Exception("This doesn't look like an email address.")
    self._email = value

def get_email(self):
    return self._email

email = property(get_email, set_email)

إليكم مقالة مختصرة تطرح موضوع "الحاصلون" و "المستوطنون" في "بايثون".


في المشاريع المعقدة ، أفضل استخدام خصائص للقراءة فقط (أو الحصول عليها) مع وظيفة الواضحة الصريحة:

class MyClass(object):
...        
@property
def my_attr(self):
    ...

def set_my_attr(self, value):
    ...

في مشاريع الحياة الطويلة ، يستغرق تصحيح الأخطاء وإعادة البناء وقتًا أطول من كتابة الشفرة نفسها. هناك العديد من الجوانب السلبية لاستخدام @property.setter التي تجعل تصحيح الأخطاء أكثر صعوبة:

1) يسمح python لإنشاء سمات جديدة لكائن موجود. هذا يجعل من الصعب تتبع الخطأ التالي:

my_object.my_atttr = 4.

إذا كان الكائن الخاص بك هو خوارزمية معقدة ، فستقضي بعض الوقت في محاولة معرفة سبب عدم تلاقيها (لاحظ "t" إضافية في السطر أعلاه)

2) قد تتطور أداة الإعداد أحيانًا إلى طريقة معقدة وبطيئة (مثل ضرب قاعدة بيانات). سيكون من الصعب على مطور آخر معرفة السبب في أن الوظيفة التالية بطيئة للغاية. قد يقضي الكثير من الوقت في تحديد طريقة do_something() :

def slow_function(my_object):
    my_object.my_attr = 4.
    my_object.do_something()


تفضل الخصائص . هذا ما هم هناك من أجله

السبب هو أن جميع السمات عامة في بايثون. إن بدء الأسماء التي تحتوي على تسطير أسفل السطر أو اثنين هو مجرد تحذير بأن السمة المحددة هي تفاصيل تنفيذ قد لا تبقى كما هي في الإصدارات المستقبلية من التعليمة البرمجية. لا يمنعك من الحصول على أو تحديد تلك الخاصية. لذلك ، يعد الوصول إلى السمة القياسية هو الطريقة العادية ، أو Pythonic ، أو الوصول إلى السمات.

تكمن ميزة الخصائص في أنها متماثلة في بناء الجملة مع إمكانية الوصول إلى السمة ، بحيث يمكنك التغيير من واحد إلى آخر دون أي تغييرات على رمز العميل. يمكنك حتى الحصول على إصدار واحد من الفئة التي تستخدم خصائص (على سبيل المثال ، للتعاقب عن طريق الكود أو التصحيح) وواحدة لا تستخدم للإنتاج ، دون تغيير الشفرة التي تستخدمها. في الوقت نفسه ، لا يتعين عليك كتابة getters والمستأجرين لكل شيء فقط في حالة قد تحتاج إلى التحكم في الوصول بشكل أفضل في وقت لاحق.





getter-setter