Apa tujuan kelas dalam python?


98

Kelas dalam / bersarang Python membingungkan saya. Apakah ada sesuatu yang tidak dapat dicapai tanpa mereka? Jika ya, benda apakah itu?

Jawaban:


86

Dikutip dari http://www.geekinterview.com/question_details/64739 :

Keuntungan dari kelas dalam:

  • Pengelompokan logis kelas : Jika kelas hanya berguna untuk satu kelas lain maka logis untuk menyematkannya di kelas itu dan menjaga keduanya bersama-sama. Menyusun "kelas pembantu" seperti itu membuat paket mereka lebih efisien.
  • Enkapsulasi yang ditingkatkan : Pertimbangkan dua kelas A dan B tingkat atas di mana B membutuhkan akses ke anggota A yang dinyatakan sebagai pribadi. Dengan menyembunyikan kelas B di dalam kelas AA, anggota dapat dideklarasikan sebagai pribadi dan B dapat mengaksesnya. Selain itu B sendiri bisa disembunyikan dari dunia luar.
  • Kode yang lebih mudah dibaca dan dapat dipelihara : Menumpuk kelas kecil dalam kelas tingkat atas akan menempatkan kode lebih dekat ke tempatnya digunakan.

Keuntungan utama adalah organisasi. Apa pun yang bisa dicapai dengan kelas batin bisa dicapai tanpanya.


50
Argumen enkapsulasi tentu saja tidak berlaku untuk Python.
bobince

31
Poin pertama juga tidak berlaku untuk Python. Anda dapat mendefinisikan sebanyak mungkin kelas dalam satu file modul sesuka Anda, sehingga menjaga mereka tetap bersama dan organisasi paket juga tidak terpengaruh. Poin terakhir sangat subjektif dan saya tidak percaya itu valid. Singkatnya, saya tidak menemukan argumen yang mendukung penggunaan kelas dalam dengan Python dalam jawaban ini.
Chris Arndt

17
Namun demikian, ini ADALAH alasan mengapa kelas dalam digunakan dalam pemrograman. Anda hanya mencoba menjatuhkan jawaban yang bersaing. Jawaban yang diberikan pria ini di sini kuat.
Inversus

16
@Inversus: Saya tidak setuju. Ini bukan jawaban, ini adalah kutipan tambahan dari jawaban orang lain tentang bahasa yang berbeda (yaitu Jawa). Tidak memilih dan saya berharap orang lain melakukan hal yang sama.
Kevin

4
Saya setuju dengan jawaban ini dan tidak setuju dengan keberatan. Meskipun kelas bersarang bukanlah kelas dalam Java, kelas tersebut berguna. Tujuan dari kelas bertingkat adalah organisasi. Secara efektif, Anda meletakkan satu kelas di bawah namespace yang lain. Ketika masuk akal untuk melakukannya, ini adalah Pythonic: "Namespaces adalah salah satu ide bagus yang membunyikan klakson - mari kita lakukan lebih banyak lagi!". Misalnya, pertimbangkan DataLoaderkelas yang bisa mengeluarkan CacheMisspengecualian. Menumpuk pengecualian di bawah kelas utama sebagai DataLoader.CacheMisssarana Anda dapat mengimpor saja DataLoadertetapi masih menggunakan pengecualian.
cbarrick

50

Apakah ada sesuatu yang tidak dapat dicapai tanpa mereka?

Tidak. Mereka benar-benar setara dengan mendefinisikan kelas secara normal di tingkat atas, dan kemudian menyalin referensi ke kelas tersebut ke kelas luar.

Saya tidak berpikir ada alasan khusus mengapa kelas bersarang 'diizinkan', selain itu tidak masuk akal untuk secara eksplisit 'melarang' mereka juga.

Jika Anda mencari kelas yang ada dalam siklus hidup objek luar / pemilik, dan selalu memiliki referensi ke instance kelas luar - kelas dalam seperti yang dilakukan Java - maka kelas bertingkat Python bukanlah hal itu. Tetapi Anda dapat meretas sesuatu seperti itu:

import weakref, new

