Temiz Kodun Sağlanması: Python'a Bir Bakış, Parametreli
Yayınlanan: 2022-03-11Bu yazıda, temiz, Pythonic kod üretmede en önemli teknik veya model olarak düşündüğüm şeyden, yani parametreleştirmeden bahsedeceğim. Bu gönderi sizin için, eğer:
- Tüm tasarım kalıpları konusunda nispeten yenisiniz ve belki de uzun kalıp adları ve sınıf diyagramları listeleri tarafından biraz şaşkına dönmüşsünüz. İyi haber şu ki, Python için kesinlikle bilmeniz gereken gerçekten tek bir tasarım modeli var. Daha da iyisi, muhtemelen zaten biliyorsunuzdur, ancak belki de uygulanabileceği tüm yollar değil.
- Python'a Java veya C# gibi başka bir OOP dilinden geldiniz ve tasarım kalıpları bilginizi o dilden Python'a nasıl çevireceğinizi bilmek istiyorsunuz. Python ve diğer dinamik olarak yazılan dillerde, statik olarak yazılan OOP dillerinde yaygın olan birçok kalıp, yazar Peter Norvig'in belirttiği gibi "görünmez veya daha basittir".
Bu makalede, "parametreleştirme" uygulamasını ve bunun bağımlılık enjeksiyonu , strateji , şablon yöntemi , soyut fabrika , fabrika yöntemi ve dekoratör olarak bilinen ana tasarım kalıplarıyla nasıl ilişkili olabileceğini keşfedeceğiz. Python'da bunların birçoğunun basit olduğu ortaya çıkıyor veya Python'daki parametrelerin çağrılabilir nesneler veya sınıflar olabileceği gerçeğiyle gereksiz hale geliyor.
Parametreleştirme, kodu genelleştirmek için bir işlev veya yöntem içinde tanımlanan değerleri veya nesneleri alma ve bunları o işleve veya yönteme parametre yapma işlemidir. Bu işlem aynı zamanda "özüt parametresi" yeniden düzenleme olarak da bilinir. Bir bakıma, bu makale tasarım kalıpları ve yeniden düzenleme hakkındadır.
Python Parametreli En Basit Durum
Örneklerimizin çoğunda, bazı grafikler yapmak için eğitici standart kitaplık kaplumbağa modülünü kullanacağız.
turtle
kullanarak 100x100 kare çizecek bazı kodlar:
from turtle import Turtle turtle = Turtle() for i in range(0, 4): turtle.forward(100) turtle.left(90)
Şimdi farklı boyutta bir kare çizmek istediğimizi varsayalım. Bu noktada çok genç bir programcı, bu bloğu kopyalayıp yapıştırmak ve değiştirmek için cazip olacaktır. Açıkçası, çok daha iyi bir yöntem, önce kare çizim kodunu bir fonksiyona çıkarmak ve ardından karenin boyutunu bu fonksiyona bir parametre yapmak olacaktır:
def draw_square(size): for i in range(0, 4): turtle.forward(size) turtle.left(90) draw_square(100)
Artık draw_square
kullanarak herhangi bir boyutta kareler çizebiliriz. Parametrelendirmenin temel tekniğinin hepsi bu kadar ve biz sadece ilk ana kullanımı gördük - kopyala-yapıştır programlamayı ortadan kaldırdık.
Yukarıdaki kodla ilgili acil bir sorun, draw_square
global bir değişkene bağlı olmasıdır. Bunun birçok kötü sonucu vardır ve bunu düzeltmenin iki kolay yolu vardır. İlki, draw_square
Turtle
örneğinin kendisini yaratması olacaktır (bunu daha sonra tartışacağım). Tüm çizimlerimiz için tek bir Turtle
kullanmak istiyorsak bu istenmeyebilir. Şimdilik, turtle
draw_square
parametresine dönüştürmek için tekrar parametreleştirmeyi kullanacağız:
from turtle import Turtle def draw_square(turtle, size): for i in range(0, 4): turtle.forward(size) turtle.left(90) turtle = Turtle() draw_square(turtle, 100)
Bunun süslü bir adı var - bağımlılık enjeksiyonu. Bu sadece, bir fonksiyonun işini yapmak için bir tür nesneye ihtiyacı varsa, draw_square
bir Turtle
ihtiyacı olması gibi, çağıranın bu nesneyi bir parametre olarak iletmekten sorumlu olduğu anlamına gelir. Hayır, gerçekten, Python bağımlılık enjeksiyonunu merak ettiyseniz, işte bu.
Şimdiye kadar, iki çok temel kullanımla ilgilendik. Bu makalenin geri kalanı için temel gözlem, Python'da parametre haline gelebilecek çok çeşitli şeylerin - diğer bazı dillerden daha fazla - olduğu ve bu onu çok güçlü bir teknik haline getiriyor.
Nesne Olan Her Şey
Python'da, nesne olan her şeyi parametreleştirmek için bu tekniği kullanabilirsiniz ve Python'da karşılaştığınız çoğu şey aslında nesnelerdir. Bu içerir:
-
"I'm a string"
ve42
tamsayı veya sözlük gibi yerleşik türlerin örnekleri - Diğer tür ve sınıfların örnekleri, örneğin bir
datetime.datetime
nesnesi - Fonksiyonlar ve yöntemler
- Yerleşik türler ve özel sınıflar
Son ikisi, özellikle başka dillerden geliyorsanız ve biraz daha tartışmaya ihtiyaç duyuyorsanız, en şaşırtıcı olanlardır.
Parametre Olarak İşlevler
Python'daki işlev ifadesi iki şey yapar:
- Bir fonksiyon nesnesi oluşturur.
- Yerel kapsamda o nesneye işaret eden bir ad oluşturur.
Bu nesnelerle bir REPL'de oynayabiliriz:
> >> def foo(): ... return "Hello from foo" > >> > >> foo() 'Hello from foo' > >> print(foo) <function foo at 0x7fc233d706a8> > >> type(foo) <class 'function'> > >> foo.name 'foo'
Ve tüm nesneler gibi, diğer değişkenlere de işlevler atayabiliriz:
> >> bar = foo > >> bar() 'Hello from foo'
bar
aynı nesne için başka bir ad olduğunu unutmayın, bu nedenle öncekiyle aynı dahili __name__
özelliğine sahiptir:
> >> bar.name 'foo' > >> bar <function foo at 0x7fc233d706a8>
Ancak can alıcı nokta, işlevler yalnızca nesneler olduğu için, bir işlevin kullanıldığını gördüğünüz her yerde bir parametre olabilir.
Yani, kare çizme işlevimizi yukarıya genişlettiğimizi ve şimdi bazen kareler çizdiğimizde her köşede duraklatmak istediğimizi varsayalım - time.sleep()
çağrısı.
Ama bazen durmak istemediğimizi varsayalım. Bunu başarmanın en basit yolu, belki de varsayılan olarak duraklamamamız için varsayılan olarak sıfır olan bir pause
parametresi eklemek olacaktır.
Ancak daha sonra bazen aslında virajlarda tamamen farklı bir şey yapmak istediğimizi keşfediyoruz. Belki her köşeye başka bir şekil çizmek, kalem rengini değiştirmek vb. istiyoruz. Yapmamız gereken her şey için bir tane daha parametre eklemek bizi cezbedebilir. Ancak, yapılacak işlem olarak herhangi bir işlevin iletilmesine izin vermek çok daha iyi bir çözüm olacaktır. Varsayılan olarak, hiçbir şey yapmayan bir işlev yapacağız. Ayrıca, gerekli olmaları durumunda bu işlevin yerel turtle
ve size
parametrelerini kabul etmesini sağlayacağız:
def do_nothing(turtle, size): pass def draw_square(turtle, size, at_corner=do_nothing): for i in range(0, 4): turtle.forward(size) at_corner(turtle, size) turtle.left(90) def pause(turtle, size): time.sleep(5) turtle = Turtle() draw_square(turtle, 100, at_corner=pause)
Veya, her köşede yinelemeli olarak daha küçük kareler çizmek gibi biraz daha havalı bir şey yapabiliriz:
def smaller_square(turtle, size): if size < 10: return draw_square(turtle, size / 2, at_corner=smaller_square) draw_square(turtle, 128, at_corner=smaller_square)
Bunun elbette varyasyonları var. Birçok örnekte, işlevin dönüş değeri kullanılacaktır. Burada daha zorunlu bir programlama stilimiz var ve fonksiyon sadece yan etkileri için çağrılıyor.
Diğer Dillerde…
Python'da birinci sınıf işlevlere sahip olmak bunu çok kolaylaştırır. Bunları içermeyen dillerde veya parametreler için tür imzaları gerektiren bazı statik olarak yazılmış dillerde bu daha zor olabilir. Birinci sınıf fonksiyonlarımız olmasaydı bunu nasıl yapardık?
Bir çözüm, draw_square
bir sınıfa dönüştürmek olabilir, SquareDrawer
:
class SquareDrawer: def __init__(self, size): self.size = size def draw(self, t): for i in range(0, 4): t.forward(self.size) self.at_corner(t, size) t.left(90) def at_corner(self, t, size): pass
Şimdi SquareDrawer
alt sınıflara ayırabilir ve ihtiyacımız olanı yapan bir at_corner
yöntemi ekleyebiliriz. Bu piton kalıbı, şablon yöntem kalıbı olarak bilinir; bir temel sınıf, tüm işlemin veya algoritmanın şeklini tanımlar ve işlemin değişken kısımları, alt sınıflar tarafından uygulanması gereken yöntemlere yerleştirilir.
Bu bazen Python'da yardımcı olabilirken, değişken kodunu basitçe parametre olarak geçirilen bir fonksiyona çekmek genellikle çok daha basit olacaktır.
Birinci sınıf işlevleri olmayan dillerde bu soruna yaklaşmanın ikinci bir yolu, işlevlerimizi sınıfların içinde yöntemler olarak şu şekilde özetlemektir:
class DoNothing: def run(self, turtle, size): pass def draw_square(turtle, size, at_corner=DoNothing()): for i in range(0, 4): turtle.forward(size) at_corner.run(turtle, size) t.left(90) class Pauser: def run(self, turtle, size): time.sleep(5) draw_square(turtle, 100, at_corner=Pauser())
Bu strateji modeli olarak bilinir. Yine, bu kesinlikle Python'da kullanmak için geçerli bir modeldir, özellikle de strateji sınıfı gerçekten tek bir işlev yerine bir dizi ilişkili işlev içeriyorsa. Ancak, çoğu zaman gerçekten ihtiyacımız olan tek şey bir fonksiyondur ve sınıf yazmayı bırakabiliriz.
Diğer Çağrılabilirler
Yukarıdaki örneklerde, fonksiyonları diğer fonksiyonlara parametre olarak geçirmekten bahsetmiştim. Ancak, yazdığım her şey aslında çağrılabilir herhangi bir nesne için doğruydu. İşlevler en basit örnektir, ancak yöntemleri de dikkate alabiliriz.
Bir listemiz olduğunu varsayalım foo
:
foo = [1, 2, 3]
foo
artık .append()
ve .count()
gibi kendisine bağlı bir sürü yönteme sahiptir. Bu "bağlı yöntemler", işlevler gibi aktarılabilir ve kullanılabilir:
> >> appendtofoo = foo.append > >> appendtofoo(4) > >> foo [1, 2, 3, 4]
Bu örnek yöntemlerine ek olarak, başka çağrılabilir nesne türleri de vardır: staticmethods
ve classmethods
, __call__
uygulayan sınıf örnekleri ve sınıflar/türlerin kendileri.

