Python Class structure basics

A crash course in Python classes

 1 October 2013
  python ruby classes

Handy links:

Method overriding

Is niet mogelijk. Gebruik default values!

def func(i, j = 2, k  3):
    return i + j + k

func(1) # 6
func(1, 1) # 5
func(1, 1, 1) # 3

Wat wel gaat, evt met decorators, zie Five-minute multimethods in Python - is __call__ implementeren en dan met metaprogrammeren te loopen over alle methods en te kijken of de argumenten overeen komen met het type dat required is. Fancypancy!

Opgelet met pitfalls

Nummer 1: default variables worden herbruikt:

When Python executes a “def” statement, it takes some ready-made pieces (including the compiled code for the function body and the current namespace), and creates a new function object. When it does this, it also evaluates the default values. […] Another way to reset the defaults is to simply re-execute the same “def” statement. Python will then create a new binding to the code object, evaluate the defaults, and assign the function object to the same variable as before. But again, only do that if you know exactly what you’re doing.

Default als arr = [] Gaat de array muteren. Heel handig voor memoization, heel verwarrend anders. Oplossing? arr None en dan arr = [] if arr is None.

Zie ook Default parameter values in Python voor in-depth uitleg.

Nummer 2: Python’s nested scopes bind to variables, not object values.

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

variabele i gaat altijd 9 zijn - wordt niet op value gebind. Oplossing is explicit binding door de functie definitie van callback(): te veranderen naar callback(i=i):.

Fields dynamisch definiëren

Thing(a=1, b2) kan op een paar manieren gedefiniëerd worden.

fields expliciet declareren
class Thing:
   def __init__(self, a, b):
       self.a, self.b = a, b
dynamisch uw eigen dictionary updaten
class Thing:
   def __init__(self, **kwargs):
       self.__dict__.update(kwargs)

❗️ Dit is uiteraard heel gevaarlijk aangezien het al uw method bodies kan vervangen door een param value. BOEM.

* *name(zonder spatie, wiki markup, nvdr) geeft alle argumenten in een dict terug. *name gaat ook, geeft u een lijst van argumenten terug. Combinatie gaat ook, één ster moet voor de tweede komen. Zoiets is dus mogelijk: def _ _init_ _(self, arg1, arg2, *allargs, * *allargsasdict).

Alles als een message passing systeem zien

In Ruby is er een andere manier om def name block end te schrijven, hoe het geïnterpreteerd wordt: self.class.send(:name, args) { block }

def opt_to_s opt={}
  opt.empty? ? '' : ' ' + opt.map {|e,v| "#{e}=<br/>"#{v}<br/>""}.join(', ')
end

[:html, :body, :h1].each do |el|
  start="<#{el}"
  fin="</#{el}>"
  self.class.send(:define_method, el) {|options={}, &blk| start + opt_to_s(options) + '>' + blk.call + fin}
end

# Now, we can neatly nest tags and content
html do
  body do
    h1 :class=>"bold-h1", :id>"h1_99" do
      "header"
    end
  end
end
 => "<body><h1 class<br/>"bold-h1<br/>", id=<br/>"h1_99<br/>">header</h1></body>"

Voilà, een DSL in no-time. Holy crap. Bron: do you understand ruby’s objects messages and blocks?

Superclassing

Klassen aanmaken is niet al te moeilijk, maar een call uitvoeren naar de overridden method is iets minder evident: zie super() in python docs

Een voorbeeld van een custom http handler:

class HttpHandler(SimpleHTTPRequestHandler):
	def readStatus(self):
		return {
			"Status": "blabla",
			"StartTime": ""
		}

	def do_GET(self):
		try:
			print('serving %s now' % self.path)
			if "status.json" in self.path:
				self.send_response(200)
				self.send_header('Content-type', 'text/json')
				self.end_headers()
				self.wfile.write(json.dumps(self.readStatus()).encode())
			else:
				SimpleHTTPRequestHandler.do_GET(self)

		except IOError:
			self.send_error(500, 'internal server error in server python source: %s' % self.path)

Wat is hier speciaal aan:

Diamond inheritance

BaseClass.method() is impliciet hetzelfde als super().method(), behalve dat je met super een argument kan meegeven, over welke superklasse het gaat.

Zie ook Things to know about Python’s super()

Closures en lambda’s

functies in functies aanmaken werkt perfect, “closed over” die lexicale scope, net zoals je zou verwachten zoals bijvoorbeeld in javascript:

	def readBuildStatus(self):
		html = urllib.request.urlopen("http://bla/lastbuildstatus.htm").read().decode()
		def extractVersion():
			versionString = "Version: "
			versionIndex = html.find("Version: ")
			return html[versionIndex + len(versionString) : versionIndex + len(versionString) + len("YYYY.MM")]
		def extractStatus():
			return "Succeeded" if html.find("SUCCESS") != -1 else "Failed"

de twee andere methods lezen de html variabele uit. Hier hoef je geen self. prefix te gebruiken, binnen de readBuildStatus() functie zelf - hierbuiten zijn de closures verdwenen natuurlijk (out of scope).

unittest module

Spreekt voor zich:

import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):

    def setUp(self):
        self.calc = Calculator().calculate;

    def test_calculateBasicNumberReturnsNumber(self):
        self.assertEqual(3, self.calc('3'))

    def test_calculateSimpleMultiplicationReturnsResult(self):
        self.assertEqual(10, self.calc('5*2'))

    def test_calculateInvalidStringShouldThrowException(self):
        self.assertRaises(ValueError, self.calc, ('blabl'))

Zie http://docs.python.org/3/library/unittest.html

Hoe voer ik dit nu uit?

Dit stuk onder uw py file plakken:

if __name__ == '__main__':
    unittest.main()

En dan python -m unittest -v calculatorTest. de v flag geeft wat extra output, anders staat er gewoon OK. De test op zich builden in bijvoorbeeld sublime met de main method erin zorgt er ook voor dat deze automatisch uitgevoerd wordt.

automatic test case discovery

python -m unittest discover gaat alle unit testen vanaf huidig dir scannen en uitvoeren (instelbaar met params). Moet voldoen aan:

  1. extenden van unittest.TestCase
  2. voldoen aan python module structuur. Testen in files met prefix “test_x.py”.
  3. Indien in subfolder “test”: vergeet geen “init.py” file.
autotest

Mogelijk met onder andere autonose (nose is een alternatief voor unittest) en sniffer. Om die te installeren moet je via de pip package manager gaan, en dan gewoon sniffer uitvoeren in uw base directory.

Bye autotools hello Scons

Building C++ projects with Scons  26 March 2014

Ruby Class structures basics

A look at ruby's lambda's  1 October 2013

A look at dynamic languages

A comparison on some dynamic languages  1 October 2013

 Top