class innerclass(object):
    """Descriptor for making inner classes.

    Adds a property 'owner' to the inner class, pointing to the outer
    owner instance.
    """

    # Use a weakref dict to memoise previous results so that
    # instance.Inner() always returns the same inner classobj.
    #
    def __init__(self, inner):
        self.inner= inner
        self.instances= weakref.WeakKeyDictionary()

    # Not thread-safe - consider adding a lock.
    #
    def __get__(self, instance, _):
        if instance is None:
            return self.inner
        if instance not in self.instances:
            self.instances[instance]= new.classobj(
                self.inner.__name__, (self.inner,), {'owner': instance}
            )
        return self.instances[instance]


# Using an inner class
#
class Outer(object):
    @innerclass
    class Inner(object):
        def __repr__(self):
            return '<%s.%s inner object of %r>' % (
                self.owner.__class__.__name__,
                self.__class__.__name__,
                self.owner
            )

>>> o1= Outer()
>>> o2= Outer()
>>> i1= o1.Inner()
>>> i1
<Outer.Inner inner object of <__main__.Outer object at 0x7fb2cd62de90>>
>>> isinstance(i1, Outer.Inner)
True
>>> isinstance(i1, o1.Inner)
True
>>> isinstance(i1, o2.Inner)
False

(Ini menggunakan dekorator kelas, yang baru di Python 2.6 dan 3.0. Jika tidak, Anda harus mengatakan "Inner = innerclass (Inner)" setelah definisi kelas.)


5
Penggunaan-kasus yang panggilan untuk yang (yaitu Jawa-esque kelas batin, yang contoh memiliki hubungan dengan contoh dari kelas luar) biasanya dapat diatasi dengan Python dengan mendefinisikan kelas batin dalam metode dari kelas luar - mereka akan melihat outer selftanpa pekerjaan tambahan yang diperlukan (cukup gunakan pengenal berbeda di mana Anda biasanya meletakkan inner self; like innerself), dan akan dapat mengakses instance luar melalui itu.
Evgeni Sergeev

Menggunakan a WeakKeyDictionarydalam contoh ini sebenarnya tidak mengizinkan kunci untuk dikumpulkan sampahnya, karena nilainya sangat mereferensikan kunci masing-masing melalui owneratributnya.
Kritzefitz

36

Ada sesuatu yang Anda butuhkan untuk membungkus kepala Anda agar dapat memahami ini. Dalam kebanyakan bahasa, definisi kelas adalah arahan untuk kompilator. Artinya, kelas dibuat sebelum program dijalankan. Dalam python, semua pernyataan dapat dieksekusi. Artinya pernyataan ini:

class foo(object):
    pass

adalah pernyataan yang dieksekusi saat runtime seperti ini:

x = y + z

Ini berarti bahwa Anda tidak hanya dapat membuat kelas di dalam kelas lain, Anda juga dapat membuat kelas di mana pun Anda mau. Pertimbangkan kode ini:

def foo():
    class bar(object):
        ...
    z = bar()

Jadi, gagasan tentang "kelas dalam" sebenarnya bukanlah konstruksi bahasa; itu konstruksi programmer. Guido memiliki ringkasan yang sangat bagus tentang bagaimana hal ini bisa terjadi di sini . Tetapi pada dasarnya, ide dasarnya adalah ini menyederhanakan tata bahasa bahasa.


16

Kelas bersarang di dalam kelas:

  • Kelas bertingkat membengkak definisi kelas sehingga lebih sulit untuk melihat apa yang sedang terjadi.

  • Kelas bersarang dapat membuat penggandengan yang akan mempersulit pengujian.

  • Dengan Python Anda dapat meletakkan lebih dari satu kelas dalam sebuah file / modul, tidak seperti Java, sehingga kelas tersebut masih dekat dengan kelas tingkat atas dan bahkan dapat memiliki nama kelas yang diawali dengan "_" untuk membantu menandakan bahwa kelas lain tidak boleh menggunakannya.

Tempat kelas bersarang terbukti berguna adalah di dalam fungsi

def some_func(a, b, c):
   class SomeClass(a):
      def some_method(self):
         return b
   SomeClass.__doc__ = c
   return SomeClass

Kelas menangkap nilai dari fungsi yang memungkinkan Anda membuat kelas secara dinamis seperti metaprogramming template di C ++


7