Parametre Olarak Sınıflar
Python'da sınıflar “birinci sınıftır”—onlar tıpkı dikteler, dizgiler vb. gibi çalışma zamanı nesneleridir. Bu, işlevlerin nesne olmasından daha garip görünebilir, ama neyse ki, bu gerçeği göstermek işlevlerden daha kolaydır.
Aşina olduğunuz sınıf ifadesi, sınıf oluşturmanın güzel bir yoludur, ancak tek yol bu değildir; ayrıca türün üç bağımsız değişken sürümünü de kullanabiliriz. Aşağıdaki iki ifade tam olarak aynı şeyi yapar:
class Foo: pass Foo = type('Foo', (), {})
İkinci versiyonda, az önce yaptığımız iki şeye dikkat edin (ki bunlar class deyimi kullanılarak daha rahat yapılır):
- Eşittir işaretinin sağ tarafında, dahili adı
Foo
olan yeni bir sınıf oluşturduk.Foo.__name__
yaparsanız geri alacağınız isim budur. - Atama ile, şimdiki kapsamda, az önce yarattığımız sınıf nesnesine atıfta bulunan Foo adında bir ad oluşturduk.
Aynı gözlemleri function ifadesinin yaptığı şey için de yaptık.
Buradaki temel fikir, sınıfların adları atanabilen (yani bir değişkene konulabilen) nesneler olduğudur. Bir sınıfın kullanımda olduğunu gördüğünüz her yerde, aslında sadece kullanımda olan bir değişken görüyorsunuz. Ve eğer bir değişkense, bir parametre olabilir.
Bunu birkaç kullanıma ayırabiliriz:
Fabrika Olarak Sınıflar
Sınıf, kendisinin bir örneğini oluşturan çağrılabilir bir nesnedir:
> >> class Foo: ... pass > >> Foo() <__main__.Foo at 0x7f73e0c96780>
Ve bir nesne olarak diğer değişkenlere atanabilir:
> >> myclass = Foo > >> myclass() <__main__.Foo at 0x7f73e0ca93c8>
Yukarıdaki kaplumbağa örneğimize geri dönersek, çizim için kaplumbağa kullanmayla ilgili bir sorun, çizimin konumu ve yönünün kaplumbağanın mevcut konumuna ve yönüne bağlı olmasıdır ve aynı zamanda, kaplumbağayı farklı bir durumda bırakabilir ve bu da onun için yararlı olmayabilir. arayan. Bunu çözmek için, draw_square
işlevimiz kendi kaplumbağasını oluşturabilir, onu istenen konuma taşıyabilir ve ardından bir kare çizebilir:
def draw_square(x, y, size): turtle = Turtle() turtle.penup() # Don't draw while moving to the start position turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)
Ancak, şimdi bir özelleştirme sorunumuz var. Arayanın, kaplumbağanın bazı özelliklerini ayarlamak veya aynı arayüze sahip ancak bazı özel davranışları olan farklı bir tür kaplumbağa kullanmak istediğini varsayalım?
Bunu, daha önce yaptığımız gibi, bağımlılık enjeksiyonuyla çözebiliriz - arayan, Turtle
nesnesini kurmaktan sorumlu olacaktır. Ama ya işlevimiz bazen farklı çizim amaçları için çok sayıda kaplumbağa yapmaya ihtiyaç duyarsa veya belki de karenin bir tarafını çizmek için her biri kendi kaplumbağası olan dört ipliği başlatmak isterse? Cevap basitçe Kaplumbağa sınıfını fonksiyona bir parametre yapmaktır. Umursamayan arayanlar için işleri basit tutmak için varsayılan değere sahip bir anahtar kelime argümanı kullanabiliriz:
def draw_square(x, y, size, make_turtle=Turtle): turtle = make_turtle() turtle.penup() turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)
Bunu kullanmak için bir kaplumbağa yaratan ve onu değiştiren bir make_turtle
işlevi yazabiliriz. Kare çizerken kaplumbağayı gizlemek istediğimizi varsayalım:
def make_hidden_turtle(): turtle = Turtle() turtle.hideturtle() return turtle draw_square(5, 10, 20, make_turtle=make_hidden_turtle)
Veya bu davranışı yerleşik hale getirmek ve alt sınıfı parametre olarak geçirmek için Turtle
alt sınıfını yapabiliriz:
class HiddenTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.hideturtle() draw_square(5, 10, 20, make_turtle=HiddenTurtle)
Diğer Dillerde…
Java ve C# gibi diğer bazı OOP dilleri birinci sınıf sınıflardan yoksundur. Bir sınıfı somutlaştırmak için, new
anahtar kelimeyi ve ardından gerçek bir sınıf adını kullanmanız gerekir.
Bu sınırlama, soyut fabrika (tek işi diğer sınıfları başlatmak olan bir sınıflar kümesinin oluşturulmasını gerektirir) ve Fabrika Yöntemi kalıbı gibi kalıpların nedenidir. Gördüğünüz gibi, Python'da, bir sınıf kendi fabrikası olduğu için sadece sınıfı bir parametre olarak çıkarmak meselesidir.
Temel Sınıflar Olarak Sınıflar
Aynı özelliği farklı sınıflara eklemek için kendimizi alt sınıflar oluştururken bulduğumuzu varsayalım. Örneğin, oluşturulduğunda bir günlüğe yazacak bir Turtle
alt sınıfı istiyoruz:
import logging logger = logging.getLogger() class LoggingTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Turtle got created")
Ama sonra kendimizi başka bir sınıfla tamamen aynı şeyi yaparken buluyoruz:
class LoggingHippo(Hippo): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Hippo got created")
Bu ikisi arasında değişen tek şey:
- temel sınıf
- Alt sınıfın adı—ancak bununla gerçekten ilgilenmiyoruz ve onu temel sınıf
__name__
özniteliğinden otomatik olarak oluşturabiliriz. -
debug
çağrısında kullanılan ad—ama yine, bunu temel sınıf adından oluşturabiliriz.
Sadece bir değişkenli çok benzer iki kod parçasıyla karşı karşıya kaldık, ne yapabiliriz? Tıpkı ilk örneğimizde olduğu gibi, bir fonksiyon oluşturuyoruz ve değişken kısmını parametre olarak çekiyoruz:
def make_logging_class(cls): class LoggingThing(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("{0} got created".format(cls.__name__)) LoggingThing.__name__ = "Logging{0}".format(cls.__name__) return LoggingThing LoggingTurtle = make_logging_class(Turtle) LoggingHippo = make_logging_class(Hippo)
Burada, birinci sınıf sınıfların bir gösterimi var:
- Sınıf anahtar sözcüğüyle çakışmayı önlemek için parametreye geleneksel bir ad
cls
vererek birclass
bir işleve geçirdik (bu amaç için kullanılanclass_
veklass
da göreceksiniz). - Fonksiyonun içinde bir sınıf oluşturduk; bu fonksiyona yapılan her çağrının yeni bir sınıf oluşturduğuna dikkat edin.
- Bu sınıfı, işlevin dönüş değeri olarak döndürdük.
Ayrıca tamamen isteğe bağlı olan ancak hata ayıklamaya yardımcı olabilecek LoggingThing.__name__
da yaptık.
Bu tekniğin başka bir uygulaması, bazen bir sınıfa eklemek istediğimiz bir sürü özelliğe sahip olduğumuz ve bu özelliklerin çeşitli kombinasyonlarını eklemek isteyebileceğimiz zamandır. İhtiyacımız olan tüm farklı kombinasyonları manuel olarak oluşturmak çok hantal olabilir.
Sınıfların çalışma zamanı yerine derleme zamanında oluşturulduğu dillerde bu mümkün değildir. Bunun yerine dekoratör kalıbını kullanmalısınız. Bu model bazen Python'da faydalı olabilir, ancak çoğunlukla yukarıdaki tekniği kullanabilirsiniz.
Normalde, özelleştirme için çok sayıda alt sınıf oluşturmaktan kaçınırım. Genellikle, sınıfları hiç içermeyen daha basit ve daha Pythonic yöntemler vardır. Ancak ihtiyacınız varsa bu teknik mevcuttur. Ayrıca Brandon Rhodes'un Python'daki dekoratör kalıbını tam olarak ele almasına bakın.
İstisna Olarak Sınıflar
Sınıfların kullanıldığını gördüğünüz başka bir yer, try/ except
/finally ifadesinin istisna yan tümcesidir. Bu sınıfları da parametreleştirebileceğimizi tahmin etmek için sürpriz yok.
Örneğin, aşağıdaki kod, başarısız olabilecek bir eylemi denemeye ve maksimum sayıda denemeye ulaşılana kadar üstel geri çekilmeyle yeniden denemeye yönelik çok genel bir strateji uygular:
import time def retry_with_backoff(action, exceptions_to_catch, max_attempts=10, attempts_so_far=0): try: return action() except exceptions_to_catch: attempts_so_far += 1 if attempts_so_far >= max_attempts: raise else: time.sleep(attempts_so_far ** 2) return retry_with_backoff(action, exceptions_to_catch, attempts_so_far=attempts_so_far, max_attempts=max_attempts)
Hem yapılacak eylemi hem de yakalanacak istisnaları parametre olarak çıkardık. exceptions_to_catch
parametresi, IOError
veya httplib.client.HTTPConnectionError
gibi tek bir sınıf veya bu tür sınıfların bir demeti olabilir. ("Çıplak hariç" yan tümcelerinden ve hatta except Exception
hariçten kaçınmak istiyoruz çünkü bunun diğer programlama hatalarını gizlediği biliniyor).
Uyarılar ve Sonuç
Parametreleştirme, kodu yeniden kullanmak ve kod tekrarını azaltmak için güçlü bir tekniktir. Bazı dezavantajları olmadan değil. Kodu yeniden kullanma arayışında, genellikle birkaç sorun ortaya çıkar:
- Anlaşılması çok zor hale gelen aşırı genel veya soyut kod.
- Gerçekte, yalnızca belirli parametre kombinasyonları uygun şekilde test edildiğinden, büyük resmi gizleyen veya hatalara neden olan parametrelerin çoğalmasıyla kodlayın.
- "Ortak kodları" tek bir yerde hesaba katıldığından, kod tabanının farklı bölümlerinin yararsız bir şekilde birleştirilmesi. Bazen iki yerdeki kod sadece tesadüfen benzerdir ve bağımsız olarak değişmeleri gerekebileceğinden iki yerin birbirinden bağımsız olması gerekir.
Bazen biraz "kopyalanmış" kod bu sorunlardan çok daha iyidir, bu yüzden bu tekniği dikkatli kullanın.
Bu yazıda, bağımlılık enjeksiyonu , strateji , şablon yöntemi , soyut fabrika , fabrika yöntemi ve dekoratör olarak bilinen tasarım kalıplarını ele aldık. Python'da, bunların çoğu gerçekten basit bir parametreleştirme uygulaması olarak ortaya çıkıyor veya Python'daki parametrelerin çağrılabilir nesneler veya sınıflar olabileceği gerçeğiyle kesinlikle gereksiz hale getiriliyor. Umarım bu, "gerçek bir Python geliştiricisi olarak bilmeniz gereken şeylerin" kavramsal yükünü hafifletmeye yardımcı olur ve özlü, Pythonic kodu yazmanıza olanak tanır!
Daha fazla okuma:
- Python Tasarım Modelleri: Şık ve Modaya Uygun Kod İçin
- Python Kalıpları: Python Tasarım Kalıpları İçin
- Python Günlüğü: Derinlemesine Bir Eğitim