Saya memahami argumen terhadap kelas bersarang, tetapi ada alasan untuk menggunakannya dalam beberapa kesempatan. Bayangkan saya membuat kelas daftar tertaut ganda, dan saya perlu membuat kelas node untuk memelihara node. Saya memiliki dua pilihan, membuat kelas Node di dalam kelas DoublyLinkedList, atau membuat kelas Node di luar kelas DoublyLinkedList. Saya lebih suka pilihan pertama dalam kasus ini, karena kelas Node hanya berarti di dalam kelas DoublyLinkedList. Meskipun tidak ada manfaat penyembunyian / enkapsulasi, ada manfaat pengelompokan karena dapat mengatakan bahwa kelas Node adalah bagian dari kelas DoublyLinkedList.


5
Ini benar dengan asumsi Nodekelas yang sama tidak berguna untuk jenis kelas daftar tertaut lain yang mungkin juga Anda buat, dalam hal ini kelas tersebut mungkin hanya berada di luar.
Acumenus

Cara lain untuk mengatakannya: Nodeberada di bawah namespace dari DoublyLinkedList, dan masuk akal untuk begitu. Ini adalah Pythonic: "Namespaces adalah salah satu ide bagus yang membunyikan klakson - mari kita lakukan lebih banyak lagi!"
cbarrick

@cbarrick: Melakukan "lebih dari itu" tidak berarti apa-apa tentang membuat mereka bersarang.
Ethan Furman

4

Apakah ada sesuatu yang tidak dapat dicapai tanpa mereka? Jika ya, benda apakah itu?

Ada sesuatu yang tidak dapat dengan mudah dilakukan tanpanya : warisan kelas terkait .

Berikut adalah contoh minimalis dengan kelas terkait Adan B:

class A(object):
    class B(object):
        def __init__(self, parent):
            self.parent = parent

    def make_B(self):
        return self.B(self)


class AA(A):  # Inheritance
    class B(A.B):  # Inheritance, same class name
        pass

Kode ini mengarah pada perilaku yang cukup masuk akal dan dapat diprediksi:

>>> type(A().make_B())
<class '__main__.A.B'>
>>> type(A().make_B().parent)
<class '__main__.A'>
>>> type(AA().make_B())
<class '__main__.AA.B'>
>>> type(AA().make_B().parent)
<class '__main__.AA'>

Jika Badalah kelas tingkat atas, Anda tidak dapat menulis self.B()di metode make_Btetapi hanya akan menulis B(), dan dengan demikian kehilangan pengikatan dinamis ke kelas yang memadai.

Perhatikan bahwa dalam konstruksi ini, Anda tidak boleh merujuk ke kelas Adi badan kelas B. Inilah motivasi untuk memperkenalkan parentatribut di kelas B.

Tentu saja, pengikatan dinamis ini dapat dibuat ulang tanpa kelas dalam dengan biaya instrumentasi kelas yang membosankan dan rawan kesalahan.


1

Kasus penggunaan utama yang saya gunakan untuk ini adalah untuk mencegah proliferasi modul kecil dan untuk mencegah polusi namespace ketika modul terpisah tidak diperlukan. Jika saya memperluas kelas yang sudah ada, tetapi kelas yang sudah ada itu harus mereferensikan subkelas lain yang harus selalu digabungkan dengannya. Misalnya, saya mungkin memiliki utils.pymodul yang memiliki banyak kelas pembantu di dalamnya, yang belum tentu digabungkan bersama, tetapi saya ingin memperkuat kopling untuk beberapa kelas pembantu tersebut. Misalnya, ketika saya menerapkan https://stackoverflow.com/a/8274307/2718295

: utils.py:

import json, decimal

class Helper1(object):
    pass

class Helper2(object):
    pass

# Here is the notorious JSONEncoder extension to serialize Decimals to JSON floats
class DecimalJSONEncoder(json.JSONEncoder):

    class _repr_decimal(float): # Because float.__repr__ cannot be monkey patched
        def __init__(self, obj):
            self._obj = obj
        def __repr__(self):
            return '{:f}'.format(self._obj)

    def default(self, obj): # override JSONEncoder.default
        if isinstance(obj, decimal.Decimal):
            return self._repr_decimal(obj)
        # else
        super(self.__class__, self).default(obj)
        # could also have inherited from object and used return json.JSONEncoder.default(self, obj) 

Lalu kita bisa:

>>> from utils import DecimalJSONEncoder
>>> import json, decimal
>>> json.dumps({'key1': decimal.Decimal('1.12345678901234'), 
... 'key2':'strKey2Value'}, cls=DecimalJSONEncoder)
{"key2": "key2_value", "key_1": 1.12345678901234}

Tentu saja, kita bisa menghindari pewarisan json.JSONEnocdersama sekali dan hanya mengganti default ():

:

import decimal, json

class Helper1(object):
    pass

def json_encoder_decimal(obj):
    class _repr_decimal(float):
        ...

    if isinstance(obj, decimal.Decimal):
        return _repr_decimal(obj)

    return json.JSONEncoder(obj)


>>> json.dumps({'key1': decimal.Decimal('1.12345678901234')}, default=json_decimal_encoder)
'{"key1": 1.12345678901234}'

Tetapi terkadang hanya untuk konvensi, Anda ingin utilsterdiri dari kelas untuk ekstensibilitas.

Berikut kasus penggunaan lain: Saya ingin pabrik untuk mutable di OuterClass saya tanpa harus memanggil copy:

class OuterClass(object):

    class DTemplate(dict):
        def __init__(self):
            self.update({'key1': [1,2,3],
                'key2': {'subkey': [4,5,6]})


    def __init__(self):
        self.outerclass_dict = {
            'outerkey1': self.DTemplate(),
            'outerkey2': self.DTemplate()}



obj = OuterClass()
obj.outerclass_dict['outerkey1']['key2']['subkey'].append(4)
assert obj.outerclass_dict['outerkey2']['key2']['subkey'] == [4,5,6]

Saya lebih suka pola ini daripada @staticmethoddekorator yang seharusnya Anda gunakan untuk fungsi pabrik.


1

1. Dua cara yang secara fungsional setara

Dua cara yang ditunjukkan sebelumnya secara fungsional identik. Namun, ada beberapa perbedaan halus, dan ada situasi ketika Anda ingin memilih satu sama lain.

Cara 1: Definisi kelas bersarang
(= "Kelas bersarang")

class MyOuter1:
    class Inner:
        def show(self, msg):
            print(msg)

Cara 2: Dengan level modul Kelas dalam yang dilampirkan ke Kelas luar
(= "Kelas dalam yang direferensikan")

class _InnerClass:
    def show(self, msg):
        print(msg)

class MyOuter2:
    Inner = _InnerClass

Garis bawah digunakan untuk mengikuti PEP8 "antarmuka internal (paket, modul, kelas, fungsi, atribut, atau nama lain) harus - diawali dengan satu garis bawah."

2. Persamaan

Potongan kode di bawah ini menunjukkan kesamaan fungsional dari "Kelas bersarang" vs "Kelas bagian dalam yang direferensikan"; Mereka akan berperilaku dengan cara yang sama dalam memeriksa kode untuk jenis instance kelas dalam. Tak perlu dikatakan, itu m.inner.anymethod()akan berperilaku serupa dengan m1danm2

m1 = MyOuter1()
m2 = MyOuter2()

innercls1 = getattr(m1, 'Inner', None)
innercls2 = getattr(m2, 'Inner', None)

isinstance(innercls1(), MyOuter1.Inner)
# True

isinstance(innercls2(), MyOuter2.Inner)
# True

type(innercls1()) == mypackage.outer1.MyOuter1.Inner
# True (when part of mypackage)

type(innercls2()) == mypackage.outer2.MyOuter2.Inner
# True (when part of mypackage)

3. Perbedaan

Perbedaan dari "Kelas bersarang" dan "Kelas dalam yang direferensikan" tercantum di bawah ini. Mereka tidak besar, tetapi terkadang Anda ingin memilih satu atau yang lain berdasarkan ini.

3.1 Enkapsulasi Kode

Dengan "Kelas bersarang" dimungkinkan untuk merangkum kode lebih baik daripada dengan "Kelas dalam yang direferensikan". Kelas dalam namespace modul adalah variabel global . Tujuan dari kelas bersarang adalah untuk mengurangi kekacauan dalam modul dan meletakkan kelas dalam di dalam kelas luar.

Meskipun tidak ada * yang menggunakan from packagename import *, jumlah variabel level modul yang rendah bisa jadi bagus misalnya saat menggunakan IDE dengan penyelesaian kode / intellisense.

* Benar?

3.2 Keterbacaan kode

Dokumentasi Django menginstruksikan untuk menggunakan Meta kelas dalam untuk metadata model. Sedikit lebih jelas * untuk menginstruksikan pengguna framework untuk menulis class Foo(models.Model)dengan inner class Meta;

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

alih-alih "tulis a class _Meta, lalu tulis a class Foo(models.Model)dengan Meta = _Meta";

class _Meta:
    ordering = ["horn_length"]
    verbose_name_plural = "oxen"

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • Dengan pendekatan "Kelas bersarang", kode dapat dibaca sebagai daftar poin bertingkat , tetapi dengan metode "Kelas dalam yang direferensikan" seseorang harus menggulir kembali ke atas untuk melihat definisi _Metauntuk melihat "item anak" (atribut) -nya.

  • Metode "Kelas dalam yang direferensikan" bisa lebih mudah dibaca jika level bertingkat kode Anda bertambah atau barisnya panjang karena beberapa alasan lain.

* Tentu saja, soal selera

3.3 Pesan kesalahan yang sedikit berbeda

Ini bukan masalah besar, tetapi hanya untuk kelengkapan: Saat mengakses atribut non-existing untuk kelas dalam, kami melihat pengecualian yang sedikit berbeda. Melanjutkan contoh yang diberikan di Bagian 2:

innercls1.foo()
# AttributeError: type object 'Inner' has no attribute 'foo'

innercls2.foo()
# AttributeError: type object '_InnerClass' has no attribute 'foo'

Ini karena types dari kelas dalam adalah

type(innercls1())
#mypackage.outer1.MyOuter1.Inner

type(innercls2())
#mypackage.outer2._InnerClass

0

Saya telah menggunakan kelas dalam Python untuk membuat subclass sengaja buggy dalam fungsi unittest (yaitu di dalam def test_something():) untuk mendekati cakupan pengujian 100% (misalnya menguji pernyataan logging yang sangat jarang dipicu dengan menimpa beberapa metode).

Dalam retrospeksi, ini mirip dengan jawaban Ed https://stackoverflow.com/a/722036/1101109

Kelas dalam tersebut harus keluar dari ruang lingkup dan siap untuk pengumpulan sampah setelah semua referensi ke kelas tersebut telah dihapus. Misalnya, ambil inner.pyfile berikut :

class A(object):
    pass

def scope():
    class Buggy(A):
        """Do tests or something"""
    assert isinstance(Buggy(), A)

Saya mendapatkan hasil penasaran berikut di bawah OSX Python 2.7.6:

>>> from inner import A, scope
>>> A.__subclasses__()
[]
>>> scope()
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A, scope
>>> from inner import A
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A
>>> import gc
>>> gc.collect()
0
>>> gc.collect()  # Yes I needed to call the gc twice, seems reproducible
3
>>> from inner import A
>>> A.__subclasses__()
[]

Petunjuk - Jangan melanjutkan dan mencoba melakukan ini dengan model Django, yang tampaknya menyimpan referensi lain (disimpan dalam cache?) Ke kelas buggy saya.

Jadi secara umum, saya tidak akan merekomendasikan menggunakan kelas dalam untuk tujuan semacam ini kecuali Anda benar-benar menghargai cakupan tes 100% dan tidak dapat menggunakan metode lain. Meskipun saya pikir bagus untuk menyadari bahwa jika Anda menggunakan __subclasses__(), kadang - kadang bisa tercemar oleh kelas dalam. Either way jika Anda mengikuti sejauh ini, saya pikir kita cukup jauh ke dalam Python pada saat ini, dunderscores pribadi dan semuanya.


3
Bukankah ini semua tentang subclass dan bukan kelas dalam ?? A
klaas

Dalam Above casse Buggy mewarisi dari A. Jadi sub kelas menunjukkan itu. Juga lihat built-in function issubclass ()
klaas

Terima kasih @klaas, saya pikir itu bisa dibuat lebih jelas bahwa saya hanya menggunakan .__subclasses__()untuk memahami bagaimana kelas dalam berinteraksi dengan pengumpul sampah ketika ada hal-hal yang keluar dari ruang lingkup dengan Python. Itu secara visual tampaknya mendominasi pos sehingga 1-3 paragraf pertama layak mendapatkan lebih banyak perluasan.
pzrq
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.